Skip to content

Commit 07e8a0b

Browse files
committed
Unit tests for /remind
1 parent 5bd313c commit 07e8a0b

File tree

4 files changed

+383
-1
lines changed

4 files changed

+383
-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: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
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.togetherjava.tjbot.commands.Routine;
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.concurrent.TimeUnit;
18+
19+
import static org.junit.jupiter.api.Assertions.assertEquals;
20+
import static org.junit.jupiter.api.Assertions.assertTrue;
21+
import static org.mockito.Mockito.*;
22+
import static org.togetherjava.tjbot.db.generated.tables.PendingReminders.PENDING_REMINDERS;
23+
24+
final class RemindRoutineTest {
25+
private Routine routine;
26+
private JdaTester jdaTester;
27+
private RawReminderTestHelper rawReminders;
28+
29+
@BeforeEach
30+
void setUp() {
31+
Database database = Database.createMemoryDatabase(PENDING_REMINDERS);
32+
routine = new RemindRoutine(database);
33+
jdaTester = new JdaTester();
34+
rawReminders = new RawReminderTestHelper(database, jdaTester);
35+
}
36+
37+
private void triggerRoutine() {
38+
routine.runRoutine(jdaTester.getJdaMock());
39+
}
40+
41+
private static @NotNull MessageEmbed getLastMessageFrom(@NotNull MessageChannel channel) {
42+
ArgumentCaptor<MessageEmbed> responseCaptor = ArgumentCaptor.forClass(MessageEmbed.class);
43+
verify(channel).sendMessageEmbeds(responseCaptor.capture());
44+
return responseCaptor.getValue();
45+
}
46+
47+
private @NotNull Member createAndSetupUnknownMember() {
48+
int unknownMemberId = 2;
49+
50+
Member member = jdaTester.createMemberSpy(unknownMemberId);
51+
52+
RestAction<User> unknownMemberAction = jdaTester.createFailedActionMock(
53+
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
54+
when(jdaTester.getJdaMock().retrieveUserById(unknownMemberId))
55+
.thenReturn(unknownMemberAction);
56+
57+
RestAction<PrivateChannel> unknownPrivateChannelAction = jdaTester.createFailedActionMock(
58+
jdaTester.createErrorResponseException(ErrorResponse.UNKNOWN_USER));
59+
when(jdaTester.getJdaMock().openPrivateChannelById(anyLong()))
60+
.thenReturn(unknownPrivateChannelAction);
61+
when(jdaTester.getJdaMock().openPrivateChannelById(anyString()))
62+
.thenReturn(unknownPrivateChannelAction);
63+
64+
return member;
65+
}
66+
67+
private @NotNull TextChannel createAndSetupUnknownChannel() {
68+
int unknownChannelId = 2;
69+
70+
TextChannel channel = jdaTester.createTextChannelSpy(unknownChannelId);
71+
when(jdaTester.getJdaMock().getTextChannelById(unknownChannelId)).thenReturn(null);
72+
73+
return channel;
74+
}
75+
76+
@Test
77+
@DisplayName("Sends out a pending reminder to a guild channel, the base case")
78+
void sendsPendingReminderChannelFoundAuthorFound() {
79+
// GIVEN a pending reminder
80+
Instant remindAt = Instant.now();
81+
String reminderContent = "foo";
82+
Member author = jdaTester.getMemberSpy();
83+
rawReminders.insertReminder("foo", remindAt, author);
84+
85+
// WHEN running the routine
86+
triggerRoutine();
87+
88+
// THEN the reminder is sent out and deleted from the database
89+
assertTrue(rawReminders.readReminders().isEmpty());
90+
91+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy());
92+
assertEquals(reminderContent, lastMessage.getDescription());
93+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
94+
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName());
95+
}
96+
97+
@Test
98+
@DisplayName("Sends out a pending reminder to a guild channel, even if the author could not be retrieved anymore")
99+
void sendsPendingReminderChannelFoundAuthorNotFound() {
100+
// GIVEN a pending reminder from an unknown user
101+
Instant remindAt = Instant.now();
102+
String reminderContent = "foo";
103+
Member unknownAuthor = createAndSetupUnknownMember();
104+
rawReminders.insertReminder("foo", remindAt, unknownAuthor);
105+
106+
// WHEN running the routine
107+
triggerRoutine();
108+
109+
// THEN the reminder is sent out and deleted from the database
110+
assertTrue(rawReminders.readReminders().isEmpty());
111+
112+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getTextChannelSpy());
113+
assertEquals(reminderContent, lastMessage.getDescription());
114+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
115+
assertEquals("Unknown user", lastMessage.getAuthor().getName());
116+
}
117+
118+
@Test
119+
@DisplayName("Sends out a pending reminder via DM, even if the channel could not be retrieved anymore")
120+
void sendsPendingReminderChannelNotFoundAuthorFound() {
121+
// GIVEN a pending reminder from an unknown channel
122+
Instant remindAt = Instant.now();
123+
String reminderContent = "foo";
124+
Member author = jdaTester.getMemberSpy();
125+
TextChannel unknownChannel = createAndSetupUnknownChannel();
126+
rawReminders.insertReminder("foo", remindAt, author, unknownChannel);
127+
128+
// WHEN running the routine
129+
triggerRoutine();
130+
131+
// THEN the reminder is sent out and deleted from the database
132+
assertTrue(rawReminders.readReminders().isEmpty());
133+
134+
MessageEmbed lastMessage = getLastMessageFrom(jdaTester.getPrivateChannelSpy());
135+
assertEquals(reminderContent, lastMessage.getDescription());
136+
assertSimilar(remindAt, lastMessage.getTimestamp().toInstant());
137+
assertEquals(author.getUser().getAsTag(), lastMessage.getAuthor().getName());
138+
}
139+
140+
@Test
141+
@DisplayName("Skips a pending reminder if sending it out resulted in an error")
142+
void skipPendingReminderOnErrorChannelNotFoundAuthorNotFound() {
143+
// GIVEN a pending reminder and from an unknown channel and author
144+
Instant remindAt = Instant.now();
145+
String reminderContent = "foo";
146+
Member unknownAuthor = createAndSetupUnknownMember();
147+
TextChannel unknownChannel = createAndSetupUnknownChannel();
148+
rawReminders.insertReminder("foo", remindAt, unknownAuthor, unknownChannel);
149+
150+
// WHEN running the routine
151+
triggerRoutine();
152+
153+
// THEN the reminder is skipped and deleted from the database
154+
assertTrue(rawReminders.readReminders().isEmpty());
155+
}
156+
157+
@Test
158+
@DisplayName("A reminder that is not pending yet, is not send out")
159+
void reminderIsNotSendIfNotPending() {
160+
// GIVEN a reminder that is not pending yet
161+
Instant remindAt = Instant.now().plus(1, ChronoUnit.HOURS);
162+
String reminderContent = "foo";
163+
rawReminders.insertReminder("foo", remindAt);
164+
165+
// WHEN running the routine
166+
triggerRoutine();
167+
168+
// THEN the reminder is not send yet and still in the database
169+
assertEquals(1, rawReminders.readReminders().size());
170+
verify(jdaTester.getTextChannelSpy(), never()).sendMessageEmbeds(any(MessageEmbed.class));
171+
}
172+
173+
private static void assertSimilar(@NotNull Instant expected, @NotNull Instant actual) {
174+
// NOTE For some reason, the instant ends up in the database slightly wrong already (about
175+
// half a second), seems to be an issue with jOOQ
176+
assertEquals(expected.toEpochMilli(), actual.toEpochMilli(), TimeUnit.SECONDS.toMillis(1));
177+
}
178+
}

0 commit comments

Comments
 (0)