Skip to content

Commit

Permalink
Unit tests for /remind
Browse files Browse the repository at this point in the history
  • Loading branch information
Zabuzard committed May 9, 2022
1 parent 327126b commit 01b3e2e
Show file tree
Hide file tree
Showing 4 changed files with 383 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
* Reminders can be set by using {@link RemindCommand}.
*/
public final class RemindRoutine implements Routine {
private static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class);
static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class);
private static final Color AMBIENT_COLOR = Color.decode("#F7F492");
private static final int SCHEDULE_INTERVAL_SECONDS = 30;
private final Database database;
Expand Down
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());
}
}
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));
}
}
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));
}
}

0 comments on commit 01b3e2e

Please sign in to comment.