feat: add first changeLanguage event implementation
continuous-integration/drone/push Build is passing Details

pull/2/head
Davide Polonio 2023-03-30 17:12:43 +02:00
parent 421909d5ae
commit 3705840989
18 changed files with 522 additions and 177 deletions

View File

@ -40,6 +40,7 @@
<org.testcontainers.junit-jupiter.version>1.16.3</org.testcontainers.junit-jupiter.version> <org.testcontainers.junit-jupiter.version>1.16.3</org.testcontainers.junit-jupiter.version>
<jsonschema2pojo.version>1.1.1</jsonschema2pojo.version> <jsonschema2pojo.version>1.1.1</jsonschema2pojo.version>
<jackson-databind.version>2.13.3</jackson-databind.version> <jackson-databind.version>2.13.3</jackson-databind.version>
<junit-jupiter-params.version>5.9.1</junit-jupiter-params.version>
</properties> </properties>
<dependencies> <dependencies>
@ -68,6 +69,13 @@
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit-jupiter-params.version}</version>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>io.jooby</groupId> <groupId>io.jooby</groupId>
<artifactId>jooby-test</artifactId> <artifactId>jooby-test</artifactId>

View File

@ -5,7 +5,6 @@
"default": {}, "default": {},
"required": [ "required": [
"event", "event",
"messageId",
"telegramChatId" "telegramChatId"
], ],
"additionalProperties": true, "additionalProperties": true,

View File

@ -3,6 +3,7 @@ package com.github.polpetta.mezzotre;
import com.github.polpetta.mezzotre.orm.di.Db; import com.github.polpetta.mezzotre.orm.di.Db;
import com.github.polpetta.mezzotre.route.Telegram; import com.github.polpetta.mezzotre.route.Telegram;
import com.github.polpetta.mezzotre.route.di.Route; import com.github.polpetta.mezzotre.route.di.Route;
import com.github.polpetta.mezzotre.telegram.callbackquery.di.CallbackQuery;
import com.github.polpetta.mezzotre.telegram.command.di.Command; import com.github.polpetta.mezzotre.telegram.command.di.Command;
import com.github.polpetta.mezzotre.util.di.ThreadPool; import com.github.polpetta.mezzotre.util.di.ThreadPool;
import com.google.inject.*; import com.google.inject.*;
@ -26,6 +27,7 @@ public class App extends Jooby {
modules.add(new ThreadPool()); modules.add(new ThreadPool());
modules.add(new Route()); modules.add(new Route());
modules.add(new Command()); modules.add(new Command());
modules.add(new CallbackQuery());
return modules; return modules;
}; };

View File

@ -22,8 +22,6 @@ public class LocalizedMessageFactory {
public ToolManager create(Locale locale) { public ToolManager create(Locale locale) {
// properties.setProperty("file.resource.loader.class", FileResourceLoader.class.getName());.
final ToolManager toolManager = new ToolManager(); final ToolManager toolManager = new ToolManager();
toolManager.setVelocityEngine(velocityEngine); toolManager.setVelocityEngine(velocityEngine);
final FactoryConfiguration factoryConfiguration = new FactoryConfiguration(); final FactoryConfiguration factoryConfiguration = new FactoryConfiguration();

View File

@ -2,6 +2,7 @@ package com.github.polpetta.mezzotre.orm.model;
import com.github.polpetta.types.json.CallbackQueryMetadata; import com.github.polpetta.types.json.CallbackQueryMetadata;
import io.ebean.annotation.DbJson; import io.ebean.annotation.DbJson;
import io.ebean.annotation.Length;
import javax.persistence.Entity; import javax.persistence.Entity;
import javax.persistence.Id; import javax.persistence.Id;
import javax.persistence.Table; import javax.persistence.Table;
@ -22,12 +23,19 @@ import javax.validation.constraints.NotNull;
@Table(name = "callback_query_context") @Table(name = "callback_query_context")
public class CallbackQueryContext extends Base { public class CallbackQueryContext extends Base {
@Id private final String id; @Id
@Length(36)
private final String id;
@NotNull
@Length(36)
private final String entryGroup;
@DbJson @NotNull private final CallbackQueryMetadata fields; @DbJson @NotNull private final CallbackQueryMetadata fields;
public CallbackQueryContext(String id, CallbackQueryMetadata fields) { public CallbackQueryContext(String id, String entryGroup, CallbackQueryMetadata fields) {
this.id = id; this.id = id;
this.entryGroup = entryGroup;
this.fields = fields; this.fields = fields;
} }
@ -35,6 +43,10 @@ public class CallbackQueryContext extends Base {
return id; return id;
} }
public String getEntryGroup() {
return entryGroup;
}
public CallbackQueryMetadata getFields() { public CallbackQueryMetadata getFields() {
return fields; return fields;
} }

View File

@ -1,27 +1,157 @@
package com.github.polpetta.mezzotre.telegram.callbackquery; package com.github.polpetta.mezzotre.telegram.callbackquery;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory;
import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext; import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.query.QCallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.query.QTgChat;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.model.request.ParseMode;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage;
import io.vavr.control.Try;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import java.util.NoSuchElementException;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton; import javax.inject.Singleton;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.tools.ToolManager;
import org.apache.velocity.util.StringBuilderWriter;
import org.slf4j.Logger;
@Singleton @Singleton
public class ChangeLanguage implements Processor { public class ChangeLanguage implements Processor {
public static final String EVENT_NAME = "changeLanguage";
private final Executor threadPool;
private final LocalizedMessageFactory localizedMessageFactory;
private final Logger log;
public enum Field {
NewLanguage("newLanguage");
private final String name;
Field(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
public enum Language {
English("en-US"),
Italian("it-IT");
private final String locale;
Language(String locale) {
this.locale = locale;
}
public String getLocale() {
return locale;
}
}
@Inject @Inject
public ChangeLanguage() {} public ChangeLanguage(
@Named("eventThreadPool") Executor threadPool,
LocalizedMessageFactory localizedMessageFactory,
Logger log) {
this.threadPool = threadPool;
this.localizedMessageFactory = localizedMessageFactory;
this.log = log;
}
@Override @Override
public String getEventName() { public String getEventName() {
return "changeLanguage"; return EVENT_NAME;
} }
@Override @Override
public CompletableFuture<Optional<BaseRequest<?, ?>>> process( public CompletableFuture<Optional<BaseRequest<?, ?>>> process(
CallbackQueryContext callbackQueryContext, Update update) { CallbackQueryContext callbackQueryContext, Update update) {
return null; return CompletableFuture.supplyAsync(
() ->
Optional.of(callbackQueryContext.getFields().getTelegramChatId())
.filter(
chatId ->
chatId.longValue() != 0L
&& !chatId.isNaN()
&& !chatId.isInfinite()
&& chatId != Double.MIN_VALUE)
.flatMap(chatId -> new QTgChat().id.eq(chatId.longValue()).findOneOrEmpty())
.map(
tgChat -> {
tgChat.setLocale(
(String)
callbackQueryContext
.getFields()
.getAdditionalProperties()
.getOrDefault(
Field.NewLanguage.getName(),
Language.English.getLocale()));
tgChat.save();
return tgChat;
})
.orElseThrow(
() ->
new NoSuchElementException(
"Unable to find telegram chat "
+ Double.valueOf(
callbackQueryContext.getFields().getTelegramChatId())
.longValue()
+ " in the database")),
threadPool)
.thenApplyAsync(
// If we are here then we're sure there is at least a chat associated with this callback
tgChat -> {
final String message =
Try.of(
() -> {
final Locale locale = Locale.forLanguageTag(tgChat.getLocale());
final ToolManager toolManager = localizedMessageFactory.create(locale);
final VelocityContext velocityContext =
new VelocityContext(toolManager.createContext());
final StringBuilder content = new StringBuilder();
final StringBuilderWriter stringBuilderWriter =
new StringBuilderWriter(content);
toolManager
.getVelocityEngine()
.mergeTemplate(
"/template/callbackQuery/changeLanguage.vm",
StandardCharsets.UTF_8.name(),
velocityContext,
stringBuilderWriter);
stringBuilderWriter.close();
return content.toString();
})
.get();
log.trace("ChangeLanguage 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);
return Optional.of(
new SendMessage(tgChat.getId(), message).parseMode(ParseMode.Markdown));
},
threadPool);
} }
} }

View File

@ -1,10 +1,17 @@
package com.github.polpetta.mezzotre.telegram.command; package com.github.polpetta.mezzotre.telegram.command;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory;
import com.github.polpetta.mezzotre.orm.model.CallbackQueryContext;
import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.mezzotre.telegram.callbackquery.ChangeLanguage;
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.github.polpetta.types.json.ChatContext;
import com.google.inject.Singleton; import com.google.inject.Singleton;
import com.pengrad.telegrambot.model.Update; 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.model.request.ParseMode;
import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage; import com.pengrad.telegrambot.request.SendMessage;
@ -13,6 +20,7 @@ import java.nio.charset.StandardCharsets;
import java.util.Locale; import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import javax.inject.Inject; import javax.inject.Inject;
import javax.inject.Named; import javax.inject.Named;
import org.apache.velocity.VelocityContext; import org.apache.velocity.VelocityContext;
@ -29,18 +37,24 @@ import org.slf4j.Logger;
@Singleton @Singleton
public class Start implements Processor { public class Start implements Processor {
private final java.util.concurrent.Executor threadPool; private final Executor threadPool;
private final Logger log; private final Logger log;
private final UUIDGenerator uuidGenerator;
private final Clock clock;
private final LocalizedMessageFactory localizedMessageFactory; private final LocalizedMessageFactory localizedMessageFactory;
@Inject @Inject
public Start( public Start(
LocalizedMessageFactory localizedMessageFactory, LocalizedMessageFactory localizedMessageFactory,
@Named("eventThreadPool") java.util.concurrent.Executor threadPool, @Named("eventThreadPool") Executor threadPool,
Logger log) { Logger log,
UUIDGenerator uuidGenerator,
Clock clock) {
this.localizedMessageFactory = localizedMessageFactory; this.localizedMessageFactory = localizedMessageFactory;
this.threadPool = threadPool; this.threadPool = threadPool;
this.log = log; this.log = log;
this.uuidGenerator = uuidGenerator;
this.clock = clock;
} }
@Override @Override
@ -62,17 +76,17 @@ public class Start implements Processor {
Try.of( Try.of(
() -> { () -> {
final Locale locale = Locale.forLanguageTag(chat.getLocale()); final Locale locale = Locale.forLanguageTag(chat.getLocale());
final ToolManager toolContext = localizedMessageFactory.create(locale); final ToolManager toolManager = localizedMessageFactory.create(locale);
final VelocityContext context = final VelocityContext context =
new VelocityContext(toolContext.createContext()); new VelocityContext(toolManager.createContext());
context.put("firstName", update.message().chat().firstName()); context.put("firstName", update.message().chat().firstName());
context.put("programName", "Mezzotre"); context.put("programName", "_Mezzotre_");
final StringBuilder content = new StringBuilder(); final StringBuilder content = new StringBuilder();
final StringBuilderWriter stringBuilderWriter = final StringBuilderWriter stringBuilderWriter =
new StringBuilderWriter(content); new StringBuilderWriter(content);
toolContext toolManager
.getVelocityEngine() .getVelocityEngine()
.mergeTemplate( .mergeTemplate(
"template/command/start.0.vm", "template/command/start.0.vm",
@ -87,14 +101,51 @@ public class Start implements Processor {
log.trace("Start command - message to send back: " + message); log.trace("Start command - message to send back: " + message);
final ChatContext chatContext = chat.getChatContext(); final ChatContext chatContext = chat.getChatContext();
chatContext.setLastMessageSentId(update.message().messageId());
chatContext.setStage(getTriggerKeyword()); chatContext.setStage(getTriggerKeyword());
chatContext.setStep(0); chatContext.setStep(0);
chatContext.setPreviousMessageUnixTimestampInSeconds(update.message().date()); chatContext.setPreviousMessageUnixTimestampInSeconds(clock.now());
chat.setChatContext(chatContext); chat.setChatContext(chatContext);
chat.save(); chat.save();
return Optional.of(new SendMessage(chat.getId(), message).parseMode(ParseMode.Markdown)); // To get the messageId we should send the message first, then save it in the database!
final String groupId = uuidGenerator.generateAsString();
final CallbackQueryContext switchToEnglish =
new CallbackQueryContext(
uuidGenerator.generateAsString(),
groupId,
new CallbackQueryMetadata.CallbackQueryMetadataBuilder()
.withEvent(ChangeLanguage.EVENT_NAME)
.withTelegramChatId(update.message().chat().id())
.withAdditionalProperty(
ChangeLanguage.Field.NewLanguage.getName(),
ChangeLanguage.Language.English)
.build());
final CallbackQueryContext switchToItalian =
new CallbackQueryContext(
uuidGenerator.generateAsString(),
groupId,
new CallbackQueryMetadata.CallbackQueryMetadataBuilder()
.withEvent(ChangeLanguage.EVENT_NAME)
.withTelegramChatId(update.message().chat().id())
.withAdditionalProperty(
ChangeLanguage.Field.NewLanguage.getName(),
ChangeLanguage.Language.Italian)
.build());
final SendMessage messageToSend =
new SendMessage(chat.getId(), message)
.parseMode(ParseMode.Markdown)
.replyMarkup(
new InlineKeyboardMarkup(
new InlineKeyboardButton("English").callbackData(switchToEnglish.getId()),
new InlineKeyboardButton("Italian")
.callbackData(switchToItalian.getId())));
switchToEnglish.save();
switchToItalian.save();
return Optional.of(messageToSend);
}, },
threadPool); threadPool);
} }

View File

@ -1,3 +1,6 @@
start.hello=Hello start.helloFirstName=Hello {0}! \ud83d\udc4b
start.thisIs=This is start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47
start.description=a simple bot focused on DnD content management! Please start by choosing a language down below. changeLanguage.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid*
changeLanguage.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language you prefer!
changeLanguage.instructions=You can always change your language settings by typing /changeLanguage in the chat.
spell.speakWithAnimals=Speak with animals

View File

@ -1,3 +1,6 @@
start.hello=Hello start.helloFirstName=Hello {0}! \ud83d\udc4b
start.thisIs=This is start.description=This is {0}, a simple bot focused on DnD content management! Please start by choosing a language down below \ud83d\udc47
start.description=a simple bot focused on DnD content management! Please start by choosing a language down below. changeLanguage.drinkAction=*Proceeds to drink a potion with a strange, multicolor liquid*
changeLanguage.setLanguage=Thanks! Now that I drank this modified potion of {0} that I''ve found at the "Crystal Fermentary" magic potion shop yesterday I can speak with you in the language you prefer!
changeLanguage.instructions=You can always change your language settings by typing /changeLanguage in the chat.
spell.speakWithAnimals=Speak with animals

View File

@ -1,3 +1,6 @@
start.hello=Ciao start.helloFirstName=Ciao {0}! \ud83d\udc4b
start.thisIs=Questo è start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47
start.description=un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto changeLanguage.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore*
changeLanguage.setLanguage=Grazie! Ora che ho bevuto quest posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci!
changeLanguage.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat.
spell.speakWithAnimals=Parlare con animali

View File

@ -1,3 +1,6 @@
start.hello=Ciao start.helloFirstName=Ciao {0}! \ud83d\udc4b
start.thisIs=Questo è start.description=Questo è {0}, un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto \ud83d\udc47
start.description=un semplice bot che ci concenta sulla gestione di contenuto per DnD! Per favore comincia selezionando la lingua qui sotto changeLanguage.drinkAction=*Procede a bere una pozione al cui suo interno si trova uno strano liquido multicolore*
changeLanguage.setLanguage=Grazie! Ora che ho bevuto quest posizione modificata di {0} che ho trovato ieri al negozio di pozioni magiche la "Cristalleria Fermentatrice" posso parlare con te nel linguaggio che preferisci!
changeLanguage.instructions=Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage nella chat.
spell.speakWithAnimals=Parlare con animali

View File

@ -0,0 +1,5 @@
_${i18n.changeLanguage.drinkAction}_
${i18n.changeLanguage.setLanguage.insert(${i18n.spell.speakWithAnimals})}
${i18n.changeLanguage.instructions}

View File

@ -1,4 +1,3 @@
## https://velocity.apache.org/tools/2.0/apidocs/org/apache/velocity/tools/generic/ResourceTool.html **${i18n.start.helloFirstName.insert(${firstName})}**
**$i18n.start.hello $firstName! 👋**
$i18n.start.thisIs _${programName}_, $i18n.start.description 👇 ${i18n.start.description.insert(${programName})}

View File

@ -11,6 +11,9 @@ import java.io.InputStream;
import java.net.URL; import java.net.URL;
import java.util.Properties; import java.util.Properties;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
import org.testcontainers.containers.PostgreSQLContainer; import org.testcontainers.containers.PostgreSQLContainer;
public class Loader { public class Loader {
@ -49,4 +52,13 @@ public class Loader {
public static Database connectToDatabase(Pair<Properties, Properties> connectionProperties) { public static Database connectToDatabase(Pair<Properties, Properties> connectionProperties) {
return connectToDatabase(connectionProperties.getLeft(), connectionProperties.getRight()); return connectToDatabase(connectionProperties.getLeft(), connectionProperties.getRight());
} }
public static VelocityEngine defaultVelocityEngine() {
final VelocityEngine velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath");
velocityEngine.setProperty(
"resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
velocityEngine.init();
return velocityEngine;
}
} }

View File

@ -120,6 +120,7 @@ class TelegramIntegrationTest {
final CallbackQueryContext callbackQueryContext = final CallbackQueryContext callbackQueryContext =
new CallbackQueryContext( new CallbackQueryContext(
"41427473-0d81-40a8-af60-9517163615a4", "41427473-0d81-40a8-af60-9517163615a4",
"2ee7f5c6-93f0-4859-b902-af9476cf74ad",
new CallbackQueryMetadata.CallbackQueryMetadataBuilder() new CallbackQueryMetadata.CallbackQueryMetadataBuilder()
.withTelegramChatId(666L) .withTelegramChatId(666L)
.withMessageId(42L) .withMessageId(42L)

View File

@ -0,0 +1,148 @@
package com.github.polpetta.mezzotre.telegram.callbackquery;
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.i18n.LocalizedMessageFactory;
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.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.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage;
import io.ebean.Database;
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.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.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 ChangeLanguageIntegrationTest {
private static Gson gson;
@Container
private final PostgreSQLContainer<?> postgresServer =
new PostgreSQLContainer<>(TestConfig.POSTGRES_DOCKER_IMAGE);
private Database database;
private ChangeLanguage changeLanguage;
@BeforeAll
static void beforeAll() {
gson = new Gson();
}
@BeforeEach
void setUp() throws Exception {
database =
Loader.connectToDatabase(Loader.loadDefaultEbeanConfigWithPostgresSettings(postgresServer));
changeLanguage =
new ChangeLanguage(
Executors.newSingleThreadExecutor(),
new LocalizedMessageFactory(Loader.defaultVelocityEngine()),
LoggerFactory.getLogger(ChangeLanguage.class));
}
private static Stream<Arguments> getTestLocales() {
return Stream.of(
Arguments.of(
ChangeLanguage.Language.Italian,
"_*Procede a bere una pozione al cui suo interno si trova uno strano liquido"
+ " multicolore*_\n"
+ "\n"
+ "Grazie! Ora che ho bevuto quest posizione modificata di Parlare con animali che"
+ " ho trovato ieri al negozio di pozioni magiche la \"Cristalleria Fermentatrice\""
+ " posso parlare con te nel linguaggio che preferisci!\n"
+ "\n"
+ "Puoi sempre cambiare le preferenze della tua lingua scrivendo /changeLanguage"
+ " nella chat.",
"en-US"),
Arguments.of(
ChangeLanguage.Language.English,
"_*Proceeds to drink a potion with a strange, multicolor liquid*_\n"
+ "\n"
+ "Thanks! Now that I drank this modified potion of Speak with animals that I've"
+ " found at the \"Crystal Fermentary\" magic potion shop yesterday I can speak"
+ " with you in the language you prefer!\n"
+ "\n"
+ "You can always change your language settings by typing /changeLanguage in the"
+ " chat.",
"it-IT"));
}
@ParameterizedTest
@Timeout(value = 1, unit = TimeUnit.MINUTES)
@MethodSource("getTestLocales")
void shouldProcessChangeLanguageToDesiredOne(
ChangeLanguage.Language language, String expectedResult, String startingLocale)
throws Exception {
final Update update =
gson.fromJson(
"{\n"
+ "\"update_id\":10000,\n"
+ "\"callback_query\":{\n"
+ " \"id\": \"4382bfdwdsb323b2d9\",\n"
+ " \"from\":{\n"
+ " \"last_name\":\"Test Lastname\",\n"
+ " \"type\": \"private\",\n"
+ " \"id\":1111111,\n"
+ " \"first_name\":\"Test Firstname\",\n"
+ " \"username\":\"Testusername\"\n"
+ " },\n"
+ " \"data\": \"Data from button callback\",\n"
+ " \"inline_message_id\": \"1234csdbsk4839\"\n"
+ "}\n"
+ "}",
Update.class);
final long tgChatId = 1111111L;
final ChatContext chatContext = new ChatContext();
final TgChat tgChat = new TgChat(tgChatId, chatContext, startingLocale);
tgChat.save();
final CallbackQueryMetadata callbackQueryMetadata =
new CallbackQueryMetadata.CallbackQueryMetadataBuilder()
.withEvent("changeLanguage")
.withTelegramChatId(tgChatId)
.withAdditionalProperty(
ChangeLanguage.Field.NewLanguage.getName(), language.getLocale())
.build();
final String entryGroupId = "2e67774a-e4e4-4369-a414-a7f8bfe74b80";
final CallbackQueryContext changeLanguageCallbackQueryContext =
new CallbackQueryContext(
"c018108f-6612-4848-8fca-cf301460d4eb", entryGroupId, callbackQueryMetadata);
changeLanguageCallbackQueryContext.save();
final CompletableFuture<Optional<BaseRequest<?, ?>>> processFuture =
changeLanguage.process(changeLanguageCallbackQueryContext, update);
final Optional<BaseRequest<?, ?>> gotResponseOpt = processFuture.get();
final SendMessage gotMessage = (SendMessage) gotResponseOpt.get();
assertEquals(expectedResult, gotMessage.getParameters().get("text"));
final TgChat retrievedTgChat = new QTgChat().id.eq(tgChatId).findOne();
assertNotNull(retrievedTgChat);
assertEquals(language.getLocale(), retrievedTgChat.getLocale());
assertEquals(0, new QCallbackQueryContext().entryGroup.eq(entryGroupId).findCount());
}
}

View File

@ -1,12 +1,15 @@
package com.github.polpetta.mezzotre.telegram.command; package com.github.polpetta.mezzotre.telegram.command;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.github.polpetta.mezzotre.helper.Loader; import com.github.polpetta.mezzotre.helper.Loader;
import com.github.polpetta.mezzotre.helper.TestConfig; import com.github.polpetta.mezzotre.helper.TestConfig;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory; import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory;
import com.github.polpetta.mezzotre.orm.model.TgChat; import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.mezzotre.orm.model.query.QTgChat; import com.github.polpetta.mezzotre.orm.model.query.QTgChat;
import com.github.polpetta.mezzotre.util.Clock;
import com.github.polpetta.mezzotre.util.UUIDGenerator;
import com.github.polpetta.types.json.ChatContext; import com.github.polpetta.types.json.ChatContext;
import com.google.gson.Gson; import com.google.gson.Gson;
import com.pengrad.telegrambot.model.Update; import com.pengrad.telegrambot.model.Update;
@ -15,6 +18,7 @@ import com.pengrad.telegrambot.request.SendMessage;
import io.ebean.Database; import io.ebean.Database;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors; import java.util.concurrent.Executors;
import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants; import org.apache.velocity.runtime.RuntimeConstants;
@ -45,6 +49,8 @@ class StartIntegrationTest {
private LocalizedMessageFactory localizedMessageFactory; private LocalizedMessageFactory localizedMessageFactory;
private Start start; private Start start;
private Database database; private Database database;
private Clock fakeClock;
private UUIDGenerator fakeUUIDGenerator;
@BeforeAll @BeforeAll
static void beforeAll() { static void beforeAll() {
@ -64,11 +70,25 @@ class StartIntegrationTest {
final Logger log = LoggerFactory.getLogger(Start.class); final Logger log = LoggerFactory.getLogger(Start.class);
start = new Start(localizedMessageFactory, Executors.newSingleThreadExecutor(), log); fakeClock = mock(Clock.class);
fakeUUIDGenerator = mock(UUIDGenerator.class);
start =
new Start(
localizedMessageFactory,
Executors.newSingleThreadExecutor(),
log,
fakeUUIDGenerator,
fakeClock);
} }
@Test @Test
void shouldUpdateContextInTheDatabase() throws Exception { void shouldUpdateContextInTheDatabase() throws Exception {
when(fakeClock.now()).thenReturn(42L);
when(fakeUUIDGenerator.generateAsString())
.thenReturn("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5")
.thenReturn("16507fbd-9f28-48a8-9de1-3ea1c943af67")
.thenReturn("0b0ac18e-f621-484e-aa8d-9b176be5b930");
final TgChat tgChat = new TgChat(1111111L, new ChatContext()); final TgChat tgChat = new TgChat(1111111L, new ChatContext());
tgChat.setLocale("en-US"); tgChat.setLocale("en-US");
tgChat.save(); tgChat.save();
@ -108,16 +128,104 @@ class StartIntegrationTest {
assertEquals( 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" + "This is _Mezzotre_, a simple bot focused on DnD content management! Please start by"
+ " choosing a language down below. \uD83D\uDC47", + " choosing a language down below \uD83D\uDC47",
message); message);
assertEquals(1111111L, (Long) gotMessage.getParameters().get("chat_id")); assertEquals(1111111L, (Long) gotMessage.getParameters().get("chat_id"));
final TgChat retrievedTgChat = new QTgChat().id.eq(1111111L).findOne(); final TgChat retrievedTgChat = new QTgChat().id.eq(1111111L).findOne();
assertNotNull(retrievedTgChat); assertNotNull(retrievedTgChat);
final ChatContext gotChatContext = retrievedTgChat.getChatContext(); final ChatContext gotChatContext = retrievedTgChat.getChatContext();
assertEquals(1441645532, gotChatContext.getPreviousMessageUnixTimestampInSeconds()); assertEquals(42, gotChatContext.getPreviousMessageUnixTimestampInSeconds());
assertEquals(1365, gotChatContext.getLastMessageSentId()); assertEquals(0, gotChatContext.getLastMessageSentId());
assertEquals("/start", gotChatContext.getStage()); assertEquals("/start", gotChatContext.getStage());
assertEquals(0, gotChatContext.getStep()); assertEquals(0, gotChatContext.getStep());
} }
@Test
void shouldReceiveHelloIntroduction() throws Exception {
when(fakeClock.now()).thenReturn(42L);
when(fakeUUIDGenerator.generateAsString())
.thenReturn("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5")
.thenReturn("16507fbd-9f28-48a8-9de1-3ea1c943af67")
.thenReturn("0b0ac18e-f621-484e-aa8d-9b176be5b930");
final TgChat tgChat = new TgChat(1111111L, new ChatContext(), "en-US");
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\":\"/start\"\n"
+ "}\n"
+ "}",
Update.class);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotFuture = start.process(tgChat, update);
assertDoesNotThrow(() -> gotFuture.get());
final Optional<BaseRequest<?, ?>> gotMessageOptional = gotFuture.get();
assertDoesNotThrow(gotMessageOptional::get);
final BaseRequest<?, ?> gotMessage = gotMessageOptional.get();
assertInstanceOf(SendMessage.class, gotMessage);
final String message = (String) gotMessage.getParameters().get("text");
assertEquals(
"**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);
assertEquals(1111111L, (Long) gotMessage.getParameters().get("chat_id"));
}
@Test
void shouldThrowErrorIfLocaleNonExists() {
when(fakeClock.now()).thenReturn(42L);
when(fakeUUIDGenerator.generateAsString())
.thenReturn("e86e6fa1-fdd4-4120-b85d-a5482db2e8b5")
.thenReturn("16507fbd-9f28-48a8-9de1-3ea1c943af67")
.thenReturn("0b0ac18e-f621-484e-aa8d-9b176be5b930");
final TgChat tgChat = new TgChat(1111111L, null);
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\":\"/start\"\n"
+ "}\n"
+ "}",
Update.class);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotFuture = start.process(tgChat, update);
assertThrows(ExecutionException.class, gotFuture::get);
}
} }

View File

@ -1,140 +0,0 @@
package com.github.polpetta.mezzotre.telegram.command;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
import com.github.polpetta.mezzotre.i18n.LocalizedMessageFactory;
import com.github.polpetta.mezzotre.orm.model.TgChat;
import com.github.polpetta.types.json.ChatContext;
import com.google.gson.Gson;
import com.pengrad.telegrambot.model.Update;
import com.pengrad.telegrambot.request.BaseRequest;
import com.pengrad.telegrambot.request.SendMessage;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.runtime.RuntimeConstants;
import org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader;
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.junit.jupiter.api.parallel.Execution;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.slf4j.Logger;
@Tag("velocity")
@Execution(ExecutionMode.CONCURRENT)
class StartTest {
private VelocityEngine velocityEngine;
private LocalizedMessageFactory localizedMessageFactory;
private Start start;
private static Gson gson;
@BeforeAll
static void beforeAll() {
gson = new Gson();
}
@BeforeEach
void setUp() {
velocityEngine = new VelocityEngine();
velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADERS, "classpath");
velocityEngine.setProperty(
"resource.loader.classpath.class", ClasspathResourceLoader.class.getName());
velocityEngine.init();
localizedMessageFactory = new LocalizedMessageFactory(velocityEngine);
final Logger fakeLog = mock(Logger.class);
start = new Start(localizedMessageFactory, Executors.newSingleThreadExecutor(), fakeLog);
}
@Test
void shouldReceiveHelloIntroduction() throws Exception {
final TgChat fakeChat = mock(TgChat.class);
when(fakeChat.getLocale()).thenReturn("en-US");
when(fakeChat.getChatContext()).thenReturn(new ChatContext());
when(fakeChat.getId()).thenReturn(1111111L);
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\":\"/start\"\n"
+ "}\n"
+ "}",
Update.class);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotFuture =
start.process(fakeChat, update);
assertDoesNotThrow(() -> gotFuture.get());
final Optional<BaseRequest<?, ?>> gotMessageOptional = gotFuture.get();
assertDoesNotThrow(gotMessageOptional::get);
final BaseRequest<?, ?> gotMessage = gotMessageOptional.get();
assertInstanceOf(SendMessage.class, gotMessage);
final String message = (String) gotMessage.getParameters().get("text");
assertEquals(
"**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);
assertEquals(1111111L, (Long) gotMessage.getParameters().get("chat_id"));
verify(fakeChat, times(1)).save();
}
@Test
void shouldThrowErrorIfLocaleNonExists() {
final TgChat fakeChat = mock(TgChat.class);
// Do not set Locale on purpose
when(fakeChat.getId()).thenReturn(1111111L);
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\":\"/start\"\n"
+ "}\n"
+ "}",
Update.class);
final CompletableFuture<Optional<BaseRequest<?, ?>>> gotFuture =
start.process(fakeChat, update);
assertThrows(ExecutionException.class, gotFuture::get);
}
}