Files
ThothII/backend/test/catalog-databases-routes.test.ts
T

289 lines
11 KiB
TypeScript

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 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";
const roots: string[] = [];
afterEach(() => { for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true }); });
const workspace: WorkspaceDescriptor = {
workspace: { schema_version: 3, id: "psd-clinical", name: "Policlinico San Donato", language: "it" },
dwh: {
engine: "postgres", database: "warehouse", schema: "datawarehouse", port: 5432,
supported_transports: ["postgres_direct", "rest_api"],
},
semantic_index: {
vector_store: { engine: "qdrant", collection: "psd", dimensions: 1024, distance: "cosine" },
embedding: { provider: "ollama_internal", model: "qwen3-embedding:0.6b", dimensions: 1024 },
},
llm_policy: { allowed: ["zai/glm-5.2"] },
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<string, string> = {}) {
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, revision })),
} 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(),
});
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 YAML workspace and creates its one database configuration", async () => {
const { app } = 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: "warehouse" }]);
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("lists orphaned records and takes the REST diagnostic path from workspace YAML", 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: "/health" } });
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("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("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",
});
});