import { INestApplicationContext } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Prisma } from '../generated/prisma'; import { DailyEvaluationJob } from '../src/jobs/daily-evaluation.job'; import { JobsModule } from '../src/jobs/jobs.module'; import { PrismaService } from '../src/prisma/prisma.service'; const d = (day: string): Date => new Date(`${day}T00:00:00.000Z`); /** When the CronJob runs on `day` (04:30 Europe/Berlin). */ const at = (day: string): Date => new Date(`${day}T02:30:00.000Z`); const NOTHING_NEW = { WithdrawalEnded: 0, TaskDue: 0, QueenAged: 0, VarroaMeasurementDue: 0, InspectionOverdue: 0, }; /** * The job evaluates the whole database. The e2e files share it and run in * parallel, so the evaluation runs on days in 2001: no other file has * records there, and the events of this file stay its own. Called through * evaluate(day) instead of runCli, which would also write JobRun rows that * test/jobs.e2e-spec.ts counts. */ describe('daily-evaluation (e2e)', () => { const suffix = `${Date.now()}-${Math.round(Math.random() * 1e6)}`; let app: INestApplicationContext; let prisma: PrismaService; let job: DailyEvaluationJob; const ids: Record = {}; const eventsOf = (aggregateId: string) => prisma.domainEvent.findMany({ where: { aggregateId }, select: { type: true, aggregateType: true, payload: true, dedupeKey: true, actorUserId: true, }, orderBy: { type: 'asc' }, }); beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [JobsModule], }).compile(); app = await moduleRef.init(); prisma = app.get(PrismaService); job = app.get(DailyEvaluationJob); const user = await prisma.user.create({ data: { email: `daily-evaluation-e2e-${suffix}@example.com`, passwordHash: 'not-used', name: 'Daily Evaluation E2E', role: { connect: { key: 'STAFF' } }, }, }); ids.user = user.id; const apiary = await prisma.apiary.create({ data: { name: `Daily Evaluation ${suffix}` }, }); ids.apiary = apiary.id; const queen = async ( name: string, data: Pick, ) => { const created = await prisma.queen.create({ data: { code: `DE-E2E-${suffix}-${name}`, ...data }, }); ids[`queen:${name}`] = created.id; }; await queen('aged', { hatchYear: 1999, status: 'ACTIVE' }); await queen('young', { hatchYear: 2000, status: 'ACTIVE' }); await queen('reserve', { hatchYear: 1999, status: 'RESERVE' }); // Living, created 2001-04-20 and never measured: varroa due 2001-05-18. const living = await prisma.colony.create({ data: { code: `DE-E2E-${suffix}-living`, apiaryId: apiary.id, colonyType: 'PRODUCTION_COLONY', currentQueenId: ids['queen:aged'], createdAt: d('2001-04-20'), }, }); ids.living = living.id; const dead = await prisma.colony.create({ data: { code: `DE-E2E-${suffix}-dead`, apiaryId: apiary.id, colonyType: 'PRODUCTION_COLONY', status: 'DECEASED', createdAt: d('2001-04-20'), }, }); ids.dead = dead.id; // Inspected 2001-04-28: the next inspection is overdue on 2001-05-19. for (const colonyId of [living.id, dead.id]) { await prisma.inspection.create({ data: { colonyId, userId: user.id, date: d('2001-04-28') }, }); } const treatment = async ( name: string, data: Pick< Prisma.TreatmentUncheckedCreateInput, 'date' | 'endDate' | 'withdrawalPeriodDays' >, ) => { const created = await prisma.treatment.create({ data: { colonyId: living.id, userId: user.id, product: 'Oxalsäure', ...data, }, }); ids[`treatment:${name}`] = created.id; }; // 2001-05-10 + 7 = 2001-05-17, in the window of 2001-05-20. await treatment('ending', { date: d('2001-05-08'), endDate: d('2001-05-10'), withdrawalPeriodDays: 7, }); // 2001-05-01 + 7 = 2001-05-08, before the window. await treatment('ended-earlier', { date: d('2001-05-01'), withdrawalPeriodDays: 7, }); await treatment('no-withdrawal', { date: d('2001-05-15') }); const task = async ( name: string, data: Pick, ) => { const created = await prisma.task.create({ data: { title: `Daily evaluation ${suffix} ${name}`, ...data }, }); ids[`task:${name}`] = created.id; }; await task('open', { dueDate: d('2001-05-18'), status: 'OPEN' }); await task('done', { dueDate: d('2001-05-18'), status: 'DONE' }); }); afterAll(async () => { const aggregateIds = Object.entries(ids) .filter(([key]) => !['user', 'apiary'].includes(key)) .map(([, id]) => id); await prisma.domainEvent.deleteMany({ where: { aggregateId: { in: aggregateIds } }, }); const colonyIds = [ids.living, ids.dead].filter(Boolean); await prisma.treatment.deleteMany({ where: { colonyId: { in: colonyIds } }, }); await prisma.inspection.deleteMany({ where: { colonyId: { in: colonyIds } }, }); await prisma.task.deleteMany({ where: { title: { startsWith: `Daily evaluation ${suffix}` } }, }); await prisma.colony.deleteMany({ where: { id: { in: colonyIds } } }); await prisma.queen.deleteMany({ where: { code: { startsWith: `DE-E2E-${suffix}` } }, }); if (ids.apiary) { await prisma.apiary.deleteMany({ where: { id: ids.apiary } }); } if (ids.user) { await prisma.user.deleteMany({ where: { id: ids.user } }); } await app.close(); }); it('running the job twice yields exactly one WithdrawalEnded per treatment and day', async () => { const first = await job.evaluate(at('2001-05-20')); const second = await job.evaluate(at('2001-05-20')); // The next day the occurrence is still in the window - and still known. const nextDay = await job.evaluate(at('2001-05-21')); expect(first.WithdrawalEnded).toBeGreaterThanOrEqual(1); expect(second).toEqual(NOTHING_NEW); expect(nextDay).toEqual(NOTHING_NEW); expect(await eventsOf(ids['treatment:ending'])).toEqual([ { type: 'WithdrawalEnded', aggregateType: 'Treatment', payload: { colonyId: ids.living, endedOn: '2001-05-17' }, dedupeKey: `WithdrawalEnded:${ids['treatment:ending']}:2001-05-17`, actorUserId: null, }, ]); expect(await eventsOf(ids['treatment:ended-earlier'])).toEqual([]); expect(await eventsOf(ids['treatment:no-withdrawal'])).toEqual([]); }); it('records due tasks and overdue checks only for open tasks and living colonies', async () => { await job.evaluate(at('2001-05-20')); expect(await eventsOf(ids['task:open'])).toEqual([ { type: 'TaskDue', aggregateType: 'Task', payload: { dueOn: '2001-05-18' }, dedupeKey: `TaskDue:${ids['task:open']}:2001-05-18`, actorUserId: null, }, ]); expect(await eventsOf(ids['task:done'])).toEqual([]); expect(await eventsOf(ids.living)).toEqual([ { type: 'InspectionOverdue', aggregateType: 'Colony', payload: { dueOn: '2001-05-19', lastInspectedOn: '2001-04-28' }, dedupeKey: `InspectionOverdue:${ids.living}:2001-05-19`, actorUserId: null, }, { type: 'VarroaMeasurementDue', aggregateType: 'Colony', payload: { dueOn: '2001-05-18', lastMeasuredOn: null }, dedupeKey: `VarroaMeasurementDue:${ids.living}:2001-05-18`, actorUserId: null, }, ]); expect(await eventsOf(ids.dead)).toEqual([]); }); it('reports active queens from their second year in the first days of January', async () => { await job.evaluate(at('2001-01-05')); expect(await job.evaluate(at('2001-01-08'))).toEqual(NOTHING_NEW); expect(await eventsOf(ids['queen:aged'])).toEqual([ { type: 'QueenAged', aggregateType: 'Queen', payload: { hatchYear: 1999, colonyId: ids.living }, dedupeKey: `QueenAged:${ids['queen:aged']}:2001-01-01`, actorUserId: null, }, ]); expect(await eventsOf(ids['queen:young'])).toEqual([]); expect(await eventsOf(ids['queen:reserve'])).toEqual([]); }); });