import { INestApplication } from '@nestjs/common'; import * as bcrypt from 'bcryptjs'; import request from 'supertest'; import { App } from 'supertest/types'; import { PrismaService } from '../src/prisma/prisma.service'; import { createTestApp } from './utils/create-test-app'; import { login } from './utils/api'; interface TakeoverBody { id: string; colonyTakeover: { applied: string[]; skipped: { field: string; reason: string }[]; }; } describe('Inspection takeover (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const suffix = `${Date.now()}`; const password = 'SuperSecret123!'; let staffToken: string; let narrowToken: string; let staffId: string; let narrowId: string; let narrowRoleId: string; let apiaryId: string; const colonyIds: string[] = []; // One colony per test: no test depends on what another one wrote. let colonyId: string; const post = (token: string, body: object) => request(app.getHttpServer()) .post('/api/v1/inspections') .set('Authorization', `Bearer ${token}`) .send({ colonyId, ...body }); const colony = () => prisma.colony.findUniqueOrThrow({ where: { id: colonyId } }); beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const passwordHash = await bcrypt.hash(password, 12); const staff = await prisma.user.create({ data: { email: `takeover-staff-${suffix}@example.com`, passwordHash, name: 'Admin', role: { connect: { key: 'ADMIN' } }, }, }); staffId = staff.id; const narrowRole = await prisma.role.create({ data: { key: `TAKEOVER_NARROW_${suffix}`, name: 'Nur Durchsicht', permissions: { create: ['inspections:create', 'colonies:read'].map((key) => ({ permission: { connect: { key } }, })), }, }, }); narrowRoleId = narrowRole.id; const narrow = await prisma.user.create({ data: { email: `takeover-narrow-${suffix}@example.com`, passwordHash, name: 'Narrow', roleId: narrowRole.id, }, }); narrowId = narrow.id; staffToken = (await login(app, staff.email, password)).accessToken; narrowToken = (await login(app, narrow.email, password)).accessToken; apiaryId = ( await prisma.apiary.create({ data: { name: `Takeover ${suffix}` } }) ).id; }); beforeEach(async () => { const created = await prisma.colony.create({ data: { code: `TK-${suffix}-${colonyIds.length}`, apiaryId, colonyType: 'PRODUCTION_COLONY', strength: 6, }, }); colonyId = created.id; colonyIds.push(created.id); }); afterAll(async () => { const ids = ( await prisma.inspection.findMany({ where: { colonyId: { in: colonyIds } }, select: { id: true }, }) ).map((row) => row.id); await prisma.domainEvent.deleteMany({ where: { OR: [ { aggregateType: 'Inspection', aggregateId: { in: ids } }, { aggregateType: 'Colony', aggregateId: { in: colonyIds } }, ], }, }); await prisma.calendarSyncOutboxEntry.deleteMany({ where: { sourceType: 'INSPECTION', sourceId: { in: ids } }, }); await prisma.inspection.deleteMany({ where: { colonyId: { in: colonyIds } }, }); await prisma.colony.deleteMany({ where: { id: { in: colonyIds } } }); await prisma.apiary.deleteMany({ where: { id: apiaryId } }); const userIds = [staffId, narrowId]; await prisma.refreshToken.deleteMany({ where: { userId: { in: userIds } }, }); await prisma.auditLog.deleteMany({ where: { userId: { in: userIds } } }); await prisma.user.deleteMany({ where: { id: { in: userIds } } }); await prisma.role.deleteMany({ where: { id: narrowRoleId } }); await app.close(); }); it('takes over feed and brood in the same request and writes an audit entry Colony UPDATE', async () => { const response = await post(staffToken, { date: '2026-06-01T10:00:00.000Z', feedStatus: 'WEAK', broodPattern: 4, colonyStrength: 18, applyToColony: ['feedStatus', 'broodStatus', 'occupiedFrames'], }).expect(201); const body = response.body as TakeoverBody; expect(body.colonyTakeover.applied).toEqual([ 'feedStatus', 'broodStatus', 'occupiedFrames', ]); expect(body.colonyTakeover.skipped).toEqual([]); expect(await colony()).toMatchObject({ feedStatus: 'WEAK', broodStatus: 'GOOD', occupiedFrames: 18, strength: 6, }); const audit = await prisma.auditLog.findFirstOrThrow({ where: { entityType: 'Colony', entityId: colonyId, action: 'UPDATE', userId: staffId, }, }); expect(audit.beforeData).toMatchObject({ feedStatus: null, broodStatus: null, occupiedFrames: null, }); expect(audit.afterData).toMatchObject({ feedStatus: 'WEAK', broodStatus: 'GOOD', occupiedFrames: 18, }); }); it('skips with NEWER_INSPECTION when a newer inspection of the colony exists', async () => { await post(staffToken, { date: '2026-06-01T10:00:00.000Z' }).expect(201); const response = await post(staffToken, { date: '2026-05-01T10:00:00.000Z', colonyStrength: 22, applyToColony: ['occupiedFrames'], }).expect(201); const body = response.body as TakeoverBody; expect(body.colonyTakeover.applied).toEqual([]); expect(body.colonyTakeover.skipped).toEqual([ { field: 'occupiedFrames', reason: 'NEWER_INSPECTION' }, ]); expect((await colony()).occupiedFrames).toBeNull(); }); it('answers 201 and skips with NO_PERMISSION without colonies:update', async () => { const response = await post(narrowToken, { date: '2026-07-01T10:00:00.000Z', colonyStrength: 25, applyToColony: ['occupiedFrames'], }).expect(201); const body = response.body as TakeoverBody; expect(body.colonyTakeover.skipped).toEqual([ { field: 'occupiedFrames', reason: 'NO_PERMISSION' }, ]); expect((await colony()).occupiedFrames).toBeNull(); expect(await prisma.inspection.count({ where: { id: body.id } })).toBe(1); }); it('skips with COLONY_ENDED for a dissolved colony and still saves the inspection', async () => { await prisma.colony.update({ where: { id: colonyId }, data: { status: 'DISSOLVED', deactivatedAt: new Date() }, }); const response = await post(staffToken, { date: '2026-07-01T10:00:00.000Z', colonyStrength: 12, applyToColony: ['occupiedFrames'], }).expect(201); const body = response.body as TakeoverBody; expect(body.colonyTakeover.skipped).toEqual([ { field: 'occupiedFrames', reason: 'COLONY_ENDED' }, ]); expect(await prisma.inspection.count({ where: { id: body.id } })).toBe(1); expect((await colony()).occupiedFrames).toBeNull(); }); it('takes values only from the new inspection, never from an older stored row (even an out-of-range legacy value)', async () => { await prisma.inspection.create({ data: { colonyId, date: new Date('2026-04-01T10:00:00.000Z'), colonyStrength: 45, userId: staffId, }, }); const response = await post(staffToken, { date: '2026-06-01T10:00:00.000Z', applyToColony: ['occupiedFrames'], }).expect(201); expect((response.body as TakeoverBody).colonyTakeover.skipped).toEqual([ { field: 'occupiedFrames', reason: 'NO_VALUE' }, ]); expect((await colony()).occupiedFrames).toBeNull(); }); it('a correction (PATCH) takes nothing over', async () => { const created = await post(staffToken, { date: '2026-06-01T10:00:00.000Z', }).expect(201); const inspectionId = (created.body as TakeoverBody).id; await request(app.getHttpServer()) .patch(`/api/v1/inspections/${inspectionId}`) .set('Authorization', `Bearer ${staffToken}`) .send({ colonyStrength: 30 }) .expect(200); expect((await colony()).occupiedFrames).toBeNull(); await request(app.getHttpServer()) .patch(`/api/v1/inspections/${inspectionId}`) .set('Authorization', `Bearer ${staffToken}`) .send({ applyToColony: ['occupiedFrames'] }) .expect(400); }); it.each([ [['feedStatus', 'broodStatus', 'occupiedFrames', 'feedStatus']], [['feedStatus', 'feedStatus']], [['nonsense']], ])('applyToColony %p answers 400', async (applyToColony) => { await post(staffToken, { date: '2026-08-01T10:00:00.000Z', applyToColony, }).expect(400); }); });