import { NotFoundException } from '@nestjs/common'; import { AutomationService } from './automation.service'; import { PrismaService } from '../prisma/prisma.service'; import { AuditLogService } from '../audit/audit-log.service'; import { NotificationsService } from '../notifications/notifications.service'; import { WebhooksService } from '../webhooks/webhooks.service'; describe('AutomationService', () => { let service: AutomationService; let prisma: { automationRule: { upsert: jest.Mock; findMany: jest.Mock; findUnique: jest.Mock; update: jest.Mock; }; task: { create: jest.Mock }; }; let auditLog: { record: jest.Mock }; let notifications: { record: jest.Mock }; let webhooks: { dispatch: jest.Mock }; const queenlessRule = { id: 'rule-1', triggerType: 'QUEENLESS_NO_EGGS', active: true, config: {}, actionTemplate: 'Kontrolle empfohlen', }; const feedRule = { id: 'rule-2', triggerType: 'FEED_STATUS_CRITICAL', active: true, config: {}, actionTemplate: 'Fütterung erforderlich', }; const varroaRule = { id: 'rule-3', triggerType: 'VARROA_THRESHOLD', active: true, config: { thresholdMitesPerDay: 10 }, actionTemplate: 'Behandlung prüfen', }; beforeEach(() => { prisma = { automationRule: { upsert: jest.fn().mockResolvedValue(undefined), findMany: jest.fn(), findUnique: jest.fn(), update: jest.fn(), }, task: { create: jest.fn().mockResolvedValue({ id: 'task-1' }) }, }; auditLog = { record: jest.fn() }; notifications = { record: jest.fn() }; webhooks = { dispatch: jest.fn().mockResolvedValue(undefined) }; service = new AutomationService( prisma as unknown as PrismaService, auditLog as unknown as AuditLogService, notifications as unknown as NotificationsService, webhooks as unknown as WebhooksService, ); }); it('findById throws NotFoundException when missing', async () => { prisma.automationRule.findUnique.mockResolvedValue(null); await expect(service.findById('missing')).rejects.toBeInstanceOf( NotFoundException, ); }); it('findAll seeds the three default rules before returning them', async () => { prisma.automationRule.findMany.mockResolvedValue([ queenlessRule, varroaRule, feedRule, ]); const result = await service.findAll(); expect(prisma.automationRule.upsert).toHaveBeenCalledTimes(3); expect(result).toHaveLength(3); }); describe('evaluateInspection', () => { it('creates a task and notification when queen not seen and no eggs present', async () => { prisma.automationRule.findMany.mockResolvedValue([queenlessRule]); await service.evaluateInspection({ id: 'inspection-1', colonyId: 'colony-1', queenSeen: false, eggsPresent: false, feedStatus: null, } as never); expect(prisma.task.create).toHaveBeenCalledTimes(1); const [[taskArg]] = prisma.task.create.mock.calls as [ [{ data: { relatedId: string; sourceInspectionId?: string } }], ]; expect(taskArg.data.relatedId).toBe('colony-1'); expect(taskArg.data.sourceInspectionId).toBe('inspection-1'); expect(notifications.record).toHaveBeenCalledTimes(1); }); it('does not trigger when the queen was seen', async () => { prisma.automationRule.findMany.mockResolvedValue([queenlessRule]); await service.evaluateInspection({ id: 'inspection-2', colonyId: 'colony-1', queenSeen: true, eggsPresent: false, feedStatus: null, } as never); expect(prisma.task.create).not.toHaveBeenCalled(); }); it('triggers when feed status is kritisch, case-insensitive', async () => { prisma.automationRule.findMany.mockResolvedValue([feedRule]); await service.evaluateInspection({ id: 'inspection-3', colonyId: 'colony-1', queenSeen: true, eggsPresent: true, feedStatus: 'Kritisch', } as never); expect(prisma.task.create).toHaveBeenCalledTimes(1); }); it('does not trigger a deactivated rule', async () => { prisma.automationRule.findMany.mockResolvedValue([]); await service.evaluateInspection({ id: 'inspection-4', colonyId: 'colony-1', queenSeen: false, eggsPresent: false, feedStatus: 'kritisch', } as never); expect(prisma.task.create).not.toHaveBeenCalled(); }); }); describe('evaluateVarroaMeasurement', () => { it('creates a task when the calculated value meets the threshold', async () => { prisma.automationRule.findUnique.mockResolvedValue(varroaRule); await service.evaluateVarroaMeasurement({ id: 'measurement-1', colonyId: 'colony-1', calculatedValue: 12, miteCount: null, } as never); expect(prisma.task.create).toHaveBeenCalledTimes(1); }); it('does not trigger when below the threshold', async () => { prisma.automationRule.findUnique.mockResolvedValue(varroaRule); await service.evaluateVarroaMeasurement({ id: 'measurement-2', colonyId: 'colony-1', calculatedValue: 5, miteCount: null, } as never); expect(prisma.task.create).not.toHaveBeenCalled(); }); it('falls back to miteCount when calculatedValue is null', async () => { prisma.automationRule.findUnique.mockResolvedValue(varroaRule); await service.evaluateVarroaMeasurement({ id: 'measurement-3', colonyId: 'colony-1', calculatedValue: null, miteCount: 15, } as never); expect(prisma.task.create).toHaveBeenCalledTimes(1); }); }); describe('update', () => { it('records an audit log entry with before/after data', async () => { prisma.automationRule.findUnique.mockResolvedValue(varroaRule); prisma.automationRule.update.mockResolvedValue({ ...varroaRule, active: false, }); await service.update('rule-3', { active: false }, 'user-1'); expect(auditLog.record).toHaveBeenCalledWith( expect.objectContaining({ entityType: 'AutomationRule', entityId: 'rule-3', action: 'UPDATE', }), ); }); }); });