diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java
index be7f970..acbff5d 100644
--- a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java
+++ b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java
@@ -17,6 +17,19 @@ import javax.inject.Named;
import org.apache.commons.lang3.tuple.Pair;
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.
+ *
+ *
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 {
private final LinkedBlockingDeque<
@@ -24,6 +37,10 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
entriesToRemove;
private final Logger log;
private final Pair 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<
Consumer>, CompletableFuture>>>
deletionListener;
@@ -40,6 +57,7 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
@Override
protected void run() throws Exception {
+ log.trace("BatchBeanCleanerService run() method invoked");
while (isRunning()) {
// 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
@@ -78,19 +96,39 @@ public class BatchBeanCleanerService extends AbstractExecutionThreadService {
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 removeAsync(
- String id, PString extends TQRootBean, ?>> row) {
+ String id, PString extends TQRootBean, ?>> column) {
final CompletableFuture jobExecution = new CompletableFuture<>();
- entriesToRemove.offer(Tuple.of(id, row, jobExecution));
+ entriesToRemove.offer(Tuple.of(id, column, 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(
Consumer>, CompletableFuture>>
listener) {
deletionListener.add(listener);
}
+ /**
+ * Remove a listener
+ *
+ * @param listener the listener to be removed
+ */
public void removeListener(
Consumer>, CompletableFuture>>
listener) {
diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java
index 302a931..81d9aa1 100644
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelp.java
@@ -73,7 +73,7 @@ class ShowHelp implements Processor {
return CompletableFuture.supplyAsync(
() ->
chatUtil
- .extractChat(callbackQueryContext, update) // FIXME callbackquerycontext removal?
+ .extractChat(callbackQueryContext, update)
.map(
chat -> {
final String message = modelHelp.getMessage(chat, tgCommandProcessors);
diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java
index 7f6d6f9..3637292 100644
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/Util.java
@@ -4,9 +4,21 @@ import com.pengrad.telegrambot.model.Message;
import com.pengrad.telegrambot.model.Update;
import java.util.Optional;
+/**
+ * A class with misc utilities
+ *
+ * @author Davide Polonio
+ * @since 1.0
+ */
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 extractMessageId(Update update) {
return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
}
diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java
index bb2b859..390c6ea 100644
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Start.java
@@ -87,7 +87,6 @@ class Start implements Processor {
"template/telegram/start.vm");
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());
// To get the messageId we should send the message first, then save it in the database!
diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java
index 83ee044..2cc8e70 100644
--- a/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java
+++ b/src/main/java/com/github/polpetta/mezzotre/telegram/model/Help.java
@@ -13,11 +13,17 @@ import java.util.stream.Collectors;
import javax.inject.Inject;
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 {
- private static final String TRIGGERING_STAGING_NAME = "/help";
-
private final TemplateContentGenerator templateContentGenerator;
private final UUIDGenerator uuidGenerator;
@@ -27,6 +33,15 @@ public class Help {
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 tgCommandProcessors) {
return templateContentGenerator.mergeTemplate(
velocityContext -> {
@@ -45,6 +60,17 @@ public class Help {
"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(
TgChat chat,
Map eventProcessors) {
diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java
new file mode 100644
index 0000000..ffc2d78
--- /dev/null
+++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java
@@ -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");
+ }
+}
diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java
new file mode 100644
index 0000000..fba44e3
--- /dev/null
+++ b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java
@@ -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 getTriggerKeywords() {
+ return Set.of("/example", "/another");
+ }
+
+ @Override
+ public CompletableFuture>> 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 getPrettyPrintLocaleKeyName() {
+ return Optional.of("changeLanguage.inlineKeyboardButtonName");
+ }
+
+ @Override
+ public CompletableFuture>> process(
+ CallbackQueryContext callbackQueryContext, Update update) {
+ return CompletableFuture.completedFuture(Optional.empty());
+ }
+ };
+
+ final Processor invisibleProcessor1 =
+ new Processor() {
+ @Override
+ public String getEventName() {
+ return "invisible";
+ }
+
+ @Override
+ public CompletableFuture>> 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());
+ }
+}
diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java
new file mode 100644
index 0000000..fbb5eb6
--- /dev/null
+++ b/src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java
@@ -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"));
+ }
+}