import { expect, test } from "bun:test"; import type { Context } from "@wrnexus/core"; import { createAuthEngine } from "../src/engine.ts"; import { createAuthHttpHandlers } from "../src/http/index.ts"; import { MemoryAuthStore } from "../src/stores/memory.ts"; function context(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: {} as Context["cookies"], localStorage: {} as Context["localStorage"], session: { id: () => "http-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("login handler never trusts a browser captchaVerified field", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "http-handler-secret-that-is-longer-than-thirty-two-characters", captchaThreshold: 0, }); await engine.register({ email: "captcha@example.com", password: "StrongPassword123" }); const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ identifier: "captcha@example.com", password: "StrongPassword123", captchaVerified: true, }), }), ); const response = await handlers.login(ctx); expect(response.status).toBe(401); expect(await response.json()).toMatchObject({ code: "captcha-required" }); }); test("server-populated CAPTCHA verification permits the login attempt", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "http-handler-secret-that-is-longer-than-thirty-two-characters", captchaThreshold: 0, }); await engine.register({ email: "verified@example.com", password: "StrongPassword123" }); const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ identifier: "verified@example.com", password: "StrongPassword123" }), }), ); ctx.locals.captcha = { success: true }; const response = await handlers.login(ctx); expect(response.status).toBe(200); expect(await response.json()).toMatchObject({ ok: true }); }); test("login API returns a safe actionable invalid-credentials message", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "http-login-message-secret-longer-than-thirty-two-characters", }); const handlers = createAuthHttpHandlers({ engine }); const response = await handlers.login( context( new Request("https://example.test/api/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ identifier: "missing@example.com", password: "WrongPassword999", }), }), ), ); expect(response.status).toBe(401); expect(await response.json()).toMatchObject({ ok: false, code: "invalid-credentials", message: "The email, phone, username, or password you entered is incorrect.", }); }); test("HTTP handlers use navigation hooks configured on the auth engine", async () => { const calls: string[] = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "engine-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: "navigation@example.com", password: "StrongPassword123", }); const handlers = createAuthHttpHandlers({ engine }); const loginResponse = await handlers.login( context( new Request("https://example.test/api/auth/login", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ identifier: "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 handlers.logout( context(new Request("https://example.test/api/auth/logout", { method: "POST" })), ); expect(logoutResponse.status).toBe(303); expect(logoutResponse.headers.get("location")).toBe("https://example.test/sign-in"); expect(calls).toEqual(["in:/dashboard", "out"]); }); test("register handler returns validation field errors before calling the engine", async () => { let registerCalls = 0; const engine = { register: async () => { registerCalls += 1; return { ok: true }; }, } as unknown as ReturnType; const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "A", email: "bad", password: "short" }), }), ); const response = await handlers.register(ctx); expect(response.status).toBe(400); expect(await response.json()).toMatchObject({ ok: false, errors: { displayName: expect.any(String), email: expect.any(String), password: expect.any(String), }, }); expect(registerCalls).toBe(0); }); test("successful signup redirects to sign-in by default", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "default-signup-redirect-secret-longer-than-thirty-two-characters", }); const handlers = createAuthHttpHandlers({ engine }); const response = await handlers.register( context( new Request("https://example.test/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "Default Redirect", email: "default-signup@example.com", password: "StrongPassword123", consent: true, }), }), ), ); expect(response.status).toBe(303); expect(response.headers.get("location")).toBe("https://example.test/sign-in"); }); test("onSuccessfulSignUp can safely auto-sign-in and redirect the new user", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "automatic-signup-login-secret-longer-than-thirty-two-characters", onSuccessfulSignUp() { return { autoSignIn: true, redirectTo: "/welcome" }; }, }); const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "Automatic Login", email: "automatic-signup@example.com", password: "StrongPassword123", consent: true, }), }), ); const response = await handlers.register(ctx); expect(response.status).toBe(303); expect(response.headers.get("location")).toBe("https://example.test/welcome"); expect(ctx.locals.authUser).toMatchObject({ displayName: "Automatic Login" }); expect(ctx.locals.authSession).toBeDefined(); }); test("signup auto-sign-in does not bypass verification policy", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "verified-signup-policy-secret-longer-than-thirty-two-characters", requireVerifiedEmail: true, onSuccessfulSignUp() { return { autoSignIn: true, redirectTo: "/account" }; }, }); const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/register", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ displayName: "Verification Required", email: "verify-signup@example.com", password: "StrongPassword123", consent: true, }), }), ); const response = await handlers.register(ctx); expect(response.status).toBe(401); expect(await response.json()).toMatchObject({ ok: false, code: "email-unverified", message: "Verify your email address before signing in.", }); expect(ctx.locals.authSession).toBeUndefined(); }); test("authenticated OTP issue rejects an unlinked destination without throwing", async () => { const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "http-otp-secret-that-is-longer-than-thirty-two-characters", }); const registered = await engine.register({ email: "owner@example.com", password: "StrongPassword123", }); const handlers = createAuthHttpHandlers({ engine }); const ctx = context( new Request("https://example.test/api/auth/otp", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ method: "email-otp", destination: "other@example.com" }), }), ); ctx.user = registered.user!; ctx.locals.authUser = registered.user!; const response = await handlers.issueOtp(ctx); expect(response.status).toBe(400); expect(await response.json()).toMatchObject({ ok: false }); }); test("verification resend is generic and can resolve an unauthenticated identifier", async () => { const messages: Array<{ template: string }> = []; const engine = createAuthEngine({ store: new MemoryAuthStore(), secret: "verification-resend-secret-that-is-longer-than-thirty-two-characters", delivery: { async send(message) { messages.push(message); }, }, }); await engine.register({ email: "resend@example.com", password: "StrongPassword123", }); const handlers = createAuthHttpHandlers({ engine }); const before = messages.length; const known = await handlers.requestVerification( context( new Request("https://example.test/api/auth/verification/request", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "email", identifier: "resend@example.com" }), }), ), ); expect(known.status).toBe(200); expect(await known.json()).toEqual({ ok: true }); expect(messages).toHaveLength(before + 1); const unknown = await handlers.requestVerification( context( new Request("https://example.test/api/auth/verification/request", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ type: "email", identifier: "missing@example.com" }), }), ), ); expect(unknown.status).toBe(200); expect(await unknown.json()).toEqual({ ok: true }); expect(messages).toHaveLength(before + 1); });