112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { expect, test } from "vitest";
|
|
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 { CatalogService } from "../src/catalog/service.js";
|
|
import type { DatabaseTestResult, WorkspaceDatabase } from "../src/catalog/types.js";
|
|
import type { WorkspaceRegistry } from "../src/workspaces/registry.js";
|
|
import { WorkspaceSecretStore } from "../src/workspaces/secret-store.js";
|
|
|
|
interface Deferred {
|
|
promise: Promise<void>;
|
|
resolve: () => void;
|
|
}
|
|
|
|
function deferred(): Deferred {
|
|
let resolve!: () => void;
|
|
const promise = new Promise<void>((done) => { resolve = done; });
|
|
return { promise, resolve };
|
|
}
|
|
|
|
class PausingRecordRepository extends MemoryCatalogRepository {
|
|
constructor(
|
|
private readonly recordStarted: Deferred,
|
|
private readonly continueRecord: Deferred,
|
|
) {
|
|
super();
|
|
}
|
|
|
|
override async recordTest(
|
|
id: string,
|
|
expectedVersion: number,
|
|
result: DatabaseTestResult,
|
|
): Promise<WorkspaceDatabase | undefined> {
|
|
this.recordStarted.resolve();
|
|
await this.continueRecord.promise;
|
|
return await super.recordTest(id, expectedVersion, result);
|
|
}
|
|
}
|
|
|
|
test("holds the database reservation until the connection result is recorded", async () => {
|
|
const secretRoot = mkdtempSync(join(tmpdir(), "catalog-service-secret-"));
|
|
const runtimeRoot = mkdtempSync(join(tmpdir(), "catalog-service-runtime-"));
|
|
const recordStarted = deferred();
|
|
const continueRecord = deferred();
|
|
const repository = new PausingRecordRepository(recordStarted, continueRecord);
|
|
const coordinator = new CatalogOperationCoordinator();
|
|
const secretStore = new WorkspaceSecretStore({
|
|
root: secretRoot,
|
|
runtimeRoot,
|
|
installationId: "test",
|
|
});
|
|
const postgres: CatalogPostgresAccess = {
|
|
connect: async () => ({
|
|
query: async () => ({ rows: [{ database: "warehouse", schema: "datawarehouse" }] }),
|
|
end: async () => undefined,
|
|
}),
|
|
};
|
|
|
|
try {
|
|
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",
|
|
},
|
|
});
|
|
const service = new CatalogService(
|
|
repository,
|
|
{} as WorkspaceRegistry,
|
|
secretStore,
|
|
[],
|
|
1_000,
|
|
postgres,
|
|
coordinator,
|
|
);
|
|
|
|
const connectionTest = service.test(database);
|
|
const firstCompletedPhase = await Promise.race([
|
|
recordStarted.promise.then(() => "recording" as const),
|
|
connectionTest.then(() => "returned" as const),
|
|
]);
|
|
expect(firstCompletedPhase).toBe("recording");
|
|
|
|
try {
|
|
await expect(coordinator.run(database.id, async () => "overlapped"))
|
|
.rejects.toThrow("A database operation is already in progress");
|
|
} finally {
|
|
continueRecord.resolve();
|
|
}
|
|
|
|
expect(await connectionTest).toMatchObject({
|
|
id: database.id,
|
|
version: 1,
|
|
connectionStatus: "reachable",
|
|
testedVersion: 1,
|
|
});
|
|
expect(await coordinator.run(database.id, async () => "released")).toBe("released");
|
|
} finally {
|
|
continueRecord.resolve();
|
|
rmSync(secretRoot, { recursive: true, force: true });
|
|
rmSync(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|