Files
ApiCore/ApiCore-Backend/src/calendar-sync/calendar-sync.handlers.spec.ts
T
Nick SzaunigandClaude Opus 5.5 f468dc5f75 feat: T4 wave 3 - notifications per user, suggestions API, varroa bands, split/queen events
- Schema III: per-user notifications (broadcast fan-out migration,
  category, link, requiredPermissions filter), notification preferences
  and settings endpoints, calendar withdrawal switch, occupiedFrames
- Suggestions API: list/summary with permission visibility and paging,
  accept/dismiss/reopen incl. bulk, SuggestionView on all responses
- Varroa traffic light by season and method, young colonies, treatment
  effect window, sample-based percentage; shared withdrawal-window helper
  on Berlin calendar days; GET /varroa-measurements/thresholds
- ColonySplit and QueenAssigned domain events with calendar and webhook
  handlers
- Outbox claims compare against now() rounded to timestamp(3); fixes the
  flaky e2e claims of rows written with SQL now()

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-10-14 14:25:24 +02:00

140 lines
4.3 KiB
TypeScript

import { CalendarSyncService } from './calendar-sync.service';
import { CalendarSyncEventHandlers } from './calendar-sync.handlers';
import type {
AnyDomainEventEnvelope,
DomainEventType,
} from '../domain-events/domain-event-types';
import { domainEventHandlerMetadata } from '../domain-events/on-domain-event.decorator';
import { Prisma } from '../../generated/prisma';
describe('CalendarSyncEventHandlers', () => {
let handlers: CalendarSyncEventHandlers;
let calendarSync: { enqueueIn: jest.Mock };
const tx = {} as Prisma.TransactionClient;
const envelope = <T extends DomainEventType>(
type: T,
aggregateId: string,
payload: Record<string, unknown>,
) => ({ type, aggregateId, payload }) as unknown as AnyDomainEventEnvelope<T>;
beforeEach(() => {
calendarSync = { enqueueIn: jest.fn().mockResolvedValue(undefined) };
handlers = new CalendarSyncEventHandlers(
calendarSync as unknown as CalendarSyncService,
);
});
it.each([
['inspection', 'INSPECTION', 'InspectionRecorded'],
['inspection', 'INSPECTION', 'InspectionCorrected'],
['treatment', 'TREATMENT', 'TreatmentRecorded'],
['treatment', 'TREATMENT', 'TreatmentCorrected'],
['feeding', 'FEEDING', 'FeedingRecorded'],
['feeding', 'FEEDING', 'FeedingCorrected'],
['harvest', 'HARVEST', 'HarvestRecorded'],
['harvest', 'HARVEST', 'HarvestCorrected'],
] as const)(
'%s handler enqueues %s on %s through tx',
async (method, sourceType, type) => {
await handlers[method](envelope(type, 'src-1', {}) as never, tx);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(
tx,
sourceType,
'src-1',
);
},
);
it.each(['TaskCreated', 'TaskUpdated', 'TaskCompleted'] as const)(
'task handler enqueues the task on %s',
async (type) => {
await handlers.task(
envelope(type, 'task-1', { dueDate: '2027-05-01' }),
tx,
);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(tx, 'TASK', 'task-1');
},
);
it('QueenAssigned enqueues QUEEN_ASSIGNMENT with the history id', async () => {
await handlers.queenAssignment(
envelope('QueenAssigned', 'queen-1', {
colonyId: 'colony-1',
historyId: 'history-1',
assignedOn: '2026-10-14',
previousColonyId: null,
}),
tx,
);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(
tx,
'QUEEN_ASSIGNMENT',
'history-1',
);
});
it('TaskCreated without a due date enqueues nothing', async () => {
await handlers.task(
envelope('TaskCreated', 'task-1', { dueDate: null }),
tx,
);
expect(calendarSync.enqueueIn).not.toHaveBeenCalled();
});
it('TaskUpdated without a due date still enqueues, so the worker removes the event', async () => {
await handlers.task(
envelope('TaskUpdated', 'task-1', { dueDate: null, status: 'OPEN' }),
tx,
);
expect(calendarSync.enqueueIn).toHaveBeenCalledWith(tx, 'TASK', 'task-1');
});
it('lets an enqueue failure propagate so the delivery is retried', async () => {
calendarSync.enqueueIn.mockRejectedValue(new Error('db down'));
await expect(
handlers.inspection(envelope('InspectionRecorded', 'i-1', {}), tx),
).rejects.toThrow('db down');
});
it('registers the stable handler ids and event types', () => {
const registered = (method: keyof CalendarSyncEventHandlers) =>
domainEventHandlerMetadata(
Object.getOwnPropertyDescriptor(
CalendarSyncEventHandlers.prototype,
method,
)?.value as object,
);
expect(registered('inspection')).toEqual([
{
types: ['InspectionRecorded', 'InspectionCorrected'],
handlerId: 'calendar-sync.inspection',
},
]);
expect(registered('treatment')[0].handlerId).toBe(
'calendar-sync.treatment',
);
expect(registered('feeding')[0].handlerId).toBe('calendar-sync.feeding');
expect(registered('harvest')[0].handlerId).toBe('calendar-sync.harvest');
expect(registered('queenAssignment')).toEqual([
{
types: ['QueenAssigned'],
handlerId: 'calendar-sync.queen-assignment',
},
]);
expect(registered('task')).toEqual([
{
types: ['TaskCreated', 'TaskUpdated', 'TaskCompleted'],
handlerId: 'calendar-sync.task',
},
]);
});
});