import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { buildApp } from "../src/app.js"; import { loadEnv } from "../src/config/env.js"; import { prisma } from "../src/db/prisma.js"; import { createSession } from "../src/security/authStore.js"; import { hashPassword } from "../src/security/password.js"; import { createHash } from "node:crypto"; import { apiPaths } from "@projectbrain/contracts"; import { exportJWK, generateKeyPair, SignJWT } from "jose"; const databaseUrl = process.env.TEST_DATABASE_URL; if (databaseUrl) process.env.DATABASE_URL = databaseUrl; const databaseDescribe = databaseUrl ? describe : describe.skip; const ownerPassword = "database-owner-password-value"; const coreLoopIds = { capture: [] as string[], review: [] as string[], truth: [] as string[], update: [] as string[], handoff: [] as string[], task: [] as string[], runImport: [] as string[], }; const testEnv = () => loadEnv({ NODE_ENV: "test", LOG_LEVEL: "silent", DATABASE_URL: databaseUrl, PROJECTBRAIN_DATA_SOURCE: "database", PROJECTBRAIN_OWNER_USERNAME: "database-owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "database-test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_SESSION_TTL_HOURS: "1", PROJECTBRAIN_TRUST_PROXY: "false", }); const entraEnv = () => loadEnv({ NODE_ENV: "test", LOG_LEVEL: "silent", DATABASE_URL: databaseUrl, PROJECTBRAIN_DATA_SOURCE: "database", PROJECTBRAIN_OWNER_USERNAME: "database-owner", PROJECTBRAIN_OWNER_PASSWORD: ownerPassword, PROJECTBRAIN_AUTH_SECRET: "database-test-secret-with-at-least-thirty-two-characters", PROJECTBRAIN_SESSION_TTL_HOURS: "1", PROJECTBRAIN_TRUST_PROXY: "false", PROJECTBRAIN_PUBLIC_URL: "https://projectbrain.example.com", PROJECTBRAIN_ENTRA_TENANT_ID: "11111111-1111-4111-8111-111111111111", PROJECTBRAIN_ENTRA_CLIENT_ID: "22222222-2222-4222-8222-222222222222", PROJECTBRAIN_ENTRA_CLIENT_SECRET: "database-entra-test-secret", }); databaseDescribe("database authentication persistence", () => { beforeAll(async () => { await prisma.oidcLoginAttempt.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.externalIdentity.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.authSession.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.projectMembership.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.user.deleteMany({ where: { username: { in: ["database-owner", "database-member"] } } }); }); afterAll(async () => { await prisma.oidcLoginAttempt.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.externalIdentity.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); const markedTasks = await prisma.codexTask.findMany({ where: { goal: "Execute the database core-loop proof" }, select: { id: true } }); await prisma.codexRunImport.deleteMany({ where: { OR: [{ sourceRun: "core-loop-database-proof" }, { codexTaskId: { in: markedTasks.map((item) => item.id) } }] } }); await prisma.reviewQueueItem.deleteMany({ where: { OR: [{ source: "core-loop-database-proof" }, { title: { contains: "Core-loop database proof" } }] } }); await prisma.codexTask.deleteMany({ where: { goal: "Execute the database core-loop proof" } }); await prisma.handoffBundle.deleteMany({ where: { title: "Core-loop database proof handoff" } }); const markedCaptures = await prisma.captureItem.findMany({ where: { content: "Core-loop database proof source material" }, select: { linkedReviewItemId: true } }); const markedReviewIds = markedCaptures.flatMap((item) => item.linkedReviewItemId ? [item.linkedReviewItemId] : []); const markedTruth = await prisma.acceptedTruthItem.findMany({ where: { sourceReviewItemId: { in: markedReviewIds } }, select: { id: true } }); await prisma.projectUpdate.deleteMany({ where: { sourceTruthItemId: { in: markedTruth.map((item) => item.id) } } }); await prisma.acceptedTruthItem.deleteMany({ where: { id: { in: markedTruth.map((item) => item.id) } } }); await prisma.reviewQueueItem.deleteMany({ where: { id: { in: markedReviewIds } } }); await prisma.captureItem.deleteMany({ where: { content: "Core-loop database proof source material" } }); await prisma.codexRunImport.deleteMany({ where: { id: { in: coreLoopIds.runImport } } }); await prisma.codexTask.deleteMany({ where: { id: { in: coreLoopIds.task } } }); await prisma.handoffBundle.deleteMany({ where: { id: { in: coreLoopIds.handoff } } }); await prisma.projectUpdate.deleteMany({ where: { id: { in: coreLoopIds.update } } }); await prisma.acceptedTruthItem.deleteMany({ where: { id: { in: coreLoopIds.truth } } }); await prisma.reviewQueueItem.deleteMany({ where: { id: { in: coreLoopIds.review } } }); await prisma.captureItem.deleteMany({ where: { id: { in: coreLoopIds.capture } } }); await prisma.idempotencyRecord.deleteMany({ where: { key: { contains: "core-loop-database-proof" } } }); await prisma.operatingLogEntry.deleteMany({ where: { title: "Durable idempotency restart proof" } }); await prisma.idempotencyRecord.deleteMany({ where: { key: { contains: "database-restart-replay" } } }); await prisma.authSession.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.projectMembership.deleteMany({ where: { user: { username: { in: ["database-owner", "database-member"] } } } }); await prisma.user.deleteMany({ where: { username: { in: ["database-owner", "database-member"] } } }); await prisma.$disconnect(); }); it("keeps a server-side owner session valid across API restart", async () => { const firstApp = await buildApp({ env: testEnv() }); const login = await firstApp.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } }); expect(login.statusCode).toBe(200); const cookie = login.cookies.find((item) => item.name === "projectbrain_session"); expect(cookie).toBeDefined(); const cookieHeader = `${cookie!.name}=${cookie!.value}`; await firstApp.close(); const restartedApp = await buildApp({ env: testEnv() }); const response = await restartedApp.inject({ method: "GET", url: "/api/projects", headers: { cookie: cookieHeader } }); expect(response.statusCode).toBe(200); expect(response.json().data.length).toBeGreaterThan(0); await restartedApp.close(); }, 15_000); it("creates a one-time PKCE Entra link attempt without persisting raw state", async () => { const app = await buildApp({ env: entraEnv() }); const login = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "database-owner", password: ownerPassword } }); const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session"); const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`; const csrfToken = login.json().data.csrfToken as string; const response = await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart, headers: { cookie, "x-csrf-token": csrfToken } }); expect(response.statusCode).toBe(200); const authorizationUrl = new URL(response.json().data.authorizationUrl); expect(authorizationUrl.hostname).toBe("login.microsoftonline.com"); expect(authorizationUrl.pathname).toContain("11111111-1111-4111-8111-111111111111"); expect(authorizationUrl.searchParams.get("code_challenge_method")).toBe("S256"); expect(authorizationUrl.searchParams.get("nonce")?.length).toBeGreaterThan(30); const state = authorizationUrl.searchParams.get("state")!; const attempt = await prisma.oidcLoginAttempt.findUnique({ where: { stateHash: createHash("sha256").update(state).digest("hex") } }); expect(attempt?.purpose).toBe("link"); expect(attempt?.returnTo).toBe("/settings"); expect(attempt?.stateHash).not.toBe(state); await app.close(); }); it("validates the Entra ID token and turns a linked identity into a ProjectBrain session", async () => { const app = await buildApp({ env: entraEnv() }); const login = await app.inject({ method: "POST", url: apiPaths.auth.login, payload: { username: "database-owner", password: ownerPassword } }); const localCookie = login.cookies.find((item) => item.name === "projectbrain_session")!; const linkStart = await app.inject({ method: "POST", url: apiPaths.auth.entraLinkStart, headers: { cookie: `${localCookie.name}=${localCookie.value}`, "x-csrf-token": login.json().data.csrfToken } }); const authorizationUrl = new URL(linkStart.json().data.authorizationUrl); const state = authorizationUrl.searchParams.get("state")!; const nonce = authorizationUrl.searchParams.get("nonce")!; const tenantId = "11111111-1111-4111-8111-111111111111"; const clientId = "22222222-2222-4222-8222-222222222222"; const objectId = "33333333-3333-4333-8333-333333333333"; const { publicKey, privateKey } = await generateKeyPair("RS256"); const publicJwk = { ...await exportJWK(publicKey), kid: "entra-test-key", alg: "RS256", use: "sig" }; const idToken = await new SignJWT({ tid: tenantId, oid: objectId, nonce, name: "Entra Owner", preferred_username: "owner@example.com" }) .setProtectedHeader({ alg: "RS256", kid: "entra-test-key" }) .setIssuer(`https://login.microsoftonline.com/${tenantId}/v2.0`) .setAudience(clientId) .setIssuedAt() .setExpirationTime("5m") .sign(privateKey); vi.stubGlobal("fetch", vi.fn(async (input: string | URL | Request) => { const url = String(input instanceof Request ? input.url : input); if (url.endsWith("/oauth2/v2.0/token")) return new Response(JSON.stringify({ id_token: idToken }), { status: 200, headers: { "content-type": "application/json" } }); if (url.endsWith("/discovery/v2.0/keys")) return new Response(JSON.stringify({ keys: [publicJwk] }), { status: 200, headers: { "content-type": "application/json" } }); return new Response("not found", { status: 404 }); })); try { const callback = await app.inject({ method: "GET", url: `${apiPaths.auth.entraCallback}?code=one-time-code&state=${encodeURIComponent(state)}` }); expect(callback.statusCode).toBe(302); expect(callback.headers.location).toBe("https://projectbrain.example.com/settings"); const entraCookie = callback.cookies.find((item) => item.name === "projectbrain_session"); expect(entraCookie).toBeDefined(); const identity = await prisma.externalIdentity.findUnique({ where: { provider_tenantId_subject: { provider: "microsoft-entra", tenantId, subject: objectId } } }); expect(identity?.preferredUsername).toBe("owner@example.com"); expect((await app.inject({ method: "GET", url: apiPaths.auth.session, headers: { cookie: `${entraCookie!.name}=${entraCookie!.value}` } })).statusCode).toBe(200); expect((await app.inject({ method: "GET", url: `${apiPaths.auth.entraCallback}?code=replay&state=${encodeURIComponent(state)}` })).headers.location).toContain("expired_attempt"); } finally { vi.unstubAllGlobals(); await app.close(); } }, 15_000); it("returns 403 until a member receives project access", async () => { const app = await buildApp({ env: testEnv() }); const member = await prisma.user.create({ data: { username: "database-member", passwordHash: await hashPassword("database-member-password"), role: "member" } }); const session = await createSession(testEnv(), { id: member.id, username: member.username, role: "member", credentialVersion: member.credentialVersion }); const cookie = `projectbrain_session=${app.signCookie(session.token)}`; const projectCreateDenied = await app.inject({ method: "POST", url: "/api/projects", headers: { cookie, "x-csrf-token": session.csrfToken }, payload: { name: "Unauthorized project", description: "A member must not provision a Gitea repository through project creation." }, }); expect(projectCreateDenied.statusCode).toBe(403); expect(projectCreateDenied.json().error.code).toBe("PROJECT_CREATION_FORBIDDEN"); const denied = await app.inject({ method: "GET", url: "/api/projects/widefrog", headers: { cookie } }); expect(denied.statusCode).toBe(403); expect(denied.json().error.code).toBe("PROJECT_ACCESS_DENIED"); const project = await prisma.project.findUniqueOrThrow({ where: { slug: "widefrog" } }); await prisma.projectMembership.create({ data: { projectId: project.id, userId: member.id, role: "reader" } }); const allowed = await app.inject({ method: "GET", url: "/api/projects/widefrog", headers: { cookie } }); expect(allowed.statusCode).toBe(200); const writeDenied = await app.inject({ method: "PATCH", url: "/api/projects/widefrog", headers: { cookie, "x-csrf-token": session.csrfToken }, payload: { name: "Forbidden update" } }); expect(writeDenied.statusCode).toBe(403); await app.close(); }); it("replays a core-loop mutation durably after API restart", async () => { const firstApp = await buildApp({ env: testEnv() }); const login = await firstApp.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } }); const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session"); const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`; const csrfToken = login.json().data.csrfToken as string; const headers = { cookie, "x-csrf-token": csrfToken, "idempotency-key": "database-restart-replay" }; const payload = { type: "project update", title: "Durable idempotency restart proof", summary: "This entry must be created once across API restarts.", source: "database-test" }; const created = await firstApp.inject({ method: "POST", url: "/api/projects/widefrog/operating-log", headers, payload }); expect(created.statusCode).toBe(201); await firstApp.close(); const restartedApp = await buildApp({ env: testEnv() }); const replayed = await restartedApp.inject({ method: "POST", url: "/api/projects/widefrog/operating-log", headers, payload }); expect(replayed.statusCode).toBe(201); expect(replayed.json().data.id).toBe(created.json().data.id); expect(await prisma.operatingLogEntry.count({ where: { title: payload.title } })).toBe(1); expect(await prisma.idempotencyRecord.count({ where: { key: { contains: "database-restart-replay" } } })).toBe(1); await restartedApp.close(); }, 15_000); it("executes the complete governed core loop in PostgreSQL mode", async () => { const app = await buildApp({ env: testEnv() }); const login = await app.inject({ method: "POST", url: "/api/auth/login", payload: { username: "database-owner", password: ownerPassword } }); const sessionCookie = login.cookies.find((item) => item.name === "projectbrain_session"); const cookie = `${sessionCookie!.name}=${sessionCookie!.value}`; const csrfToken = login.json().data.csrfToken as string; let operation = 0; const mutate = (method: "POST" | "PATCH", url: string, payload: Record = {}) => app.inject({ method, url, payload, headers: { cookie, "x-csrf-token": csrfToken, "idempotency-key": `core-loop-database-proof-${++operation}` }, }); const capture = await mutate("POST", "/api/projects/widefrog/capture-items", { type: "note", content: "Core-loop database proof source material", source: "database-integration-test" }); expect(capture.statusCode).toBe(201); coreLoopIds.capture.push(capture.json().data.id); expect((await mutate("PATCH", `/api/projects/widefrog/capture-items/${capture.json().data.id}/classify`)).statusCode).toBe(200); const queued = await mutate("POST", `/api/projects/widefrog/capture-items/${capture.json().data.id}/send-to-review`); expect(queued.statusCode).toBe(201); const reviewId = queued.json().data.createdEntityId as string; coreLoopIds.review.push(reviewId); const concurrentReviewDecisions = await Promise.all([ mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/accept`, { reason: "Accepted by database core-loop proof A" }), mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/accept`, { reason: "Accepted by database core-loop proof B" }), ]); expect(concurrentReviewDecisions.map((response) => response.statusCode).sort()).toEqual([200, 409]); expect(concurrentReviewDecisions.find((response) => response.statusCode === 409)!.json().error.code).toBe("REVIEW_ALREADY_DECIDED"); const materialized = await mutate("POST", `/api/projects/widefrog/review-queue/${reviewId}/materialize-truth`); expect(materialized.statusCode).toBe(200); const truthId = materialized.json().data.createdEntityId as string; coreLoopIds.truth.push(truthId); const conversionCapture = await mutate("POST", "/api/projects/widefrog/capture-items", { type: "note", content: "Convert this reviewed decision into a durable truth artifact", source: "database-conversion-test" }); expect(conversionCapture.statusCode).toBe(201); coreLoopIds.capture.push(conversionCapture.json().data.id); expect((await mutate("PATCH", `/api/projects/widefrog/capture-items/${conversionCapture.json().data.id}/classify`)).statusCode).toBe(200); const conversionQueue = await mutate("POST", `/api/projects/widefrog/capture-items/${conversionCapture.json().data.id}/send-to-review`); expect(conversionQueue.statusCode).toBe(201); const conversionReviewId = conversionQueue.json().data.createdEntityId as string; coreLoopIds.review.push(conversionReviewId); const converted = await mutate("POST", `/api/projects/widefrog/review-queue/${conversionReviewId}/convert`, { target: "decision", reason: "Database artifact proof" }); expect(converted.statusCode).toBe(200); const convertedTruthId = converted.json().data.createdEntityId as string; const convertedTruth = await prisma.acceptedTruthItem.findFirst({ where: { slug: convertedTruthId } }); expect(convertedTruth).toMatchObject({ kind: "decision", status: "converted" }); coreLoopIds.truth.push(convertedTruth!.id); const duplicateConversion = await mutate("POST", `/api/projects/widefrog/review-queue/${conversionReviewId}/convert`, { target: "decision" }); expect(duplicateConversion.statusCode).toBe(409); expect(duplicateConversion.json().error.code).toBe("REVIEW_ALREADY_DECIDED"); const prepared = await mutate("POST", `/api/projects/widefrog/accepted-truth/${truthId}/project-updates`); expect(prepared.statusCode).toBe(200); const updateId = prepared.json().data.createdEntityId as string; coreLoopIds.update.push(updateId); const concurrentUpdateApplications = await Promise.all([ mutate("PATCH", `/api/projects/widefrog/project-updates/${updateId}/apply`), mutate("PATCH", `/api/projects/widefrog/project-updates/${updateId}/apply`), ]); expect(concurrentUpdateApplications.map((response) => response.statusCode).sort()).toEqual([200, 409]); expect(concurrentUpdateApplications.find((response) => response.statusCode === 409)!.json().error.code).toBe("PROJECT_UPDATE_ALREADY_APPLIED"); const handoff = await mutate("POST", "/api/projects/widefrog/handoff-bundles", { target: "codex-task", title: "Core-loop database proof handoff", includeAcceptedTruth: true, includeProjectUpdates: true, includeOperatingLog: true, autoSelectContext: true }); expect(handoff.statusCode).toBe(201); coreLoopIds.handoff.push(handoff.json().data.id); expect(handoff.json().data.sourceTruthItemIds).toContain(truthId); const task = await mutate("POST", `/api/projects/widefrog/handoff-bundles/${handoff.json().data.id}/codex-task`, { goal: "Execute the database core-loop proof", validationCommands: ["pnpm validate:all"] }); expect(task.statusCode).toBe(201); coreLoopIds.task.push(task.json().data.id); const imported = await mutate("POST", `/api/projects/widefrog/codex-tasks/${task.json().data.id}/import-output`, { sourceRun: "core-loop-database-proof", summary: "Execution feedback returned to governance.", outputText: "Changed files: none. Validation passed. Review this evidence." }); expect(imported.statusCode).toBe(201); coreLoopIds.runImport.push(imported.json().data.importDto.id); coreLoopIds.review.push(imported.json().data.mutation.createdEntityId); const publicId = (value: string) => ({ OR: [{ id: value }, { slug: value }, { readableId: value }] }); expect(await prisma.captureItem.count({ where: publicId(capture.json().data.id) })).toBe(1); expect(await prisma.acceptedTruthItem.count({ where: { AND: [publicId(truthId), { status: "accepted" }] } })).toBe(1); expect(await prisma.projectUpdate.count({ where: { AND: [publicId(updateId), { status: "applied" }] } })).toBe(1); expect(await prisma.codexTask.count({ where: { AND: [publicId(task.json().data.id), { status: "imported" }] } })).toBe(1); expect(await prisma.reviewQueueItem.count({ where: publicId(imported.json().data.mutation.createdEntityId) })).toBe(1); await app.close(); }, 15_000); });