import { OpenAPIHono } from "@hono/zod-openapi"; import { createOpenAPIRegistry } from "@suaveplan/api-openapi"; import { createPluginContext } from "@suaveplan/api-plugin"; import { EventBus } from "@suaveplan/events"; import { afterEach, describe, expect, it } from "@suaveplan/testing/runner"; import type { Auth } from "better-auth"; import { AuthError } from "../error-codes/error-codes.js"; import type { AuthPluginConfig } from "../types/types.js"; import { __setBetterAuthFactory, createAuthPlugin } from "./plugin.js"; const noopLogger = { debug: () => undefined, info: () => undefined, warn: () => undefined, error: () => undefined, }; function makeAuthStub(): Auth { return { handler: async () => new Response("ok", { status: 200 }), api: { getSession: async () => null }, } as unknown as Auth; } afterEach(() => { __setBetterAuthFactory(undefined); }); describe("createAuthPlugin", () => { it("createAuthPlugin returns valid PluginDescriptor", { timeout: 5000, }, () => { const plugin = createAuthPlugin({ betterAuth: {} }); expect(plugin.name).toBe("auth"); expect(plugin.version).toBe("0.1.0"); expect(typeof plugin.onInit).toBe("function"); expect(typeof plugin.onClose).toBe("function"); }); it("invalid options raise AUTH_INVALID_INPUT", { timeout: 5000 }, () => { let caught: AuthError | undefined; try { createAuthPlugin({} as never); } catch (err) { caught = err as AuthError; } expect(caught?.code).toBe("AUTH.INVALID_INPUT"); expect(caught?.cause).toBeDefined(); }); it("descriptor exposes resolved defaults", { timeout: 5000 }, () => { const plugin = createAuthPlugin({ betterAuth: {}, routePrefix: "/v2/auth", }); expect((plugin.defaults as AuthPluginConfig).routePrefix).toBe( "/v2/auth" ); expect((plugin.defaults as AuthPluginConfig).applySchemaOnInit).toBe( true ); }); it("onClose clears internal state without error", { timeout: 5000, }, async () => { const plugin = createAuthPlugin({ betterAuth: {} }); const services = new Map(); const ctrl = new AbortController(); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: ctrl.signal, config: plugin.defaults as AuthPluginConfig, services, }); await plugin.onClose?.(ctx); }); it("onInit raises AUTH_SERVICE_NOT_AVAILABLE when app missing", { timeout: 10_000, }, async () => { __setBetterAuthFactory(() => makeAuthStub()); const plugin = createAuthPlugin({ betterAuth: {}, applySchemaOnInit: false, }); const services = new Map(); const ctrl = new AbortController(); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: ctrl.signal, config: plugin.defaults as AuthPluginConfig, services, }); let caught: AuthError | undefined; try { await plugin.onInit?.(ctx); } catch (err) { caught = err as AuthError; } expect(caught?.code).toBe("AUTH.SERVICE_NOT_AVAILABLE"); }); it("onInit completes with applySchemaOnInit: false + stubbed auth", { timeout: 15_000, }, async () => { __setBetterAuthFactory(() => makeAuthStub()); const plugin = createAuthPlugin({ betterAuth: {}, applySchemaOnInit: false, }); const services = new Map(); const app = new OpenAPIHono(); const registry = createOpenAPIRegistry({ info: { title: "t", version: "0.1.0" }, }); services.set("app", app); services.set("openAPIRegistry", registry); services.set("logger", noopLogger); services.set("eventBus", new EventBus()); services.set("clock", () => Date.now()); const ctrl = new AbortController(); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: ctrl.signal, config: plugin.defaults as AuthPluginConfig, services, }); await plugin.onInit?.(ctx); expect(services.get("authService")).toBeDefined(); expect(registry.getRoutesByPlugin("auth")).toHaveLength(9); }); it("default factory delegates to real betterAuth", { timeout: 10_000, }, async () => { // Restore the default factory and exercise the real `betterAuth` // import — invoked with deliberately bad input so the test // surfaces the BETTER_AUTH_INIT_FAILED translation rather than // standing up a real Better-Auth instance. __setBetterAuthFactory(undefined); const plugin = createAuthPlugin({ betterAuth: { __invalid__: Symbol("nope") as unknown as string }, applySchemaOnInit: false, }); const services = new Map(); services.set("app", new OpenAPIHono()); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: new AbortController().signal, config: plugin.defaults as AuthPluginConfig, services, }); let caught: AuthError | undefined; try { await plugin.onInit?.(ctx); } catch (err) { caught = err as AuthError; } // Real betterAuth may succeed with default in-memory config // depending on version; both outcomes count as covering the // default factory path. We only assert NO exception other than // an AuthError reaches here. if (caught !== undefined) { expect(caught).toBeInstanceOf(AuthError); } }); it("onInit raises AUTH_BETTER_AUTH_INIT_FAILED when betterAuth throws", { timeout: 10_000, }, async () => { __setBetterAuthFactory(() => { throw new Error("better-auth: bad config"); }); const plugin = createAuthPlugin({ betterAuth: {}, applySchemaOnInit: false, }); const services = new Map(); services.set("app", new OpenAPIHono()); const ctrl = new AbortController(); const ctx = createPluginContext({ logger: noopLogger, eventBus: new EventBus(), signal: ctrl.signal, config: plugin.defaults as AuthPluginConfig, services, }); let caught: AuthError | undefined; try { await plugin.onInit?.(ctx); } catch (err) { caught = err as AuthError; } expect(caught?.code).toBe("AUTH.BETTER_AUTH_INIT_FAILED"); }); });