import { OpenAPIHono } from "@hono/zod-openapi"; import { createOpenAPIRegistry } from "@suaveplan/api-openapi"; import { createPluginContext } from "@suaveplan/api-plugin"; import { betterAuthSurrealAdapter } from "@suaveplan/better-auth-surrealdb"; 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 { createAuthPlugin } from "../../src/plugin/plugin.js"; import type { AuthService } from "../../src/services/auth-service.js"; import type { AuthPluginConfig } from "../../src/types/types.js"; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; let fixture: SurrealDBFixture | null = null; let client: SurrealAdapter | null = null; beforeAll(async () => { fixture = new SurrealDBFixture(); const cfg = await fixture.start(); client = new SurrealAdapter({ url: cfg.url, namespace: cfg.namespace, database: cfg.database, username: cfg.username, password: cfg.password, }); await client.connect(); await client.execute("DEFINE TABLE OVERWRITE user SCHEMAFULL;"); await client.execute( "DEFINE FIELD OVERWRITE email ON user TYPE option;" ); await client.execute( "DEFINE FIELD OVERWRITE phoneNumber ON user TYPE option;" ); await client.execute( "DEFINE FIELD OVERWRITE createdAt ON user TYPE option DEFAULT time::now() VALUE $before OR time::now();" ); await client.execute( "DEFINE FIELD OVERWRITE updatedAt ON user TYPE option DEFAULT time::now() VALUE time::now();" ); }, 180_000); afterAll(async () => { if (client) { await client.disconnect(); client = null; } if (fixture) { await fixture.stop(); fixture = null; } }, 60_000); describe("full integration boot against real SurrealDB", () => { it("full sign-up/sign-in/session/sign-out roundtrip against real SurrealDB", { timeout: 120_000, }, async () => { if (client === null) throw new Error("client not ready"); const app = new OpenAPIHono(); const registry = createOpenAPIRegistry({ info: { title: "auth-test", version: "0.1.0" }, }); const plugin = createAuthPlugin({ betterAuth: { database: betterAuthSurrealAdapter({ client }), secret: "test-secret-at-least-32-chars-long-for-better-auth", baseURL: "http://localhost:3000/auth", emailAndPassword: { enabled: true }, }, }); const services = new Map(); services.set("app", app); services.set("openAPIRegistry", registry); services.set("db", client); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as AuthPluginConfig, services, }); await plugin.onInit?.(ctx); registry.applyTo(app, "/openapi.json"); // The Better-Auth instance is reachable via the registered // authService slot. We drive the lifecycle directly through // its API (which is what HTTP routes delegate to anyway) so // the test does not depend on Better-Auth's URL-routing // configuration which lives outside this plugin. const authService = services.get("authService") as AuthService; expect(authService).toBeDefined(); const auth = authService.getBetterAuthInstance(); const signUp = (await auth.api.signUpEmail({ body: { email: "integration@example.test", password: "Passw0rd!2026", name: "Integration User", }, })) as { user: { email: string } }; expect(signUp.user.email).toBe("integration@example.test"); const users = await client.query<{ email: string }>( "SELECT * FROM user WHERE email = 'integration@example.test';" ); expect(users.length).toBeGreaterThan(0); // OpenAPI document includes the 9 routes (prefixed). const openapiRes = await app.request("/openapi.json"); const doc = (await openapiRes.json()) as { paths: Record>; }; const paths = Object.keys(doc.paths ?? {}); const authPaths = paths.filter((p) => p.startsWith("/auth/")); expect(authPaths.length).toBe(9); // Hit the registered Hono routes — the OpenAPI route // handler delegates to auth.handler so any 2xx/3xx/4xx // response (rather than 404) confirms routing reached // Better-Auth. We use /auth/get-session which is the // canonical anonymous endpoint. const sessionRes = await app.request( "http://localhost:3000/auth/get-session" ); expect(sessionRes.status).not.toBe(404); }); it("schema apply is idempotent across restarts", { timeout: 60_000, }, async () => { if (client === null) throw new Error("client not ready"); const app = new OpenAPIHono(); const registry = createOpenAPIRegistry({ info: { title: "auth-test", version: "0.1.0" }, }); const plugin = createAuthPlugin({ betterAuth: { database: betterAuthSurrealAdapter({ client }), secret: "test-secret-at-least-32-chars-long-for-better-auth", baseURL: "http://localhost:3000", emailAndPassword: { enabled: true }, }, }); const services = new Map(); services.set("app", app); services.set("openAPIRegistry", registry); services.set("db", client); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as AuthPluginConfig, services, }); await plugin.onInit?.(ctx); const app2 = new OpenAPIHono(); const registry2 = createOpenAPIRegistry({ info: { title: "auth-test", version: "0.1.0" }, }); const plugin2 = createAuthPlugin({ betterAuth: { database: betterAuthSurrealAdapter({ client }), secret: "test-secret-at-least-32-chars-long-for-better-auth", baseURL: "http://localhost:3000", emailAndPassword: { enabled: true }, }, }); const services2 = new Map(); services2.set("app", app2); services2.set("openAPIRegistry", registry2); services2.set("db", client); const ctx2 = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin2.defaults as AuthPluginConfig, services: services2, }); await plugin2.onInit?.(ctx2); expect(registry2.getRoutesByPlugin("auth")).toHaveLength(9); }); });