import { expect, test } from "vitest"; import { chmodSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { stringify } from "yaml"; import { loadConfig } from "../src/config.js"; function authFile(value: unknown): { directory: string; file: string } { const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-app-auth-config-")); chmodSync(directory, 0o700); const file = join(directory, "auth.yaml"); writeFileSync(file, stringify(value), { encoding: "utf8", mode: 0o600 }); chmodSync(file, 0o600); return { directory, file }; } function oidcAuthConfig(): Record { return { version: 1, mode: "oidc", publicUrl: "https://thothii.example.org", oidc: { issuer: "https://authentik.example.org/application/o/thothii/", clientId: "thothii", clientSecretRef: "THT_OIDC_CLIENT_SECRET", scopes: ["openid"], groupsClaim: "groups", }, groupCatalog: { driver: "authentik", baseUrl: "https://authentik.example.org", apiTokenRef: "THT_AUTHENTIK_API_TOKEN" }, authorization: { groupRoles: { "TOT Users": ["user"], "TOT Admin": ["admin"] } }, }; } test("loadConfig accepts container listening and runtime paths", () => { expect(loadConfig({ HOST: "0.0.0.0", PORT: "9000", THT_HARNESS_DIR: "/app/harness", THT_BIN: "/opt/venv/bin/tht", PI_BIN: "/usr/local/bin/pi", SETTINGS_FILE: "/data/settings/settings.json", THT_DATA_ROOT: "/data", })).toMatchObject({ host: "0.0.0.0", port: 9000, harnessDir: "/app/harness", thtBin: "/opt/venv/bin/tht", piBin: "/usr/local/bin/pi", settingsFile: "/data/settings/settings.json", maintenanceFile: "/data/settings/maintenance.json", dataRoot: "/data", }); }); test("loadConfig keeps local development defaults", () => { expect(loadConfig({})).toMatchObject({ host: "127.0.0.1", port: 8787, harnessDir: "../harness", thtBin: "tht", piBin: "pi", settingsFile: "data/settings.json", maintenanceFile: "data/maintenance.json", workspaceRegistry: { root: "/data/workspace-registry", branch: "main", }, workspaceSecretStoreRoot: "/data/workspace-secrets", workspaceSecretRuntimeRoot: "/tmp/thothii-workspace-secrets", internalQdrantUrl: "http://qdrant:6333", internalEmbeddingUrl: "http://embedding:11434", internalEmbeddingModel: "qwen3-embedding:0.6b", internalEmbeddingDimensions: 1024, authMode: "none", authConfigFile: "/run/thothii-auth/auth.yaml", authStateRoot: "/data/auth", }); expect(loadConfig({}).dataRoot).toBeUndefined(); }); test("loadConfig allows none and mock only outside production when auth.yaml is absent", () => { const originalNodeEnvironment = process.env.NODE_ENV; delete process.env.NODE_ENV; try { expect(() => loadConfig({ AUTH_MODE: "none" })).toThrow("production requires auth.yaml or AUTH_MODE=upstream"); } finally { if (originalNodeEnvironment === undefined) delete process.env.NODE_ENV; else process.env.NODE_ENV = originalNodeEnvironment; } expect(loadConfig({ NODE_ENV: "test", AUTH_MODE: "mock" }).authMode).toBe("mock"); expect(loadConfig({ NODE_ENV: "development", AUTH_MODE: "none" }).authMode).toBe("none"); expect(loadConfig({ NODE_ENV: "production", AUTH_MODE: "upstream" }).authMode).toBe("upstream"); expect(() => loadConfig({ NODE_ENV: "production" })).toThrow("production requires auth.yaml or AUTH_MODE=upstream"); expect(() => loadConfig({ NODE_ENV: "production", AUTH_MODE: "mock" })) .toThrow("production requires auth.yaml or AUTH_MODE=upstream"); }); test("local Compose profiles explicitly select the development auth environment", () => { for (const profile of ["../../deploy/compose.local.yaml", "../../docker-compose.dev.yml"]) { expect(readFileSync(new URL(profile, import.meta.url), "utf8")).toMatch(/NODE_ENV:\s*development/); } }); test("loadConfig makes an existing auth.yaml authoritative and rejects AUTH_MODE split-brain", () => { const { directory, file } = authFile(oidcAuthConfig()); try { const config = loadConfig({ THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: "/state/auth" }); expect(config.authMode).toBe("oidc"); expect(config.authStateRoot).toBe("/state/auth"); expect(config.authentication?.current().sourcePath).toBe(file); expect(() => loadConfig({ THT_AUTH_CONFIG_FILE: file, AUTH_MODE: "upstream" })) .toThrow("authentication configuration and AUTH_MODE cannot both be set"); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("loadConfig rejects an auth config path that exists but is not a regular file", () => { const directory = mkdtempSync(join(realpathSync(tmpdir()), "thothii-app-auth-config-directory-")); try { expect(() => loadConfig({ THT_AUTH_CONFIG_FILE: directory })) .toThrow("authentication configuration is invalid"); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("public exposure accepts configured OIDC and the upstream migration mode only", () => { const { directory, file } = authFile(oidcAuthConfig()); try { expect(loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", THT_AUTH_CONFIG_FILE: file, THT_SESSION_STORAGE: "postgres", THT_SESSION_DB_HOST: "db.internal", THT_SESSION_DB_NAME: "thoth", THT_SESSION_RUNTIME_USER: "thoth_sessions_app", THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password", THT_SESSION_DB_SSLMODE: "verify-full", THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem", }).authMode).toBe("oidc"); expect(() => loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "mock" })) .toThrow("public exposure requires AUTH_MODE=upstream or configured OIDC"); } finally { rmSync(directory, { recursive: true, force: true }); } }); test("loadConfig accepts only the allowed internal semantic runtime hosts", () => { expect(loadConfig({ THT_INTERNAL_QDRANT_URL: "http://localhost:6333", THT_INTERNAL_EMBEDDING_URL: "http://127.0.0.1:11434", })).toMatchObject({ internalQdrantUrl: "http://localhost:6333", internalEmbeddingUrl: "http://127.0.0.1:11434", }); expect(() => loadConfig({ THT_INTERNAL_QDRANT_URL: "http://qdrant.internal:6333" })) .toThrow(/internal.*qdrant|host validation|invalid/i); expect(() => loadConfig({ THT_INTERNAL_EMBEDDING_URL: "http://example.com:11434" })) .toThrow(/internal.*embedding|host validation|invalid/i); expect(() => loadConfig({ THT_INTERNAL_QDRANT_URL: "https://qdrant:6333" })) .toThrow(/internal.*qdrant|invalid/i); expect(() => loadConfig({ THT_INTERNAL_EMBEDDING_URL: "https://embedding:11434" })) .toThrow(/internal.*embedding|invalid/i); }); test("loadConfig enables the legacy workspace request only through explicit local mode", () => { expect(loadConfig({ THT_LEGACY_WORKSPACE_MODE: "local" }).legacyWorkspaceMode).toBe(true); expect(() => loadConfig({ THT_LEGACY_WORKSPACE_MODE: "local", AUTH_MODE: "upstream", THT_SESSION_STORAGE: "postgres", THT_SESSION_DB_HOST: "db.internal", THT_SESSION_DB_NAME: "thoth", THT_SESSION_RUNTIME_USER: "thoth_sessions_app", THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password", THT_SESSION_DB_SSLMODE: "verify-full", THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem", })).toThrow(/legacy workspace mode requires local session storage/); expect(() => loadConfig({ THT_LEGACY_WORKSPACE_MODE: "true" })) .toThrow(/legacy workspace mode configuration is invalid/); }); test("loadConfig rejects unauthenticated public exposure", () => { expect(() => loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "none", })).toThrow(/public exposure requires AUTH_MODE=upstream/); }); test("loadConfig accepts an authenticated upstream trust boundary", () => { expect(loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "upstream", THT_SESSION_STORAGE: "postgres", THT_SESSION_DB_HOST: "db.internal", THT_SESSION_DB_NAME: "thoth", THT_SESSION_RUNTIME_USER: "thoth_sessions_app", THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password", THT_SESSION_DB_SSLMODE: "verify-full", THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem", }).authMode).toBe("upstream"); }); test("loadConfig requires direct PostgreSQL TLS inputs for the public server session store", () => { const env = { THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "upstream", THT_SESSION_STORAGE: "postgres", THT_SESSION_DB_HOST: "db.internal", THT_SESSION_DB_NAME: "thoth", THT_SESSION_RUNTIME_USER: "thoth_sessions_app", THT_SESSION_RUNTIME_PASSWORD_FILE: "/run/secrets/session_runtime_password", THT_SESSION_DB_SSLMODE: "verify-full", THT_SESSION_DB_SSLROOTCERT: "/run/secrets/session_ca.pem", }; expect(loadConfig(env).sessionStorage).toMatchObject({ mode: "postgres", host: "db.internal", port: 5432, database: "thoth", runtimeUser: "thoth_sessions_app", runtimePasswordFile: "/run/secrets/session_runtime_password", sslmode: "verify-full", sslrootcert: "/run/secrets/session_ca.pem", }); for (const required of [ "THT_SESSION_DB_HOST", "THT_SESSION_DB_NAME", "THT_SESSION_RUNTIME_USER", "THT_SESSION_RUNTIME_PASSWORD_FILE", "THT_SESSION_DB_SSLMODE", "THT_SESSION_DB_SSLROOTCERT", ]) { const missing = { ...env, [required]: undefined }; expect(() => loadConfig(missing)).toThrow(/server session storage configuration is invalid/); } }); test("loadConfig rejects public local storage and server storage without upstream auth", () => { expect(() => loadConfig({ THOTH_PUBLIC_EXPOSURE: "true", AUTH_MODE: "upstream", THT_SESSION_STORAGE: "local", })).toThrow(/local session storage requires loopback-only deployment/); expect(() => loadConfig({ THT_SESSION_STORAGE: "postgres", AUTH_MODE: "none", })).toThrow(/server session storage requires AUTH_MODE=upstream/); }); test("loadConfig accepts only an absolute generic model key file", () => { expect(loadConfig({ THT_MODEL_API_KEY_FILE: "/run/secrets/model_api_key" }).modelApiKeyFile) .toBe("/run/secrets/model_api_key"); expect(() => loadConfig({ THT_MODEL_API_KEY_FILE: "relative/key" })) .toThrow(/model credential configuration is invalid/); expect(() => loadConfig({ THT_MODEL_API_KEY_FILE: " /run/secrets/key" })) .toThrow(/model credential configuration is invalid/); });