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

123 lines
5.6 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 { 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: 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",
};
async function setup() {
const secretRoot = mkdtempSync(join(tmpdir(), "catalog-table-secret-"));
const runtimeRoot = mkdtempSync(join(tmpdir(), "catalog-table-runtime-"));
roots.push(secretRoot, runtimeRoot);
const repository = new MemoryCatalogRepository();
const database = await repository.create({
workspaceId: "psd-clinical",
engine: "postgres",
databaseName: "warehouse",
schema: "datawarehouse",
binding: { transport: "postgres_direct", host: "db.internal", port: 5432, username: "reader" },
});
await repository.recordTest(database.id, database.version, {
connectionStatus: "reachable",
testedVersion: database.version,
lastTestedAt: new Date().toISOString(),
});
const scan = vi.fn(async () => [
{ name: "patients", sourceComment: "Clinical patients" },
{ name: "visits", sourceComment: null },
]);
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 })),
readPinned: vi.fn(async () => ({ workspace, workspaceConfigPath: revision.snapshotPath })),
} as unknown as WorkspaceRegistry;
const secretStore = new WorkspaceSecretStore({ root: secretRoot, runtimeRoot, installationId: "test" });
const app = buildApp(loadConfig({ THT_HARNESS_DIR: "/missing", NODE_ENV: "test" }), {
thtRunner: {} as never,
workspaceRegistry: registry,
workspaceSecretStore: secretStore,
catalogRepository: repository,
workspaceDiagnoser: vi.fn(),
});
return { app, repository, database: (await repository.get(database.id))!, scan };
}
test("lists physical tables and updates only review metadata", async () => {
const { app, repository, database, scan } = await setup();
const synchronized = await repository.reconcileTables(database.id, database.version, await scan(), []);
expect(synchronized).toMatchObject({ kind: "applied", createdCount: 2, deletedCount: 0 });
expect(scan).toHaveBeenCalledOnce();
const tables = (await app.inject({
method: "GET", url: `/catalog/databases/${database.id}/tables`,
})).json();
expect(tables.map((table: { name: string }) => table.name)).toEqual(["patients", "visits"]);
const patients = tables[0];
const edited = await app.inject({
method: "PATCH",
url: `/catalog/databases/${database.id}/tables/${patients.id}`,
payload: { version: patients.version, description: "Curated patient registry" },
});
expect(edited.statusCode).toBe(200);
expect(edited.json()).toMatchObject({
name: "patients",
sourceComment: "Clinical patients",
description: "Curated patient registry",
version: 2,
});
});
test("requires an exact deletion confirmation before applying the atomic diff", async () => {
const { app, repository, database, scan } = await setup();
await repository.reconcileTables(database.id, database.version, await scan(), []);
scan.mockResolvedValue([{ name: "patients", sourceComment: "Clinical patients" }]);
const preview = await repository.reconcileTables(database.id, database.version, await scan(), []);
expect(preview).toEqual({ kind: "confirmation_required", deletedNames: ["visits"] });
expect((await app.inject({ method: "GET", url: `/catalog/databases/${database.id}/tables` })).json()).toHaveLength(2);
const applied = await repository.reconcileTables(database.id, database.version, await scan(), ["visits"]);
expect(applied).toMatchObject({ kind: "applied", deletedCount: 1 });
expect((await app.inject({ method: "GET", url: `/catalog/databases/${database.id}/tables` })).json()).toMatchObject([
{ name: "patients" },
]);
});
test("refuses synchronization until the current binding has passed its connection test", async () => {
const { app, repository, database } = await setup();
await repository.update(database.id, database.version, {
workspaceId: database.workspaceId,
engine: database.engine,
databaseName: database.databaseName,
schema: database.schema,
binding: database.binding,
});
const response = await app.inject({
method: "POST",
url: `/catalog/databases/${database.id}/sync-runs`,
payload: { version: database.version + 1, scope: "tables", tableIds: [] },
});
expect(response.statusCode).toBe(409);
expect(response.json()).toMatchObject({ code: "schema_sync_conflict" });
});