135 lines
8.2 KiB
TypeScript
135 lines
8.2 KiB
TypeScript
import { afterEach, expect, test } from 'bun:test';
|
|
import { eq } from 'drizzle-orm';
|
|
import { reminderDeliveries, users } from '../db/schema';
|
|
import { discordFixture, control, submit, messageText, components } from './test-fixture';
|
|
import { commands } from './commands';
|
|
let f: ReturnType<typeof discordFixture>;
|
|
afterEach(() => f?.close());
|
|
const click = (reply: any, label: string) => ({ type: 3, message: { id: '123' }, data: { custom_id: control(reply, label) } });
|
|
const draft = { name: 'Take a breath', times: '09:00, 18:30', days: 'Mon, Wed, Fri', date: '' };
|
|
async function choose(list: any, guild = false, index = '0') {
|
|
return f.run(click(list, 'Manage reminder'), guild);
|
|
}
|
|
test('settings and reminders are global commands in both contexts', () => {
|
|
for (const name of ['settings', 'reminders']) expect(commands.find(c => c.name === name)?.contexts).toEqual([0, 1]);
|
|
});
|
|
for (const guild of [false, true]) test(`full named reminder management shares website state in ${guild ? 'server' : 'DM'}`, async () => {
|
|
f = discordFixture(true);
|
|
await f.json('/reminders', 'PUT', { enabled: false, reminders: [], quietStart: '22:00', quietEnd: '08:00' });
|
|
let list = await f.run({ data: { name: 'reminders' } }, guild);
|
|
expect(list.data.flags).toBe(32768 | (guild ? 64 : 0));
|
|
const form = await f.run(click(list, 'Add reminder'), guild);
|
|
const interaction = f.payload(submit(form.data.custom_id, draft), guild);
|
|
list = await (await f.requestDiscord(interaction)).json();
|
|
f.restart(); await f.requestDiscord(interaction);
|
|
let web = await f.json('/reminders');
|
|
expect(web.reminders).toEqual(['09:00', '18:30'].map(time => ({ type: 'weekly', name: draft.name, time, days: [1, 3, 5] })));
|
|
expect(f.changed).toEqual(['alice']);
|
|
list = await f.run(click(list, 'Enable delivery'), guild);
|
|
expect((await f.json('/reminders')).enabled).toBe(true);
|
|
let detail = await choose(list, guild);
|
|
const edit = await f.run(click(detail, 'Edit reminder'), guild);
|
|
list = await f.run(submit(edit.data.custom_id, { name: 'Birthday', times: '12:00, 14:00', days: '', date: '2026-09-30' }), guild);
|
|
web = await f.json('/reminders');
|
|
expect(web.reminders).toEqual(['12:00', '14:00'].map(time => ({ type: 'date', name: 'Birthday', time, date: '2026-09-30' })));
|
|
const quiet = await f.run(click(list, 'Quiet hours'), guild);
|
|
list = await f.run(submit(quiet.data.custom_id, { start: '23:00', end: '07:00' }), guild);
|
|
expect((await f.json('/reminders')).quietStart).toBe('23:00');
|
|
detail = await choose(list, guild);
|
|
const confirmation = await f.run(click(detail, 'Remove reminder'), guild);
|
|
list = await f.run(click(confirmation, 'Confirm removal'), guild);
|
|
expect((await f.json('/reminders')).reminders).toEqual([]);
|
|
expect((await f.json('/reminders')).enabled).toBe(false);
|
|
expect(messageText(list)).toContain('No reminders');
|
|
const settings = await f.run({ data: { name: 'settings' } }, guild);
|
|
const tz = await f.run(click(settings, 'Change timezone'), guild);
|
|
await f.run(submit(tz.data.custom_id, { timezone: 'America/New_York' }), guild);
|
|
expect((await f.json('/me')).timezone).toBe('America/New_York');
|
|
expect((await f.json('/today')).timezone).toBe('America/New_York');
|
|
expect(f.changed.length).toBe(6);
|
|
});
|
|
|
|
test('bad input can reopen a prefilled form without consuming the valid control', async () => {
|
|
f = discordFixture();
|
|
const list = await f.run({ data: { name: 'reminders' } });
|
|
let form = await f.run(click(list, 'Add reminder'));
|
|
for (const invalid of [{ ...draft, times: '25:00' }, { ...draft, days: 'Funday' }, { ...draft, days: '', date: '2026-02-30' }, { ...draft, date: '2026-09-30' }]) {
|
|
const failed = await f.run(submit(form.data.custom_id, invalid));
|
|
expect(failed.type).toBe(7);
|
|
form = await f.run(click(failed, 'Try again'));
|
|
expect(form.type).toBe(9);
|
|
expect(form.data.components[0].components[0].value).toBe(draft.name);
|
|
}
|
|
const saved = await f.run(submit(form.data.custom_id, draft));
|
|
expect(messageText(saved)).toContain(draft.name);
|
|
const enabled = await f.run(click(saved, 'Enable delivery'));
|
|
expect(messageText(enabled)).toContain('not configured');
|
|
expect((await f.json('/reminders')).enabled).toBe(false);
|
|
const settings = await f.run({ data: { name: 'settings' } });
|
|
const timezone = await f.run(click(settings, 'Change timezone'));
|
|
const invalid = await f.run(submit(timezone.data.custom_id, { timezone: 'Invalid/Zone' }));
|
|
expect(messageText(invalid)).toContain('Use a timezone');
|
|
const retry = await f.run(click(invalid, 'Try again'));
|
|
await f.run(submit(retry.data.custom_id, { timezone: 'UTC' }));
|
|
expect((await f.json('/me')).timezone).toBe('UTC');
|
|
});
|
|
|
|
test('website edits and timezone changes invalidate old controls even within one millisecond', async () => {
|
|
f = discordFixture();
|
|
const list = await f.run({ data: { name: 'reminders' } });
|
|
const form = await f.run(click(list, 'Add reminder'));
|
|
const before = await f.json('/reminders');
|
|
const prefs = { enabled: false, reminders: before.reminders, quietStart: '22:00', quietEnd: '08:00' };
|
|
await f.json('/reminders', 'PUT', prefs);
|
|
const blocked = await f.run(submit(form.data.custom_id, draft));
|
|
expect(messageText(blocked)).toContain('Settings changed');
|
|
const newer = await f.run({ data: { name: 'reminders' } });
|
|
const newerForm = await f.run(click(newer, 'Add reminder'));
|
|
await f.json('/reminders', 'PUT', { ...prefs, quietStart: '21:00' });
|
|
await f.json('/reminders', 'PUT', prefs);
|
|
expect(messageText((await f.run(submit(newerForm.data.custom_id, draft))))).toContain('Settings changed');
|
|
const settings = await f.run({ data: { name: 'settings' } });
|
|
const tz = await f.run(click(settings, 'Change timezone'));
|
|
await f.json('/me', 'PATCH', { timezone: 'UTC' });
|
|
await f.json('/me', 'PATCH', { timezone: 'Europe/Belgrade' });
|
|
expect(messageText((await f.run(submit(tz.data.custom_id, { timezone: 'Asia/Tokyo' }))))).toContain('Settings changed');
|
|
expect((await f.json('/me')).timezone).toBe('Europe/Belgrade');
|
|
});
|
|
|
|
test('old forms cannot cross accounts; new Discord accounts discover UTC setup', async () => {
|
|
f = discordFixture();
|
|
const list = await f.run({ data: { name: 'reminders' } });
|
|
const form = await f.run(click(list, 'Add reminder'));
|
|
const blocked = await f.run(submit(form.data.custom_id, draft), true, f.bob);
|
|
expect(blocked.data.flags).toBe(64);
|
|
expect(messageText(blocked)).toContain('belongs to another user');
|
|
expect((await f.json('/reminders', 'GET', undefined, 200, 'b')).reminders).toHaveLength(1);
|
|
const id = '555555555555555555';
|
|
const settings = await f.run({ data: { name: 'settings' } }, false, id);
|
|
expect(messageText(settings)).toContain('New accounts start in UTC');
|
|
expect(f.db.select().from(users).where(eq(users.discordId, id)).get()?.timezone).toBe('UTC');
|
|
});
|
|
|
|
test('paginated list, 20-slot limit, delivery status and quiet-hour validation', async () => {
|
|
f = discordFixture(true);
|
|
const reminders = Array.from({ length: 20 }, (_, i) => ({ type: 'weekly', name: `Reminder ${i + 1}`, time: '12:00', days: [1] }));
|
|
await f.json('/reminders', 'PUT', { enabled: false, reminders, quietStart: '22:00', quietEnd: '08:00' });
|
|
f.db.insert(reminderDeliveries).values({ id: 'delivery', userId: 'alice', date: '2026-09-04', status: 'pending', updatedAt: 1 }).run();
|
|
const list = await f.run({ data: { name: 'reminders' } });
|
|
expect(messageText(list)).toContain('Delivery unconfirmed');
|
|
const next = await f.run(click(list, 'Next'));
|
|
expect(messageText(next)).toContain('Reminder 12');
|
|
const form = await f.run(click(next, 'Add reminder'));
|
|
const fail = await f.run(submit(form.data.custom_id, draft));
|
|
expect(messageText(fail)).toContain('up to 20');
|
|
expect((await f.json('/reminders')).reminders).toHaveLength(20);
|
|
const quiet = await f.run(click(list, 'Quiet hours'));
|
|
const bad = await f.run(submit(quiet.data.custom_id, { start: '25:00', end: '00:00' }));
|
|
const retry = await f.run(click(bad, 'Try again'));
|
|
const done = await f.run(submit(retry.data.custom_id, { start: '00:00', end: '00:00' }));
|
|
expect(messageText(done)).toContain('(off)');
|
|
const expired = await f.run({ data: { name: 'settings' } });
|
|
f.setDiscordTime('2026-09-04T12:16:00Z');
|
|
expect(messageText((await f.run(click(expired, 'Change timezone'))))).toContain('expired');
|
|
});
|