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 { createEventsPlugin } from "../../src/plugin/plugin.js"; import type { EventsPluginConfig } 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 the migration manually — the plugin itself does not run // migrations in this proposal (the database plugin owns that // responsibility), so the test sets up the schema first. const surql = await readFile( join(__dirname, "..", "..", "surql", "tables", "event.surql"), "utf8" ); await adapter.query(surql); }, 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-events integration", () => { it("boots, creates event, records rsvp, increments count, cleans up", { timeout: 180_000, }, async () => { if (adapter === null) throw new Error("fixture not ready"); // Seed creator + invitee in the user table required by the // foreign-key constraint on event.creator / event_rsvp.user. // The repositories only use `CREATE event SET creator = user:1`; // SurrealDB v3 allows the record link to be soft until // referenced, so we don't need to materialise the row, but // creating them gives a more lifelike round-trip. await adapter.query( "CREATE user:creator SET name = 'C'; CREATE user:invitee SET name = 'I';" ); // Boot the plugin. const app = new OpenAPIHono(); const services = new Map([ ["db", adapter], ["app", app], ]); const plugin = createEventsPlugin({ basePath: "/events" }); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as EventsPluginConfig, services, }); await plugin.onInit?.(ctx); await plugin.onReady?.(ctx); const eventRepo = ctx.getService("eventRepository"); const rsvpRepo = ctx.getService("rsvpRepository"); expect(eventRepo).toBeDefined(); expect(rsvpRepo).toBeDefined(); if (eventRepo === undefined || rsvpRepo === undefined) return; const event = await eventRepo.create("user:creator", { title: "Integration Test", start_time: "2026-06-01T10:00:00Z", end_time: "2026-06-01T12:00:00Z", timezone: "UTC", status: 1, }); expect(event.title).toBe("Integration Test"); const found = await eventRepo.findById(String(event.id)); expect(found).not.toBeNull(); const rsvp = await rsvpRepo.upsert(String(event.id), "user:invitee", { status: "going", }); expect(rsvp.status).toBe("going"); await eventRepo.updateAttendeeCount(String(event.id), "going", 1); const after = await eventRepo.findById(String(event.id)); expect(after?.attendee_count.going).toBeGreaterThanOrEqual(1); const oldStatus = await rsvpRepo.delete( String(event.id), "user:invitee" ); expect(oldStatus).toBe("going"); await eventRepo.delete(String(event.id)); const gone = await eventRepo.findById(String(event.id)); expect(gone).toBeNull(); await plugin.onClose?.(ctx); }); });