feat: complete catalog fleet management workflow
This commit is contained in:
@@ -5,6 +5,7 @@ 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";
|
||||
@@ -27,7 +28,7 @@ const workspace: WorkspaceDescriptor = {
|
||||
};
|
||||
const revision: WorkspaceRevision = { id: "psd-clinical", commit: "a".repeat(40), blob: "b".repeat(40), snapshotPath: "/tmp/psd.yaml" };
|
||||
|
||||
function setup() {
|
||||
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);
|
||||
@@ -38,7 +39,11 @@ function setup() {
|
||||
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" }), {
|
||||
const app = buildApp(loadConfig({
|
||||
THT_HARNESS_DIR: "/missing",
|
||||
NODE_ENV: "test",
|
||||
...environment,
|
||||
}), {
|
||||
thtRunner: {} as never,
|
||||
workspaceRegistry: registry,
|
||||
workspaceSecretStore: secretStore,
|
||||
@@ -56,6 +61,31 @@ const direct = {
|
||||
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" });
|
||||
@@ -120,3 +150,139 @@ test("uses optimistic versions, keeps secrets write-only, and hard-deletes only
|
||||
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",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user