import { randomUUID } from "node:crypto"; import { PrismaClient } from "@prisma/client"; import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { PrismaService } from "../../apps/api/src/database/prisma.service"; import { assertSafeTestDatabaseUrl, resolveTestDatabaseUrl, } from "../../prisma/database-safety"; const databaseUrl = resolveTestDatabaseUrl(); const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } }); async function expectConstraintViolation(operation: Promise) { await expect(operation).rejects.toMatchObject({ name: "PrismaClientUnknownRequestError", }); } async function expectUniqueViolation(operation: Promise) { await expect(operation).rejects.toMatchObject({ code: "P2002" }); } async function createAccount(suffix: string) { return prisma.account.create({ data: { phoneCiphertext: Buffer.from(`ciphertext-${suffix}`), phoneHmac: `hmac-${suffix}`, anonymousProfile: { create: { publicId: randomUUID(), nickname: `漂友-${suffix}`, avatarColor: "#66CCFF", reviewStatus: "APPROVED", }, }, }, }); } async function createBottle(authorId: string, suffix: string) { return prisma.bottle.create({ data: { authorId, contentText: `测试瓶子-${suffix}`, reviewStatus: "APPROVED", poolStatus: "IN_POOL", version: 1, approvedAt: new Date(), }, }); } describe("database authority constraints", () => { beforeAll(async () => { assertSafeTestDatabaseUrl(databaseUrl); await prisma.$connect(); }); beforeEach(async () => { assertSafeTestDatabaseUrl(databaseUrl); await prisma.$executeRawUnsafe(`TRUNCATE TABLE "accounts" CASCADE`); }); afterAll(async () => prisma.$disconnect()); it("defines indexed refresh-token history", async () => { const columns = await prisma.$queryRaw>` SELECT column_name FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'refresh_tokens'`; expect(columns.map(({ column_name }) => column_name)).toEqual( expect.arrayContaining([ "token_hash", "session_id", "generation", "status", "used_at", "replaced_by_hash", "created_at", "expires_at", ]), ); const indexes = await prisma.$queryRaw>` SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'refresh_tokens'`; expect( indexes.some( ({ indexdef }) => indexdef.includes("UNIQUE") && indexdef.includes("token_hash"), ), ).toBe(true); expect( indexes.some( ({ indexdef }) => indexdef.includes("session_id") && indexdef.includes("generation"), ), ).toBe(true); }); it("uses the database-only bottle pick candidate partial index", async () => { const author = await createAccount("candidate-index-author"); await prisma.bottle.createMany({ data: [ ...Array.from({ length: 2_000 }, (_, index) => ({ authorId: author.id, clientRequestId: `ineligible-${index}`, contentText: `ineligible-${index}`, reviewStatus: "REVIEWING" as const, poolStatus: "CLOSED" as const, })), ...Array.from({ length: 3 }, (_, index) => ({ authorId: author.id, clientRequestId: `eligible-${index}`, contentText: `eligible-${index}`, reviewStatus: "APPROVED" as const, poolStatus: "IN_POOL" as const, approvedAt: new Date(), })), ], }); const [index] = await prisma.$queryRaw>` SELECT indexdef FROM pg_indexes WHERE schemaname = 'public' AND tablename = 'bottles' AND indexname = 'bottles_pick_candidates_id_idx'`; expect(index?.indexdef).toMatch( /ON public\.bottles USING btree \(id\) WHERE .*review_status = 'APPROVED'.*pool_status = 'IN_POOL'.*active_lease_id IS NULL/, ); const { plan, rows } = await prisma.$transaction(async (tx) => { await tx.$executeRawUnsafe("SET LOCAL enable_seqscan = off"); const explained = await tx.$queryRawUnsafe< Array<{ "QUERY PLAN": Array<{ Plan: { "Index Name"?: string } }> }> >(`EXPLAIN (FORMAT JSON) SELECT id FROM bottles WHERE review_status = 'APPROVED' AND pool_status = 'IN_POOL' AND active_lease_id IS NULL ORDER BY id LIMIT 32`); const selected = await tx.$queryRaw>` SELECT content_text FROM bottles WHERE review_status = 'APPROVED' AND pool_status = 'IN_POOL' AND active_lease_id IS NULL ORDER BY id LIMIT 32`; return { plan: explained[0]!["QUERY PLAN"][0]!.Plan, rows: selected }; }); const indexNames = (node: unknown): string[] => { if (!node || typeof node !== "object") return []; const record = node as Record; return [ ...(typeof record["Index Name"] === "string" ? [record["Index Name"]] : []), ...Object.values(record).flatMap(indexNames), ]; }; expect(indexNames(plan)).toContain("bottles_pick_candidates_id_idx"); expect(rows.map(({ content_text }) => content_text).sort()).toEqual([ "eligible-0", "eligible-1", "eligible-2", ]); }); it("backfills and requires the session token version snapshot", async () => { const columns = await prisma.$queryRaw< Array<{ is_nullable: string; column_default: string | null }> >` SELECT is_nullable, column_default FROM information_schema.columns WHERE table_schema = 'public' AND table_name = 'sessions' AND column_name = 'token_version'`; expect(columns).toHaveLength(1); expect(columns[0]).toMatchObject({ is_nullable: "NO", column_default: "0", }); }); it("expresses independent moderation and pool lifecycle state", async () => { const author = await createAccount("states"); const bottle = await prisma.bottle.create({ data: { authorId: author.id, contentText: "state", reviewStatus: "MANUAL_REVIEW", poolStatus: "LEASED", version: 3, approvedAt: new Date(), consumedAt: new Date(), }, include: { author: { include: { anonymousProfile: true } } }, }); expect(bottle).toMatchObject({ reviewStatus: "MANUAL_REVIEW", poolStatus: "LEASED", version: 3, }); expect(bottle.author.anonymousProfile).toMatchObject({ avatarColor: "#66CCFF", reviewStatus: "APPROVED", }); }); it("enforces unique anonymous public ids", async () => { const first = await createAccount("public-id"); const publicId = ( await prisma.anonymousProfile.findUniqueOrThrow({ where: { accountId: first.id }, }) ).publicId; await expectUniqueViolation( prisma.account.create({ data: { phoneCiphertext: Buffer.from("other"), phoneHmac: "other-public-id", anonymousProfile: { create: { publicId, nickname: "另一位", avatarColor: "#000000", }, }, }, }), ); }); it("allows only one ACTIVE lease per bottle and allows ended leases", async () => { const author = await createAccount("lease-author"); const firstPicker = await createAccount("lease-picker-a"); const secondPicker = await createAccount("lease-picker-b"); const bottle = await createBottle(author.id, "lease"); const first = await prisma.bottlePickLease.create({ data: { bottleId: bottle.id, pickerId: firstPicker.id, leaseTokenHash: "lease-a", leaseTokenCiphertext: Buffer.from("ciphertext-a"), expiresAt: new Date(Date.now() + 60_000), }, }); await expectUniqueViolation( prisma.bottlePickLease.create({ data: { bottleId: bottle.id, pickerId: secondPicker.id, leaseTokenHash: "lease-b", leaseTokenCiphertext: Buffer.from("ciphertext-b"), expiresAt: new Date(Date.now() + 60_000), }, }), ); await prisma.bottlePickLease.update({ where: { id: first.id }, data: { status: "RETURNED", endedAt: new Date() }, }); await expect( prisma.bottlePickLease.create({ data: { bottleId: bottle.id, pickerId: secondPicker.id, leaseTokenHash: "lease-c", leaseTokenCiphertext: Buffer.from("ciphertext-c"), expiresAt: new Date(Date.now() + 60_000), }, }), ).resolves.toMatchObject({ status: "ACTIVE" }); }); it("supports bottle-targeted moderation tasks without a report", async () => { const author = await createAccount("moderation"); const bottle = await createBottle(author.id, "moderation"); await expect( prisma.moderationTask.create({ data: { targetType: "BOTTLE", targetId: bottle.id, provider: "internal", result: { verdict: "review" }, riskLabels: ["safety"], payloadHash: "payload-hash", status: "PENDING", }, }), ).resolves.toMatchObject({ targetType: "BOTTLE", reportId: null }); }); it("stores conversation/message snapshots and bigint sequences", async () => { const author = await createAccount("message"); const bottle = await createBottle(author.id, "message"); const conversation = await prisma.conversation.create({ data: { sourceBottleId: bottle.id, nextSeq: 2n }, }); const member = await prisma.conversationMember.create({ data: { conversationId: conversation.id, accountId: author.id, peerAliasSnapshot: "匿名海风", }, }); const message = await prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "msg", seq: 1n, contentText: "hello", reviewStatus: "REVIEWING", sentAt: new Date(), recalledAt: new Date(), }, }); expect(conversation.lastMessageAt).toBeInstanceOf(Date); expect(member).toMatchObject({ peerAliasSnapshot: "匿名海风", blockedAt: null, }); expect(message.seq).toBe(1n); }); it("rejects duplicate message client ids within a conversation", async () => { const author = await createAccount("message-client-id"); const bottle = await createBottle(author.id, "message-client-id"); const conversation = await prisma.conversation.create({ data: { sourceBottleId: bottle.id }, }); await expect( prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "duplicate-client-id", seq: 1n, contentText: "first", }, }), ).resolves.toMatchObject({ clientMsgId: "duplicate-client-id", seq: 1n }); await expectUniqueViolation( prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "duplicate-client-id", seq: 2n, contentText: "second", }, }), ); }); it("rejects duplicate message sequences within a conversation", async () => { const author = await createAccount("message-seq"); const bottle = await createBottle(author.id, "message-seq"); const conversation = await prisma.conversation.create({ data: { sourceBottleId: bottle.id }, }); await expect( prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "first-client-id", seq: 1n, contentText: "first", }, }), ).resolves.toMatchObject({ clientMsgId: "first-client-id", seq: 1n }); await expectUniqueViolation( prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "second-client-id", seq: 1n, contentText: "second", }, }), ); }); it("rejects self-blocks", async () => { const account = await createAccount("self-block"); await expectConstraintViolation( prisma.block.create({ data: { blockerId: account.id, blockedId: account.id }, }), ); }); it("rejects negative daily counters", async () => { const account = await createAccount("negative-usage"); await expectConstraintViolation( prisma.dailyUsage.create({ data: { accountId: account.id, usageDate: new Date(), messagesSent: -1, }, }), ); }); it("rejects invalid conversation, member, message and outbox counters", async () => { const author = await createAccount("counters"); const bottle = await createBottle(author.id, "counters"); await expectConstraintViolation( prisma.conversation.create({ data: { sourceBottleId: bottle.id, nextSeq: 0n }, }), ); const conversation = await prisma.conversation.create({ data: { sourceBottleId: bottle.id }, }); await expectConstraintViolation( prisma.conversationMember.create({ data: { conversationId: conversation.id, accountId: author.id, peerAliasSnapshot: "x", lastReadSeq: -1n, }, }), ); await expectConstraintViolation( prisma.message.create({ data: { conversationId: conversation.id, senderId: author.id, clientMsgId: "zero", seq: 0n, contentText: "x", }, }), ); await expectConstraintViolation( prisma.outboxEvent.create({ data: { aggregateType: "Bottle", aggregateId: bottle.id, eventType: "created", payload: {}, attempts: -1, }, }), ); }); it("requires reports to have exactly one target and stores snapshot/resolution", async () => { const reporter = await createAccount("reporter"); const target = await createAccount("reported"); const bottle = await createBottle(target.id, "report"); await expectConstraintViolation( prisma.report.create({ data: { reporterId: reporter.id, reason: "missing", targetSnapshot: {}, }, }), ); await expectConstraintViolation( prisma.report.create({ data: { reporterId: reporter.id, reportedAccountId: target.id, bottleId: bottle.id, reason: "many", targetSnapshot: {}, }, }), ); await expect( prisma.report.create({ data: { reporterId: reporter.id, bottleId: bottle.id, reason: "one", targetSnapshot: { text: "snapshot" }, resolution: "REMOVE", }, }), ).resolves.toMatchObject({ resolution: "REMOVE" }); }); it("keeps the original authority uniqueness constraints", async () => { const author = await createAccount("uniques-author"); const picker = await createAccount("uniques-picker"); const bottle = await createBottle(author.id, "uniques"); await prisma.conversation.create({ data: { sourceBottleId: bottle.id } }); await expectUniqueViolation( prisma.conversation.create({ data: { sourceBottleId: bottle.id } }), ); await prisma.bottlePickHistory.create({ data: { bottleId: bottle.id, pickerId: picker.id }, }); await expectUniqueViolation( prisma.bottlePickHistory.create({ data: { bottleId: bottle.id, pickerId: picker.id }, }), ); await prisma.block.create({ data: { blockerId: author.id, blockedId: picker.id }, }); await expectUniqueViolation( prisma.block.create({ data: { blockerId: author.id, blockedId: picker.id }, }), ); }); }); describe("PrismaService lifecycle", () => { it("connects and disconnects through module lifecycle hooks", async () => { const service = new PrismaService({ datasources: { db: { url: databaseUrl } }, }); await service.onModuleInit(); await expect(service.$queryRaw`SELECT 1`).resolves.toBeDefined(); await service.onModuleDestroy(); }); });