// Ways a real collection goes wrong. The page lets you apply them; the tests apply the same functions. import type { Snapshot, SourceKey, SourceStatus } from "./types"; export type ScenarioId = "no_email_channel" | "cursor_expired" | "no_calendar_permission" | "stale_run"; export const scenarios: { id: ScenarioId; label: string; what: string }[] = [ { id: "no_email_channel", label: "Export messages without channel=Email", what: "Without that parameter the export returns every channel except email.", }, { id: "cursor_expired", label: "Export cursor expires halfway", what: "A cursor is valid for two minutes after the last request. A slow run loses the rest of the pages.", }, { id: "no_calendar_permission", label: "Token without calendar read access", what: "The token owner chose not to grant calendar scopes.", }, { id: "stale_run", label: "Report built from a three-day-old run", what: "The scheduled collection stopped and nobody noticed.", }, ]; function setStatus(s: Snapshot, key: SourceKey, status: SourceStatus, note: string): Snapshot { return { ...s, sources: s.sources.map((x) => (x.source === key ? { ...x, status, note } : x)) }; } export function applyScenario(s: Snapshot, id: ScenarioId): Snapshot { switch (id) { case "no_email_channel": return setStatus( { ...s, messages: s.messages.filter((m) => m.messageType !== "TYPE_EMAIL") }, "messages.email", "not_read", "Export called without channel=Email: email messages were not returned.", ); case "cursor_expired": { const sorted = [...s.messages].filter((m) => m.messageType !== "TYPE_EMAIL") .sort((a, b) => a.dateAdded.localeCompare(b.dateAdded)); const kept = new Set(sorted.slice(0, Math.floor(sorted.length / 2)).map((m) => m.id)); return setStatus( { ...s, messages: s.messages.filter((m) => m.messageType === "TYPE_EMAIL" || kept.has(m.id)) }, "messages.nonEmail", "partial", "Cursor expired after page 1; the remaining pages were not read.", ); } case "no_calendar_permission": return setStatus( setStatus({ ...s, events: [], notifications: {} }, "calendars.events", "forbidden", "401: token lacks calendar read scope."), "calendars.notifications", "forbidden", "401: token lacks calendar read scope.", ); case "stale_run": { // The last successful run was three days ago: it could not contain anything newer. const at = new Date(new Date(s.collectedAt).getTime() - 72 * 3600_000).toISOString(); const before = (d: string) => new Date(d).getTime() <= new Date(at).getTime(); return { ...s, collectedAt: at, contacts: s.contacts.filter((c) => before(c.dateAdded)), submissions: s.submissions.filter((x) => before(x.createdAt)), messages: s.messages.filter((m) => before(m.dateAdded)), events: s.events.filter((e) => before(e.dateAdded)), opportunities: s.opportunities.filter((o) => before(o.createdAt)), }; } } } export function applyScenarios(s: Snapshot, ids: ScenarioId[]): Snapshot { return ids.reduce(applyScenario, s); }