chore: perform refactor for db-cleaning and more testing
continuous-integration/drone/push Build is passing Details

* Add Javadoc where missing
* Solve some FIXMEs here and there
pull/3/head
Davide Polonio 2023-04-14 15:00:25 +02:00
parent 3e094ec72a
commit 9ff5748964
31 changed files with 1930 additions and 129 deletions

View File

@ -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<Module> 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());

View File

@ -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<Service> getApplicationServices(
Logger logger,
@Named("serviceRunningCheckTime") Pair<Integer, TimeUnit> serviceRunningCheckTime) {
return List.of(new BatchBeanCleanerService(logger, serviceRunningCheckTime));
}
}

View File

@ -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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
entriesToRemove;
private final Logger log;
private final Pair<Integer, TimeUnit> serviceRunningCheckTime;
private final LinkedList<
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>>
deletionListener;
@Inject
public BatchBeanCleanerService(
Logger logger,
@Named("serviceRunningCheckTime") Pair<Integer, TimeUnit> 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<Integer> removeAsync(
String id, PString<? extends TQRootBean<?, ?>> row) {
final CompletableFuture<Integer> jobExecution = new CompletableFuture<>();
entriesToRemove.offer(Tuple.of(id, row, jobExecution));
return jobExecution;
}
public void addListener(
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) {
deletionListener.add(listener);
}
public void removeListener(
Consumer<Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
listener) {
deletionListener.remove(listener);
}
}

View File

@ -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<PString<QCallbackQueryContext>> ENTRY_GROUP =
() -> new QCallbackQueryContext().entryGroup;
private static final Supplier<PString<QCallbackQueryContext>> 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<Integer> removeGroupAsync(String id) {
log.trace("CallbackQueryContext entry group " + id + " queued for removal");
return batchBeanCleanerService.removeAsync(id, ENTRY_GROUP.get());
}
public CompletableFuture<Integer> removeIdAsync(String id) {
log.trace("CallbackQueryContext single entity " + id + " queued for removal");
return batchBeanCleanerService.removeAsync(id, SINGLE_ENTRY.get());
}
}

View File

@ -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<String, Object> 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<TgChat> 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> T cleanCallbackQuery(T toReturn, CallbackQueryContext callbackQueryContext) {
new QCallbackQueryContext().entryGroup.eq(callbackQueryContext.getId()).delete();
return toReturn;
}
}

View File

@ -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());
}
}

View File

@ -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<Integer> messageId = Util.extractMessageId(update);
BaseRequest<?, ?> baseRequest;

View File

@ -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<String, com.github.polpetta.mezzotre.telegram.command.Processor>
tgCommandProcessors;
private final Map<String, Processor> 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<String, com.github.polpetta.mezzotre.telegram.command.Processor> tgCommandProcessors,
@Named("eventProcessors")
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor>
eventProcessor) {
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<Integer> 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);
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);
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);

View File

@ -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<TgChat> 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<Integer> extractMessageId(Update update) {
return Optional.ofNullable(update.callbackQuery().message()).map(Message::messageId);
}

View File

@ -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<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor>
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<String, Processor> tgCommandProcessors,
@Named("eventProcessors")
Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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);

View File

@ -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;

View File

@ -1,5 +1,5 @@
package com.github.polpetta.mezzotre.telegram.command;
public interface NotFoundFactory {
interface NotFoundFactory {
NotFound create(String commandName);
}

View File

@ -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();

View File

@ -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,21 +19,16 @@ 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<String, Processor> tgCommandProcessors) {
final String message =
templateContentGenerator.mergeTemplate(
return templateContentGenerator.mergeTemplate(
velocityContext -> {
velocityContext.put(
"commands",
@ -44,24 +37,12 @@ public class Help {
.map(
p ->
Pair.of(
p.getTriggerKeywords().stream()
.sorted()
.collect(Collectors.toList()),
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;
}
public InlineKeyboardButton[] generateInlineKeyBoardButton(

View File

@ -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<Service> services) {
serviceManager = new ServiceManager(services);
}
@Override
public boolean lateinit() {
return true;
}
@Override
public void install(@NotNull Jooby application) throws Exception {
final CompletableFuture<Void> 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();
}
}

View File

@ -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<Service> moduleList) {
return new ServiceModule(moduleList);
}
@Provides
@Singleton
@Named("serviceRunningCheckTime")
public Pair<Integer, TimeUnit> getTime() {
return Pair.of(5, TimeUnit.SECONDS);
}
}

View File

@ -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();
}
}

View File

@ -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}

View File

@ -1,3 +1,3 @@
**${i18n.start.helloFirstName.insert(${firstName})}**
*${i18n.start.helloFirstName.insert(${firstName})}*
${i18n.start.description.insert(${programName})}

View File

@ -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<Integer, TimeUnit> getTime() {
return Pair.of(0, TimeUnit.MILLISECONDS);
}
}
@Test

View File

@ -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<Service> moduleList) {
return new ServiceModule(moduleList);
}
@Provides
@Singleton
@Named("serviceRunningCheckTime")
public Pair<Integer, TimeUnit> getTime() {
return Pair.of(0, TimeUnit.MILLISECONDS);
}
}
public static App loadCustomDbApplication() {

View File

@ -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<Integer> 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<Integer> integerCompletableFuture =
batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup);
final CompletableFuture<Integer> integerCompletableFuture2 =
batchBeanCleanerService.removeAsync("4567", new QCallbackQueryContext().entryGroup);
final Integer gotDeletion1 = integerCompletableFuture.get();
final Integer gotDeletion2 = integerCompletableFuture2.get();
assertEquals(2, gotDeletion1);
assertEquals(0, gotDeletion2);
}
}

View File

@ -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<TgChat> 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<TgChat> tgChatOptional =
chatUtil.extractChat(
callbackQueryContext, update); // null just for test purposes, not usually expected
assertEquals(expectedChat, tgChatOptional.get());
}
}

View File

@ -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<ChatContext> 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<ChatContext> chatContextArgumentCaptor =
ArgumentCaptor.forClass(ChatContext.class);
verify(fakeTgChat, times(1)).setChatContext(chatContextArgumentCaptor.capture());
final ChatContext capturedChatContextValue = chatContextArgumentCaptor.getValue();
final Map<String, Object> gotAdditionalProperties =
capturedChatContextValue.getAdditionalProperties();
assertEquals(2, gotAdditionalProperties.size());
assertEquals(obj1, gotAdditionalProperties.get("field1"));
assertEquals(obj2, gotAdditionalProperties.get("field2"));
}
}

View File

@ -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<Optional<BaseRequest<?, ?>>> gotResult =
notFound.process(fakeCallbackQuery, fakeUpdate);
verify(fakeCallbackQueryCleaner, times(1)).removeGroupAsync(eq("123"));
assertEquals(Optional.empty(), gotResult.get());
}
}

View File

@ -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<Arguments> getTestLocales() {
@ -193,6 +219,11 @@ class SelectLanguageTutorialIntegrationTest {
"c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata);
changeLanguageCallbackQueryContext.save();
final CompletableFuture<
Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> processFuture =
this.selectLanguageTutorial.process(changeLanguageCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
deletionCallback = callBackFuture.get();
assertEquals(entryGroupId, deletionCallback._1());
assertEquals(1, deletionCallback._3().get());
assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount());
}
}

View File

@ -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<String, Processor> eventProcessors,
Map<String, com.github.polpetta.mezzotre.telegram.command.Processor> 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<Arguments> 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<String, com.github.polpetta.mezzotre.telegram.command.Processor> commands =
new HashMap<>();
final com.github.polpetta.mezzotre.telegram.command.Processor dummy1 =
new com.github.polpetta.mezzotre.telegram.command.Processor() {
@Override
public Set<String> getTriggerKeywords() {
return Set.of("/a", "/b");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String> getTriggerKeywords() {
return Set.of("/different");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<String, Processor> events = new HashMap<>();
final Processor dummyEvent1 =
new Processor() {
@Override
public String getEventName() {
return "exampleEvent";
}
@Override
public boolean canBeDirectlyInvokedByTheUser() {
return true;
}
@Override
public Optional<String> getPrettyPrintLocaleKeyName() {
return Optional.of("changeLanguage.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
CallbackQueryContext callbackQueryContext, Update update) {
return 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<String> getPrettyPrintLocaleKeyName() {
return Optional.of("selectLanguageTutorial.inlineKeyboardButtonName");
}
@Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotResultFuture =
showHelp.process(callbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<InlineKeyboardButton> 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<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>>
callBackFuture = new CompletableFuture<>();
batchBeanCleanerService.addListener(callBackFuture::complete);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotResultFuture =
showHelp.process(callbackQueryContext, update);
final Optional<BaseRequest<?, ?>> baseRequestOptional = gotResultFuture.get();
assertDoesNotThrow(baseRequestOptional::get);
final Tuple3<String, PString<? extends TQRootBean<?, ?>>, CompletableFuture<Integer>>
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!");
}
}

View File

@ -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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
showHelp.process(fakeCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = gotResponseFuture.get();
final BaseRequest<?, ?> gotResponse = gotResponseOpt.get();
final InlineKeyboardMarkup replyMarkup =
(InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup");
assertNull(replyMarkup);
}
}

View File

@ -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",

View File

@ -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<String, Processor> commands = Collections.emptyMap();
final Map<String, com.github.polpetta.mezzotre.telegram.callbackquery.Processor> 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<Optional<BaseRequest<?, ?>>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
help.process(fakeTgChat, update);
final Optional<BaseRequest<?, ?>> 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<Optional<BaseRequest<?, ?>>> gotResponseFuture =
help.process(fakeTgChat, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = gotResponseFuture.get();
final BaseRequest<?, ?> gotResponse = gotResponseOpt.get();
final InlineKeyboardMarkup replyMarkup =
(InlineKeyboardMarkup) gotResponse.getParameters().get("reply_markup");
assertNull(replyMarkup);
}
}

View File

@ -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);