Files
ThothII/backend/test/routes-workspaces.test.ts
T

499 lines
19 KiB
TypeScript

import { execFile } from "node:child_process";
import { existsSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { afterEach, expect, test, vi } from "vitest";
import { buildApp } from "../src/app.js";
import { loadConfig } from "../src/config.js";
import { createProductionWorkspaceDiagnoser } from "../src/workspaces/diagnostics.js";
import { WorkspaceRegistry, type WorkspaceRevision } from "../src/workspaces/registry.js";
import { serializeWorkspaceYaml, type CanonicalWorkspace } from "../src/workspaces/schema.js";
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
import type { AuthDiagnoser, AuthDiagnostics } from "../src/auth/diagnostics.js";
const workspace: CanonicalWorkspace = {
workspace: {
schema_version: 4,
id: "psd-clinical",
name: "Policlinico San Donato",
description: "Clinical analytics workspace",
language: "it",
},
dwh: {
engine: "postgres",
database: "warehouse",
schema: "datawarehouse",
supported_transports: ["postgres_direct"],
},
};
const revision: WorkspaceRevision = {
id: workspace.workspace.id,
commit: "a".repeat(40),
blob: "b".repeat(40),
snapshotPath: "/registry/snapshots/psd-clinical.yaml",
};
type RegistryFake = Pick<WorkspaceRegistry, "bootstrap" | "pull" | "list" | "listCatalog" | "read">;
function registryFake(overrides: Partial<RegistryFake> = {}): RegistryFake {
return {
bootstrap: vi.fn(async () => ({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
})),
pull: vi.fn(async () => ({
branch: "main", head: revision.commit, ahead: 0, behind: 0, degraded: false,
})),
list: vi.fn(async () => [revision]),
listCatalog: vi.fn(async () => [{
id: "psd-clinical",
name: "Policlinico San Donato",
configurationState: "ready" as const,
revision,
}]),
read: vi.fn(async () => ({ workspace, revision })),
...overrides,
};
}
const readyAuthentication: AuthDiagnostics = {
ready: true,
mode: "none",
checks: [{ level: "info", code: "auth_ready", message: "Authentication is ready." }],
};
function appFor(
registry: RegistryFake,
diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] })),
secretStore = testSecretStore(),
env: Record<string, string> = {},
authDiagnoser: AuthDiagnoser = { inspect: vi.fn(async () => readyAuthentication) },
) {
return buildApp(loadConfig({
THT_HARNESS_DIR: "/missing-harness",
THT_WORKSPACE_REGISTRY_ROOT: "/tmp/thoth-route-test-registry",
...env,
}), {
thtRunner: {} as any,
workspaceRegistry: registry as WorkspaceRegistry,
workspaceDiagnoser: diagnose,
workspaceSecretStore: secretStore,
authDiagnoser,
} as any);
}
const userHeaders = {
"x-thoth-principal-issuer": "portal",
"x-thoth-principal-subject": "alice",
"x-thoth-is-admin": "0",
};
const adminHeaders = { ...userHeaders, "x-thoth-principal-subject": "admin", "x-thoth-is-admin": "1" };
const secretStoreRoots: string[] = [];
function testSecretStore(): WorkspaceSecretStore {
const root = mkdtempSync(join(tmpdir(), "thoth-route-secret-store-"));
const runtimeRoot = mkdtempSync(join(tmpdir(), "thoth-route-secret-runtime-"));
secretStoreRoots.push(root, runtimeRoot);
return new WorkspaceSecretStore({ root, runtimeRoot, installationId: "route-test" });
}
test("returns a redacted registry status and pulls without Git credential details", async () => {
const registry = registryFake({
bootstrap: vi.fn(async () => ({
branch: "main",
head: revision.commit,
ahead: 0,
behind: 0,
degraded: true,
lastError: "git_auth_failed" as const,
})),
});
const app = appFor(registry);
const status = await app.inject({ method: "GET", url: "/workspace-registry/status" });
const pull = await app.inject({ method: "POST", url: "/workspace-registry/pull" });
expect(status.statusCode).toBe(200);
expect(status.json()).toMatchObject({ branch: "main", degraded: true, lastError: "git_auth_failed" });
expect(status.body).not.toMatch(/token|credential|private.?key/i);
expect(pull.statusCode).toBe(200);
expect(registry.pull).toHaveBeenCalledTimes(1);
});
test("workspace mutations and secret writes require their catalog permissions", async () => {
const registry = registryFake();
const app = appFor(registry, undefined, testSecretStore(), { AUTH_MODE: "upstream" });
try {
const deniedPull = await app.inject({ method: "POST", url: "/workspace-registry/pull", headers: userHeaders });
const allowedPull = await app.inject({ method: "POST", url: "/workspace-registry/pull", headers: adminHeaders });
const deniedBootstrap = await app.inject({ method: "GET", url: "/workspace-registry/status", headers: userHeaders });
const allowedBootstrap = await app.inject({ method: "GET", url: "/workspace-registry/status", headers: adminHeaders });
const deniedSecret = await app.inject({
method: "PUT", url: "/workspaces/psd-clinical/secrets", headers: userHeaders,
payload: { values: { "dwh.password": "secret" } },
});
const allowedSecret = await app.inject({
method: "PUT", url: "/workspaces/psd-clinical/secrets", headers: adminHeaders,
payload: { values: { "dwh.password": "secret" } },
});
expect(deniedPull.statusCode).toBe(403);
expect(deniedPull.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted" });
expect(allowedPull.statusCode).toBe(200);
expect(deniedBootstrap.statusCode).toBe(403);
expect(deniedBootstrap.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted" });
expect(allowedBootstrap.statusCode).toBe(200);
expect(deniedSecret.statusCode).toBe(403);
expect(deniedSecret.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted" });
expect(allowedSecret.statusCode).toBe(200);
} finally {
await app.close();
}
});
test.each([
["POST", "/workspaces/publish"],
["GET", "/workspaces/psd-clinical/export"],
["POST", "/workspaces/import"],
] as const)("does not register the removed %s %s mutation or bundle route", async (method, url) => {
const response = await appFor(registryFake()).inject({ method, url });
expect(response.statusCode).toBe(404);
});
test("lists workspace summaries and reads a validated immutable workspace", async () => {
const app = appFor(registryFake());
const list = await app.inject({ method: "GET", url: "/workspaces" });
const read = await app.inject({ method: "GET", url: "/workspaces/psd-clinical" });
expect(list.statusCode).toBe(200);
expect(list.json()).toEqual([expect.objectContaining({
id: "psd-clinical",
displayName: "Policlinico San Donato",
configurationState: "configuration_required",
revision,
})]);
expect(read.statusCode).toBe(200);
expect(read.json()).toEqual({ workspace, revision });
});
test("validates a schema v4 workspace without mutating the repository", async () => {
const app = appFor(registryFake());
const response = await app.inject({
method: "POST", url: "/workspaces/validate", payload: { workspace },
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({ workspace });
});
test("aggregates one static and one live authentication report without reordering connector diagnostics", async () => {
const connectorDiagnostics = [{
level: "info" as const,
code: "binding_ok" as const,
message: "Installation bindings and diagnostics succeeded.",
}];
const diagnose = vi.fn(async () => ({ activatable: true, diagnostics: connectorDiagnostics }));
const authentication: AuthDiagnostics = {
ready: false,
mode: "oidc",
checks: [{
level: "error",
code: "oidc_mapped_group_missing",
field: "Thoth Administrators",
message: "A configured authorization group does not exist.",
}],
};
const authDiagnoser: AuthDiagnoser = { inspect: vi.fn(async () => authentication) };
const app = appFor(registryFake(), diagnose, testSecretStore(), {}, authDiagnoser);
const validation = await app.inject({
method: "POST", url: "/workspaces/validate", payload: { workspace },
});
const connection = await app.inject({
method: "POST", url: "/workspaces/psd-clinical/test", payload: {},
});
expect(validation.statusCode).toBe(200);
expect(validation.json()).toMatchObject({
workspace,
activatable: false,
diagnostics: [],
authentication,
});
expect(connection.statusCode).toBe(200);
expect(connection.json()).toEqual({
activatable: false,
diagnostics: connectorDiagnostics,
authentication,
});
expect(diagnose).toHaveBeenCalledTimes(1);
expect(authDiagnoser.inspect).toHaveBeenNthCalledWith(1, { live: false });
expect(authDiagnoser.inspect).toHaveBeenNthCalledWith(2, { live: true });
});
test("fails closed without reflecting a hostile authentication report", async () => {
const attacker = "attacker-field-SENTINEL";
const authDiagnoser: AuthDiagnoser = { inspect: vi.fn(async () => ({
ready: false,
mode: "oidc",
checks: [{
level: "error", code: "oidc_secret_missing", message: "failure", field: attacker,
}],
} as AuthDiagnostics)) };
const app = appFor(registryFake(), undefined, testSecretStore(), {}, authDiagnoser);
const response = await app.inject({
method: "POST", url: "/workspaces/validate", payload: { workspace },
});
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({ code: "workspace_invalid", message: "Workspace request is invalid." });
expect(response.body).not.toContain(attacker);
});
test.each([1, 2])("rejects schema v%s at the validation boundary with a sanitized error", async (version) => {
const legacy = {
...workspace,
workspace: { ...workspace.workspace, schema_version: version },
};
const response = await appFor(registryFake()).inject({
method: "POST", url: "/workspaces/validate", payload: { workspace: legacy },
});
expect(response.statusCode).toBe(400);
expect(response.json()).toEqual({ code: "workspace_invalid", message: "Workspace request is invalid." });
expect(response.body).not.toMatch(/migration_required|schema version/i);
});
test("runs diagnostics for a schema v4 workspace", async () => {
const diagnose = vi.fn(async () => ({ activatable: true, diagnostics: [] }));
const app = appFor(registryFake(), diagnose);
const response = await app.inject({
method: "POST", url: "/workspaces/psd-clinical/test", payload: {},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toEqual({ activatable: true, diagnostics: [], authentication: readyAuthentication });
expect(diagnose).toHaveBeenCalledWith(workspace, {
dwh: expect.objectContaining({ transport: "postgres_direct" }),
evidence: { missing: [], values: {} },
}, { writeProbe: false });
});
test("reports runtime secret requirements without returning stored values", async () => {
const secretStore = testSecretStore();
const app = appFor(registryFake(), undefined, secretStore);
const missing = await app.inject({
method: "GET", url: "/workspaces/psd-clinical/runtime-configuration",
});
expect(missing.statusCode).toBe(200);
expect(missing.json()).toMatchObject({
workspaceId: "psd-clinical",
revision,
configurationState: "configuration_required",
requirements: [{
id: "dwh.password",
connector: "dwh",
label: "Data warehouse password",
required: true,
configured: false,
}],
});
const secret = "never-return-this-password";
const save = await app.inject({
method: "PUT",
url: "/workspaces/psd-clinical/secrets",
payload: { values: { "dwh.password": secret } },
});
expect(save.statusCode).toBe(200);
expect(save.body).not.toContain(secret);
expect(save.json()).toMatchObject({
configurationState: "ready",
requirements: [{ id: "dwh.password", configured: true }],
});
const configured = await app.inject({
method: "GET", url: "/workspaces/psd-clinical/runtime-configuration",
});
expect(configured.body).not.toContain(secret);
expect(configured.json()).toMatchObject({ configurationState: "ready" });
});
test("rejects undeclared secret identifiers and supports forgetting a configured secret", async () => {
const secretStore = testSecretStore();
const app = appFor(registryFake(), undefined, secretStore);
const unknown = await app.inject({
method: "PUT",
url: "/workspaces/psd-clinical/secrets",
payload: { values: { "evidence.secret_key": "not-applicable" } },
});
expect(unknown.statusCode).toBe(400);
expect(secretStore.configured("psd-clinical")).toEqual([]);
secretStore.put("psd-clinical", "dwh.password", "temporary-password");
const forget = await app.inject({
method: "DELETE",
url: "/workspaces/psd-clinical/secrets/dwh.password",
});
expect(forget.statusCode).toBe(200);
expect(forget.json()).toMatchObject({ configurationState: "configuration_required" });
expect(secretStore.has("psd-clinical", "dwh.password")).toBe(false);
});
test("materializes stored secrets only for the diagnostic lease", async () => {
const secretStore = testSecretStore();
secretStore.put("psd-clinical", "dwh.password", "diagnostic-password");
let materializedPath = "";
const diagnose = vi.fn(async (_workspace, bindings) => {
materializedPath = bindings.dwh.values.THT_WS_PSD_CLINICAL_DWH_PASSWORD_FILE;
expect(readFileSync(materializedPath, "utf8")).toBe("diagnostic-password");
return { activatable: true, diagnostics: [] };
});
const app = appFor(registryFake(), diagnose, secretStore);
const response = await app.inject({
method: "POST", url: "/workspaces/psd-clinical/test", payload: {},
});
expect(response.statusCode).toBe(200);
expect(materializedPath).not.toBe("");
expect(existsSync(materializedPath)).toBe(false);
});
test("reports a missing Evidence credential without changing the registry revision", async () => {
const evidenceWorkspace: CanonicalWorkspace = {
...workspace,
evidence: {
source: {
type: "http",
uris: ["https://evidence.example.test/guide.md"],
authentication: "signed_urls_file",
connect_timeout_ms: 5_000,
read_timeout_ms: 30_000,
max_bytes: 10 * 1024 * 1024,
max_redirects: 5,
allow_private_hosts: false,
max_cache_bytes: 64 * 1024 * 1024,
},
policy: { max_chunk_chars: 4_000, retain_published_generations: 3 },
},
};
const read = vi.fn(async () => ({ workspace: evidenceWorkspace, revision }));
const app = appFor(registryFake({ read }), createProductionWorkspaceDiagnoser(100));
const variable = "THT_WS_PSD_CLINICAL_EVIDENCE_SIGNED_URLS_FILE";
const previous = process.env[variable];
delete process.env[variable];
try {
const response = await app.inject({
method: "POST", url: "/workspaces/psd-clinical/test", payload: {},
});
expect(response.statusCode).toBe(200);
expect(response.json()).toMatchObject({
activatable: false,
diagnostics: expect.arrayContaining([expect.objectContaining({
code: "binding_missing",
field: "evidence.source.authentication",
variable,
})]),
});
expect(read).toHaveBeenCalledTimes(1);
} finally {
if (previous === undefined) delete process.env[variable];
else process.env[variable] = previous;
}
});
const runFile = promisify(execFile);
const realRouteRoots: string[] = [];
async function git(cwd: string, args: string[]): Promise<string> {
const { stdout } = await runFile("git", args, { cwd });
return stdout.trim();
}
async function createRealRouteFixture() {
const root = mkdtempSync(join(tmpdir(), "thoth-workspace-route-"));
realRouteRoots.push(root);
const remote = join(root, "remote.git");
const author = join(root, "author");
const registryRoot = join(root, "registry");
await git(root, ["init", "--bare", "--initial-branch=main", remote]);
mkdirSync(author);
await git(author, ["init", "--initial-branch=main"]);
await git(author, ["config", "user.name", "Workspace Route Test"]);
await git(author, ["config", "user.email", "workspace-route@example.invalid"]);
const descriptor: CanonicalWorkspace = {
...workspace,
evidence: {
source: {
type: "filesystem",
uri: "psd-clinical/evidence",
patterns: ["**/*.md"],
max_bytes: 1024 * 1024,
},
policy: { max_chunk_chars: 4_000, retain_published_generations: 3 },
},
};
writeFileSync(join(author, "thoth-workspaces.yaml"), [
"schema_version: 1",
"workspaces:",
" - id: psd-clinical",
" name: Policlinico San Donato",
" description: Clinical analytics workspace",
"",
].join("\n"));
mkdirSync(join(author, "psd-clinical", "evidence"), { recursive: true });
writeFileSync(join(author, "psd-clinical", "workspace.yaml"), serializeWorkspaceYaml(descriptor));
writeFileSync(join(author, "psd-clinical", "evidence", "guide.md"), "Evidence bytes\n");
await git(author, ["add", "."]);
await git(author, ["commit", "-m", "Initial workspace"]);
await git(author, ["remote", "add", "origin", remote]);
await git(author, ["push", "origin", "main"]);
const initialCommit = await git(author, ["rev-parse", "HEAD"]);
const config = loadConfig({
THT_HARNESS_DIR: "/missing-harness",
THT_WORKSPACE_REGISTRY_ROOT: registryRoot,
THT_WORKSPACE_GIT_REMOTE: remote,
});
const registry = new WorkspaceRegistry(config.workspaceRegistry);
const app = buildApp(config, {
thtRunner: {} as any,
workspaceRegistry: registry,
workspaceDiagnoser: vi.fn(async () => ({ activatable: true, diagnostics: [] })),
});
return { author, initialCommit, app, registry };
}
afterEach(() => {
realRouteRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
secretStoreRoots.splice(0).forEach((root) => rmSync(root, { recursive: true, force: true }));
});
test("a failed candidate pull keeps the last valid active workspace", async () => {
const fixture = await createRealRouteFixture();
await fixture.registry.bootstrap();
rmSync(join(fixture.author, "psd-clinical", "evidence"), { recursive: true });
await git(fixture.author, ["add", "-A"]);
await git(fixture.author, ["commit", "-m", "Remove required Evidence tree"]);
await git(fixture.author, ["push", "origin", "main"]);
const pull = await fixture.app.inject({ method: "POST", url: "/workspace-registry/pull" });
expect(pull.statusCode).toBe(400);
expect(pull.json()).toEqual({ code: "workspace_invalid", message: "Workspace request is invalid." });
await expect(fixture.registry.read("psd-clinical")).resolves.toMatchObject({
revision: { commit: fixture.initialCommit },
});
});