Skip to content

Unit tests for /remind #415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.togetherjava.tjbot.commands.reminder;

import net.dv8tion.jda.api.entities.Guild;
import net.dv8tion.jda.api.entities.ISnowflake;
import net.dv8tion.jda.api.entities.User;
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
Expand All @@ -25,24 +26,24 @@
*
* <pre>
* {@code
* /remind amount: 5 unit: weeks content: Hello World!
* /remind time-amount: 5 time-unit: weeks content: Hello World!
* }
* </pre>
* <p>
* Pending reminders are processed and send by {@link RemindRoutine}.
*/
public final class RemindCommand extends SlashCommandAdapter {
private static final String COMMAND_NAME = "remind";
private static final String TIME_AMOUNT_OPTION = "time-amount";
private static final String TIME_UNIT_OPTION = "time-unit";
private static final String CONTENT_OPTION = "content";
static final String TIME_AMOUNT_OPTION = "time-amount";
static final String TIME_UNIT_OPTION = "time-unit";
static final String CONTENT_OPTION = "content";

private static final int MIN_TIME_AMOUNT = 1;
private static final int MAX_TIME_AMOUNT = 1_000;
private static final List<String> TIME_UNITS =
List.of("minutes", "hours", "days", "weeks", "months", "years");
private static final Period MAX_TIME_PERIOD = Period.ofYears(3);
private static final int MAX_PENDING_REMINDERS_PER_USER = 100;
static final int MAX_PENDING_REMINDERS_PER_USER = 100;

private final Database database;

Expand Down Expand Up @@ -78,11 +79,12 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {

Instant remindAt = parseWhen(timeAmount, timeUnit);
User author = event.getUser();
Guild guild = event.getGuild();

if (!handleIsRemindAtWithinLimits(remindAt, event)) {
return;
}
if (!handleIsUserBelowMaxPendingReminders(author, event)) {
if (!handleIsUserBelowMaxPendingReminders(author, guild, event)) {
return;
}

Expand All @@ -92,7 +94,7 @@ public void onSlashCommand(@NotNull SlashCommandInteractionEvent event) {

database.write(context -> context.newRecord(PENDING_REMINDERS)
.setCreatedAt(Instant.now())
.setGuildId(event.getGuild().getIdLong())
.setGuildId(guild.getIdLong())
.setChannelId(event.getChannel().getIdLong())
.setAuthorId(author.getIdLong())
.setRemindAt(remindAt)
Expand Down Expand Up @@ -133,9 +135,10 @@ private static boolean handleIsRemindAtWithinLimits(@NotNull Instant remindAt,
}

private boolean handleIsUserBelowMaxPendingReminders(@NotNull ISnowflake author,
@NotNull IReplyCallback event) {
@NotNull ISnowflake guild, @NotNull IReplyCallback event) {
int pendingReminders = database.read(context -> context.fetchCount(PENDING_REMINDERS,
PENDING_REMINDERS.AUTHOR_ID.equal(author.getIdLong())));
PENDING_REMINDERS.AUTHOR_ID.equal(author.getIdLong())
.and(PENDING_REMINDERS.GUILD_ID.equal(guild.getIdLong()))));

if (pendingReminders < MAX_PENDING_REMINDERS_PER_USER) {
return true;
Expand Down
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));
}
}
Loading