import { describe, expect, it } from "@suaveplan/testing/runner"; import type { Notification } from "../types/notification.types.js"; import type { InAppNotification, InAppStore } from "./in-app-channel.js"; import { InAppChannel } from "./in-app-channel.js"; function makeNotification(overrides: Partial = {}): Notification { return { id: "notif_1", channel: "in_app", recipient: "user123", subject: "Test", body: "In-app message", priority: "normal", status: "sending", createdAt: new Date().toISOString(), ...overrides, }; } describe("InAppChannel", () => { it('has name "in_app"', { timeout: 5000 }, () => { const channel = new InAppChannel(); expect(channel.name).toBe("in_app"); }); it("canSend returns true for in_app channel", { timeout: 5000 }, () => { const channel = new InAppChannel(); expect(channel.canSend(makeNotification())).toBe(true); }); it("canSend returns false for non in_app channel", { timeout: 5000, }, () => { const channel = new InAppChannel(); expect(channel.canSend(makeNotification({ channel: "email" }))).toBe( false ); }); it("sends and stores notification in memory", { timeout: 30000, }, async () => { const channel = new InAppChannel(); const result = await channel.send(makeNotification()); expect(result.success).toBe(true); expect(result.messageId).toBe("notif_1"); const stored = await channel.getByRecipient("user123"); expect(stored).toHaveLength(1); const first = stored[0]; expect(first?.body).toBe("In-app message"); }); it("stores multiple notifications per recipient", { timeout: 30000, }, async () => { const channel = new InAppChannel(); await channel.send(makeNotification({ id: "n1" })); await channel.send(makeNotification({ id: "n2", body: "Second" })); const stored = await channel.getByRecipient("user123"); expect(stored).toHaveLength(2); }); it("returns empty array for unknown recipient", { timeout: 30000, }, async () => { const channel = new InAppChannel(); const stored = await channel.getByRecipient("unknown"); expect(stored).toHaveLength(0); }); it("marks notification as read", { timeout: 30000 }, async () => { const channel = new InAppChannel(); await channel.send(makeNotification()); await channel.markRead("notif_1"); const stored = await channel.getByRecipient("user123"); expect(stored[0]?.read).toBe(true); }); it("markRead on unknown id is a no-op", { timeout: 5000 }, async () => { const channel = new InAppChannel(); await channel.send(makeNotification()); await channel.markRead("does-not-exist"); const stored = await channel.getByRecipient("user123"); expect(stored[0]?.read).toBe(false); }); it("uses custom store when provided", { timeout: 30000 }, async () => { const records: InAppNotification[] = []; const customStore: InAppStore = { async save(n) { records.push(n); }, async getByRecipient() { return records; }, async markRead() { /* noop */ }, }; const channel = new InAppChannel(customStore); await channel.send(makeNotification()); expect(records).toHaveLength(1); }); it("handles store save failure gracefully", { timeout: 30000, }, async () => { const failingStore: InAppStore = { async save() { throw new Error("DB write failed"); }, async getByRecipient() { return []; }, async markRead() { /* noop */ }, }; const channel = new InAppChannel(failingStore); const result = await channel.send(makeNotification()); expect(result.success).toBe(false); expect(result.error).toBe("DB write failed"); }); it("returns string body when store throws non-Error", { timeout: 5000, }, async () => { const failingStore: InAppStore = { async save() { const literal: unknown = "boom"; throw literal; }, async getByRecipient() { return []; }, async markRead() { /* noop */ }, }; const channel = new InAppChannel(failingStore); const result = await channel.send(makeNotification()); expect(result.success).toBe(false); expect(result.error).toBe("boom"); }); it("notification stored as unread by default", { timeout: 30000, }, async () => { const channel = new InAppChannel(); await channel.send(makeNotification()); const stored = await channel.getByRecipient("user123"); expect(stored[0]?.read).toBe(false); }); it("preserves metadata when notification carries it", { timeout: 5000, }, async () => { const channel = new InAppChannel(); await channel.send(makeNotification({ metadata: { trace: "x-1" } })); const stored = await channel.getByRecipient("user123"); expect(stored[0]?.metadata).toEqual({ trace: "x-1" }); }); it("supports notifications without metadata and without subject", { timeout: 5000, }, async () => { const channel = new InAppChannel(); const { subject: _drop, ...rest } = makeNotification(); await channel.send(rest as Notification); const stored = await channel.getByRecipient("user123"); expect(stored[0]?.subject).toBeUndefined(); }); });