123 lines
5.6 KiB
TypeScript
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" });
|
|
});
|