import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { randomUUID } from "node:crypto"; import { afterEach, describe, expect, it } from "vitest"; import { createApp } from "../../apps/api/src/app.js"; import { RegistrationService } from "../../apps/api/src/registration.js"; import { MockResendAdapter } from "../../apps/api/src/resend-adapter.js"; const roots: string[] = []; const services: RegistrationService[] = []; const writeHeaders = { host: "127.0.0.1:43121", origin: "http://127.0.0.1:43121" }; function harness() { const root = mkdtempSync(join(tmpdir(), "dada-wp1-02-api-")); roots.push(root); const resend = new MockResendAdapter(); const registration = new RegistrationService({ challengePepper: Buffer.alloc(32, 0x61), codeGenerator: () => "621904", currentPrivacyNoticeVersion: "p0a-notice-v1", databasePath: join(root, "dada.sqlite3"), invitePepper: Buffer.alloc(32, 0x62), resend, sessionPepper: Buffer.alloc(32, 0x63), }); services.push(registration); return { registration, resend }; } function seedUser(registration: RegistrationService, email: string, status: "active" | "suspended" = "active") { const userId = randomUUID(); registration.database.prepare(` INSERT INTO users ( user_id, normalized_email, role, status, counts_toward_stage_limit, registration_id, created_at ) VALUES (?, ?, 'user', ?, 1, ?, ?) `).run(userId, email, status, randomUUID(), Date.now()); registration.database.prepare(` INSERT INTO user_profiles ( user_id, creator_name, social_id, private_content_notice_version, private_content_notice_acknowledged_at ) VALUES (?, 'API User', '@api_user', NULL, NULL) `).run(userId); registration.database.prepare(` INSERT INTO credit_accounts (user_id, available_balance, reserved_balance, updated_at) VALUES (?, 10, 0, ?) `).run(userId, Date.now()); return userId; } afterEach(() => { for (const service of services.splice(0)) service.close(); for (const root of roots.splice(0)) rmSync(root, { force: true, recursive: true }); }); describe("TASK-WP1-02 login and session API", () => { it("logs an active user in, issues CSRF, and revokes all sessions on logout", async () => { const { registration, resend } = harness(); const userId = seedUser(registration, "login-api@example.invalid"); registration.issueAuthenticatedSession(userId, "user"); const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration }); const sent = await app.inject({ headers: writeHeaders, method: "POST", payload: { email: "login-api@example.invalid" }, url: "/api/v1/auth/login/send", }); expect(sent.statusCode).toBe(200); const flowCookie = sent.headers["set-cookie"]; const completed = await app.inject({ headers: { ...writeHeaders, cookie: flowCookie, "idempotency-key": "wp1-02-api-login-complete-000000001" }, method: "POST", payload: { registration_id: sent.json().registration_id, verification_code: resend.readLatestCode("login-api@example.invalid"), }, url: "/api/v1/auth/login/complete", }); expect(completed.statusCode).toBe(200); expect(completed.json()).toMatchObject({ audience: "user", status: "authenticated" }); const sessionCookie = completed.headers["set-cookie"]; const session = await app.inject({ headers: { cookie: sessionCookie, host: writeHeaders.host }, method: "GET", url: "/api/v1/auth/session", }); expect(session.statusCode).toBe(200); expect(session.json().csrf_token).toMatch(/^[A-Za-z0-9_-]{43}$/); const logout = await app.inject({ headers: { ...writeHeaders, cookie: sessionCookie, "idempotency-key": "wp1-02-api-logout-00000000000001", "x-csrf-token": session.json().csrf_token, }, method: "POST", url: "/api/v1/auth/logout", }); expect(logout.statusCode).toBe(200); expect(logout.json()).toEqual({ status: "logged_out" }); const revoked = await app.inject({ headers: { cookie: sessionCookie, host: writeHeaders.host }, method: "GET", url: "/api/v1/auth/session", }); expect(revoked.statusCode).toBe(401); await app.close(); }); it("returns stable entry-state and 429 errors without switching flows", async () => { const { registration } = harness(); seedUser(registration, "suspended-api@example.invalid", "suspended"); const app = await createApp({ browserGate: false, networkBoundary: { allowTestPort: true }, registration }); const suspended = await app.inject({ headers: writeHeaders, method: "POST", payload: { email: "suspended-api@example.invalid" }, url: "/api/v1/auth/login/send", }); expect(suspended.statusCode).toBe(409); expect(suspended.json()).toMatchObject({ error: { code: "AUTH_ENTRY_REJECTED", details: { field_errors: [{ message_key: "auth.account.suspended" }] } }, }); const missing = await app.inject({ headers: writeHeaders, method: "POST", payload: { email: "missing-api@example.invalid" }, url: "/api/v1/auth/login/send", }); expect(missing.statusCode).toBe(409); expect(missing.json()).toMatchObject({ error: { details: { field_errors: [{ message_key: "auth.login.registration_required" }] } }, }); await app.close(); }); });