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[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`, ); } }); });