import { readFile } from "node:fs/promises"; import { join } from "node:path"; import { OpenAPIHono } from "@hono/zod-openapi"; import { createPluginContext } from "@suaveplan/api-plugin"; import { EventBus } from "@suaveplan/events"; import { SurrealAdapter } from "@suaveplan/surrealdb"; import { afterAll, beforeAll, describe, expect, it, } from "@suaveplan/testing/runner"; import { SurrealDBFixture } from "@suaveplan/testing-integration-surrealdb"; import { createNotificationsPlugin } from "../../src/plugin/plugin.js"; import type { NotificationsPluginConfig } from "../../src/schemas/options.js"; let fixture: SurrealDBFixture | null = null; let cfg: { url: string; namespace: string; database: string; username: string; password: string; } | null = null; let adapter: SurrealAdapter | null = null; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; beforeAll(async () => { fixture = new SurrealDBFixture(); cfg = await fixture.start(); adapter = new SurrealAdapter({ url: cfg.url, namespace: cfg.namespace, database: cfg.database, username: cfg.username, password: cfg.password, }); await adapter.connect(); // Apply both migrations manually — the plugin does not own // migration execution. const notif = await readFile( join(__dirname, "..", "..", "surql", "tables", "notification.surql"), "utf8" ); const device = await readFile( join(__dirname, "..", "..", "surql", "tables", "device_token.surql"), "utf8" ); await adapter.query(notif); await adapter.query(device); }, 180_000); afterAll(async () => { if (adapter !== null) { await adapter.disconnect(); adapter = null; } if (fixture !== null) { await fixture.stop(); fixture = null; } }, 60_000); describe("@suaveplan/api-plugin-notifications integration", () => { it("boots, registers device, creates + reads + dismisses + deletes notification", { timeout: 180_000, }, async () => { if (adapter === null) throw new Error("fixture not ready"); // Seed recipient + site so the record-link constraints pass. await adapter.query( "CREATE user:recipient SET name = 'R'; CREATE site:main SET name = 'S';" ); const app = new OpenAPIHono(); const services = new Map([ ["db", adapter], ["app", app], ]); const plugin = createNotificationsPlugin(); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as NotificationsPluginConfig, services, }); await plugin.onInit?.(ctx); await plugin.onReady?.(ctx); const repo = ctx.getService("notificationRepository"); const tokens = ctx.getService("deviceTokenRepository"); expect(repo).toBeDefined(); expect(tokens).toBeDefined(); if (repo === undefined || tokens === undefined) return; const created = await repo.create({ type: "friend_request", recipient: "user:recipient", site: "site:main", content: "sent you a friend request", }); expect(created.id).toBeDefined(); const fetched = await repo.findById(String(created.id)); expect(fetched).not.toBeNull(); const unread = await repo.getUnreadCount("user:recipient", "site:main"); expect(unread).toBeGreaterThanOrEqual(1); const read = await repo.markAsRead(String(created.id)); expect(read?.read).toBe(true); const dismissed = await repo.dismiss(String(created.id)); expect(dismissed?.dismissed).toBe(true); const list = await repo.findByRecipient("user:recipient", "site:main", { limit: 10, }); expect(list.length).toBeGreaterThanOrEqual(1); const deleted = await repo.delete(String(created.id)); expect(deleted).toBe(true); // Device-token round-trip. const tok = await tokens.register("user:recipient", { deviceToken: `tok-${Date.now()}`, deviceId: "dev1", deviceType: "ios", environment: "sandbox", }); expect(tok.platform).toBe("ios"); const active = await tokens.findActiveForUser("user:recipient"); expect(active.length).toBeGreaterThanOrEqual(1); const off = await tokens.deactivate(String(tok.id)); expect(off).toBe(true); await plugin.onClose?.(ctx); }); });