Files
ThothII/backend/test/secret-bundle.test.ts

108 lines
4.6 KiB
TypeScript

import { afterEach, expect, test } from "vitest";
import { chmodSync, mkdtempSync, renameSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import {
loadSecretBundle,
loadSecretBundleWithFs,
METADATA_GENERATION_SECRET_KEYS,
secretValue,
} from "../src/config/secret-bundle.js";
const dirs: string[] = [];
afterEach(() => { for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); });
function bundle(contents: string, mode = 0o600): string {
const dir = mkdtempSync(join(tmpdir(), "thothii-secret-bundle-"));
dirs.push(dir);
const file = join(dir, "bundle");
writeFileSync(file, contents, { mode });
chmodSync(file, mode);
return file;
}
test("parses comments, blank lines and values containing equals", () => {
const file = bundle("# comment\n\nTHT_MODEL_API_KEY=abc=123\nTHT_DWH_API_KEY=dwh\n");
expect(loadSecretBundle(file)).toEqual(new Map([
["THT_MODEL_API_KEY", "abc=123"], ["THT_DWH_API_KEY", "dwh"],
]));
});
test("existing secret consumers accept a bundle containing an allowed metadata-model key", () => {
const file = bundle("THT_DWH_API_KEY=dwh-secret\nOPENAI_API_KEY=metadata-secret\n");
expect(secretValue({ secretsFile: file }, "THT_DWH_API_KEY")).toBe("dwh-secret");
});
test.each(METADATA_GENERATION_SECRET_KEYS)("accepts audited metadata-model key %s", (name) => {
expect(loadSecretBundle(bundle(`${name}=metadata-secret\n`)).get(name)).toBe("metadata-secret");
});
test("accepts the fixed OIDC and Authentik secret references", () => {
const file = bundle("THT_OIDC_CLIENT_SECRET=oidc-secret\nTHT_AUTHENTIK_API_TOKEN=authentik-token\n");
expect(loadSecretBundle(file)).toEqual(new Map([
["THT_OIDC_CLIENT_SECRET", "oidc-secret"],
["THT_AUTHENTIK_API_TOKEN", "authentik-token"],
]));
});
test.each([
["THT_OIDC_CLIENT_SECRET", 4096],
["THT_AUTHENTIK_API_TOKEN", 16 * 1024],
])("enforces the shared exact value boundary for %s", (name, maximum) => {
expect(loadSecretBundle(bundle(`${name}=${"x".repeat(maximum)}\n`)).get(name)).toHaveLength(maximum);
expect(() => loadSecretBundle(bundle(`${name}=${"x".repeat(maximum + 1)}\n`)))
.toThrow("secret bundle is unavailable");
expect(() => loadSecretBundle(bundle(`${name}=invalid\u0000secret\n`)))
.toThrow("secret bundle is unavailable");
});
test.each([
["duplicate", "THT_MODEL_API_KEY=a\nTHT_MODEL_API_KEY=b\n"],
["unknown", "UNKNOWN_KEY=x\n"],
["empty", "THT_MODEL_API_KEY=\n"],
["syntax", "THT_MODEL_API_KEY\n"],
])("rejects %s bundle lines without exposing values", (_name, contents) => {
expect(() => loadSecretBundle(bundle(contents))).toThrow("secret bundle is unavailable");
expect(() => loadSecretBundle(bundle(contents))).not.toThrow(/abc|dwh/);
});
test("rejects missing and insecure files", () => {
const file = bundle("THT_MODEL_API_KEY=secret\n", 0o644);
expect(() => loadSecretBundle(file)).toThrow("secret bundle is unavailable");
expect(() => loadSecretBundle(join(file, "missing"))).toThrow("secret bundle is unavailable");
});
test("checks inode identity before parsing", () => {
const file = bundle("THT_MODEL_API_KEY=secret\n");
const replacement = `${file}.replacement`;
writeFileSync(replacement, "THT_MODEL_API_KEY=replaced\n", { mode: 0o600 });
// A real replacement is safe because the loader's open/fstat check is the invariant;
// this also ensures the normal post-replacement file remains parseable.
renameSync(replacement, file);
expect(loadSecretBundle(file).get("THT_MODEL_API_KEY")).toBe("replaced");
});
test("rejects inode replacement between lstat and open without reading", () => {
let reads = 0;
const stat = (ino: number) => ({
dev: 7, ino, uid: process.getuid?.() ?? 0, mode: 0o100600, nlink: 1, size: 24,
isFile: () => true, isDirectory: () => false, isSymbolicLink: () => false,
});
expect(() => loadSecretBundleWithFs("/safe/bundle", {
lstat: () => stat(1) as any,
open: () => 9,
fstat: () => stat(2) as any,
read: () => { reads += 1; return "THT_MODEL_API_KEY=secret\n"; },
close: () => undefined,
})).toThrow("secret bundle is unavailable");
expect(reads).toBe(0);
});
test("secretValue prefers bundle and supports the legacy file fallback", () => {
const file = bundle("THT_MODEL_API_KEY=from-bundle\n");
const legacy = bundle("from-legacy");
expect(secretValue({ secretsFile: file, secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY"))
.toBe("from-bundle");
expect(secretValue({ secretFiles: { THT_MODEL_API_KEY_SECRET_FILE: legacy } }, "THT_MODEL_API_KEY"))
.toBe("from-legacy");
});