From 9ff574896479f8b1856da1ac7a2269f79115c8db Mon Sep 17 00:00:00 2001 From: Davide Polonio Date: Fri, 14 Apr 2023 15:00:25 +0200 Subject: [PATCH] chore: perform refactor for db-cleaning and more testing * Add Javadoc where missing * Solve some FIXMEs here and there --- .../com/github/polpetta/mezzotre/App.java | 6 +- .../com/github/polpetta/mezzotre/AppDI.java | 14 + .../mezzotre/orm/BatchBeanCleanerService.java | 99 ++++ .../orm/CallbackQueryContextCleaner.java | 37 ++ .../mezzotre/orm/telegram/ChatUtil.java | 89 ++++ .../telegram/callbackquery/NotFound.java | 9 +- .../callbackquery/SelectLanguageTutorial.java | 31 +- .../telegram/callbackquery/ShowHelp.java | 49 +- .../mezzotre/telegram/callbackquery/Util.java | 18 +- .../mezzotre/telegram/command/Help.java | 23 +- .../mezzotre/telegram/command/NotFound.java | 9 +- .../telegram/command/NotFoundFactory.java | 2 +- .../mezzotre/telegram/command/Start.java | 22 +- .../mezzotre/telegram/model/Help.java | 51 +- .../polpetta/mezzotre/util/ServiceModule.java | 71 +++ .../github/polpetta/mezzotre/util/UtilDI.java | 43 ++ .../polpetta/mezzotre/util/di/ThreadPool.java | 25 - src/main/resources/template/telegram/help.vm | 3 +- src/main/resources/template/telegram/start.vm | 2 +- .../github/polpetta/mezzotre/UnitTest.java | 22 +- .../helper/IntegrationAppFactory.java | 20 +- ...atchBeanCleanerServiceIntegrationTest.java | 93 ++++ .../orm/telegram/ChatUtilIntegrationTest.java | 176 +++++++ .../mezzotre/orm/telegram/ChatUtilTest.java | 70 +++ .../telegram/callbackquery/NotFoundTest.java | 46 ++ ...SelectLanguageTutorialIntegrationTest.java | 49 +- .../ShowHelpIntegrationTest.java | 457 ++++++++++++++++++ .../telegram/callbackquery/ShowHelpTest.java | 314 ++++++++++++ .../telegram/command/HelpIntegrationTest.java | 14 +- .../mezzotre/telegram/command/HelpTest.java | 186 +++++++ .../command/StartIntegrationTest.java | 9 +- 31 files changed, 1930 insertions(+), 129 deletions(-) create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java create mode 100644 src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java delete mode 100644 src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java create mode 100644 src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java diff --git a/src/main/java/com/github/polpetta/mezzotre/App.java b/src/main/java/com/github/polpetta/mezzotre/App.java index c19f1fe..9ac428e 100644 --- a/src/main/java/com/github/polpetta/mezzotre/App.java +++ b/src/main/java/com/github/polpetta/mezzotre/App.java @@ -5,7 +5,8 @@ import com.github.polpetta.mezzotre.route.RouteDI; import com.github.polpetta.mezzotre.route.Telegram; import com.github.polpetta.mezzotre.telegram.callbackquery.CallbackQueryDI; import com.github.polpetta.mezzotre.telegram.command.CommandDI; -import com.github.polpetta.mezzotre.util.di.ThreadPool; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.github.polpetta.mezzotre.util.UtilDI; import com.google.inject.*; import com.google.inject.Module; import com.google.inject.name.Names; @@ -24,7 +25,7 @@ public class App extends Jooby { (jooby) -> { final HashSet modules = new HashSet<>(); modules.add(new OrmDI()); - modules.add(new ThreadPool()); + modules.add(new UtilDI()); modules.add(new RouteDI()); modules.add(new CommandDI()); modules.add(new CallbackQueryDI()); @@ -55,6 +56,7 @@ public class App extends Jooby { install( injector.getInstance(Key.get(Extension.class, Names.named("flyWayMigrationExtension")))); install(injector.getInstance(Key.get(Extension.class, Names.named("ebeanExtension")))); + install(injector.getInstance(Key.get(ServiceModule.class, Names.named("serviceModule")))); decorator(new AccessLogHandler()); decorator(new TransactionalRequest()); diff --git a/src/main/java/com/github/polpetta/mezzotre/AppDI.java b/src/main/java/com/github/polpetta/mezzotre/AppDI.java index 650f733..15d945f 100644 --- a/src/main/java/com/github/polpetta/mezzotre/AppDI.java +++ b/src/main/java/com/github/polpetta/mezzotre/AppDI.java @@ -1,10 +1,15 @@ package com.github.polpetta.mezzotre; +import com.github.polpetta.mezzotre.orm.BatchBeanCleanerService; +import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Provides; import io.jooby.Jooby; +import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Named; import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; public class AppDI extends AbstractModule { @@ -28,4 +33,13 @@ public class AppDI extends AbstractModule { public String getAppName() { return APPLICATION_NAME; } + + @Provides + @Singleton + @Named("services") + public List getApplicationServices( + Logger logger, + @Named("serviceRunningCheckTime") Pair serviceRunningCheckTime) { + return List.of(new BatchBeanCleanerService(logger, serviceRunningCheckTime)); + } } diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java new file mode 100644 index 0000000..be7f970 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerService.java @@ -0,0 +1,99 @@ +package com.github.polpetta.mezzotre.orm; + +import com.google.common.util.concurrent.AbstractExecutionThreadService; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple; +import io.vavr.Tuple3; +import io.vavr.control.Try; +import java.util.LinkedList; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import javax.inject.Inject; +import javax.inject.Named; +import org.apache.commons.lang3.tuple.Pair; +import org.slf4j.Logger; + +public class BatchBeanCleanerService extends AbstractExecutionThreadService { + + private final LinkedBlockingDeque< + Tuple3>, CompletableFuture>> + entriesToRemove; + private final Logger log; + private final Pair serviceRunningCheckTime; + private final LinkedList< + Consumer>, CompletableFuture>>> + deletionListener; + + @Inject + public BatchBeanCleanerService( + Logger logger, + @Named("serviceRunningCheckTime") Pair serviceRunningCheckTime) { + this.log = logger; + this.serviceRunningCheckTime = serviceRunningCheckTime; + this.entriesToRemove = new LinkedBlockingDeque<>(); + this.deletionListener = new LinkedList<>(); + } + + @Override + protected void run() throws Exception { + 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 + Optional.ofNullable( + entriesToRemove.poll( + serviceRunningCheckTime.getLeft(), serviceRunningCheckTime.getRight())) + .ifPresent( + entryToRemove -> { + Try.of( + () -> { + final int deleted = entryToRemove._2().eq(entryToRemove._1()).delete(); + if (deleted > 0) { + log.trace( + "Bean with id " + entryToRemove._1() + " removed successfully "); + } else { + log.warn( + "Bean(s) with id " + + entryToRemove._1() + + " for query " + + entryToRemove._2() + + " was not removed from the database because it was not" + + " found"); + } + entryToRemove._3().complete(deleted); + deletionListener.parallelStream().forEach(l -> l.accept(entryToRemove)); + return null; + }) + .onFailure(ex -> entryToRemove._3().completeExceptionally(ex)); + }); + } + } + + @Override + protected void shutDown() throws Exception { + super.shutDown(); + entriesToRemove.clear(); + } + + public CompletableFuture removeAsync( + String id, PString> row) { + final CompletableFuture jobExecution = new CompletableFuture<>(); + entriesToRemove.offer(Tuple.of(id, row, jobExecution)); + return jobExecution; + } + + public void addListener( + Consumer>, CompletableFuture>> + listener) { + deletionListener.add(listener); + } + + public void removeListener( + Consumer>, CompletableFuture>> + listener) { + deletionListener.remove(listener); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java new file mode 100644 index 0000000..0119168 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/CallbackQueryContextCleaner.java @@ -0,0 +1,37 @@ +package com.github.polpetta.mezzotre.orm; + +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import io.ebean.typequery.PString; +import java.util.concurrent.CompletableFuture; +import java.util.function.Supplier; +import javax.inject.Inject; +import javax.inject.Singleton; +import org.slf4j.Logger; + +@Singleton +public class CallbackQueryContextCleaner { + + private static final Supplier> ENTRY_GROUP = + () -> new QCallbackQueryContext().entryGroup; + private static final Supplier> SINGLE_ENTRY = + () -> new QCallbackQueryContext().id; + + private final BatchBeanCleanerService batchBeanCleanerService; + private final Logger log; + + @Inject + public CallbackQueryContextCleaner(BatchBeanCleanerService batchBeanCleanerService, Logger log) { + this.batchBeanCleanerService = batchBeanCleanerService; + this.log = log; + } + + public CompletableFuture removeGroupAsync(String id) { + log.trace("CallbackQueryContext entry group " + id + " queued for removal"); + return batchBeanCleanerService.removeAsync(id, ENTRY_GROUP.get()); + } + + public CompletableFuture removeIdAsync(String id) { + log.trace("CallbackQueryContext single entity " + id + " queued for removal"); + return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get()); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java new file mode 100644 index 0000000..b722f2b --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtil.java @@ -0,0 +1,89 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +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.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import com.pengrad.telegrambot.model.Message; +import com.pengrad.telegrambot.model.Update; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import javax.inject.Inject; + +/** + * ChatUtil provides utilities for interacting in an easier way when manipulating chat related data. + * In particular, it provides an easy and DRY way to modify a set of frequently-accessed fields, or + * simply to extract data performing all the necessary steps + * + * @author Davide Polonio + * @since 1.0 + */ +public class ChatUtil { + + private final Clock clock; + + @Inject + public ChatUtil(Clock clock) { + this.clock = clock; + } + + /** + * Update the {@link ChatContext} with the values passed to the method. The updated values are + * then saved in the database + * + * @param chat the chat that will be updated with the new {@link ChatContext} values + * @param stepName the step name to set + * @param stageNumber the stage number to set + * @param additionalFields if there are, additional custom fields that will be added to {@link + * ChatContext}. Note that these values will have to be manually retrieved since no method is + * available for custom entries. Use {@link Collections#emptyMap()} if you don't wish to add + * any additional field + */ + public void updateChatContext( + TgChat chat, String stepName, int stageNumber, Map additionalFields) { + final ChatContext chatContext = chat.getChatContext(); + chatContext.setStage(stepName); + chatContext.setStep(stageNumber); + chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); + additionalFields.forEach(chatContext::setAdditionalProperty); + + chat.setChatContext(chatContext); + chat.save(); + } + + /** + * Retrieves a possible {@link TgChat} from the given {@link CallbackQueryContext} or the {@link + * Update}. Note that first the {@link CallbackQueryContext} is checked, then {@link Update} is + * used as fallback. Once the Telegram chat id is retrieved from one of these two, the persistence + * layer is queried and if there is any {@link TgChat} which the corresponding ID it is returned + * to the caller. + * + * @param callbackQueryContext the {@link CallbackQueryContext} coming from a Telegram callback + * query + * @param update the whole {@link Update} object that is sent from Telegram servers + * @return an {@link Optional} that may contain a {@link TgChat} if the ID is found in the + * persistence layer, otherwise {@link Optional#empty()} is given back either if the id is not + * present or if the chat is not present in the persistence layer. + */ + public Optional extractChat(CallbackQueryContext callbackQueryContext, Update update) { + return Optional.of(callbackQueryContext.getFields().getTelegramChatId()) + .map(Double::longValue) + // If we're desperate, search in the message for the chat id + .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) + .or( + () -> + Optional.ofNullable(update.callbackQuery().message()) + .map(Message::messageId) + .map(Long::valueOf)) + .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) + .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); + } + + public T cleanCallbackQuery(T toReturn, CallbackQueryContext callbackQueryContext) { + new QCallbackQueryContext().entryGroup.eq(callbackQueryContext.getId()).delete(); + return toReturn; + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java index 605fd4e..b63a9bd 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFound.java @@ -1,5 +1,6 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.google.inject.assistedinject.Assisted; import com.pengrad.telegrambot.model.Update; @@ -12,11 +13,16 @@ import org.slf4j.Logger; public class NotFound implements Processor { private final Logger log; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; private final String eventName; @Inject - public NotFound(Logger logger, @Assisted String eventName) { + public NotFound( + Logger logger, + CallbackQueryContextCleaner callbackQueryContextCleaner, + @Assisted String eventName) { this.log = logger; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; this.eventName = eventName; } @@ -33,6 +39,7 @@ public class NotFound implements Processor { + callbackQueryContext.getId() + " event name " + eventName); + callbackQueryContextCleaner.removeGroupAsync(callbackQueryContext.getEntryGroup()); return CompletableFuture.completedFuture(Optional.empty()); } } diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java index c596a17..3b2abeb 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorial.java @@ -1,8 +1,9 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; -import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; import com.pengrad.telegrambot.model.Update; @@ -36,17 +37,23 @@ public class SelectLanguageTutorial implements Processor { private final TemplateContentGenerator templateContentGenerator; private final Logger log; private final UUIDGenerator uuidGenerator; + private final ChatUtil chatUtil; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; @Inject public SelectLanguageTutorial( @Named("eventThreadPool") Executor threadPool, TemplateContentGenerator templateContentGenerator, Logger log, - UUIDGenerator uuidGenerator) { + UUIDGenerator uuidGenerator, + ChatUtil chatUtil, + CallbackQueryContextCleaner callbackQueryContextCleaner) { this.threadPool = threadPool; this.templateContentGenerator = templateContentGenerator; this.log = log; this.uuidGenerator = uuidGenerator; + this.chatUtil = chatUtil; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; } @Override @@ -59,7 +66,8 @@ public class SelectLanguageTutorial implements Processor { CallbackQueryContext callbackQueryContext, Update update) { return CompletableFuture.supplyAsync( () -> - Util.extractChat(callbackQueryContext, update) + chatUtil + .extractChat(callbackQueryContext, update) .map( tgChat -> { tgChat.setLocale( @@ -90,6 +98,14 @@ public class SelectLanguageTutorial implements Processor { .thenApplyAsync( // If we are here then we're sure there is at least a chat associated with this callback tgChat -> { + if (tgChat.getHasHelpBeenShown()) { + log.trace( + "Help message has already been shown for this user - no help button will be" + + " present"); + } else { + log.trace("No help message shown yet - the help button will be added"); + } + final String message = templateContentGenerator.mergeTemplate( velocityContext -> @@ -99,14 +115,7 @@ public class SelectLanguageTutorial implements Processor { log.trace("SelectLanguageTutorial event - message to send back: " + message); - final String callBackGroupToDelete = callbackQueryContext.getEntryGroup(); - final int delete = - new QCallbackQueryContext().entryGroup.eq(callBackGroupToDelete).delete(); - log.trace( - "Deleted " - + delete - + " entries regarding callback group " - + callBackGroupToDelete); + callbackQueryContextCleaner.removeGroupAsync(callbackQueryContext.getEntryGroup()); final Optional messageId = Util.extractMessageId(update); BaseRequest baseRequest; 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 baeee31..302a931 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 @@ -1,6 +1,8 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.telegram.model.Help; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; @@ -9,13 +11,28 @@ import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; +/** + * ShowHelp callback query event edits a previous message (if available), otherwise it sends a new + * message with a list of available commands (the one implemented by {@link + * com.github.polpetta.mezzotre.telegram.command.Processor}), and a list of buttons that are + * callback queries (the ones implemented by {@link Processor}). The class is a Singleton since it + * is stateless and having multiple instances would only be a waste of memory. + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.command.Help same functionality but as {@link + * com.github.polpetta.mezzotre.telegram.command.Processor} command + */ +@Singleton class ShowHelp implements Processor { public static String EVENT_NAME = "showHelp"; @@ -24,8 +41,9 @@ class ShowHelp implements Processor { private final Map tgCommandProcessors; private final Map eventProcessor; + private final ChatUtil chatUtil; + private final CallbackQueryContextCleaner callbackQueryContextCleaner; - // FIXME tests @Inject public ShowHelp( @Named("eventThreadPool") Executor threadPool, @@ -33,12 +51,15 @@ class ShowHelp implements Processor { @Named("commandProcessor") Map tgCommandProcessors, @Named("eventProcessors") - Map - eventProcessor) { + Map eventProcessor, + ChatUtil chatUtil, + CallbackQueryContextCleaner callbackQueryContextCleaner) { this.threadPool = threadPool; this.modelHelp = modelHelp; this.tgCommandProcessors = tgCommandProcessors; this.eventProcessor = eventProcessor; + this.chatUtil = chatUtil; + this.callbackQueryContextCleaner = callbackQueryContextCleaner; } @Override @@ -51,26 +72,40 @@ class ShowHelp implements Processor { CallbackQueryContext callbackQueryContext, Update update) { return CompletableFuture.supplyAsync( () -> - Util.extractChat(callbackQueryContext, update) + chatUtil + .extractChat(callbackQueryContext, update) // FIXME callbackquerycontext removal? .map( chat -> { final String message = modelHelp.getMessage(chat, tgCommandProcessors); + chatUtil.updateChatContext(chat, EVENT_NAME, 0, Collections.emptyMap()); + chat.setHasHelpBeenShown(true); + chat.save(); final Optional messageId = Util.extractMessageId(update); final InlineKeyboardButton[] buttons = modelHelp.generateInlineKeyBoardButton(chat, eventProcessor); BaseRequest request; if (messageId.isPresent()) { final EditMessageText editMessageText = - new EditMessageText(chat.getId(), messageId.get(), message); - editMessageText.replyMarkup(new InlineKeyboardMarkup(buttons)); + new EditMessageText(chat.getId(), messageId.get(), message) + .parseMode(ParseMode.Markdown); + if (buttons.length > 0) { + editMessageText.replyMarkup(new InlineKeyboardMarkup(buttons)); + } request = editMessageText; } else { final SendMessage sendMessage = new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); - sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + if (buttons.length > 0) { + sendMessage.replyMarkup(new InlineKeyboardMarkup(buttons)); + } request = sendMessage; } + // We don't check if the element is removed or what - we just schedule its + // removal, then it is someone else problem + callbackQueryContextCleaner.removeGroupAsync( + callbackQueryContext.getEntryGroup()); + return request; }), threadPool); 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 67ef62f..7f6d6f9 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 @@ -1,28 +1,12 @@ package com.github.polpetta.mezzotre.telegram.callbackquery; -import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; -import com.github.polpetta.mezzotre.orm.model.TgChat; -import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.pengrad.telegrambot.model.Message; import com.pengrad.telegrambot.model.Update; import java.util.Optional; public class Util { - public static Optional extractChat( - CallbackQueryContext callbackQueryContext, Update update) { - return Optional.of(callbackQueryContext.getFields().getTelegramChatId()) - .map(Double::longValue) - // If we're desperate, search in the message for the chat id - .or( - () -> - Optional.ofNullable(update.callbackQuery().message()) - .map(Message::messageId) - .map(Long::valueOf)) - .filter(chatId -> chatId != 0L && chatId != Long.MIN_VALUE) - .flatMap(chatId -> new QTgChat().id.eq(chatId).findOneOrEmpty()); - } - + // FIXME tests, doc 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/Help.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java index 286b43a..9b05c14 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/Help.java @@ -1,12 +1,14 @@ package com.github.polpetta.mezzotre.telegram.command; import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -14,8 +16,19 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import javax.inject.Inject; import javax.inject.Named; +import javax.inject.Singleton; -public class Help implements Processor { +/** + * The Help command class allows the user to be informed in any moment of commands that are + * available in the system. If possible, it also provides a {@code reply_markup} keyboard ({@link + * InlineKeyboardMarkup} for easier bot interactions + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.callbackquery.ShowHelp for event-based help + */ +@Singleton +class Help implements Processor { private static final String TRIGGERING_STAGING_NAME = "/help"; @@ -24,6 +37,7 @@ public class Help implements Processor { private final Map eventProcessor; private final com.github.polpetta.mezzotre.telegram.model.Help modelHelp; + private final ChatUtil chatUtil; @Inject public Help( @@ -31,11 +45,13 @@ public class Help implements Processor { @Named("commandProcessor") Map tgCommandProcessors, @Named("eventProcessors") Map eventProcessor, - com.github.polpetta.mezzotre.telegram.model.Help modelHelp) { + com.github.polpetta.mezzotre.telegram.model.Help modelHelp, + ChatUtil chatUtil) { this.threadPool = threadPool; this.tgCommandProcessors = tgCommandProcessors; this.eventProcessor = eventProcessor; this.modelHelp = modelHelp; + this.chatUtil = chatUtil; } @Override @@ -48,6 +64,9 @@ public class Help implements Processor { return CompletableFuture.supplyAsync( () -> { final String message = modelHelp.getMessage(chat, tgCommandProcessors); + chatUtil.updateChatContext(chat, TRIGGERING_STAGING_NAME, 0, Collections.emptyMap()); + chat.setHasHelpBeenShown(true); + chat.save(); final SendMessage sendMessage = new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown); diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java index 71714d8..deef282 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFound.java @@ -11,7 +11,14 @@ import java.util.Set; import java.util.concurrent.CompletableFuture; import javax.inject.Inject; -public class NotFound implements Processor { +/** + * Generates a "Command not found" message + * + * @author Davide Polonio + * @since 1.0 + * @see com.github.polpetta.mezzotre.telegram.callbackquery.NotFound for the event-based version + */ +class NotFound implements Processor { private final String commandName; diff --git a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java index c4e2278..381c449 100644 --- a/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java +++ b/src/main/java/com/github/polpetta/mezzotre/telegram/command/NotFoundFactory.java @@ -1,5 +1,5 @@ package com.github.polpetta.mezzotre.telegram.command; -public interface NotFoundFactory { +interface NotFoundFactory { NotFound create(String commandName); } 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 e3a84f9..bb2b859 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 @@ -3,12 +3,11 @@ package com.github.polpetta.mezzotre.telegram.command; 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.telegram.ChatUtil; import com.github.polpetta.mezzotre.telegram.callbackquery.Field; import com.github.polpetta.mezzotre.telegram.callbackquery.Value; -import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.github.polpetta.types.json.ChatContext; import com.google.inject.Singleton; import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; @@ -16,6 +15,7 @@ import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; import com.pengrad.telegrambot.model.request.ParseMode; import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.SendMessage; +import java.util.Collections; import java.util.Locale; import java.util.Optional; import java.util.Set; @@ -32,15 +32,15 @@ import org.slf4j.Logger; * @since 1.0 */ @Singleton -public class Start implements Processor { +class Start implements Processor { private static final String TRIGGERING_STAGING_NAME = "/start"; private final Executor threadPool; private final Logger log; private final UUIDGenerator uuidGenerator; - private final Clock clock; private final String applicationName; + private final ChatUtil chatUtil; private final TemplateContentGenerator templateContentGenerator; @@ -50,14 +50,14 @@ public class Start implements Processor { @Named("eventThreadPool") Executor threadPool, Logger log, UUIDGenerator uuidGenerator, - Clock clock, - @Named("applicationName") String applicationName) { + @Named("applicationName") String applicationName, + ChatUtil chatUtil) { this.templateContentGenerator = templateContentGenerator; this.threadPool = threadPool; this.log = log; this.uuidGenerator = uuidGenerator; - this.clock = clock; this.applicationName = applicationName; + this.chatUtil = chatUtil; } @Override @@ -87,12 +87,8 @@ public class Start implements Processor { "template/telegram/start.vm"); log.trace("Start command - message to send back: " + message); - final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(TRIGGERING_STAGING_NAME); - chatContext.setStep(0); - chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); - chat.setChatContext(chatContext); - chat.save(); + // 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! final String groupId = uuidGenerator.generateAsString(); 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 401c8e0..83ee044 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 @@ -5,10 +5,8 @@ import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.telegram.callbackquery.Field; import com.github.polpetta.mezzotre.telegram.command.Processor; -import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; -import com.github.polpetta.types.json.ChatContext; import com.pengrad.telegrambot.model.request.InlineKeyboardButton; import java.util.Map; import java.util.stream.Collectors; @@ -21,47 +19,30 @@ public class Help { private static final String TRIGGERING_STAGING_NAME = "/help"; private final TemplateContentGenerator templateContentGenerator; - private final Clock clock; private final UUIDGenerator uuidGenerator; @Inject - public Help( - TemplateContentGenerator templateContentGenerator, Clock clock, UUIDGenerator uuidGenerator) { - + public Help(TemplateContentGenerator templateContentGenerator, UUIDGenerator uuidGenerator) { this.templateContentGenerator = templateContentGenerator; - this.clock = clock; this.uuidGenerator = uuidGenerator; } public String getMessage(TgChat chat, Map tgCommandProcessors) { - final String message = - templateContentGenerator.mergeTemplate( - velocityContext -> { - velocityContext.put( - "commands", - tgCommandProcessors.values().stream() - .distinct() - .map( - p -> - Pair.of( - p.getTriggerKeywords().stream() - .sorted() - .collect(Collectors.toList()), - p.getLocaleDescriptionKeyword())) - .collect(Collectors.toList())); - }, - chat.getLocale(), - "template/telegram/help.vm"); - - // FIXME this shouldn't stay here. We need to move it into another method - final ChatContext chatContext = chat.getChatContext(); - chatContext.setStage(TRIGGERING_STAGING_NAME); - chatContext.setStep(0); - chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now()); - chat.setChatContext(chatContext); - chat.setHasHelpBeenShown(true); - chat.save(); - return message; + return templateContentGenerator.mergeTemplate( + velocityContext -> { + velocityContext.put( + "commands", + tgCommandProcessors.values().stream() + .distinct() + .map( + p -> + Pair.of( + p.getTriggerKeywords().stream().sorted().collect(Collectors.toList()), + p.getLocaleDescriptionKeyword())) + .collect(Collectors.toList())); + }, + chat.getLocale(), + "template/telegram/help.vm"); } public InlineKeyboardButton[] generateInlineKeyBoardButton( diff --git a/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java new file mode 100644 index 0000000..e725c4d --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/util/ServiceModule.java @@ -0,0 +1,71 @@ +package com.github.polpetta.mezzotre.util; + +import com.google.common.util.concurrent.MoreExecutors; +import com.google.common.util.concurrent.Service; +import com.google.common.util.concurrent.ServiceManager; +import io.jooby.Extension; +import io.jooby.Jooby; +import java.time.Duration; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeoutException; +import javax.inject.Inject; +import javax.inject.Named; +import org.jetbrains.annotations.NotNull; + +public class ServiceModule implements Extension { + + private final ServiceManager serviceManager; + + @Inject + public ServiceModule(@Named("services") List services) { + serviceManager = new ServiceManager(services); + } + + @Override + public boolean lateinit() { + return true; + } + + @Override + public void install(@NotNull Jooby application) throws Exception { + final CompletableFuture initialization = new CompletableFuture<>(); + serviceManager.addListener( + new ServiceManager.Listener() { + @Override + public void healthy() { + super.healthy(); + application.getLog().info("All internal application services are up and running"); + initialization.complete(null); + } + + @Override + public void failure(@NotNull Service service) { + super.failure(service); + initialization.completeExceptionally(service.failureCause()); + } + }, + MoreExecutors.directExecutor()); + + Runtime.getRuntime() + .addShutdownHook( + new Thread( + () -> { + try { + serviceManager.stopAsync().awaitStopped(Duration.ofSeconds(15)); + } catch (TimeoutException ex) { + application + .getLog() + .warn( + "Unable to correctly stop all the services, got the following error" + + " while stopping: " + + ex.getMessage()); + throw new RuntimeException(ex); + } + })); + + // Blocking call to wait for all the services to be up and running + serviceManager.startAsync(); + initialization.get(); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java b/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java new file mode 100644 index 0000000..cfdb144 --- /dev/null +++ b/src/main/java/com/github/polpetta/mezzotre/util/UtilDI.java @@ -0,0 +1,43 @@ +package com.github.polpetta.mezzotre.util; + +import com.google.common.util.concurrent.Service; +import com.google.inject.AbstractModule; +import com.google.inject.Provides; +import java.util.List; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; +import java.util.concurrent.ForkJoinPool; +import java.util.concurrent.TimeUnit; +import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; + +public class UtilDI extends AbstractModule { + @Provides + @Singleton + @Named("eventThreadPool") + public Executor getEventExecutor() { + return ForkJoinPool.commonPool(); + } + + @Provides + @Singleton + @Named("longJobThreadPool") + public Executor getLongJobExecutor() { + return Executors.newCachedThreadPool(); + } + + @Provides + @Singleton + @Named("serviceModule") + public ServiceModule getServiceModule(@Named("services") List moduleList) { + return new ServiceModule(moduleList); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(5, TimeUnit.SECONDS); + } +} diff --git a/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java b/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java deleted file mode 100644 index e4ddf60..0000000 --- a/src/main/java/com/github/polpetta/mezzotre/util/di/ThreadPool.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.github.polpetta.mezzotre.util.di; - -import com.google.inject.AbstractModule; -import com.google.inject.Provides; -import java.util.concurrent.Executor; -import java.util.concurrent.Executors; -import java.util.concurrent.ForkJoinPool; -import javax.inject.Named; -import javax.inject.Singleton; - -public class ThreadPool extends AbstractModule { - @Provides - @Singleton - @Named("eventThreadPool") - public Executor getEventExecutor() { - return ForkJoinPool.commonPool(); - } - - @Provides - @Singleton - @Named("longJobThreadPool") - public Executor getLongJobExecutor() { - return Executors.newCachedThreadPool(); - } -} diff --git a/src/main/resources/template/telegram/help.vm b/src/main/resources/template/telegram/help.vm index 7033d0c..0ff5078 100644 --- a/src/main/resources/template/telegram/help.vm +++ b/src/main/resources/template/telegram/help.vm @@ -1,8 +1,7 @@ ${i18n.help.description}: #foreach(${command} in ${commands}) -## FIXME this is not displayed correctly in telegram! -*#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} +-#foreach(${key} in ${command.left}) ${key}#end: ${i18n.get(${command.right})} #end ${i18n.help.buttonsToo} \ No newline at end of file diff --git a/src/main/resources/template/telegram/start.vm b/src/main/resources/template/telegram/start.vm index 7889c0b..3d55cb5 100644 --- a/src/main/resources/template/telegram/start.vm +++ b/src/main/resources/template/telegram/start.vm @@ -1,3 +1,3 @@ -**${i18n.start.helloFirstName.insert(${firstName})}** +*${i18n.start.helloFirstName.insert(${firstName})}* ${i18n.start.description.insert(${programName})} \ No newline at end of file diff --git a/src/test/java/com/github/polpetta/mezzotre/UnitTest.java b/src/test/java/com/github/polpetta/mezzotre/UnitTest.java index 9c8a6bd..69cf976 100644 --- a/src/test/java/com/github/polpetta/mezzotre/UnitTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/UnitTest.java @@ -3,15 +3,21 @@ package com.github.polpetta.mezzotre; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; -import com.google.inject.*; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.google.inject.AbstractModule; import com.google.inject.Module; +import com.google.inject.Provides; +import com.google.inject.Stage; import io.jooby.*; import io.jooby.ebean.EbeanModule; import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.TimeUnit; import javax.inject.Named; +import javax.inject.Singleton; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.Test; public class UnitTest { @@ -37,6 +43,20 @@ public class UnitTest { public Extension getEbeanExtension() { return mock(EbeanModule.class); } + + @Provides + @Singleton + @Named("serviceModule") + public ServiceModule getServiceModule() { + return mock(ServiceModule.class); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(0, TimeUnit.MILLISECONDS); + } } @Test diff --git a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java index 147fa4d..5d6d0c0 100644 --- a/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java +++ b/src/test/java/com/github/polpetta/mezzotre/helper/IntegrationAppFactory.java @@ -2,18 +2,22 @@ package com.github.polpetta.mezzotre.helper; import com.github.polpetta.mezzotre.App; import com.github.polpetta.mezzotre.route.RouteDI; +import com.github.polpetta.mezzotre.util.ServiceModule; +import com.google.common.util.concurrent.Service; import com.google.inject.AbstractModule; import com.google.inject.Provides; -import com.google.inject.Singleton; import com.google.inject.Stage; import com.zaxxer.hikari.HikariConfig; import io.jooby.Extension; import io.jooby.ebean.EbeanModule; import io.jooby.flyway.FlywayModule; import io.jooby.hikari.HikariModule; +import java.util.List; import java.util.Properties; import java.util.Set; +import java.util.concurrent.TimeUnit; import javax.inject.Named; +import javax.inject.Singleton; import org.apache.commons.lang3.tuple.Pair; import org.testcontainers.containers.PostgreSQLContainer; @@ -50,6 +54,20 @@ public class IntegrationAppFactory { public Extension getEbeanExtension() { return new EbeanModule(); } + + @Singleton + @Provides + @Named("serviceModule") + public ServiceModule getServiceModule(@Named("services") List moduleList) { + return new ServiceModule(moduleList); + } + + @Provides + @Singleton + @Named("serviceRunningCheckTime") + public Pair getTime() { + return Pair.of(0, TimeUnit.MILLISECONDS); + } } public static App loadCustomDbApplication() { diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java new file mode 100644 index 0000000..40a5e78 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/BatchBeanCleanerServiceIntegrationTest.java @@ -0,0 +1,93 @@ +package com.github.polpetta.mezzotre.orm; + +import static org.junit.jupiter.api.Assertions.*; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import io.ebean.Database; +import java.time.Duration; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import org.apache.commons.lang3.tuple.Pair; +import org.junit.jupiter.api.*; +import org.slf4j.LoggerFactory; +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 BatchBeanCleanerServiceIntegrationTest { + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private BatchBeanCleanerService batchBeanCleanerService; + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(BatchBeanCleanerService.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + } + + @AfterEach + void tearDown() throws Exception { + if (batchBeanCleanerService != null) { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); + } + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldRemoveEntryFromDatabase() throws Exception { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("1234", "4567", new CallbackQueryMetadata()); + callbackQueryContext.save(); + + batchBeanCleanerService.startAsync(); + batchBeanCleanerService.awaitRunning(); + + final CompletableFuture integerCompletableFuture = + batchBeanCleanerService.removeAsync("1234", new QCallbackQueryContext().id); + + final Integer gotDeletion = integerCompletableFuture.get(); + + assertEquals(1, gotDeletion); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldRemoveMultipleEntriesFromDatabase() throws Exception { + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("1234", "4567", new CallbackQueryMetadata()); + callbackQueryContext.save(); + final CallbackQueryContext callbackQueryContext2 = + new CallbackQueryContext("4321", "4567", new CallbackQueryMetadata()); + callbackQueryContext2.save(); + + batchBeanCleanerService.startAsync(); + batchBeanCleanerService.awaitRunning(); + + final CompletableFuture integerCompletableFuture = + batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup); + final CompletableFuture integerCompletableFuture2 = + batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup); + + final Integer gotDeletion1 = integerCompletableFuture.get(); + final Integer gotDeletion2 = integerCompletableFuture2.get(); + + assertEquals(2, gotDeletion1); + assertEquals(0, gotDeletion2); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java new file mode 100644 index 0000000..0c90d6b --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilIntegrationTest.java @@ -0,0 +1,176 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; + +import com.github.polpetta.mezzotre.helper.Loader; +import com.github.polpetta.mezzotre.helper.TestConfig; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import io.ebean.Database; +import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; +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") +@Testcontainers +class ChatUtilIntegrationTest { + + private static Gson gson; + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private Clock fakeClock; + private ChatUtil chatUtil; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + + fakeClock = mock(Clock.class); + chatUtil = new ChatUtil(fakeClock); + } + + @Test + void shouldExtractChatFromCallBackQueryContext() { + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "123", + "456", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(69420L) + .build()); + + final TgChat expectedChat = new TgChat(69420L, new ChatContext()); + expectedChat.save(); + + final Optional tgChatOptional = + chatUtil.extractChat( + callbackQueryContext, null); // null just for test purposes, not usually expected + + assertEquals(expectedChat, tgChatOptional.get()); + } + + @Test + void shouldExtractChatFromUpdateIfCallbackQueryContextIsEmpty() { + 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\": 69420,\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\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\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); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext("123", "456", new CallbackQueryMetadata()); + + final TgChat expectedChat = new TgChat(69420L, new ChatContext()); + expectedChat.save(); + + final Optional tgChatOptional = + chatUtil.extractChat( + callbackQueryContext, update); // null just for test purposes, not usually expected + + assertEquals(expectedChat, tgChatOptional.get()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java new file mode 100644 index 0000000..7de3fc5 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/orm/telegram/ChatUtilTest.java @@ -0,0 +1,70 @@ +package com.github.polpetta.mezzotre.orm.telegram; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.types.json.ChatContext; +import java.util.Collections; +import java.util.Map; +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; +import org.mockito.ArgumentCaptor; + +@Execution(ExecutionMode.CONCURRENT) +class ChatUtilTest { + private Clock fakeClock; + private ChatUtil chatUtil; + + @BeforeEach + void setUp() { + fakeClock = mock(Clock.class); + chatUtil = new ChatUtil(fakeClock); + } + + @Test + void shouldUpdateChatContext() { + final TgChat fakeTgChat = mock(TgChat.class); + final ChatContext chatContext = new ChatContext(); + when(fakeTgChat.getChatContext()).thenReturn(chatContext); + when(fakeClock.now()).thenReturn(69420L); + chatUtil.updateChatContext(fakeTgChat, "/test1", 42, Collections.emptyMap()); + + verify(fakeTgChat, times(1)).getChatContext(); + final ArgumentCaptor chatContextArgumentCaptor = + ArgumentCaptor.forClass(ChatContext.class); + verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture()); + verify(fakeTgChat, times(0)).setHasHelpBeenShown(eq(true)); + final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue(); + assertEquals("/test1", capturedChatContextValue.getStage()); + assertEquals(42, capturedChatContextValue.getStep()); + assertEquals(69420L, capturedChatContextValue.getPreviousMessageUnixTimestampInSeconds()); + } + + @Test + void shouldAddMapElementsToChatContextToo() { + final TgChat fakeTgChat = mock(TgChat.class); + final ChatContext chatContext = new ChatContext(); + when(fakeTgChat.getChatContext()).thenReturn(chatContext); + when(fakeClock.now()).thenReturn(69420L); + + final Object obj1 = new Object(); + final Object obj2 = new Object(); + + chatUtil.updateChatContext(fakeTgChat, "/test1", 42, Map.of("field1", obj1, "field2", obj2)); + + verify(fakeTgChat, times(1)).getChatContext(); + final ArgumentCaptor chatContextArgumentCaptor = + ArgumentCaptor.forClass(ChatContext.class); + verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture()); + final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue(); + final Map gotAdditionalProperties = + capturedChatContextValue.getAdditionalProperties(); + assertEquals(2, gotAdditionalProperties.size()); + assertEquals(obj1, gotAdditionalProperties.get("field1")); + assertEquals(obj2, gotAdditionalProperties.get("field2")); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java new file mode 100644 index 0000000..eca4652 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/NotFoundTest.java @@ -0,0 +1,46 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +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; +import org.slf4j.LoggerFactory; + +@Execution(ExecutionMode.CONCURRENT) +class NotFoundTest { + + private CallbackQueryContextCleaner fakeCallbackQueryCleaner; + private NotFound notFound; + + @BeforeEach + void setUp() { + + fakeCallbackQueryCleaner = mock(CallbackQueryContextCleaner.class); + + notFound = + new NotFound(LoggerFactory.getLogger(NotFound.class), fakeCallbackQueryCleaner, "anEvent"); + } + + @Test + void shouldCallCallbackQueryContextCleaner() throws Exception { + final CallbackQueryContext fakeCallbackQuery = mock(CallbackQueryContext.class); + when(fakeCallbackQuery.getId()).thenReturn("anId"); + when(fakeCallbackQuery.getEntryGroup()).thenReturn("123"); + final Update fakeUpdate = mock(Update.class); + + final CompletableFuture>> gotResult = + notFound.process(fakeCallbackQuery, fakeUpdate); + + verify(fakeCallbackQueryCleaner, times(1)).removeGroupAsync(eq("123")); + assertEquals(Optional.empty(), gotResult.get()); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java index c28e6cc..3a78cb3 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/SelectLanguageTutorialIntegrationTest.java @@ -7,10 +7,14 @@ 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.BatchBeanCleanerService; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; 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.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.CallbackQueryMetadata; import com.github.polpetta.types.json.ChatContext; @@ -22,12 +26,17 @@ import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.EditMessageText; import com.pengrad.telegrambot.request.SendMessage; import io.ebean.Database; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple3; +import java.time.Duration; import java.util.List; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; import org.junit.jupiter.api.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -52,6 +61,8 @@ class SelectLanguageTutorialIntegrationTest { private Database database; private SelectLanguageTutorial selectLanguageTutorial; private UUIDGenerator fakeUUIDGenerator; + private ChatUtil chatUtil; + private BatchBeanCleanerService batchBeanCleanerService; @BeforeAll static void beforeAll() { @@ -64,6 +75,12 @@ class SelectLanguageTutorialIntegrationTest { Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); fakeUUIDGenerator = mock(UUIDGenerator.class); + chatUtil = new ChatUtil(new Clock()); + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(BatchBeanCleanerService.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10)); selectLanguageTutorial = new SelectLanguageTutorial( @@ -71,7 +88,16 @@ class SelectLanguageTutorialIntegrationTest { new TemplateContentGenerator( new LocalizedMessageFactory(Loader.defaultVelocityEngine())), LoggerFactory.getLogger(SelectLanguageTutorial.class), - fakeUUIDGenerator); + fakeUUIDGenerator, + chatUtil, + new CallbackQueryContextCleaner( + batchBeanCleanerService, + LoggerFactory.getLogger(CallbackQueryContextCleaner.class))); + } + + @AfterEach + void tearDown() throws Exception { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); } private static Stream getTestLocales() { @@ -193,6 +219,11 @@ class SelectLanguageTutorialIntegrationTest { "c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata); changeLanguageCallbackQueryContext.save(); + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); + final CompletableFuture>> processFuture = this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); final Optional> gotResponseOpt = processFuture.get(); @@ -213,6 +244,7 @@ class SelectLanguageTutorialIntegrationTest { assertEquals( 1, new QCallbackQueryContext().id.eq("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5").findCount()); } else { + callBackFuture.get(); // Await that callback are cleaned out first assertEquals(0, keyboardButtons.size()); assertEquals(0, new QCallbackQueryContext().findCount()); } @@ -221,6 +253,11 @@ class SelectLanguageTutorialIntegrationTest { assertNotNull(retrievedTgChat); assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals(entryGroupId, deletionCallback._1()); + assertEquals(1, deletionCallback._3().get()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } @@ -308,6 +345,10 @@ class SelectLanguageTutorialIntegrationTest { new CallbackQueryContext( "c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata); changeLanguageCallbackQueryContext.save(); + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); final CompletableFuture>> processFuture = this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update); @@ -329,6 +370,7 @@ class SelectLanguageTutorialIntegrationTest { assertEquals( 1, new QCallbackQueryContext().id.eq("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5").findCount()); } else { + callBackFuture.get(); // Await that callback are cleaned out first assertEquals(0, keyboardButtons.size()); assertEquals(0, new QCallbackQueryContext().findCount()); } @@ -337,6 +379,11 @@ class SelectLanguageTutorialIntegrationTest { assertNotNull(retrievedTgChat); assertEquals(selectLanguageTutorial.getLocale(), retrievedTgChat.getLocale()); + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals(entryGroupId, deletionCallback._1()); + assertEquals(1, deletionCallback._3().get()); assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount()); } } diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java new file mode 100644 index 0000000..eba6430 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpIntegrationTest.java @@ -0,0 +1,457 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +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.BatchBeanCleanerService; +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +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.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.telegram.model.Help; +import com.github.polpetta.mezzotre.util.Clock; +import com.github.polpetta.mezzotre.util.UUIDGenerator; +import com.github.polpetta.types.json.CallbackQueryMetadata; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.request.EditMessageText; +import com.pengrad.telegrambot.request.SendMessage; +import io.ebean.Database; +import io.ebean.typequery.PString; +import io.ebean.typequery.TQRootBean; +import io.vavr.Tuple3; +import java.time.Duration; +import java.util.*; +import java.util.concurrent.*; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.velocity.app.VelocityEngine; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +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 ShowHelpIntegrationTest { + + private static Gson gson; + private static Logger testLog; + + @Container + private final PostgreSQLContainer postgresServer = + new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE); + + private Database database; + private VelocityEngine velocityEngine; + private UUIDGenerator fakeUUIDGenerator; + private Clock fakeClock; + private BatchBeanCleanerService batchBeanCleanerService; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + testLog = LoggerFactory.getLogger(ShowHelpIntegrationTest.class); + } + + @BeforeEach + void setUp() throws Exception { + database = + Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer)); + velocityEngine = Loader.defaultVelocityEngine(); + + final Logger log = LoggerFactory.getLogger(ShowHelp.class); + fakeUUIDGenerator = mock(UUIDGenerator.class); + fakeClock = mock(Clock.class); + } + + @AfterEach + void tearDown() throws Exception { + if (batchBeanCleanerService != null) { + batchBeanCleanerService.stopAsync().awaitTerminated(Duration.ofSeconds(10)); + } + } + + private ShowHelp generateShowHelpWithCustomCommands( + Map eventProcessors, + Map commandProcessors) + throws TimeoutException { + + batchBeanCleanerService = + new BatchBeanCleanerService( + LoggerFactory.getLogger(CallbackQueryContextCleaner.class), + Pair.of(0, TimeUnit.MILLISECONDS)); + batchBeanCleanerService.startAsync().awaitRunning(Duration.ofSeconds(10)); + + return new ShowHelp( + Executors.newSingleThreadExecutor(), + new Help( + new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), + fakeUUIDGenerator), + commandProcessors, + eventProcessors, + // We need to implement a service that is sync - no thread creation! + new ChatUtil(fakeClock), + new CallbackQueryContextCleaner( + batchBeanCleanerService, LoggerFactory.getLogger(CallbackQueryContextCleaner.class))); + } + + public static Stream getMessageIdOrNot() { + return Stream.of( + Arguments.of( + 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), + EditMessageText.class), + Arguments.of( + 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), + SendMessage.class)); + } + + @ParameterizedTest + @Timeout(value = 1, unit = TimeUnit.MINUTES) + @MethodSource("getMessageIdOrNot") + void shouldShowHelpMessageWithButtons(Update update, Class typeOfMessage) throws Exception { + // we have 2 events + group uuid + when(fakeUUIDGenerator.generateAsString()) + .thenReturn("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5") + .thenReturn("16507fbd-9f28-48a8-9de1-3ea1c943af67") + .thenReturn("0b0ac18e-f621-484e-aa8d-9b176be5b930"); + when(fakeClock.now()).thenReturn(42L); + + final HashMap commands = + new HashMap<>(); + final com.github.polpetta.mezzotre.telegram.command.Processor dummy1 = + new com.github.polpetta.mezzotre.telegram.command.Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/a", "/b"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "start.cmdDescription"; + } + }; + final com.github.polpetta.mezzotre.telegram.command.Processor dummy2 = + new com.github.polpetta.mezzotre.telegram.command.Processor() { + @Override + public Set getTriggerKeywords() { + return Set.of("/different"); + } + + @Override + public CompletableFuture>> process( + TgChat chat, Update update) { + return null; + } + + @Override + public String getLocaleDescriptionKeyword() { + return "help.cmdDescription"; + } + }; + commands.put("/a", dummy1); + commands.put("/b", dummy1); + commands.put("/different", dummy2); + + final Map events = new HashMap<>(); + + final Processor dummyEvent1 = + new Processor() { + @Override + public String getEventName() { + return "exampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("changeLanguage.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + final Processor dummyEvent2 = + new com.github.polpetta.mezzotre.telegram.callbackquery.Processor() { + @Override + public String getEventName() { + return "secondExampleEvent"; + } + + @Override + public boolean canBeDirectlyInvokedByTheUser() { + return true; + } + + @Override + public Optional getPrettyPrintLocaleKeyName() { + return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName"); + } + + @Override + public CompletableFuture>> process( + CallbackQueryContext callbackQueryContext, Update update) { + return null; + } + }; + + events.put(dummyEvent1.getEventName(), dummyEvent1); + events.put(dummyEvent2.getEventName(), dummyEvent2); + + final ShowHelp showHelp = generateShowHelpWithCustomCommands(events, commands); + final TgChat tgChat = new TgChat(1111111L, new ChatContext()); + tgChat.save(); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "1234", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext.save(); + + final CompletableFuture>> gotResultFuture = + showHelp.process(callbackQueryContext, update); + + final Optional> baseRequestOptional = gotResultFuture.get(); + final BaseRequest gotResponse = baseRequestOptional.get(); + assertInstanceOf(typeOfMessage, gotResponse); + final String message = (String) gotResponse.getParameters().get("text"); + assertEquals( + "Here is a list of what I can do:\n" + + "\n" + + "- /a /b: Trigger this very bot\n" + + "- /different: 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", + message); + final InlineKeyboardButton[][] keyboard = + ((InlineKeyboardMarkup) + gotResponse + .getParameters() + .getOrDefault("reply_markup", new InlineKeyboardMarkup())) + .inlineKeyboard(); + + final List keyboardButtons = + Stream.of(keyboard).flatMap(Stream::of).toList(); + + assertEquals(2, keyboardButtons.size()); + + assertFalse(keyboardButtons.get(0).callbackData().isBlank()); + assertFalse(keyboardButtons.get(1).callbackData().isBlank()); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Select language"::equals)); + + assertTrue( + keyboardButtons.stream() + .map(InlineKeyboardButton::text) + .anyMatch("Change language"::equals)); + + final TgChat gotChat = new QTgChat().id.eq(1111111L).findOne(); + assertNotNull(gotChat); + assertTrue(gotChat.getHasHelpBeenShown()); + final ChatContext gotChatChatContext = gotChat.getChatContext(); + assertEquals(0, gotChatChatContext.getStep()); + assertEquals("showHelp", gotChatChatContext.getStage()); + assertEquals(42, gotChatChatContext.getPreviousMessageUnixTimestampInSeconds()); + } + + @Test + @Timeout(value = 1, unit = TimeUnit.MINUTES) + void shouldCleanUpCallbackQueryContextAfterRun() throws Exception { + when(fakeClock.now()).thenReturn(42L); + + final ShowHelp showHelp = + generateShowHelpWithCustomCommands(Collections.emptyMap(), Collections.emptyMap()); + final TgChat tgChat = new TgChat(1111111L, new ChatContext()); + tgChat.save(); + 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\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}", + Update.class); + final CallbackQueryContext callbackQueryContext = + new CallbackQueryContext( + "1234", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext.save(); + // Create another CallbackQuery context of the same group + final CallbackQueryContext callbackQueryContext2 = + new CallbackQueryContext( + "7891", + "5678", + new CallbackQueryMetadata.CallbackQueryMetadataBuilder() + .withTelegramChatId(1111111L) + .build()); + callbackQueryContext2.save(); + + // I know I know, a future containing a future...but it is for testing purposes, otherwise we'd + // have to do some while/sleep thread hack which I find horrible to do + final CompletableFuture< + Tuple3>, CompletableFuture>> + callBackFuture = new CompletableFuture<>(); + batchBeanCleanerService.addListener(callBackFuture::complete); + + final CompletableFuture>> gotResultFuture = + showHelp.process(callbackQueryContext, update); + + final Optional> baseRequestOptional = gotResultFuture.get(); + assertDoesNotThrow(baseRequestOptional::get); + + final Tuple3>, CompletableFuture> + deletionCallback = callBackFuture.get(); + + assertEquals("5678", deletionCallback._1()); + assertEquals(2, deletionCallback._3().get()); + assertEquals( + 0, + new QCallbackQueryContext().findCount(), + "The CallbackQuery context group has not been cleaned properly!"); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java new file mode 100644 index 0000000..a722059 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/callbackquery/ShowHelpTest.java @@ -0,0 +1,314 @@ +package com.github.polpetta.mezzotre.telegram.callbackquery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.CallbackQueryContextCleaner; +import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.mezzotre.telegram.model.Help; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Collections; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeAll; +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 ShowHelpTest { + + private static Gson gson; + private ShowHelp showHelp; + private Help fakeModelHelp; + private ChatUtil fakeChatUtil; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() { + + fakeModelHelp = mock(Help.class); + fakeChatUtil = mock(ChatUtil.class); + final CallbackQueryContextCleaner fakeCallbackQueryContextCleaner = + mock(CallbackQueryContextCleaner.class); + + showHelp = + new ShowHelp( + Executors.newSingleThreadExecutor(), + fakeModelHelp, + Collections.emptyMap(), + Collections.emptyMap(), + fakeChatUtil, + fakeCallbackQueryContextCleaner); + } + + @Test + void shouldUpdateChatContext() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + 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\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\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); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + + verify(fakeChatUtil, times(1)) + .updateChatContext(eq(fakeTgChat), eq("showHelp"), eq(0), eq(Collections.emptyMap())); + } + + @Test + void shouldAddInlineKeyboardIfButtonsAreReturned() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + 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\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\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); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {new InlineKeyboardButton("example1")}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertEquals(1, replyMarkup.inlineKeyboard()[0].length); + assertEquals("example1", replyMarkup.inlineKeyboard()[0][0].text()); + } + + @Test + void shouldNotAddAnyKeyboardIfThereAreNoButtons() throws Exception { + final CallbackQueryContext fakeCallbackQueryContext = mock(CallbackQueryContext.class); + 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\": \"Hello xxxxxx! \uD83D\uDC4B\\n" + + "\\n" + + "This is Mezzotre, a simple bot focused on DnD content management! Please start" + + " by choosing a language down below \uD83D\uDC47\",\n" + + " \"entities\": [\n" + + " {\n" + + " \"type\": \"bold\",\n" + + " \"offset\": 0,\n" + + " \"length\": 16\n" + + " },\n" + + " {\n" + + " \"type\": \"italic\",\n" + + " \"offset\": 26,\n" + + " \"length\": 8\n" + + " }\n" + + " ],\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); + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), any())).thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + when(fakeChatUtil.extractChat(eq(fakeCallbackQueryContext), eq(update))) + .thenReturn(Optional.of(fakeTgChat)); + + final CompletableFuture>> gotResponseFuture = + showHelp.process(fakeCallbackQueryContext, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertNull(replyMarkup); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java index 3f472aa..24453e2 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpIntegrationTest.java @@ -11,6 +11,7 @@ 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.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.ChatContext; @@ -172,10 +173,15 @@ class HelpIntegrationTest { final com.github.polpetta.mezzotre.telegram.model.Help modelHelp = new com.github.polpetta.mezzotre.telegram.model.Help( new TemplateContentGenerator(new LocalizedMessageFactory(velocityEngine)), - fakeClock, new UUIDGenerator()); - help = new Help(Executors.newSingleThreadExecutor(), commands, events, modelHelp); + help = + new Help( + Executors.newSingleThreadExecutor(), + commands, + events, + modelHelp, + new ChatUtil(fakeClock)); final Update update = gson.fromJson( @@ -210,8 +216,8 @@ class HelpIntegrationTest { assertEquals( "Here is a list of what I can do:\n" + "\n" - + "* /a /b: Trigger this very bot\n" - + "* /different: Print the help message\n" + + "- /a /b: Trigger this very bot\n" + + "- /different: 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", diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java new file mode 100644 index 0000000..7fd6de6 --- /dev/null +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/HelpTest.java @@ -0,0 +1,186 @@ +package com.github.polpetta.mezzotre.telegram.command; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.*; + +import com.github.polpetta.mezzotre.orm.model.TgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; +import com.github.polpetta.types.json.ChatContext; +import com.google.gson.Gson; +import com.pengrad.telegrambot.model.Update; +import com.pengrad.telegrambot.model.request.InlineKeyboardButton; +import com.pengrad.telegrambot.model.request.InlineKeyboardMarkup; +import com.pengrad.telegrambot.request.BaseRequest; +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executors; +import org.junit.jupiter.api.BeforeAll; +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 static Gson gson; + private com.github.polpetta.mezzotre.telegram.model.Help fakeModelHelp; + private ChatUtil fakeChatUtil; + private Help help; + + @BeforeAll + static void beforeAll() { + gson = new Gson(); + } + + @BeforeEach + void setUp() { + + final Map commands = Collections.emptyMap(); + final Map events = + Collections.emptyMap(); + + fakeModelHelp = mock(com.github.polpetta.mezzotre.telegram.model.Help.class); + fakeChatUtil = mock(ChatUtil.class); + + help = + new Help( + Executors.newSingleThreadExecutor(), commands, events, fakeModelHelp, fakeChatUtil); + } + + @Test + void shouldUpdateChatContext() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + assertDoesNotThrow(() -> gotResponseFuture.get()); + verify(fakeChatUtil, times(1)) + .updateChatContext(eq(fakeTgChat), eq("/help"), eq(0), eq(Collections.emptyMap())); + } + + @Test + void shouldAppendKeyboardIfButtonsAreReturned() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {new InlineKeyboardButton("example1")}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertEquals(1, replyMarkup.inlineKeyboard()[0].length); + assertEquals("example1", replyMarkup.inlineKeyboard()[0][0].text()); + } + + @Test + void shouldNotAppendKeyboardIfNoButtonIsPresent() throws Exception { + final TgChat fakeTgChat = mock(TgChat.class); + when(fakeTgChat.getId()).thenReturn(1111111L); + when(fakeTgChat.getLocale()).thenReturn("en-US"); + when(fakeTgChat.getChatContext()).thenReturn(new ChatContext()); + when(fakeModelHelp.getMessage(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn("doesn't matter"); + when(fakeModelHelp.generateInlineKeyBoardButton(eq(fakeTgChat), eq(Collections.emptyMap()))) + .thenReturn(new InlineKeyboardButton[] {}); + + final Update update = + gson.fromJson( + "{\n" + + "\"update_id\":10000,\n" + + "\"message\":{\n" + + " \"date\":1441645532,\n" + + " \"chat\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"type\": \"private\",\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"message_id\":1365,\n" + + " \"from\":{\n" + + " \"last_name\":\"Test Lastname\",\n" + + " \"id\":1111111,\n" + + " \"first_name\":\"Test Firstname\",\n" + + " \"username\":\"Testusername\"\n" + + " },\n" + + " \"text\":\"/help\"\n" + + "}\n" + + "}", + Update.class); + + final CompletableFuture>> gotResponseFuture = + help.process(fakeTgChat, update); + final Optional> gotResponseOpt = gotResponseFuture.get(); + final BaseRequest gotResponse = gotResponseOpt.get(); + final InlineKeyboardMarkup replyMarkup = + (InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup"); + assertNull(replyMarkup); + } +} diff --git a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java index a72a7b4..900db57 100644 --- a/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java +++ b/src/test/java/com/github/polpetta/mezzotre/telegram/command/StartIntegrationTest.java @@ -10,6 +10,7 @@ import com.github.polpetta.mezzotre.i18n.TemplateContentGenerator; import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.query.QTgChat; +import com.github.polpetta.mezzotre.orm.telegram.ChatUtil; import com.github.polpetta.mezzotre.util.Clock; import com.github.polpetta.mezzotre.util.UUIDGenerator; import com.github.polpetta.types.json.ChatContext; @@ -74,8 +75,8 @@ class StartIntegrationTest { Executors.newSingleThreadExecutor(), log, fakeUUIDGenerator, - fakeClock, - "Mezzotre"); + "Mezzotre", + new ChatUtil(fakeClock)); } @Test @@ -122,7 +123,7 @@ class StartIntegrationTest { assertInstanceOf(SendMessage.class, gotMessage); final String message = (String) gotMessage.getParameters().get("text"); assertEquals( - "**Hello Test Firstname! \uD83D\uDC4B**\n\n" + "*Hello Test Firstname! \uD83D\uDC4B*\n\n" + "This is _Mezzotre_, a simple bot focused on DnD content management! Please start by" + " choosing a language down below \uD83D\uDC47", message); @@ -180,7 +181,7 @@ class StartIntegrationTest { assertInstanceOf(SendMessage.class, gotMessage); final String message = (String) gotMessage.getParameters().get("text"); assertEquals( - "**Hello Test Firstname! \uD83D\uDC4B**\n\n" + "*Hello Test Firstname! \uD83D\uDC4B*\n\n" + "This is _Mezzotre_, a simple bot focused on DnD content management! Please start by" + " choosing a language down below \uD83D\uDC47", message);