Files
ThothII/backend/test/auth-test-fixtures.ts
T

152 lines
5.2 KiB
TypeScript

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<string, string | undefined>): Record<string, string>;
close(): Promise<void>;
}
export interface LocalAuthFixtureOptions {
publicUrl?: string;
}
function firstSetCookie(response: { headers: Record<string, string | string[] | undefined> }): 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<LocalAuthFixture> {
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<string, string> = {
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;
}
}