fix: 改进候选随机性与并发配额稳定性
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
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`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user