import { expect, test } from "bun:test"; import type { Context } from "@wrnexus/core"; import { createPluginRunner } from "@wrnexus/plugin"; import { authPlugin, validateProductionAuthConfig } from "../src/plugin.ts"; import { AUTH_ROUTE_DEFINITIONS } from "../src/routes/definitions.ts"; import { readFileSync } from "node:fs"; import { createAuthEngine } from "../src/engine.ts"; import { MemoryAuthStore } from "../src/stores/memory.ts"; import { invokeAuthHandler } from "../src/routes/api.ts"; function routeContext(request: Request): Context { const values = new Map(); return { req: request, url: new URL(request.url), params: {}, locals: {}, lang: "en", t: (key: string) => key, ip: "127.0.0.1", user: null, cookies: { get: (name: string) => (name === "wrn-csrf" ? "plugin-test-csrf-token" : undefined), } as Context["cookies"], localStorage: {} as Context["localStorage"], session: { id: () => "plugin-test-session", get: (key: string) => values.get(key) as T | undefined, getAll: () => Object.fromEntries(values), set: (key: string, value: unknown) => { values.set(key, value); }, delete: (key: string) => { values.delete(key); }, regenerate: () => {}, clear: () => { values.clear(); }, }, } as Context; } test("plugin contributes components, runtime, styles, migration, and toolbar", async () => { const metadata = new Map(); const runner = createPluginRunner( authPlugin({ includeRoutes: true, includeMigrations: true, includeMiddleware: true }), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }, ); const contributions = await runner.contributions(); expect(contributions.componentDirs).toHaveLength(1); expect(contributions.clientRuntimes[0]).toMatchObject({ id: "auth", singleton: true }); expect(contributions.styles[0]?.id).toBe("auth-components"); expect(contributions.migrations.map((migration) => migration.id)).toEqual([ "wrnexus-auth-001", "wrnexus-auth-002-otp-purpose", ]); expect(contributions.routes.length).toBeGreaterThanOrEqual(30); expect(contributions.middleware).toHaveLength(1); }); test("production auth validation rejects development secrets and origins", () => { expect(() => validateProductionAuthConfig( { baseUrl: "http://localhost:3000" }, { AUTH_SECRET: "change-me" }, ), ).toThrow("AUTH_SECRET"); expect(() => validateProductionAuthConfig( { baseUrl: "http://app.example" }, { AUTH_SECRET: "a-production-secret-that-is-longer-than-thirty-two-characters" }, ), ).toThrow("HTTPS"); }); test("unconfigured automatic discovery fails closed for routes, middleware, and migrations", async () => { const metadata = new Map(); const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }); const contributions = await runner.contributions(); expect(contributions.routes).toHaveLength(0); expect(contributions.middleware).toHaveLength(0); expect(contributions.migrations).toHaveLength(0); expect(contributions.componentDirs).toHaveLength(1); expect(contributions.clientRuntimes).toHaveLength(1); }); test("config.auth controls route groups and migrations without explicit plugin options", async () => { const metadata = new Map(); const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }); await runner.configure({ auth: { engine: {} as never, migrations: false, routes: { enabled: true, registration: false, passkeys: false }, }, }); const contributions = await runner.contributions(); expect(contributions.migrations).toHaveLength(0); expect(contributions.routes.some((route) => route.path === "/api/auth/register")).toBe(false); expect(contributions.routes.some((route) => route.path.includes("/passkeys/"))).toBe(false); expect(contributions.routes.some((route) => route.path === "/api/auth/login")).toBe(true); }); test("a hook set on config.auth.engine fires when a request goes through the route layer", async () => { const calls: string[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "plugin-navigation-hooks-secret-longer-than-thirty-two-characters", onSignedIn(ctx, returnTo) { calls.push(`in:${returnTo}`); return Response.redirect(new URL(returnTo ?? "/account", ctx.url), 303); }, onSignedOut(ctx) { calls.push("out"); return Response.redirect(new URL("/sign-in", ctx.url), 303); }, }); await engine.register({ email: "plugin-navigation@example.com", password: "StrongPassword123", }); const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata: new Map(), warn() {}, }); await runner.configure({ auth: { engine, routes: true }, }); const loginResponse = await invokeAuthHandler( "login", routeContext( new Request("https://example.test/api/auth/login", { method: "POST", headers: { "content-type": "application/json", "x-csrf-token": "plugin-test-csrf-token", }, body: JSON.stringify({ identifier: "plugin-navigation@example.com", password: "StrongPassword123", returnTo: "/dashboard", }), }), ), ); expect(loginResponse.status).toBe(303); expect(loginResponse.headers.get("location")).toBe("https://example.test/dashboard"); const logoutResponse = await invokeAuthHandler( "logout", routeContext( new Request("https://example.test/api/auth/logout", { method: "POST", headers: { "x-csrf-token": "plugin-test-csrf-token" }, }), ), ); expect(logoutResponse.status).toBe(303); expect(logoutResponse.headers.get("location")).toBe("https://example.test/sign-in"); expect(calls).toEqual(["in:/dashboard", "out"]); }); test("auth runtime contains built-in browser schemas", async () => { const metadata = new Map(); const runner = createPluginRunner(authPlugin({ includeMigrations: false }), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }); const contributions = await runner.contributions(); expect(contributions.clientRuntimes[0]?.source).toContain("auth-password-request"); expect(contributions.clientRuntimes[0]?.source).toContain("auth-register"); }); test("each package auth route uses a route-specific entry module", async () => { const metadata = new Map(); const runner = createPluginRunner( authPlugin({ includeRoutes: true, includeMigrations: false, includeMiddleware: false, }), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }, ); const contributions = await runner.contributions(); const entries = contributions.routes.map((route) => route.entry); const passwordRequest = contributions.routes.find( (route) => route.path === "/api/auth/password/request", ); expect(new Set(entries).size).toBe(entries.length); expect( passwordRequest?.entry.replace(/\\/g, "/").endsWith("/src/routes/api/password-request.ts"), ).toBe(true); for (const definition of AUTH_ROUTE_DEFINITIONS) { const route = contributions.routes.find((item) => item.path === definition.path); expect(route).toBeDefined(); const source = readFileSync(route!.entry, "utf8"); expect(source).toContain(`invokeAuthHandler("${definition.handler}"`); expect(source.includes("dispatchAuthRoute")).toBe(false); for (const method of definition.methods) { expect(source).toContain(`export function ${method}`); } } }); test("config.auth registers package routes", async () => { const metadata = new Map(); const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }); await runner.configure({ auth: { engine: {} as never, routes: true, middleware: true, migrations: false, }, }); const contributions = await runner.contributions(); expect(contributions.routes.some((route) => route.path === "/api/auth/password/request")).toBe( true, ); expect(contributions.routes.some((route) => route.path === "/api/auth/register")).toBe(true); expect(contributions.routes.some((route) => route.path === "/api/auth/login")).toBe(true); expect(contributions.middleware).toHaveLength(1); expect(contributions.migrations).toHaveLength(0); }); test("config.auth contributes a reusable forward-auth verification route", async () => { const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata: new Map(), warn() {}, }); await runner.configure({ auth: { engine: {} as never, routes: true, }, }); const contributions = await runner.contributions(); expect(contributions.routes.some((route) => route.path === "/api/auth/session/verify")).toBe( true, ); }); test("config.auth can disable route groups", async () => { const metadata = new Map(); const runner = createPluginRunner(authPlugin(), { root: process.cwd(), mode: "development", command: "dev", metadata, warn() {}, }); await runner.configure({ auth: { engine: {} as never, migrations: false, routes: { enabled: true, password: true, passkeys: false, }, }, }); const contributions = await runner.contributions(); expect(contributions.routes.some((route) => route.path === "/api/auth/password/request")).toBe( true, ); expect(contributions.routes.some((route) => route.path.includes("/passkeys/"))).toBe(false); });