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("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", expiresAt: new Date(Date.now() + 60_000), }, }); await expectUniqueViolation( prisma.bottlePickLease.create({ data: { bottleId: bottle.id, pickerId: secondPicker.id, leaseTokenHash: "lease-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", 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(); }); });