174 lines
5.8 KiB
TypeScript
174 lines
5.8 KiB
TypeScript
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> = {}): 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();
|
|
});
|
|
});
|