doc: add JavaDoc and remaining tests
continuous-integration/drone/push Build is passing Details
continuous-integration/drone/pr Build is passing Details

pull/3/head
Davide Polonio 2023-04-14 16:21:05 +02:00
parent 9ff5748964
commit 3baa041745
8 changed files with 379 additions and 8 deletions

View File

@ -17,6 +17,19 @@ import javax.inject.Named;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.slf4j.Logger; import org.slf4j.Logger;
/**
* This class works as a "batch" persistence entries remover - basically, it removes all the entries
* that are in the database in an async way. This allows better performance on other pieces of
* codebase, where we don't really care about checking that the removal has been successful, and
* instead we just want to issue a deletion and instantly continue with our codebase flow.
*
* <p>This service provides the ability to listen to the operation in two ways: via old-fashioned
* listeners and via {@link CompletableFuture}. If the caller wants to be sure that the removal has
* effectively happened, it can sync with the given {@link CompletableFuture}.
*
* @author Davide Polonio
* @since 1.0
*/
public class BatchBeanCleanerService extends AbstractExecutionThreadService { public class BatchBeanCleanerService extends AbstractExecutionThreadService {
private final LinkedBlockingDeque< private final LinkedBlockingDeque<
@ -24,6 +37,10 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
entriesToRemove; entriesToRemove;
private final Logger log; private final Logger log;
private final Pair<Integer, TimeUnit> serviceRunningCheckTime; private final Pair<Integer, TimeUnit> serviceRunningCheckTime;
// Accessing adding listeners while we iterate on the list inside the service thread _can_ be
// considered a race condition - but given the frequency of adding and removing listeners, the
// computational cost of putting Locks in place and check for them everytime is way greater
private final LinkedList< private final LinkedList<
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>> Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>>
deletionListener; deletionListener;
@ -40,6 +57,7 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
@Override @Override
protected void run() throws Exception { protected void run() throws Exception {
log.trace("BatchBeanCleanerService run() method invoked");
while (isRunning()) { while (isRunning()) {
// This statement is blocking for 1 sec if the queue is empty, and then it goes on - this way // This statement is blocking for 1 sec if the queue is empty, and then it goes on - this way
// we check if the service is still supposed to be up or what // we check if the service is still supposed to be up or what
@ -78,19 +96,39 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
entriesToRemove.clear(); entriesToRemove.clear();
} }
/**
* Add an entry to be removed
*
* @param id the id of the entry to remove
* @param column the column that will be used to perform the delete statement
* @return a {@link CompletableFuture} containing an {@link Integer} indicating the number of rows
* affected by the operation
*/
public CompletableFuture<Integer> removeAsync( public CompletableFuture<Integer> removeAsync(
String id, PString<? extends TQRootBean<?, ?>> row) { String id, PString<? extends TQRootBean<?, ?>> column) {
final CompletableFuture<Integer> jobExecution = new CompletableFuture<>(); final CompletableFuture<Integer> jobExecution = new CompletableFuture<>();
entriesToRemove.offer(Tuple.of(id, row, jobExecution)); entriesToRemove.offer(Tuple.of(id, column, jobExecution));
return jobExecution; return jobExecution;
} }
/**
* Add a listener that will be invoked once the entry has been removed. This listener is invoked
* after the {@link CompletableFuture} completion. Note that all the listeners are called in a
* parallel fashion, so call order can change any time.
*
* @param listener the listener to add.
*/
public void addListener( public void addListener(
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>> Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) { listener) {
deletionListener.add(listener); deletionListener.add(listener);
} }
/**
* Remove a listener
*
* @param listener the listener to be removed
*/
public void removeListener( public void removeListener(
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>> Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) { listener) {

View File

@ -73,7 +73,7 @@ class ShowHelp implements Processor {
return CompletableFuture.supplyAsync( return CompletableFuture.supplyAsync(
() -> () ->
chatUtil chatUtil
.extractChat(callbackQueryContext, update) // FIXME callbackquerycontext removal? .extractChat(callbackQueryContext, update)
.map( .map(
chat -> { chat -> {
final String message = modelHelp.getMessage(chat, tgCommandProcessors); final String message = modelHelp.getMessage(chat, tgCommandProcessors);

View File

@ -4,9 +4,21 @@ import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import java.util.Optional; import java.util.Optional;
/**
* A class with misc utilities
*
* @author Davide Polonio
* @since 1.0
*/
public class Util { public class Util {
// FIXME tests, doc /**
* Extract the message id of the given {@link Update}
*
* @param update the {@link Update} to check and search the message id for
* @return an {@link Optional} containing a {@link Integer} with the message id if it is present,
* otherwise a {@link Optional#empty()} if it is not found.
*/
public static Optional<Integer> extractMessageId(Update update) { public static Optional<Integer> extractMessageId(Update update) {
return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId); return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
} }

View File

@ -87,7 +87,6 @@ class Start implements Processor {
"template/telegram/start.vm"); "template/telegram/start.vm");
log.trace("Start command - message to send back: " + message); log.trace("Start command - message to send back: " + message);
// FIXME bug!! Show help button set to true but its fake news
chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap()); chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap());
// To get the messageId we should send the message first, then save it in the database! // To get the messageId we should send the message first, then save it in the database!

View File

@ -13,11 +13,17 @@ import java.util.stream.Collectors;
import javax.inject.Inject; import javax.inject.Inject;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
// FIXME tests! /**
* This class provides the model for the Help message output. It is the business logic behind the
* message, and a way to keep the codebase DRY.
*
* @author Davide Polonio
* @since 1.0
* @see com.github.polpetta.mezzotre.telegram.command.Help for the command help
* @see com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp for the event help
*/
public class Help { public class Help {
private static final String TRIGGERING_STAGING_NAME = "/help";
private final TemplateContentGenerator templateContentGenerator; private final TemplateContentGenerator templateContentGenerator;
private final UUIDGenerator uuidGenerator; private final UUIDGenerator uuidGenerator;
@ -27,6 +33,15 @@ public class Help {
this.uuidGenerator = uuidGenerator; this.uuidGenerator = uuidGenerator;
} }
/**
* Generates the message that will be sent back to the user. This takes the given {@link
* Processor} and formats them accordingly to the chat locale
*
* @param chat the {@link TgChat} conversation
* @param tgCommandProcessors a {@link Map} of all the {@link Processor} that will be printed in
* the help message
* @return a {@link String} localized ready to be sent to the user
*/
public String getMessage(TgChat chat, Map<String, Processor> tgCommandProcessors) { public String getMessage(TgChat chat, Map<String, Processor> tgCommandProcessors) {
return templateContentGenerator.mergeTemplate( return templateContentGenerator.mergeTemplate(
velocityContext -> { velocityContext -> {
@ -45,6 +60,17 @@ public class Help {
"template/telegram/help.vm"); "template/telegram/help.vm");
} }
/**
* Generates {@link InlineKeyboardButton} to be returned to the user to give them the possibility
* to interact with them via events rather than commands.
*
* @param chat the current {@link TgChat} conversation
* @param eventProcessors a {@link Map} of {@link
* com.github.polpetta.mezzotre.telegram.callbackquery.Processor} that are currently used to
* process all events. Note that only the one the user can interact with will be added as
* buttons
* @return an array of {@link InlineKeyboardButton}
*/
public InlineKeyboardButton[] generateInlineKeyBoardButton( public InlineKeyboardButton[] generateInlineKeyBoardButton(
TgChat chat, TgChat chat,
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> eventProcessors) { Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> eventProcessors) {

View File

@ -0,0 +1,100 @@
package com.github.polpetta.mezzotre.telegram.callbackquery;
import static org.junit.jupiter.api.Assertions.*;
import com.google.gson.Gson;
import com.pengrad.telegrambot.model.Update;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
@Execution(ExecutionMode.CONCURRENT)
class UtilTest {
private static Gson gson;
@BeforeAll
static void beforeAll() {
gson = new Gson();
}
@Test
void shouldProvideAMessageIdGivenTheUpdate() {
final Update update =
gson.fromJson(
"{\n"
+ " \"update_id\": 158712614,\n"
+ " \"callback_query\": {\n"
+ " \"id\": \"20496049451114620\",\n"
+ " \"from\": {\n"
+ " \"id\": 1111111,\n"
+ " \"is_bot\": false,\n"
+ " \"first_name\": \"Test Firstname\",\n"
+ " \"last_name\": \"Test Lastname\",\n"
+ " \"username\": \"Testusername\",\n"
+ " \"language_code\": \"en\"\n"
+ " },\n"
+ " \"message\": {\n"
+ " \"message_id\": 2723,\n"
+ " \"from\": {\n"
+ " \"id\": 244745330,\n"
+ " \"is_bot\": true,\n"
+ " \"first_name\": \"Dev - DavideBot\",\n"
+ " \"username\": \"devdavidebot\"\n"
+ " },\n"
+ " \"date\": 1681218838,\n"
+ " \"chat\": {\n"
+ " \"id\": 1111111,\n"
+ " \"type\": \"private\",\n"
+ " \"username\": \"Testusername\",\n"
+ " \"first_name\": \"Test Firstname\",\n"
+ " \"last_name\": \"Test Lastname\"\n"
+ " },\n"
+ " \"text\": \"a message\",\n"
+ " \"reply_markup\": {\n"
+ " \"inline_keyboard\": [\n"
+ " [\n"
+ " {\n"
+ " \"text\": \"English\",\n"
+ " \"callback_data\": \"9a64be11-d086-4bd9-859f-720c43dedcb5\"\n"
+ " },\n"
+ " {\n"
+ " \"text\": \"Italian\",\n"
+ " \"callback_data\": \"8768d660-f05f-4f4b-bda5-3451ab573d56\"\n"
+ " }\n"
+ " ]\n"
+ " ]\n"
+ " }\n"
+ " }\n"
+ " }\n"
+ "}",
Update.class);
assertEquals(
2723, Util.extractMessageId(update).get(), "A message id should be returned, 2723");
}
@Test
void shouldGiveAnEmptyOptionalWithoutMessageId() {
final Update update =
gson.fromJson(
"{\n"
+ " \"update_id\": 158712614,\n"
+ " \"callback_query\": {\n"
+ " \"id\": \"20496049451114620\",\n"
+ " \"from\": {\n"
+ " \"id\": 1111111,\n"
+ " \"is_bot\": false,\n"
+ " \"first_name\": \"Test Firstname\",\n"
+ " \"last_name\": \"Test Lastname\",\n"
+ " \"username\": \"Testusername\",\n"
+ " \"language_code\": \"en\"\n"
+ " }\n"
+ " }\n"
+ "}",
Update.class);
assertTrue(Util.extractMessageId(update).isEmpty(), "The shouldn't be any message id");
}
}

View File

@ -0,0 +1,154 @@
package com.github.polpetta.mezzotre.telegram.model;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.github.polpetta.mezzotre.helper.Loader;
import com.github.polpetta.mezzotre.helper.TestConfig;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory;
import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator;
import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext;
import com.github.polpetta.mezzotre.telegram.callbackquery.Processor;
import com.github.polpetta.mezzotre.util.UUIDGenerator;
import com.github.polpetta.types.json.ChatContext;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.InlineKeyboardButton;
import com.pengrad.telegrambot.request.BaseRequest;
import io.ebean.Database;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
import org.testcontainers.junit.jupiter.Container;
import org.testcontainers.junit.jupiter.Testcontainers;
@Tag("slow")
@Tag("database")
@Tag("velocity")
@Testcontainers
class HelpIntegrationTest {
@Container
private final PostgreSQLContainer<?> postgresServer =
new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE);
private TemplateContentGenerator templateContentGenerator;
private UUIDGenerator fakeUUIDGenerator;
private Help help;
private Database database;
@BeforeEach
void setUp() throws Exception {
database =
Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer));
templateContentGenerator =
new TemplateContentGenerator(new LocalizedMessageFactory(Loader.defaultVelocityEngine()));
fakeUUIDGenerator = mock(UUIDGenerator.class);
help = new Help(templateContentGenerator, fakeUUIDGenerator);
}
@Test
void shouldGenerateAGoodHelpMessage() {
final TgChat tgChat = new TgChat(11111L, new ChatContext());
tgChat.save();
final com.github.polpetta.mezzotre.telegram.command.Processor dummyCommand1 =
new com.github.polpetta.mezzotre.telegram.command.Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/example", "/another");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
TgChat chat, Update update) {
return CompletableFuture.completedFuture(Optional.empty());
}
@Override
public String getLocaleDescriptionKeyword() {
return "help.cmdDescription";
}
};
final String gotMessage =
help.getMessage(tgChat, Map.of("/example", dummyCommand1, "/another", dummyCommand1));
assertEquals(
"Here is a list of what I can do:\n"
+ "\n"
+ "- /another /example: Print the help message\n"
+ "\n"
+ "You can do the same operations you'd do with the commands aforementioned by"
+ " selecting the corresponding button below \uD83D\uDC47",
gotMessage);
}
@Test
void shouldGenerateButtonsCorrectly() {
final TgChat tgChat = new TgChat(111111L, new ChatContext());
tgChat.save();
when(fakeUUIDGenerator.generateAsString())
.thenReturn("53dc6dca-1042-4bc7-beb8-ce2a34df6a54")
.thenReturn("d5d3e016-7b60-4f1a-bd79-e1a6bff32f17");
final Processor visibleProcessor1 =
new Processor() {
@Override
public String getEventName() {
return "eventName";
}
@Override
public boolean canBeDirectlyInvokedByTheUser() {
return true;
}
@Override
public Optional<String> getPrettyPrintLocaleKeyName() {
return Optional.of("changeLanguage.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
CallbackQueryContext callbackQueryContext, Update update) {
return CompletableFuture.completedFuture(Optional.empty());
}
};
final Processor invisibleProcessor1 =
new Processor() {
@Override
public String getEventName() {
return "invisible";
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
CallbackQueryContext callbackQueryContext, Update update) {
return CompletableFuture.completedFuture(Optional.empty());
}
};
final InlineKeyboardButton[] buttons =
help.generateInlineKeyBoardButton(
tgChat,
Map.of(
visibleProcessor1.getEventName(),
visibleProcessor1,
invisibleProcessor1.getEventName(),
invisibleProcessor1));
assertEquals(1, buttons.length);
assertEquals("Change language", buttons[0].text());
assertEquals(1, new QCallbackQueryContext().findCount());
}
}

View File

@ -0,0 +1,42 @@
package com.github.polpetta.mezzotre.telegram.model;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator;
import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.mezzotre.util.UUIDGenerator;
import java.util.Collections;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
@Execution(ExecutionMode.CONCURRENT)
class HelpTest {
private TemplateContentGenerator fakeTemplateContentGenerator;
private UUIDGenerator fakeUUIDGenerator;
private Help help;
@BeforeEach
void setUp() {
fakeTemplateContentGenerator = mock(TemplateContentGenerator.class);
fakeUUIDGenerator = mock(UUIDGenerator.class);
help = new Help(fakeTemplateContentGenerator, fakeUUIDGenerator);
}
@Test
void shouldCallTemplateContentGeneratorRight() {
final TgChat fakeChat = mock(TgChat.class);
when(fakeChat.getLocale()).thenReturn("en-US");
final String message = help.getMessage(fakeChat, Collections.emptyMap());
verify(fakeTemplateContentGenerator, times(1))
.mergeTemplate(any(), eq("en-US"), eq("template/telegram/help.vm"));
}
}