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 AutomationRuleBody { id: string; triggerType: string; active: boolean; config: Record | null; actionTemplate: string | null; } describe('Automation rules (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const adminEmail = `automation-e2e-admin-${Date.now()}@example.com`; const staffEmail = `automation-e2e-staff-${Date.now()}@example.com`; const password = 'SuperSecret123!'; let adminId: string; let staffId: string; let adminToken: string; let staffToken: string; let apiaryId: string; let colonyId: string; let varroaRuleId: string; beforeAll(async () => { app = await createTestApp(); prisma = app.get(PrismaService); const passwordHash = await bcrypt.hash(password, 12); const admin = await prisma.user.create({ data: { email: adminEmail, passwordHash, name: 'Automation E2E Admin', role: 'ADMIN', }, }); adminId = admin.id; const staff = await prisma.user.create({ data: { email: staffEmail, passwordHash, name: 'Automation E2E Staff', role: 'STAFF', }, }); staffId = staff.id; adminToken = (await login(app, adminEmail, password)).accessToken; staffToken = (await login(app, staffEmail, password)).accessToken; const apiary = await prisma.apiary.create({ data: { name: `Automation Stand ${Date.now()}` }, }); apiaryId = apiary.id; const colony = await prisma.colony.create({ data: { code: `AUTO-${Date.now()}`, apiaryId, colonyType: 'PRODUCTION_COLONY', }, }); colonyId = colony.id; }); afterAll(async () => { await prisma.automationRule.updateMany({ where: { triggerType: 'VARROA_THRESHOLD' }, data: { active: true }, }); await prisma.task.deleteMany({ where: { relatedId: colonyId } }); await prisma.notification.deleteMany({ where: { relatedId: colonyId } }); await prisma.varroaMeasurement.deleteMany({ where: { colonyId } }); await prisma.inspection.deleteMany({ where: { colonyId } }); await prisma.colony.deleteMany({ where: { id: colonyId } }); await prisma.apiary.deleteMany({ where: { id: apiaryId } }); await prisma.refreshToken.deleteMany({ where: { userId: { in: [adminId, staffId] } }, }); await prisma.auditLog.deleteMany({ where: { userId: { in: [adminId, staffId] } }, }); await prisma.user.deleteMany({ where: { id: { in: [adminId, staffId] } } }); await app.close(); }); it('rejects a non-admin listing automation rules', () => { return request(app.getHttpServer()) .get('/api/v1/automation-rules') .set('Authorization', `Bearer ${staffToken}`) .expect(403); }); it('seeds the three fixed triggers for an admin', async () => { const response = await request(app.getHttpServer()) .get('/api/v1/automation-rules') .set('Authorization', `Bearer ${adminToken}`) .expect(200); const rules = response.body as AutomationRuleBody[]; const triggerTypes = rules.map((rule) => rule.triggerType).sort(); expect(triggerTypes).toEqual([ 'FEED_STATUS_CRITICAL', 'QUEENLESS_NO_EGGS', 'VARROA_THRESHOLD', ]); const varroaRule = rules.find( (rule) => rule.triggerType === 'VARROA_THRESHOLD', ); expect(varroaRule).toBeDefined(); varroaRuleId = varroaRule!.id; }); it('creates a task and notification when queen not seen and no eggs present', async () => { await request(app.getHttpServer()) .post('/api/v1/inspections') .set('Authorization', `Bearer ${staffToken}`) .send({ colonyId, date: new Date().toISOString(), queenSeen: false, eggsPresent: false, }) .expect(201); const tasks = await prisma.task.findMany({ where: { relatedId: colonyId }, }); expect(tasks).toHaveLength(1); expect(tasks[0].title).toContain('Kontrolle'); const notifications = await prisma.notification.findMany({ where: { relatedId: colonyId }, }); expect(notifications).toHaveLength(1); }); it('does not trigger when the queen was seen', async () => { await request(app.getHttpServer()) .post('/api/v1/inspections') .set('Authorization', `Bearer ${staffToken}`) .send({ colonyId, date: new Date().toISOString(), queenSeen: true, eggsPresent: false, }) .expect(201); const tasks = await prisma.task.findMany({ where: { relatedId: colonyId }, }); expect(tasks).toHaveLength(1); }); it('triggers a feeding task when feed status is kritisch', async () => { await request(app.getHttpServer()) .post('/api/v1/inspections') .set('Authorization', `Bearer ${staffToken}`) .send({ colonyId, date: new Date().toISOString(), feedStatus: 'kritisch', }) .expect(201); const tasks = await prisma.task.findMany({ where: { relatedId: colonyId }, }); expect(tasks).toHaveLength(2); expect(tasks.some((task) => task.title.includes('Fütterung'))).toBe(true); }); it('triggers a treatment check when the varroa threshold is exceeded', async () => { await request(app.getHttpServer()) .post('/api/v1/varroa-measurements') .set('Authorization', `Bearer ${adminToken}`) .send({ colonyId, date: new Date().toISOString(), method: 'NATURAL_MITE_FALL', miteCount: 20, }) .expect(201); const tasks = await prisma.task.findMany({ where: { relatedId: colonyId }, }); expect(tasks).toHaveLength(3); expect(tasks.some((task) => task.title.includes('Behandlung'))).toBe(true); }); it('lets an admin deactivate a rule, preventing further triggers', async () => { await request(app.getHttpServer()) .patch(`/api/v1/automation-rules/${varroaRuleId}`) .set('Authorization', `Bearer ${adminToken}`) .send({ active: false }) .expect(200); await request(app.getHttpServer()) .post('/api/v1/varroa-measurements') .set('Authorization', `Bearer ${adminToken}`) .send({ colonyId, date: new Date().toISOString(), method: 'NATURAL_MITE_FALL', miteCount: 30, }) .expect(201); const tasks = await prisma.task.findMany({ where: { relatedId: colonyId }, }); expect(tasks).toHaveLength(3); }); });