import { generateKeyPairSync, sign } from 'node:crypto'; import { eq } from 'drizzle-orm'; import { fixture } from '../habits/test-fixture'; import { users } from '../db/schema'; import { createDiscordRoutes, type DeferredDispatch } from './routes'; import type { Interaction } from './types'; export function discordFixture(setup?: boolean | ((f: ReturnType, now: () => number) => DeferredDispatch)) { const f = fixture(); const { publicKey, privateKey } = generateKeyPairSync('ed25519'); const key = publicKey.export({ format: 'der', type: 'spki' }).subarray(-32).toString('hex'); let time = Date.parse('2026-09-04T12:00:00Z'), sequence = 10000000000000000n; const alice = '111111111111111111', bob = '222222222222222222', applicationId = '333333333333333333'; f.db.update(users).set({ discordId: alice }).where(eq(users.id, 'alice')).run(); f.db.update(users).set({ discordId: bob }).where(eq(users.id, 'bob')).run(); const changed: string[] = []; const dispatch = typeof setup === "function" ? setup(f, () => time) : undefined; const remindersAvailable = setup === true; let app = createDiscordRoutes(f.db, { publicKey: key, applicationId, origin: f.origin, remindersAvailable }, () => time, id => changed.push(id), dispatch); function payload(data: Partial, guild = false, user = alice): Interaction { const profile = { id: user, username: user === alice ? 'alice' : 'bob' }; return { id: String(++sequence), application_id: applicationId, type: 2, token: 'test-interaction-token', ...(guild ? { guild_id: '444444444444444444', member: { user: profile } } : { user: profile }), ...data }; } async function request(i: Interaction, headers: Record = {}) { const body = JSON.stringify(i), timestamp = String(time / 1000); return app.request('http://localhost/', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Signature-Timestamp': timestamp, 'X-Signature-Ed25519': sign(null, Buffer.from(timestamp + body), privateKey).toString('hex'), ...headers }, body }); } async function run(data: Partial, guild = false, user = alice): Promise { return (await request(payload(data, guild, user))).json(); } return { ...f, alice, bob, changed, payload, requestDiscord: request, run, restart: () => { app = createDiscordRoutes(f.db, { publicKey: key, applicationId, origin: f.origin, remindersAvailable }, () => time, id => changed.push(id), dispatch); }, setDiscordTime: (value: string) => { time = Date.parse(value); f.setTime(value); } }; } export function control(reply: any, label: string): string { const item = reply.data.components.flatMap((r: any) => r.components ?? []).find((c: any) => c.label === label || c.placeholder === label); if (!item) throw new Error(`Missing control ${label}: ${JSON.stringify(reply)}`); return item.custom_id; } export function submit(custom_id: string, values: Record) { return { type: 5, message: { id: '123' }, data: { custom_id, components: Object.entries(values).map(([custom_id, value]) => ({ type: 1, components: [{ type: 4, custom_id, value }] })) } }; }