// services/api/test/db.semantics.e2e-spec.ts —— Task 8 语义复核(真实 MySQL 8.4, // 库级 utf8mb4_0900_bin;连接经 test/load-env.ts 载入根 .env 的 DATABASE_URL_CN)。 // 复核项是后续 auth/album 语义的依赖前提: // 1) DateTime(6) 的 UTC 瞬间往返(写入+读回毫秒精度不丢、时区不改); // 2) bin collation 下账号查找大小写敏感(token/account 语义同此); // 3) audit_logs 的 Json 元数据落库与 actor 关系; // 4) 必选关系默认 Restrict:删除仍持有 session 的用户抛 P2003——保留可审计性, // 刻意不做 onDelete: Cascade(用户决策,见计划 Task 8)。 import { PrismaClient } from '@photo/db-cn'; import { resetDb } from './reset'; describe('db semantics (e2e)', () => { let prisma: PrismaClient; beforeAll(async () => { await resetDb(); prisma = new PrismaClient(); }); afterAll(async () => { await prisma.$disconnect(); }); it('DateTime(6) round-trips a UTC instant at millisecond precision', async () => { const createdAt = new Date('2026-09-17T08:00:00.123Z'); const created = await prisma.users.create({ data: { account: 'utc-roundtrip', passwordHash: 'x', displayName: 'UTC 往返', createdAt, }, select: { createdAt: true }, }); expect(created.createdAt.getTime()).toBe(createdAt.getTime()); const reread = await prisma.users.findUnique({ where: { account: 'utc-roundtrip' }, select: { createdAt: true }, }); expect(reread?.createdAt.getTime()).toBe(createdAt.getTime()); expect(reread?.createdAt.toISOString()).toBe('2026-09-17T08:00:00.123Z'); }); it('account lookup is case-sensitive under utf8mb4_0900_bin', async () => { await prisma.users.create({ data: { account: 'AdminUser', passwordHash: 'x', displayName: '大小写敏感', }, }); const lowered = await prisma.users.findUnique({ where: { account: 'adminuser' }, }); expect(lowered).toBeNull(); const exact = await prisma.users.findUnique({ where: { account: 'AdminUser' }, }); expect(exact?.account).toBe('AdminUser'); }); it('stores audit log Json metadata and joins back to the actor', async () => { const actor = await prisma.users.create({ data: { account: 'audit-actor', passwordHash: 'x', displayName: '审计者', }, }); const log = await prisma.audit_logs.create({ data: { actorId: actor.id, action: 'user.login', resourceId: 'res-001', metadata: { ip: '127.0.0.1', userAgent: 'e2e', attempts: 1 }, }, include: { actor: true }, }); expect(log.id).toBeTruthy(); expect(log.metadata).toEqual({ ip: '127.0.0.1', userAgent: 'e2e', attempts: 1 }); expect(log.actor?.id).toBe(actor.id); expect(log.actor?.account).toBe('audit-actor'); // 反向关系:经 actor.auditLogs 能取回审计记录。 const viaActor = await prisma.users.findUnique({ where: { id: actor.id }, include: { auditLogs: true }, }); expect(viaActor?.auditLogs.map((l) => l.id)).toContain(log.id); }); it('rejects deleting a user that still has a session (FK Restrict, no cascade)', async () => { const user = await prisma.users.create({ data: { account: 'restricted', passwordHash: 'x', displayName: '受保护删除', }, }); await prisma.sessions.create({ data: { userId: user.id, tokenHash: 'a'.repeat(64), expiresAt: new Date(Date.now() + 60_000), }, }); // Prisma 对必选关系默认 onDelete: Restrict —— 删除被引用的 users 抛 P2003 // (外键约束失败)。这正是预期:会话/审计可追溯,不允许级联抹除。 await expect( prisma.users.delete({ where: { id: user.id } }), ).rejects.toMatchObject({ code: 'P2003' }); }); });