Skip to content

Commit 0e1cfb5

Browse files
committed
Unit tests for /remind
1 parent b98f37f commit 0e1cfb5

File tree

5 files changed

+388
-1
lines changed

5 files changed

+388
-1
lines changed

application/src/main/java/org/togetherjava/tjbot/commands/reminder/RemindRoutine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
* Reminders can be set by using {@link RemindCommand}.
2828
*/
2929
public final class RemindRoutine implements Routine {
30-
private static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class);
30+
static final Logger logger = LoggerFactory.getLogger(RemindRoutine.class);
3131
private static final Color AMBIENT_COLOR = Color.decode("#F7F492");
3232
private static final int SCHEDULE_INTERVAL_SECONDS = 30;
3333
private final Database database;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package org.togetherjava.tjbot.commands.reminder;
2+
3+
import net.dv8tion.jda.api.entities.Member;
4+
import net.dv8tion.jda.api.entities.TextChannel;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.togetherjava.tjbot.db.Database;
7+
import org.togetherjava.tjbot.db.generated.Tables;
8+
import org.togetherjava.tjbot.db.generated.tables.records.PendingRemindersRecord;
9+
import org.togetherjava.tjbot.jda.JdaTester;
10+
11+
import java.time.Instant;
12+
import java.util.List;
13+
14+
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS;
15+
16+
final class RawReminderTestHelper {
17+
private Database database;
18+
private JdaTester jdaTester;
19+
20+
RawReminderTestHelper(@NotNull Database database, @NotNull JdaTester jdaTester) {
21+
this.database = database;
22+
this.jdaTester = jdaTester;
23+
}
24+
25+
void insertReminder(@NotNull String content, @NotNull Instant remindAt) {
26+
insertReminder(content, remindAt, jdaTester.getMemberSpy(), jdaTester.getTextChannelSpy());
27+
}
28+
29+
void insertReminder(@NotNull String content, @NotNull Instant remindAt,
30+
@NotNull Member author) {
31+
insertReminder(content, remindAt, author, jdaTester.getTextChannelSpy());
32+
}
33+
34+
void insertReminder(@NotNull String content, @NotNull Instant remindAt, @NotNull Member author,
35+
@NotNull TextChannel channel) {
36+
long channelId = channel.getIdLong();
37+
long guildId = channel.getGuild().getIdLong();
38+
long authorId = author.getIdLong();
39+
40+
database.write(context -> context.newRecord(Tables.PENDING_REMINDERS)
41+
.setCreatedAt(Instant.now())
42+
.setGuildId(guildId)
43+
.setChannelId(channelId)
44+
.setAuthorId(authorId)
45+
.setRemindAt(remindAt)
46+
.setContent(content)
47+
.insert());
48+
}
49+
50+
@NotNull
51+
List<String> readReminders() {
52+
return readReminders(jdaTester.getMemberSpy());
53+
}
54+
55+
@NotNull
56+
List<String> readReminders(@NotNull Member author) {
57+
long guildId = jdaTester.getTextChannelSpy().getGuild().getIdLong();
58+
long authorId = author.getIdLong();
59+
60+
return database.read(context -> context.selectFrom(PENDING_REMINDERS)
61+
.where(PENDING_REMINDERS.AUTHOR_ID.eq(authorId)
62+
.and(PENDING_REMINDERS.GUILD_ID.eq(guildId)))
63+
.stream()
64+
.map(PendingRemindersRecord::getContent)
65+
.toList());
66+
}
67+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package org.togetherjava.tjbot.commands.reminder;
2+
3+
import net.dv8tion.jda.api.entities.Member;
4+
import net.dv8tion.jda.api.events.interaction.command.SlashCommandInteractionEvent;
5+
import org.jetbrains.annotations.NotNull;
6+
import org.junit.jupiter.api.Assertions;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.function.Executable;
11+
import org.togetherjava.tjbot.commands.SlashCommand;
12+
import org.togetherjava.tjbot.db.Database;
13+
import org.togetherjava.tjbot.jda.JdaTester;
14+
15+
import java.time.Instant;
16+
import java.time.temporal.ChronoUnit;
17+
import java.util.List;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
import static org.mockito.ArgumentMatchers.startsWith;
22+
import static org.mockito.Mockito.verify;
23+
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS;
24+
25+
final class RemindCommandTest {
26+
private SlashCommand command;
27+
private JdaTester jdaTester;
28+
private RawReminderTestHelper rawReminders;
29+
30+
@BeforeEach
31+
void setUp() {
32+
Database database = Database.createMemoryDatabase(PENDING_REMINDERS);
33+
command = new RemindCommand(database);
34+
jdaTester = new JdaTester();
35+
rawReminders = new RawReminderTestHelper(database, jdaTester);
36+
}
37+
38+
private @NotNull SlashCommandInteractionEvent triggerSlashCommand(int timeAmount,
39+
@NotNull String timeUnit, @NotNull String content) {
40+
return triggerSlashCommand(timeAmount, timeUnit, content, jdaTester.getMemberSpy());
41+
}
42+
43+
private @NotNull SlashCommandInteractionEvent triggerSlashCommand(int timeAmount,
44+
@NotNull String timeUnit, @NotNull String content, @NotNull Member author) {
45+
SlashCommandInteractionEvent event = jdaTester.createSlashCommandInteractionEvent(command)
46+
.setOption(RemindCommand.TIME_AMOUNT_OPTION, timeAmount)
47+
.setOption(RemindCommand.TIME_UNIT_OPTION, timeUnit)
48+
.setOption(RemindCommand.CONTENT_OPTION, content)
49+
.setUserWhoTriggered(author)
50+
.build();
51+
52+
command.onSlashCommand(event);
53+
return event;
54+
}
55+
56+
@Test
57+
@DisplayName("Throws an exception if the time unit is not supported, i.e. not part of the actual choice dialog")
58+
void throwsWhenGivenUnsupportedUnit() {
59+
// GIVEN
60+
// WHEN triggering /remind with the unsupported time unit 'nanoseconds'
61+
Executable triggerRemind = () -> triggerSlashCommand(10, "nanoseconds", "foo");
62+
63+
// THEN command throws, no reminder was created
64+
Assertions.assertThrows(IllegalArgumentException.class, triggerRemind);
65+
assertTrue(rawReminders.readReminders().isEmpty());
66+
}
67+
68+
@Test
69+
@DisplayName("Rejects a reminder time that is set too far in the future and responds accordingly")
70+
void doesNotSupportDatesTooFarInFuture() {
71+
// GIVEN
72+
// WHEN triggering /remind too far in the future
73+
SlashCommandInteractionEvent event = triggerSlashCommand(10, "years", "foo");
74+
75+
// THEN rejects and responds accordingly, no reminder was created
76+
verify(event).reply(startsWith("The reminder is set too far in the future"));
77+
assertTrue(rawReminders.readReminders().isEmpty());
78+
}
79+
80+
@Test
81+
@DisplayName("Rejects a reminder if a user has too many reminders still pending")
82+
void userIsLimitedIfTooManyPendingReminders() {
83+
// GIVEN a user with too many reminders still pending
84+
Instant remindAt = Instant.now().plus(100, ChronoUnit.DAYS);
85+
for (int i = 0; i < RemindCommand.MAX_PENDING_REMINDERS_PER_USER; i++) {
86+
rawReminders.insertReminder("foo " + i, remindAt);
87+
}
88+
89+
// WHEN triggering another reminder
90+
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo");
91+
92+
// THEN rejects and responds accordingly, no new reminder was created
93+
verify(event)
94+
.reply(startsWith("You have reached the maximum amount of pending reminders per user"));
95+
assertEquals(RemindCommand.MAX_PENDING_REMINDERS_PER_USER,
96+
rawReminders.readReminders().size());
97+
}
98+
99+
@Test
100+
@DisplayName("Does not limit a user if another user has too many reminders still pending, i.e. the limit is per user")
101+
void userIsNotLimitedIfOtherUserHasTooManyPendingReminders() {
102+
// GIVEN a user with too many reminders still pending,
103+
// and a second user with no reminders yet
104+
Member firstUser = jdaTester.createMemberSpy(1);
105+
Instant remindAt = Instant.now().plus(100, ChronoUnit.DAYS);
106+
for (int i = 0; i < RemindCommand.MAX_PENDING_REMINDERS_PER_USER; i++) {
107+
rawReminders.insertReminder("foo " + i, remindAt, firstUser);
108+
}
109+
110+
Member secondUser = jdaTester.createMemberSpy(2);
111+
112+
// WHEN the second user triggers another reminder
113+
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo", secondUser);
114+
115+
// THEN accepts the reminder and responds accordingly
116+
verify(event).reply("Will remind you about 'foo' in 5 minutes.");
117+
118+
List<String> remindersOfSecondUser = rawReminders.readReminders(secondUser);
119+
assertEquals(1, remindersOfSecondUser.size());
120+
assertEquals("foo", remindersOfSecondUser.get(0));
121+
}
122+
123+
@Test
124+
@DisplayName("The command can create a reminder, the regular base case")
125+
void canCreateReminders() {
126+
// GIVEN
127+
// WHEN triggering the /remind command
128+
SlashCommandInteractionEvent event = triggerSlashCommand(5, "minutes", "foo");
129+
130+
// THEN accepts the reminder and responds accordingly
131+
verify(event).reply("Will remind you about 'foo' in 5 minutes.");
132+
133+
List<String> pendingReminders = rawReminders.readReminders();
134+
assertEquals(1, pendingReminders.size());
135+
assertEquals("foo", pendingReminders.get(0));
136+
}
137+
}
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
package org.togetherjava.tjbot.commands.reminder;
2+
3+
import net.dv8tion.jda.api.entities.*;
4+
import net.dv8tion.jda.api.requests.ErrorResponse;
5+
import net.dv8tion.jda.api.requests.RestAction;
6+
import org.jetbrains.annotations.NotNull;
7+
import org.junit.jupiter.api.BeforeEach;
8+
import org.junit.jupiter.api.DisplayName;
9+
import org.junit.jupiter.api.Test;
10+
import org.mockito.ArgumentCaptor;
11+
import org.mockito.ArgumentMatchers;
12+
import org.togetherjava.tjbot.commands.Routine;
13+
import org.togetherjava.tjbot.db.Database;
14+
import org.togetherjava.tjbot.jda.JdaTester;
15+
16+
import java.time.Instant;
17+
import java.time.temporal.ChronoUnit;
18+
import java.util.concurrent.TimeUnit;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertTrue;
22+
import static org.mockito.Mockito.*;
23+
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS;
24+
25+
final class RemindRoutineTest {
26+
private Routine routine;
27+
private JdaTester jdaTester;
28+
private RawReminderTestHelper rawReminders;
29+
30+
@BeforeEach
31+
void setUp() {
32+
Database database = Database.createMemoryDatabase(PENDING_REMINDERS);
33+
routine = new RemindRoutine(database);
34+
jdaTester = new JdaTester();
35+
rawReminders = new RawReminderTestHelper(database, jdaTester);
36+
}
37+
38+
private void triggerRoutine() {
39+
routine.runRoutine(jdaTester.getJdaMock());
40+
}
41+
42+
private static @NotNull MessageEmbed getLastMessageFrom(@NotNull MessageChannel channel) {
43+
ArgumentCaptor<MessageEmbed> responseCaptor = ArgumentCaptor.forClass(MessageEmbed.class);
44+
verify(channel).sendMessageEmbeds(responseCaptor.capture());
45+
return responseCaptor.getValue();
46+
}
47+
48+
private @NotNull Member createAndSetupUnknownMember() {
49+
int unknownMemberId = 2;
50+
51+
Member member = jdaTester.createMemberSpy(unknownMemberId);
52+
53+
RestAction<User> unknownMemberAction = jdaTester.createFailedActionMock(
54+
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
55+
when(jdaTester.getJdaMock().retrieveUserById(unknownMemberId))
56+
.thenReturn(unknownMemberAction);
57+
58+
RestAction<PrivateChannel> unknownPrivateChannelAction = jdaTester.createFailedActionMock(
59+
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
60+
when(jdaTester.getJdaMock().openPrivateChannelById(anyLong()))
61+
.thenReturn(unknownPrivateChannelAction);
62+
when(jdaTester.getJdaMock().openPrivateChannelById(anyString()))
63+
.thenReturn(unknownPrivateChannelAction);
64+
65+
return member;
66+
}
67+
68+
private @NotNull TextChannel createAndSetupUnknownChannel() {
69+
long unknownChannelId = 2;
70+
71+
TextChannel channel = jdaTester.createTextChannelSpy(unknownChannelId);
72+
when(jdaTester.getJdaMock()
73+
.getChannelById(ArgumentMatchers.<Class<MessageChannel>>any(), eq(unknownChannelId)))
74+
.thenReturn(null);
75+
76+
return channel;
77+
}
78+
79+
@Test
80+
@DisplayName("Sends out a pending reminder to a guild channel, the base case")
81+
void sendsPendingReminderChannelFoundAuthorFound() {
82+
// GIVEN a pending reminder
83+
Instant remindAt = Instant.now();
84+
String reminderContent = "foo";
85+
Member author = jdaTester.getMemberSpy();
86+
rawReminders.insertReminder("foo", remindAt, author);
87+
88+
// WHEN running the routine
89+
triggerRoutine();
90+
91+
// THEN the reminder is sent out and deleted from the database
92+
assertTrue(rawReminders.readReminders().isEmpty());
93+
94+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy());
95+
assertEquals(reminderContent, lastMessage.getDescription());
96+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
97+
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName());
98+
}
99+
100+
@Test
101+
@DisplayName("Sends out a pending reminder to a guild channel, even if the author could not be retrieved anymore")
102+
void sendsPendingReminderChannelFoundAuthorNotFound() {
103+
// GIVEN a pending reminder from an unknown user
104+
Instant remindAt = Instant.now();
105+
String reminderContent = "foo";
106+
Member unknownAuthor = createAndSetupUnknownMember();
107+
rawReminders.insertReminder("foo", remindAt, unknownAuthor);
108+
109+
// WHEN running the routine
110+
triggerRoutine();
111+
112+
// THEN the reminder is sent out and deleted from the database
113+
assertTrue(rawReminders.readReminders().isEmpty());
114+
115+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy());
116+
assertEquals(reminderContent, lastMessage.getDescription());
117+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
118+
assertEquals("Unknown user", lastMessage.getAuthor().getName());
119+
}
120+
121+
@Test
122+
@DisplayName("Sends out a pending reminder via DM, even if the channel could not be retrieved anymore")
123+
void sendsPendingReminderChannelNotFoundAuthorFound() {
124+
// GIVEN a pending reminder from an unknown channel
125+
Instant remindAt = Instant.now();
126+
String reminderContent = "foo";
127+
Member author = jdaTester.getMemberSpy();
128+
TextChannel unknownChannel = createAndSetupUnknownChannel();
129+
rawReminders.insertReminder("foo", remindAt, author, unknownChannel);
130+
131+
// WHEN running the routine
132+
triggerRoutine();
133+
134+
// THEN the reminder is sent out and deleted from the database
135+
assertTrue(rawReminders.readReminders().isEmpty());
136+
137+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getPrivateChannelSpy());
138+
assertEquals(reminderContent, lastMessage.getDescription());
139+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
140+
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName());
141+
}
142+
143+
@Test
144+
@DisplayName("Skips a pending reminder if sending it out resulted in an error")
145+
void skipPendingReminderOnErrorChannelNotFoundAuthorNotFound() {
146+
// GIVEN a pending reminder and from an unknown channel and author
147+
Instant remindAt = Instant.now();
148+
String reminderContent = "foo";
149+
Member unknownAuthor = createAndSetupUnknownMember();
150+
TextChannel unknownChannel = createAndSetupUnknownChannel();
151+
rawReminders.insertReminder("foo", remindAt, unknownAuthor, unknownChannel);
152+
153+
// WHEN running the routine
154+
triggerRoutine();
155+
156+
// THEN the reminder is skipped and deleted from the database
157+
assertTrue(rawReminders.readReminders().isEmpty());
158+
}
159+
160+
@Test
161+
@DisplayName("A reminder that is not pending yet, is not send out")
162+
void reminderIsNotSendIfNotPending() {
163+
// GIVEN a reminder that is not pending yet
164+
Instant remindAt = Instant.now().plus(1, ChronoUnit.HOURS);
165+
String reminderContent = "foo";
166+
rawReminders.insertReminder("foo", remindAt);
167+
168+
// WHEN running the routine
169+
triggerRoutine();
170+
171+
// THEN the reminder is not send yet and still in the database
172+
assertEquals(1, rawReminders.readReminders().size());
173+
verify(jdaTester.getTextChannelSpy(), never()).sendMessageEmbeds(any(MessageEmbed.class));
174+
}
175+
176+
private static void assertSimilar(@NotNull Instant expected, @NotNull Instant actual) {
177+
// NOTE For some reason, the instant ends up in the database slightly wrong already (about
178+
// half a second), seems to be an issue with jOOQ
179+
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(1));
180+
}
181+
}

0 commit comments

Comments
 (0)