From 3baa04174529fc426eac7ae39bc96c6b74497873 Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 14 Apr 2023 16:21:05 +0200 Subject: [PATCH] doc: add JavaDoc and remaining tests --- .../mezzotre/orm/BatchBeanCleanerService.java | 42 ++++- .../telegram/callbackquery/ShowHelp.java | 2 +- .../mezzotre/telegram/callbackquery/Util.java | 14 +- .../mezzotre/telegram/command/Start.java | 1 - .../mezzotre/telegram/model/Help.java | 32 +++- .../telegram/callbackquery/UtilTest.java | 100 ++++++++++++ .../telegram/model/HelpIntegrationTest.java | 154 ++++++++++++++++++ .../mezzotre/telegram/model/HelpTest.java | 42 +++++ 8 files changed, 379 insertions(+), 8 deletions(-) create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/UtilTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/model/HelpTest.java 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> row) { + String id, PString> 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")); + } +}