Files
ThothII/backend/test/config.test.ts
T

336 lines
14 KiB
TypeScript

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<string, unknown> {
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 accepts only an absolute mounted Pi authentication source", () => {
expect(loadConfig({ THT_PI_AUTH_FILE: "/home/thoth/.pi/agent/auth.json" }).piAuthFile)
.toBe("/home/thoth/.pi/agent/auth.json");
expect(() => loadConfig({ THT_PI_AUTH_FILE: "relative/auth.json" }))
.toThrow("Pi authentication source configuration is invalid");
});
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",
internalEmbeddingId: "ollama/qwen3-embedding:0.6b",
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("workspace maintenance loads production configuration without an authentication surface", () => {
expect(loadConfig(
{ NODE_ENV: "production", THOTH_PUBLIC_EXPOSURE: "true" },
{ surface: "workspace-maintenance" },
)).toMatchObject({
authMode: "none",
authentication: undefined,
publicExposure: false,
});
});
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.each([
["relative root", { THT_AUTH_RUNTIME_PROJECTION_ROOT: "relative" }],
["conflicting direct file", {
THT_AUTH_RUNTIME_PROJECTION_ROOT: "/run/thothii-auth",
THT_AUTH_CONFIG_FILE: "/different/auth.yaml",
}],
])("rejects projected authentication configuration: %s", (_name, env) => {
expect(() => loadConfig(env)).toThrow("authentication configuration is invalid");
});
test("keeps the direct auth-file provider when the runtime projection environment is absent", () => {
const { directory, file } = authFile(oidcAuthConfig());
try {
const loaded = loadConfig({ THT_AUTH_CONFIG_FILE: file, THT_AUTH_STATE_ROOT: "/state/auth" });
const current = loaded.authentication?.current();
expect(current).toMatchObject({
sourcePath: file,
value: { mode: "oidc" },
});
expect(current?.runtimeProjection).toBeUndefined();
} 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 derives the embedding runtime model from its canonical catalog identity", () => {
expect(loadConfig({
THT_INTERNAL_EMBEDDING_ID: "ollama/nomic-embed-text",
THT_INTERNAL_EMBEDDING_MODEL: "nomic-embed-text",
})).toMatchObject({
internalEmbeddingId: "ollama/nomic-embed-text",
internalEmbeddingModel: "nomic-embed-text",
});
expect(() => loadConfig({
THT_INTERNAL_EMBEDDING_ID: "ollama/nomic-embed-text",
THT_INTERNAL_EMBEDDING_MODEL: "different-model",
})).toThrow("does not match its canonical identity");
expect(() => loadConfig({ THT_INTERNAL_EMBEDDING_ID: "not-canonical" }))
.toThrow("embedding identity configuration is invalid");
});
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/);
});
test("loadConfig accepts only an absolute runtime installation descriptor path", () => {
expect(loadConfig({
THT_INSTALLATION_CONFIG_FILE: "/run/thothii-installation/thothii-installation.yaml",
}).installationConfigFile).toBe("/run/thothii-installation/thothii-installation.yaml");
expect(() => loadConfig({ THT_INSTALLATION_CONFIG_FILE: "host/thothii-installation.yaml" }))
.toThrow("installation configuration is invalid");
});
test("loadConfig accepts a file-backed catalog role and rejects partial catalog configuration", () => {
expect(loadConfig({
THT_CATALOG_DB_HOST: "catalog-db",
THT_CATALOG_DB_NAME: "thothii_catalog",
THT_CATALOG_RUNTIME_USER: "thothii_catalog_runtime",
THT_CATALOG_RUNTIME_PASSWORD_FILE: "/run/secrets/catalog_runtime_password",
}).catalogDatabase).toEqual({
host: "catalog-db",
port: 5432,
database: "thothii_catalog",
user: "thothii_catalog_runtime",
passwordFile: "/run/secrets/catalog_runtime_password",
});
expect(() => loadConfig({ THT_CATALOG_DB_HOST: "catalog-db" }))
.toThrow(/catalog database configuration is invalid/);
expect(() => loadConfig({ THT_CATALOG_DATABASE_URL: "https://catalog.invalid/db" }))
.toThrow(/catalog database configuration is invalid/);
});