-
-
Notifications
You must be signed in to change notification settings - Fork 86
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
383 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
67 changes: 67 additions & 0 deletions
67
...ication/src/test/java/org/togetherjava/tjbot/commands/reminder/RawReminderTestHelper.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package org.togetherjava.tjbot.commands.reminder; | ||
|
||
import net.dv8tion.jda.api.entities.Member; | ||
import net.dv8tion.jda.api.entities.TextChannel; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.togetherjava.tjbot.db.Database; | ||
import org.togetherjava.tjbot.db.generated.Tables; | ||
import org.togetherjava.tjbot.db.generated.tables.records.PendingRemindersRecord; | ||
import org.togetherjava.tjbot.jda.JdaTester; | ||
|
||
import java.time.Instant; | ||
import java.util.List; | ||
|
||
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS; | ||
|
||
final class RawReminderTestHelper { | ||
private Database database; | ||
private JdaTester jdaTester; | ||
|
||
RawReminderTestHelper(@NotNull Database database, @NotNull JdaTester jdaTester) { | ||
this.database = database; | ||
this.jdaTester = jdaTester; | ||
} | ||
|
||
void insertReminder(@NotNull String content, @NotNull Instant remindAt) { | ||
insertReminder(content, remindAt, jdaTester.getMemberSpy(), jdaTester.getTextChannelSpy()); | ||
} | ||
|
||
void insertReminder(@NotNull String content, @NotNull Instant remindAt, | ||
@NotNull Member author) { | ||
insertReminder(content, remindAt, author, jdaTester.getTextChannelSpy()); | ||
} | ||
|
||
void insertReminder(@NotNull String content, @NotNull Instant remindAt, @NotNull Member author, | ||
@NotNull TextChannel channel) { | ||
long channelId = channel.getIdLong(); | ||
long guildId = channel.getGuild().getIdLong(); | ||
long authorId = author.getIdLong(); | ||
|
||
database.write(context -> context.newRecord(Tables.PENDING_REMINDERS) | ||
.setCreatedAt(Instant.now()) | ||
.setGuildId(guildId) | ||
.setChannelId(channelId) | ||
.setAuthorId(authorId) | ||
.setRemindAt(remindAt) | ||
.setContent(content) | ||
.insert()); | ||
} | ||
|
||
@NotNull | ||
List<String> readReminders() { | ||
return readReminders(jdaTester.getMemberSpy()); | ||
} | ||
|
||
@NotNull | ||
List<String> readReminders(@NotNull Member author) { | ||
long guildId = jdaTester.getTextChannelSpy().getGuild().getIdLong(); | ||
long authorId = author.getIdLong(); | ||
|
||
return database.read(context -> context.selectFrom(PENDING_REMINDERS) | ||
.where(PENDING_REMINDERS.AUTHOR_ID.eq(authorId) | ||
.and(PENDING_REMINDERS.GUILD_ID.eq(guildId))) | ||
.stream() | ||
.map(PendingRemindersRecord::getContent) | ||
.toList()); | ||
} | ||
} |
137 changes: 137 additions & 0 deletions
137
application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindCommandTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package org.togetherjava.tjbot.commands.reminder; | ||
|
||
import net.dv8tion.jda.api.entities.Member; | ||
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.junit.jupiter.api.Assertions; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.function.Executable; | ||
import org.togetherjava.tjbot.commands.SlashCommand; | ||
import org.togetherjava.tjbot.db.Database; | ||
import org.togetherjava.tjbot.jda.JdaTester; | ||
|
||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.List; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.ArgumentMatchers.startsWith; | ||
import static org.mockito.Mockito.verify; | ||
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS; | ||
|
||
final class RemindCommandTest { | ||
private SlashCommand command; | ||
private JdaTester jdaTester; | ||
private RawReminderTestHelper rawReminders; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
Database database = Database.createMemoryDatabase(PENDING_REMINDERS); | ||
command = new RemindCommand(database); | ||
jdaTester = new JdaTester(); | ||
rawReminders = new RawReminderTestHelper(database, jdaTester); | ||
} | ||
|
||
private @NotNull SlashCommandInteractionEvent triggerSlashCommand(int timeAmount, | ||
@NotNull String timeUnit, @NotNull String content) { | ||
return triggerSlashCommand(timeAmount, timeUnit, content, jdaTester.getMemberSpy()); | ||
} | ||
|
||
private @NotNull SlashCommandInteractionEvent triggerSlashCommand(int timeAmount, | ||
@NotNull String timeUnit, @NotNull String content, @NotNull Member author) { | ||
SlashCommandInteractionEvent event = jdaTester.createSlashCommandInteractionEvent(command) | ||
.setOption(RemindCommand.TIME_AMOUNT_OPTION, timeAmount) | ||
.setOption(RemindCommand.TIME_UNIT_OPTION, timeUnit) | ||
.setOption(RemindCommand.CONTENT_OPTION, content) | ||
.setUserWhoTriggered(author) | ||
.build(); | ||
|
||
command.onSlashCommand(event); | ||
return event; | ||
} | ||
|
||
@Test | ||
@DisplayName("Throws an exception if the time unit is not supported, i.e. not part of the actual choice dialog") | ||
void throwsWhenGivenUnsupportedUnit() { | ||
// GIVEN | ||
// WHEN triggering /remind with the unsupported time unit 'nanoseconds' | ||
Executable triggerRemind = () -> triggerSlashCommand(10, "nanoseconds", "foo"); | ||
|
||
// THEN command throws, no reminder was created | ||
Assertions.assertThrows(IllegalArgumentException.class, triggerRemind); | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Rejects a reminder time that is set too far in the future and responds accordingly") | ||
void doesNotSupportDatesTooFarInFuture() { | ||
// GIVEN | ||
// WHEN triggering /remind too far in the future | ||
SlashCommandInteractionEvent event = triggerSlashCommand(10, "years", "foo"); | ||
|
||
// THEN rejects and responds accordingly, no reminder was created | ||
verify(event).reply(startsWith("The reminder is set too far in the future")); | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Rejects a reminder if a user has too many reminders still pending") | ||
void userIsLimitedIfTooManyPendingReminders() { | ||
// GIVEN a user with too many reminders still pending | ||
Instant remindAt = Instant.now().plus(100, ChronoUnit.DAYS); | ||
for (int i = 0; i < RemindCommand.MAX_PENDING_REMINDERS_PER_USER; i++) { | ||
rawReminders.insertReminder("foo " + i, remindAt); | ||
} | ||
|
||
// WHEN triggering another reminder | ||
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo"); | ||
|
||
// THEN rejects and responds accordingly, no new reminder was created | ||
verify(event) | ||
.reply(startsWith("You have reached the maximum amount of pending reminders per user")); | ||
assertEquals(RemindCommand.MAX_PENDING_REMINDERS_PER_USER, | ||
rawReminders.readReminders().size()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Does not limit a user if another user has too many reminders still pending, i.e. the limit is per user") | ||
void userIsNotLimitedIfOtherUserHasTooManyPendingReminders() { | ||
// GIVEN a user with too many reminders still pending, | ||
// and a second user with no reminders yet | ||
Member firstUser = jdaTester.createMemberSpy(1); | ||
Instant remindAt = Instant.now().plus(100, ChronoUnit.DAYS); | ||
for (int i = 0; i < RemindCommand.MAX_PENDING_REMINDERS_PER_USER; i++) { | ||
rawReminders.insertReminder("foo " + i, remindAt, firstUser); | ||
} | ||
|
||
Member secondUser = jdaTester.createMemberSpy(2); | ||
|
||
// WHEN the second user triggers another reminder | ||
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo", secondUser); | ||
|
||
// THEN accepts the reminder and responds accordingly | ||
verify(event).reply("Will remind you about 'foo' in 5 minutes."); | ||
|
||
List<String> remindersOfSecondUser = rawReminders.readReminders(secondUser); | ||
assertEquals(1, remindersOfSecondUser.size()); | ||
assertEquals("foo", remindersOfSecondUser.get(0)); | ||
} | ||
|
||
@Test | ||
@DisplayName("The command can create a reminder, the regular base case") | ||
void canCreateReminders() { | ||
// GIVEN | ||
// WHEN triggering the /remind command | ||
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo"); | ||
|
||
// THEN accepts the reminder and responds accordingly | ||
verify(event).reply("Will remind you about 'foo' in 5 minutes."); | ||
|
||
List<String> pendingReminders = rawReminders.readReminders(); | ||
assertEquals(1, pendingReminders.size()); | ||
assertEquals("foo", pendingReminders.get(0)); | ||
} | ||
} |
178 changes: 178 additions & 0 deletions
178
application/src/test/java/org/togetherjava/tjbot/commands/reminder/RemindRoutineTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,178 @@ | ||
package org.togetherjava.tjbot.commands.reminder; | ||
|
||
import net.dv8tion.jda.api.entities.*; | ||
import net.dv8tion.jda.api.requests.ErrorResponse; | ||
import net.dv8tion.jda.api.requests.RestAction; | ||
import org.jetbrains.annotations.NotNull; | ||
import org.junit.jupiter.api.BeforeEach; | ||
import org.junit.jupiter.api.DisplayName; | ||
import org.junit.jupiter.api.Test; | ||
import org.mockito.ArgumentCaptor; | ||
import org.togetherjava.tjbot.commands.Routine; | ||
import org.togetherjava.tjbot.db.Database; | ||
import org.togetherjava.tjbot.jda.JdaTester; | ||
|
||
import java.time.Instant; | ||
import java.time.temporal.ChronoUnit; | ||
import java.util.concurrent.TimeUnit; | ||
|
||
import static org.junit.jupiter.api.Assertions.assertEquals; | ||
import static org.junit.jupiter.api.Assertions.assertTrue; | ||
import static org.mockito.Mockito.*; | ||
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS; | ||
|
||
final class RemindRoutineTest { | ||
private Routine routine; | ||
private JdaTester jdaTester; | ||
private RawReminderTestHelper rawReminders; | ||
|
||
@BeforeEach | ||
void setUp() { | ||
Database database = Database.createMemoryDatabase(PENDING_REMINDERS); | ||
routine = new RemindRoutine(database); | ||
jdaTester = new JdaTester(); | ||
rawReminders = new RawReminderTestHelper(database, jdaTester); | ||
} | ||
|
||
private void triggerRoutine() { | ||
routine.runRoutine(jdaTester.getJdaMock()); | ||
} | ||
|
||
private static @NotNull MessageEmbed getLastMessageFrom(@NotNull MessageChannel channel) { | ||
ArgumentCaptor<MessageEmbed> responseCaptor = ArgumentCaptor.forClass(MessageEmbed.class); | ||
verify(channel).sendMessageEmbeds(responseCaptor.capture()); | ||
return responseCaptor.getValue(); | ||
} | ||
|
||
private @NotNull Member createAndSetupUnknownMember() { | ||
int unknownMemberId = 2; | ||
|
||
Member member = jdaTester.createMemberSpy(unknownMemberId); | ||
|
||
RestAction<User> unknownMemberAction = jdaTester.createFailedActionMock( | ||
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER)); | ||
when(jdaTester.getJdaMock().retrieveUserById(unknownMemberId)) | ||
.thenReturn(unknownMemberAction); | ||
|
||
RestAction<PrivateChannel> unknownPrivateChannelAction = jdaTester.createFailedActionMock( | ||
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER)); | ||
when(jdaTester.getJdaMock().openPrivateChannelById(anyLong())) | ||
.thenReturn(unknownPrivateChannelAction); | ||
when(jdaTester.getJdaMock().openPrivateChannelById(anyString())) | ||
.thenReturn(unknownPrivateChannelAction); | ||
|
||
return member; | ||
} | ||
|
||
private @NotNull TextChannel createAndSetupUnknownChannel() { | ||
int unknownChannelId = 2; | ||
|
||
TextChannel channel = jdaTester.createTextChannelSpy(unknownChannelId); | ||
when(jdaTester.getJdaMock().getTextChannelById(unknownChannelId)).thenReturn(null); | ||
|
||
return channel; | ||
} | ||
|
||
@Test | ||
@DisplayName("Sends out a pending reminder to a guild channel, the base case") | ||
void sendsPendingReminderChannelFoundAuthorFound() { | ||
// GIVEN a pending reminder | ||
Instant remindAt = Instant.now(); | ||
String reminderContent = "foo"; | ||
Member author = jdaTester.getMemberSpy(); | ||
rawReminders.insertReminder("foo", remindAt, author); | ||
|
||
// WHEN running the routine | ||
triggerRoutine(); | ||
|
||
// THEN the reminder is sent out and deleted from the database | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
|
||
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy()); | ||
assertEquals(reminderContent, lastMessage.getDescription()); | ||
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant()); | ||
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Sends out a pending reminder to a guild channel, even if the author could not be retrieved anymore") | ||
void sendsPendingReminderChannelFoundAuthorNotFound() { | ||
// GIVEN a pending reminder from an unknown user | ||
Instant remindAt = Instant.now(); | ||
String reminderContent = "foo"; | ||
Member unknownAuthor = createAndSetupUnknownMember(); | ||
rawReminders.insertReminder("foo", remindAt, unknownAuthor); | ||
|
||
// WHEN running the routine | ||
triggerRoutine(); | ||
|
||
// THEN the reminder is sent out and deleted from the database | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
|
||
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy()); | ||
assertEquals(reminderContent, lastMessage.getDescription()); | ||
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant()); | ||
assertEquals("Unknown user", lastMessage.getAuthor().getName()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Sends out a pending reminder via DM, even if the channel could not be retrieved anymore") | ||
void sendsPendingReminderChannelNotFoundAuthorFound() { | ||
// GIVEN a pending reminder from an unknown channel | ||
Instant remindAt = Instant.now(); | ||
String reminderContent = "foo"; | ||
Member author = jdaTester.getMemberSpy(); | ||
TextChannel unknownChannel = createAndSetupUnknownChannel(); | ||
rawReminders.insertReminder("foo", remindAt, author, unknownChannel); | ||
|
||
// WHEN running the routine | ||
triggerRoutine(); | ||
|
||
// THEN the reminder is sent out and deleted from the database | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
|
||
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getPrivateChannelSpy()); | ||
assertEquals(reminderContent, lastMessage.getDescription()); | ||
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant()); | ||
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName()); | ||
} | ||
|
||
@Test | ||
@DisplayName("Skips a pending reminder if sending it out resulted in an error") | ||
void skipPendingReminderOnErrorChannelNotFoundAuthorNotFound() { | ||
// GIVEN a pending reminder and from an unknown channel and author | ||
Instant remindAt = Instant.now(); | ||
String reminderContent = "foo"; | ||
Member unknownAuthor = createAndSetupUnknownMember(); | ||
TextChannel unknownChannel = createAndSetupUnknownChannel(); | ||
rawReminders.insertReminder("foo", remindAt, unknownAuthor, unknownChannel); | ||
|
||
// WHEN running the routine | ||
triggerRoutine(); | ||
|
||
// THEN the reminder is skipped and deleted from the database | ||
assertTrue(rawReminders.readReminders().isEmpty()); | ||
} | ||
|
||
@Test | ||
@DisplayName("A reminder that is not pending yet, is not send out") | ||
void reminderIsNotSendIfNotPending() { | ||
// GIVEN a reminder that is not pending yet | ||
Instant remindAt = Instant.now().plus(1, ChronoUnit.HOURS); | ||
String reminderContent = "foo"; | ||
rawReminders.insertReminder("foo", remindAt); | ||
|
||
// WHEN running the routine | ||
triggerRoutine(); | ||
|
||
// THEN the reminder is not send yet and still in the database | ||
assertEquals(1, rawReminders.readReminders().size()); | ||
verify(jdaTester.getTextChannelSpy(), never()).sendMessageEmbeds(any(MessageEmbed.class)); | ||
} | ||
|
||
private static void assertSimilar(@NotNull Instant expected, @NotNull Instant actual) { | ||
// NOTE For some reason, the instant ends up in the database slightly wrong already (about | ||
// half a second), seems to be an issue with jOOQ | ||
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(1)); | ||
} | ||
} |