import { chmodSync, mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { FastifyInstance } from "fastify"; import { stringify } from "yaml"; import { buildApp, type BuildAppDeps } from "../src/app.js"; import { loadConfig } from "../src/config.js"; import { createFixturePosixAuthStorageBridge } from "./fixtures/posix-auth-storage-bridge.mjs"; export const localPassword = "correct horse battery staple"; export const localPasswordHash = "$argon2id$v=19$m=65536,t=3,p=1$AAECAwQFBgcICQoLDA0ODw$DRo8ZSPI8G5OCvnFFapbVEjP69aDjy1Sw9i2743cPC4"; export const localAdminId = "6ba7b810-9dad-4ed1-80b4-00c04fd430c8"; export const localPublicUrl = "http://127.0.0.1:8787"; export interface LocalAuthFixture { app: FastifyInstance; publicUrl: string; cookie: string; csrfToken: string; loginStatus: number; meStatus: number; downstreamHits(): number; resetDownstreamHits(): void; sessionHeaders(overrides?: Record): Record; close(): Promise; } export interface LocalAuthFixtureOptions { publicUrl?: string; } function firstSetCookie(response: { headers: Record }): string { const header = response.headers["set-cookie"]; return Array.isArray(header) ? header[0] ?? "" : header ?? ""; } function cookiePair(setCookie: string): string { return setCookie.split(";", 1)[0] ?? ""; } /** Unit fixtures which bypass the installed tht binary start from an already-safe native layout. */ export function prepareAuthStateRoot(root: string): void { mkdirSync(root, { mode: 0o700 }); chmodSync(root, 0o700); for (const child of ["sessions", "oidc"]) { mkdirSync(join(root, child), { mode: 0o700 }); chmodSync(join(root, child), 0o700); } } /** Explicit test-only substitute for the production tht-backed POSIX storage bridge. */ export function createFixtureAuthStorageBridge() { return createFixturePosixAuthStorageBridge(); } /** Creates a production-local app and authenticates through the real login/session boundary. */ export async function createLocalAuthFixture( deps?: BuildAppDeps, options: LocalAuthFixtureOptions = {}, ): Promise { const publicUrl = options.publicUrl ?? localPublicUrl; const publicOrigin = new URL(publicUrl).origin; const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-auth-fixture-")); chmodSync(directory, 0o700); const authConfigFile = join(directory, "auth.yaml"); const usersFile = join(directory, "users.yaml"); writeFileSync(authConfigFile, stringify({ version: 1, mode: "local", publicUrl, local: { usersFile: "users.yaml" }, }), { encoding: "utf8", mode: 0o600 }); writeFileSync(usersFile, [ "version: 1", "users:", ` - id: ${localAdminId}`, " username: Admin", " displayName: Local administrator", ` passwordHash: ${localPasswordHash}`, " roles:", " - admin", " enabled: true", " authRevision: 1", "", ].join("\n"), { encoding: "utf8", mode: 0o600 }); chmodSync(authConfigFile, 0o600); chmodSync(usersFile, 0o600); const authStateRoot = join(directory, "auth-state"); prepareAuthStateRoot(authStateRoot); const app = buildApp( loadConfig({ THT_AUTH_CONFIG_FILE: authConfigFile, THT_AUTH_STATE_ROOT: authStateRoot, THT_HARNESS_DIR: "/tmp/h", }), { ...deps, authStorageBridgeForTest: deps?.authStorageBridgeForTest ?? createFixtureAuthStorageBridge(), }, ); let downstream = 0; app.addHook("preHandler", async () => { downstream += 1; }); try { const signedIn = await app.inject({ method: "POST", url: "/auth/local/login", headers: { origin: publicOrigin, "sec-fetch-site": "same-origin" }, payload: { username: "Admin", password: localPassword }, }); if (signedIn.statusCode !== 200) throw new Error("local_auth_fixture_login_failed"); const cookie = cookiePair(firstSetCookie(signedIn)); const me = await app.inject({ method: "GET", url: "/me", headers: { cookie } }); const csrfToken = (me.json() as { csrfToken?: unknown }).csrfToken; if (me.statusCode !== 200 || typeof csrfToken !== "string") throw new Error("local_auth_fixture_session_failed"); downstream = 0; return { app, publicUrl, cookie, csrfToken, loginStatus: signedIn.statusCode, meStatus: me.statusCode, downstreamHits: () => downstream, resetDownstreamHits: () => { downstream = 0; }, sessionHeaders(overrides = {}) { const headers: Record = { cookie, origin: publicOrigin, "sec-fetch-site": "same-origin", "x-thothii-csrf": csrfToken, }; for (const [key, value] of Object.entries(overrides)) { if (value === undefined) delete headers[key]; else headers[key] = value; } return headers; }, async close() { await app.close(); rmSync(directory, { recursive: true, force: true }); }, }; } catch (error) { await app.close(); rmSync(directory, { recursive: true, force: true }); throw error; } }