import { PrismaService } from '../src/prisma/prisma.service'; /** * Schema-level guarantees of the T7 migration that the Prisma schema alone * cannot express (CHECK constraints, cascade) against a real PostgreSQL. The * test database is shared and not empty: every row here is created by this * file and removed again. */ describe('T7 schema (e2e)', () => { let prisma: PrismaService; const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; const CHECK_VIOLATION = /check constraint|violates/i; let userId: string; let apiaryId: string; let colonyId: string; let inspectionId: string; function capture(extra: Record = {}) { return prisma.voiceCapture.create({ data: { ownerUserId: userId, colonyId, inspectionDate: new Date('2026-10-16'), source: 'AUDIO', contentExpiresAt: new Date('2026-10-23T00:00:00Z'), ...extra, }, }); } beforeAll(async () => { prisma = new PrismaService(); const role = await prisma.role.findUniqueOrThrow({ where: { key: 'READONLY' }, }); const user = await prisma.user.create({ data: { email: `t7-schema-${suffix}@example.com`, passwordHash: 'x', name: 'T7 schema', roleId: role.id, }, }); userId = user.id; const apiary = await prisma.apiary.create({ data: { name: `T7 Stand ${suffix}` }, }); apiaryId = apiary.id; const colony = await prisma.colony.create({ data: { code: `T7-${suffix}`, apiaryId, colonyType: 'PRODUCTION_COLONY' }, }); colonyId = colony.id; const inspection = await prisma.inspection.create({ data: { colonyId, userId, date: new Date('2026-10-16') }, }); inspectionId = inspection.id; }); afterEach(async () => { await prisma.voiceCapture.deleteMany({ where: { ownerUserId: userId } }); }); afterAll(async () => { await prisma.voiceCapture.deleteMany({ where: { ownerUserId: userId } }); await prisma.inspection.deleteMany({ where: { id: inspectionId } }); await prisma.colony.deleteMany({ where: { id: colonyId } }); await prisma.apiary.deleteMany({ where: { id: apiaryId } }); await prisma.user.deleteMany({ where: { id: userId } }); await prisma.$disconnect(); }); it('accepts a plain queued audio capture with defaults', async () => { const row = await capture({ audioMime: 'audio/webm', audioBytes: 2097152, durationSec: 240, }); expect(row.status).toBe('QUEUED'); expect(row.attempts).toBe(0); expect(row.claimToken).toBeNull(); }); it('rejects audioBytes 0 and 2097153', async () => { await expect(capture({ audioBytes: 0 })).rejects.toThrow(CHECK_VIOLATION); await expect(capture({ audioBytes: 2097153 })).rejects.toThrow( CHECK_VIOLATION, ); }); it('rejects durationSec 241', async () => { await expect(capture({ durationSec: 241 })).rejects.toThrow( CHECK_VIOLATION, ); await expect(capture({ durationSec: 0 })).rejects.toThrow(CHECK_VIOLATION); }); it('rejects CONFIRMED without inspectionId and inspectionId on a non-confirmed row', async () => { await expect(capture({ status: 'CONFIRMED' })).rejects.toThrow( CHECK_VIOLATION, ); await expect(capture({ status: 'READY', inspectionId })).rejects.toThrow( CHECK_VIOLATION, ); const confirmed = await capture({ status: 'CONFIRMED', inspectionId }); expect(confirmed.inspectionId).toBe(inspectionId); }); it('rejects audioMime on a TEXT capture', async () => { await expect( capture({ source: 'TEXT', audioMime: 'audio/webm' }), ).rejects.toThrow(CHECK_VIOLATION); const text = await capture({ source: 'TEXT' }); expect(text.audioMime).toBeNull(); }); it('deleting a capture cascades to its audio row', async () => { const row = await capture({ audioMime: 'audio/webm', audioBytes: 3, audio: { create: { dataEnc: Buffer.from([1, 2, 3]) } }, }); expect( await prisma.voiceCaptureAudio.count({ where: { captureId: row.id } }), ).toBe(1); await prisma.voiceCapture.delete({ where: { id: row.id } }); expect( await prisma.voiceCaptureAudio.count({ where: { captureId: row.id } }), ).toBe(0); }); });