import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, expect, test, vi } from "vitest"; import { buildApp } from "../src/app.js"; import { loadConfig } from "../src/config.js"; import { MemoryCatalogRepository } from "../src/catalog/memory-repository.js"; import { CatalogOperationCoordinator } from "../src/catalog/operation-coordinator.js"; import type { CatalogPostgresAccess } from "../src/catalog/postgres-access.js"; import type { ObservedSchemaSnapshot } from "../src/catalog/types.js"; import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js"; import type { WorkspaceRegistry, WorkspaceRevision } from "../src/workspaces/registry.js"; import type { WorkspaceDescriptor } from "../src/workspaces/schema.js"; import type { WorkspaceDiagnoser } from "../src/routes/workspaces.js"; const roots: string[] = []; afterEach(() => { vi.unstubAllEnvs(); for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); }); const workspace: WorkspaceDescriptor = { workspace: { schema_version: 4, id: "psd-clinical", name: "Policlinico San Donato", language: "it" }, dwh: { engine: "postgres", database: "warehouse", schema: "datawarehouse", port: 5432, supported_transports: ["postgres_direct", "rest_api"], }, diagnostics: { dwh_rest: { method: "GET", path: "/health", auth: "bearer", response: { database: "database", schema: "schema" } } }, }; const revision: WorkspaceRevision = { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd.yaml" }; function setup( environment: Record = {}, catalogDependencies: { catalogOperationCoordinator?: CatalogOperationCoordinator; catalogPostgresAccess?: CatalogPostgresAccess; workspaceDiagnoser?: WorkspaceDiagnoser; } = {}, workspaceDescriptor: WorkspaceDescriptor = workspace, ) { const secretRoot = mkdtempSync(join(tmpdir(), "catalog-secret-")); const runtimeRoot = mkdtempSync(join(tmpdir(), "catalog-secret-runtime-")); roots.push(secretRoot, runtimeRoot); const secretStore = new WorkspaceSecretStore({ root: secretRoot, runtimeRoot, installationId: "test" }); const repository = new MemoryCatalogRepository(); const registry = { list: vi.fn(async () => [revision]), listCatalog: vi.fn(async () => [{ id: "psd-clinical", name: "Policlinico San Donato", configurationState: "ready", revision }]), read: vi.fn(async () => ({ workspace: workspaceDescriptor, revision })), readPinned: vi.fn(async () => ({ workspace: workspaceDescriptor, workspaceConfigPath: revision.snapshotPath })), } as unknown as WorkspaceRegistry; const app = buildApp(loadConfig({ THT_HARNESS_DIR: "/missing", NODE_ENV: "test", ...environment, }), { thtRunner: {} as never, workspaceRegistry: registry, workspaceSecretStore: secretStore, catalogRepository: repository, workspaceDiagnoser: vi.fn(async () => ({ activatable: true, diagnostics: [] })), ...catalogDependencies, }); return { app, secretStore, repository }; } const direct = { workspaceId: "psd-clinical", engine: "postgres", databaseName: "warehouse", schema: "datawarehouse", binding: { transport: "postgres_direct", host: "db.internal", port: 5432, username: "reader" }, }; const fleetSnapshot: ObservedSchemaSnapshot = { schemaVersion: 1, capabilities: { tables: "available", columns: "available", relationships: "available" }, tables: [ { name: "patients", sourceComment: "Clinical patients" }, { name: "visits", sourceComment: null }, ], columns: [ { tableName: "patients", name: "id", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: 1, sourceComment: null }, { tableName: "patients", name: "name", ordinalPosition: 2, dataType: "text", isNullable: true, defaultExpression: null, primaryKeyPosition: null, sourceComment: null }, { tableName: "visits", name: "id", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: 1, sourceComment: null }, { tableName: "visits", name: "patient_id", ordinalPosition: 2, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: null, sourceComment: null }, ], relationships: [{ constraintName: "visits_patient_id_fkey", sourceTableName: "visits", targetTableName: "patients", updateRule: "NO ACTION", deleteRule: "CASCADE", deferrable: false, initiallyDeferred: false, columns: [{ position: 1, sourceColumnName: "patient_id", targetColumnName: "id" }], }], }; test("lists every workspace and creates its Catalog database configuration", async () => { const { app, secretStore } = setup(); const initial = await app.inject({ method: "GET", url: "/catalog/databases" }); expect(initial.statusCode).toBe(200); expect(initial.json()).toMatchObject([{ workspaceId: "psd-clinical", configured: false, databaseName: "", workspaceRevision: { commit: revision.commit, blob: revision.blob }, workspaceEvidence: { sourceType: null, state: "not_declared" }, runtimeBinding: null, }]); secretStore.putMany("psd-clinical", { "dwh.password": "runtime-password" }); const runtimeReady = await app.inject({ method: "GET", url: "/catalog/databases" }); expect(runtimeReady.json()).toMatchObject([{ runtimeBinding: null, }]); const created = await app.inject({ method: "POST", url: "/catalog/databases", payload: direct }); expect(created.statusCode).toBe(201); expect(created.json()).toMatchObject({ configured: true, workspaceId: "psd-clinical", version: 1 }); expect((await app.inject({ method: "POST", url: "/catalog/databases", payload: direct })).statusCode).toBe(409); const listed = await app.inject({ method: "GET", url: "/catalog/databases" }); expect(listed.json()).toMatchObject([{ configured: true, binding: { transport: "postgres_direct", host: "db.internal" } }]); }); test("projects remote Evidence credential state without conflating catalog secrets", async () => { const evidenceWorkspace: WorkspaceDescriptor = { ...workspace, evidence: { schema_version: 2, 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 { app, secretStore } = setup({}, {}, evidenceWorkspace); const missing = await app.inject({ method: "GET", url: "/catalog/databases" }); expect(missing.json()).toMatchObject([{ workspaceEvidence: { sourceType: "http", state: "configuration_required" }, }]); secretStore.putMany("psd-clinical", { "evidence.signed_urls": "https://signed.example.test/evidence" }); const configured = await app.inject({ method: "GET", url: "/catalog/databases" }); expect(configured.json()).toMatchObject([{ workspaceEvidence: { sourceType: "http", state: "configured_unverified" }, }]); }); test("lists orphaned records and keeps the REST diagnostic path in the Catalog", async () => { const { app, repository } = setup(); await repository.create({ workspaceId: "removed-workspace", engine: "postgres", databaseName: "legacy", schema: "public", binding: { transport: "postgres_direct", host: "legacy.internal", port: 5432, username: "reader" }, }); const created = await app.inject({ method: "POST", url: "/catalog/databases", payload: { ...direct, binding: { transport: "rest_api", baseUrl: "https://psd.example/api", restPath: "/client-controlled", restAuth: "bearer", }, }, }); expect(created.statusCode).toBe(201); expect(created.json()).toMatchObject({ binding: { restPath: "/client-controlled" } }); const rows = (await app.inject({ method: "GET", url: "/catalog/databases" })).json(); expect(rows).toEqual(expect.arrayContaining([ expect.objectContaining({ workspaceId: "removed-workspace", configured: true, workspaceAvailable: false }), expect.objectContaining({ workspaceId: "psd-clinical", configured: true, workspaceAvailable: true }), ])); }); test.each([ "https://reader:secret@psd.example/api", "https://psd.example/api?token=secret", "https://psd.example/api#secret", "ftp://psd.example/api", ])("rejects unsafe REST base URL %s before persistence", async (baseUrl) => { const { app, repository } = setup(); const response = await app.inject({ method: "POST", url: "/catalog/databases", payload: { ...direct, binding: { transport: "rest_api", baseUrl, restPath: "/health", restAuth: "bearer" }, }, }); expect(response.statusCode).toBe(400); expect(response.json()).toEqual({ code: "database_invalid", message: "Database configuration is invalid.", }); expect(await repository.getByWorkspace("psd-clinical")).toBeUndefined(); }); test("uses optimistic versions, keeps secrets write-only, and hard-deletes only local configuration", async () => { const { app, secretStore } = setup(); const created = (await app.inject({ method: "POST", url: "/catalog/databases", payload: direct })).json(); const stale = await app.inject({ method: "PATCH", url: `/catalog/databases/${created.id}`, payload: { ...direct, version: 99 } }); expect(stale.statusCode).toBe(409); const secret = await app.inject({ method: "PUT", url: `/catalog/databases/${created.id}/secrets`, payload: { version: 1, values: { password: "do-not-return-this" } }, }); expect(secret.statusCode).toBe(200); expect(secret.body).not.toContain("do-not-return-this"); expect(secret.json()).toMatchObject({ version: 2, secrets: { password: true } }); expect(secretStore.has("psd-clinical", "catalog.dwh.password")).toBe(true); const removed = await app.inject({ method: "DELETE", url: `/catalog/databases/${created.id}?version=2` }); expect(removed.statusCode).toBe(204); expect(secretStore.has("psd-clinical", "catalog.dwh.password")).toBe(false); expect((await app.inject({ method: "GET", url: "/catalog/databases" })).json()).toMatchObject([{ configured: false }]); }); test("rejects a connection test while another catalog operation owns the database", async () => { const coordinator = new CatalogOperationCoordinator(); const postgres: CatalogPostgresAccess = { connect: vi.fn(async () => { throw new Error("connection must not start"); }), }; const { app } = setup({}, { catalogOperationCoordinator: coordinator, catalogPostgresAccess: postgres, }); const created = (await app.inject({ method: "POST", url: "/catalog/databases", payload: direct, })).json(); const release = coordinator.reserve(created.id); try { const response = await app.inject({ method: "POST", url: `/catalog/databases/${created.id}/test`, payload: { version: created.version }, }); expect(response.statusCode).toBe(409); expect(response.json()).toEqual({ code: "database_operation_in_progress", message: "A database operation is already in progress.", }); expect(postgres.connect).not.toHaveBeenCalled(); expect((await app.inject({ method: "GET", url: `/catalog/databases/${created.id}`, })).json()).toMatchObject({ connectionStatus: "untested" }); } finally { release(); } }); test("workspace and database tests use the same current catalog database binding", async () => { const connect = vi.fn(async () => ({ query: vi.fn(async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }], })), end: vi.fn(async () => undefined), })); const diagnose: WorkspaceDiagnoser = vi.fn(async () => ({ activatable: true, diagnostics: [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded.", }], })); const { app } = setup({ THT_WS_PSD_CLINICAL_DWH_TRANSPORT: "postgres_direct", THT_WS_PSD_CLINICAL_DWH_HOST: "legacy-db.internal", THT_WS_PSD_CLINICAL_DWH_PORT: "5432", THT_WS_PSD_CLINICAL_DWH_USER: "legacy-reader", }, { catalogPostgresAccess: { connect } as CatalogPostgresAccess, workspaceDiagnoser: diagnose, }); const created = (await app.inject({ method: "POST", url: "/catalog/databases", payload: { ...direct, binding: { ...direct.binding, host: "current-db.internal", username: "current-reader" }, }, })).json(); const databaseTest = await app.inject({ method: "POST", url: `/catalog/databases/${created.id}/test`, payload: { version: created.version }, }); const workspaceTest = await app.inject({ method: "POST", url: "/workspaces/psd-clinical/test", payload: {}, }); expect(databaseTest.statusCode).toBe(200); expect(workspaceTest.statusCode).toBe(200); expect(connect).toHaveBeenCalledTimes(2); expect(connect.mock.calls.map(([database]) => database)).toEqual([ expect.objectContaining({ databaseName: "warehouse", schema: "datawarehouse", binding: expect.objectContaining({ host: "current-db.internal", username: "current-reader" }), }), expect.objectContaining({ databaseName: "warehouse", schema: "datawarehouse", binding: expect.objectContaining({ host: "current-db.internal", username: "current-reader" }), }), ]); expect(diagnose).toHaveBeenCalledWith( workspace, expect.any(Object), { writeProbe: false, skipDwh: true }, ); }); test("returns exact global and per-database fleet metrics", async () => { const { app, repository } = setup(); const database = await repository.create(direct); await repository.applySchemaSync(database.id, database.version, "all", [], fleetSnapshot); const patients = (await repository.listTables(database.id)) .find((table) => table.name === "patients")!; await repository.updateTableDescription( database.id, patients.id, patients.version, "Curated patients", ); const patientName = (await repository.listColumns(database.id, patients.id)) .find((column) => column.name === "name")!; await repository.updateColumnMetadata( database.id, patients.id, patientName.id, patientName.version, null, "Generated patient name", true, ); const archive = await repository.create({ workspaceId: "removed-workspace", engine: "postgres", databaseName: "archive", schema: "public", binding: { transport: "postgres_direct", host: "archive.internal", port: 5432, username: "reader", }, }); await repository.applySchemaSync(archive.id, archive.version, "all", [], { schemaVersion: 1, capabilities: { tables: "available", columns: "available", relationships: "available" }, tables: [{ name: "events", sourceComment: null }], columns: [{ tableName: "events", name: "id", ordinalPosition: 1, dataType: "bigint", isNullable: false, defaultExpression: null, primaryKeyPosition: 1, sourceComment: null, }], relationships: [], }); const events = (await repository.listTables(archive.id))[0]!; await repository.updateTableMetadata( archive.id, events.id, events.version, null, "Generated archive events", ); const scoped = await app.inject({ method: "GET", url: `/catalog/metrics?databaseId=${database.id}`, }); expect(scoped.statusCode).toBe(200); expect(scoped.json()).toEqual({ scope: "database", databaseId: database.id, tables: 2, columns: 4, sensitiveColumns: 1, relationships: 1, descriptionTargets: 6, describedTargets: 2, descriptionCoverage: 33, updatedAt: expect.any(String), }); const global = await app.inject({ method: "GET", url: "/catalog/metrics" }); expect(global.statusCode).toBe(200); expect(global.json()).toEqual({ scope: "global", databaseId: null, tables: 3, columns: 5, sensitiveColumns: 1, relationships: 1, descriptionTargets: 8, describedTargets: 3, descriptionCoverage: 38, updatedAt: expect.any(String), }); expect(Number.isNaN(Date.parse(global.json().updatedAt))).toBe(false); }); test("validates fleet metric scope and requires database.manage", async () => { const { app } = setup(); const unknown = await app.inject({ method: "GET", url: "/catalog/metrics?databaseId=99999999-9999-4999-8999-999999999999", }); expect(unknown.statusCode).toBe(404); expect(unknown.json()).toEqual({ code: "database_not_found", message: "Database configuration was not found.", }); const invalid = await app.inject({ method: "GET", url: "/catalog/metrics?databaseId=not-a-uuid", }); expect(invalid.statusCode).toBe(400); expect(invalid.json()).toEqual({ code: "database_invalid", message: "Database configuration is invalid.", }); const { app: restrictedApp } = setup({ AUTH_MODE: "upstream" }); const forbidden = await restrictedApp.inject({ method: "GET", url: "/catalog/metrics", headers: { "x-thoth-principal-issuer": "portal", "x-thoth-principal-subject": "catalog-reader", "x-thoth-is-admin": "0", }, }); expect(forbidden.statusCode).toBe(403); expect(forbidden.json()).toEqual({ code: "auth_forbidden", error: "This operation is not permitted", }); });