138 lines
5.1 KiB
TypeScript
138 lines
5.1 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import { randomUUID } from "node:crypto";
|
|
import { PrismaClient } from "@prisma/client";
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import {
|
|
assertSafeTestDatabaseUrl,
|
|
resolveTestDatabaseUrl,
|
|
} from "../../prisma/database-safety";
|
|
|
|
const databaseUrl = resolveTestDatabaseUrl();
|
|
const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
|
|
|
|
async function executeStatements(
|
|
tx: Parameters<Parameters<typeof prisma.$transaction>[0]>[0],
|
|
sql: string,
|
|
) {
|
|
for (const statement of sql
|
|
.split(";")
|
|
.map((part) => part.trim())
|
|
.filter(Boolean))
|
|
await tx.$executeRawUnsafe(statement);
|
|
}
|
|
|
|
describe("0008 legacy lease upgrade", () => {
|
|
beforeAll(async () => {
|
|
assertSafeTestDatabaseUrl(databaseUrl);
|
|
await prisma.$connect();
|
|
});
|
|
afterAll(async () => prisma.$disconnect());
|
|
|
|
it("expires legacy ACTIVE leases, releases their bottles, and preserves terminal rows", async () => {
|
|
const schema = `migration_0008_${randomUUID().replaceAll("-", "")}`;
|
|
const authorId = randomUUID();
|
|
const pickerId = randomUUID();
|
|
const activeBottleId = randomUUID();
|
|
const returnedBottleId = randomUUID();
|
|
const activeLeaseId = randomUUID();
|
|
const returnedLeaseId = randomUUID();
|
|
const migration = await readFile(
|
|
new URL(
|
|
"../../prisma/migrations/0008_bottle_pick_leases/migration.sql",
|
|
import.meta.url,
|
|
),
|
|
"utf8",
|
|
);
|
|
|
|
try {
|
|
await prisma.$transaction(async (tx) => {
|
|
await tx.$executeRawUnsafe(`CREATE SCHEMA "${schema}"`);
|
|
await tx.$executeRawUnsafe(`SET LOCAL search_path TO "${schema}"`);
|
|
await executeStatements(
|
|
tx,
|
|
`
|
|
CREATE TYPE "BottlePoolStatus" AS ENUM ('IN_POOL', 'LEASED', 'CONSUMED', 'REMOVED', 'CLOSED');
|
|
CREATE TYPE "BottlePickLeaseStatus" AS ENUM ('ACTIVE', 'RETURNED', 'EXPIRED', 'CONSUMED');
|
|
CREATE TABLE "accounts" ("id" UUID PRIMARY KEY);
|
|
CREATE TABLE "bottles" (
|
|
"id" UUID PRIMARY KEY,
|
|
"pool_status" "BottlePoolStatus" NOT NULL,
|
|
"version" INTEGER NOT NULL
|
|
);
|
|
CREATE TABLE "bottle_pick_leases" (
|
|
"id" UUID PRIMARY KEY,
|
|
"bottle_id" UUID NOT NULL REFERENCES "bottles"("id"),
|
|
"picker_id" UUID NOT NULL REFERENCES "accounts"("id"),
|
|
"lease_token_hash" VARCHAR(255) NOT NULL UNIQUE,
|
|
"status" "BottlePickLeaseStatus" NOT NULL,
|
|
"ended_at" TIMESTAMPTZ(3),
|
|
"created_at" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
INSERT INTO "accounts" ("id") VALUES ('${authorId}'), ('${pickerId}');
|
|
INSERT INTO "bottles" ("id", "pool_status", "version") VALUES
|
|
('${activeBottleId}', 'LEASED', 4),
|
|
('${returnedBottleId}', 'CLOSED', 7);
|
|
INSERT INTO "bottle_pick_leases"
|
|
("id", "bottle_id", "picker_id", "lease_token_hash", "status", "ended_at") VALUES
|
|
('${activeLeaseId}', '${activeBottleId}', '${pickerId}', 'active-hash', 'ACTIVE', NULL),
|
|
('${returnedLeaseId}', '${returnedBottleId}', '${pickerId}', 'returned-hash', 'RETURNED', CURRENT_TIMESTAMP);
|
|
`,
|
|
);
|
|
await executeStatements(tx, migration);
|
|
|
|
const leases = await tx.$queryRawUnsafe<
|
|
Array<{
|
|
id: string;
|
|
status: string;
|
|
endedAt: Date | null;
|
|
ciphertextHex: string;
|
|
}>
|
|
>(`
|
|
SELECT "id", "status", "ended_at" AS "endedAt",
|
|
encode("lease_token_ciphertext", 'hex') AS "ciphertextHex"
|
|
FROM "bottle_pick_leases" ORDER BY "id"
|
|
`);
|
|
const bottles = await tx.$queryRawUnsafe<
|
|
Array<{ id: string; poolStatus: string; version: number }>
|
|
>(`
|
|
SELECT "id", "pool_status" AS "poolStatus", "version"
|
|
FROM "bottles" ORDER BY "id"
|
|
`);
|
|
const ciphertextColumn = await tx.$queryRawUnsafe<
|
|
Array<{ isNullable: string }>
|
|
>(`
|
|
SELECT is_nullable AS "isNullable" FROM information_schema.columns
|
|
WHERE table_schema = '${schema}' AND table_name = 'bottle_pick_leases'
|
|
AND column_name = 'lease_token_ciphertext'
|
|
`);
|
|
|
|
expect(leases.find(({ id }) => id === activeLeaseId)).toMatchObject({
|
|
status: "EXPIRED",
|
|
endedAt: expect.any(Date) as Date,
|
|
ciphertextHex: "00".repeat(48),
|
|
});
|
|
expect(leases.find(({ id }) => id === returnedLeaseId)).toMatchObject({
|
|
status: "RETURNED",
|
|
endedAt: expect.any(Date) as Date,
|
|
ciphertextHex: "00".repeat(48),
|
|
});
|
|
expect(bottles.find(({ id }) => id === activeBottleId)).toMatchObject({
|
|
poolStatus: "IN_POOL",
|
|
version: 5,
|
|
});
|
|
expect(bottles.find(({ id }) => id === returnedBottleId)).toMatchObject(
|
|
{
|
|
poolStatus: "CLOSED",
|
|
version: 7,
|
|
},
|
|
);
|
|
expect(ciphertextColumn).toEqual([{ isNullable: "NO" }]);
|
|
});
|
|
} finally {
|
|
await prisma.$executeRawUnsafe(
|
|
`DROP SCHEMA IF EXISTS "${schema}" CASCADE`,
|
|
);
|
|
}
|
|
});
|
|
});
|