- 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]>
140 lines
4.3 KiB
TypeScript
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',
|
|
},
|
|
]);
|
|
});
|
|
});
|