Initial Commit
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user