Files
plp/tests/integration/database.spec.ts
T
2026-09-14 11:10:57 +08:00

147 lines
4.6 KiB
TypeScript

import { Prisma, PrismaClient } from "@prisma/client";
import { afterAll, beforeAll, beforeEach, describe, expect, it } from "vitest";
import { PrismaService } from "../../apps/api/src/database/prisma.service";
const databaseUrl =
process.env.DATABASE_URL ??
"postgresql://drift:drift_test_only@127.0.0.1:55432/drift_bottle_test?schema=public";
const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } } });
async function expectUniqueViolation(operation: Promise<unknown>) {
await expect(operation).rejects.toMatchObject({
code: "P2002",
} satisfies Partial<Prisma.PrismaClientKnownRequestError>);
}
async function createAccount(suffix: string) {
return prisma.account.create({
data: {
phoneCiphertext: Buffer.from(`ciphertext-${suffix}`),
phoneHmac: `hmac-${suffix}`,
anonymousProfile: { create: { nickname: `漂友-${suffix}` } },
},
});
}
async function createBottle(authorId: string, suffix: string) {
return prisma.bottle.create({
data: { authorId, contentText: `测试瓶子-${suffix}`, status: "IN_POOL" },
});
}
describe("database authority constraints", () => {
beforeAll(async () => prisma.$connect());
beforeEach(async () => {
await prisma.$executeRawUnsafe(`TRUNCATE TABLE "accounts" CASCADE`);
});
afterAll(async () => prisma.$disconnect());
it("rejects a second conversation for the same source bottle with P2002", async () => {
const author = await createAccount("conversation-author");
const bottle = await createBottle(author.id, "conversation");
await prisma.conversation.create({ data: { sourceBottleId: bottle.id } });
await expectUniqueViolation(
prisma.conversation.create({ data: { sourceBottleId: bottle.id } }),
);
});
it("rejects duplicate bottle pick history with P2002", async () => {
const author = await createAccount("history-author");
const picker = await createAccount("history-picker");
const bottle = await createBottle(author.id, "history");
await prisma.bottlePickHistory.create({
data: { bottleId: bottle.id, pickerId: picker.id },
});
await expectUniqueViolation(
prisma.bottlePickHistory.create({
data: { bottleId: bottle.id, pickerId: picker.id },
}),
);
});
it("rejects duplicate directed blocks with P2002", async () => {
const blocker = await createAccount("blocker");
const blocked = await createAccount("blocked");
await prisma.block.create({
data: { blockerId: blocker.id, blockedId: blocked.id },
});
await expectUniqueViolation(
prisma.block.create({
data: { blockerId: blocker.id, blockedId: blocked.id },
}),
);
});
it("rejects duplicate client message ids within a conversation with P2002", async () => {
const author = await createAccount("client-message-author");
const bottle = await createBottle(author.id, "client-message");
const conversation = await prisma.conversation.create({
data: { sourceBottleId: bottle.id },
});
await prisma.message.create({
data: {
conversationId: conversation.id,
senderId: author.id,
clientMsgId: "client-1",
seq: 1,
contentText: "一",
},
});
await expectUniqueViolation(
prisma.message.create({
data: {
conversationId: conversation.id,
senderId: author.id,
clientMsgId: "client-1",
seq: 2,
contentText: "二",
},
}),
);
});
it("rejects duplicate message sequences within a conversation with P2002", async () => {
const author = await createAccount("sequence-author");
const bottle = await createBottle(author.id, "sequence");
const conversation = await prisma.conversation.create({
data: { sourceBottleId: bottle.id },
});
await prisma.message.create({
data: {
conversationId: conversation.id,
senderId: author.id,
clientMsgId: "client-a",
seq: 1,
contentText: "一",
},
});
await expectUniqueViolation(
prisma.message.create({
data: {
conversationId: conversation.id,
senderId: author.id,
clientMsgId: "client-b",
seq: 1,
contentText: "二",
},
}),
);
});
});
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();
});
});