Adds a one-way push (ApiCore -> M365) of Aufgaben/Durchsichten/ Behandlungen/Fütterungen/Ernten/Königinnen-Zuordnungen into the calendar of a Microsoft 365 group, via the Graph API. - CalendarSyncModule (app-only client-credentials Graph auth, separate from the interactive SSO app registration; new env vars M365_CALENDAR_TENANT_ID/_CLIENT_ID/_CLIENT_SECRET). - Outbox + minute-interval cron worker (CalendarSyncOutboxEntry) instead of a synchronous push, since Graph calls are too slow/flaky to block a domain create request on; failed syncs retry up to 5 times. - Per-source-type toggle plus a group-ID field, configurable in Verwaltung > Automatisierung > Kalender-Sync (Verwaltung > Backup, an env var still wins on those fields where one applies to related config). - Hooked into Tasks/Inspections/Treatments/Feedings/Harvests/Queens at their existing create (and Task update/complete) points; entries render as all-day Graph events to avoid timezone ambiguity. - Documents the design decision in ARCHITECTURE.md (Annahme 9), since this is a net-new capability the lastenheft doesn't scope. Co-Authored-By: Claude Sonnet 5 <[email protected]>
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import { NotFoundException } from '@nestjs/common';
|
|
import { InspectionsService } from './inspections.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { WebhooksService } from '../webhooks/webhooks.service';
|
|
import { AutomationService } from '../automation/automation.service';
|
|
import { AuditLogService } from '../audit/audit-log.service';
|
|
import { CalendarSyncService } from '../calendar-sync/calendar-sync.service';
|
|
|
|
describe('InspectionsService', () => {
|
|
let service: InspectionsService;
|
|
let prisma: {
|
|
inspection: {
|
|
findMany: jest.Mock;
|
|
findUnique: jest.Mock;
|
|
create: jest.Mock;
|
|
update: jest.Mock;
|
|
};
|
|
task: { create: jest.Mock };
|
|
};
|
|
let auditLog: { record: jest.Mock };
|
|
|
|
const inspection = {
|
|
id: 'inspection-1',
|
|
colonyId: 'colony-1',
|
|
date: new Date(),
|
|
};
|
|
|
|
beforeEach(() => {
|
|
prisma = {
|
|
inspection: {
|
|
findMany: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
task: { create: jest.fn() },
|
|
};
|
|
const webhooks = { dispatch: jest.fn().mockResolvedValue(undefined) };
|
|
const automation = {
|
|
evaluateInspection: jest.fn().mockResolvedValue(undefined),
|
|
};
|
|
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
|
const calendarSync = { enqueue: jest.fn(), cancel: jest.fn() };
|
|
service = new InspectionsService(
|
|
prisma as unknown as PrismaService,
|
|
webhooks as unknown as WebhooksService,
|
|
automation as unknown as AutomationService,
|
|
auditLog as unknown as AuditLogService,
|
|
calendarSync as unknown as CalendarSyncService,
|
|
);
|
|
});
|
|
|
|
it('findById throws NotFoundException when missing', async () => {
|
|
prisma.inspection.findUnique.mockResolvedValue(null);
|
|
await expect(service.findById('missing')).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
});
|
|
|
|
it('create stores the acting user as the inspector', async () => {
|
|
prisma.inspection.create.mockResolvedValue(inspection);
|
|
|
|
await service.create(
|
|
{ colonyId: 'colony-1', date: '2026-01-01' },
|
|
'user-1',
|
|
);
|
|
|
|
const [[createArg]] = prisma.inspection.create.mock.calls as [
|
|
[{ data: { colonyId: string; userId: string } }],
|
|
];
|
|
expect(createArg.data.colonyId).toBe('colony-1');
|
|
expect(createArg.data.userId).toBe('user-1');
|
|
});
|
|
|
|
it('update writes an audit log entry with before and after data', async () => {
|
|
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
|
const updated = { ...inspection, notes: 'geändert' };
|
|
prisma.inspection.update.mockResolvedValue(updated);
|
|
|
|
const result = await service.update(
|
|
'inspection-1',
|
|
{ notes: 'geändert' },
|
|
'user-1',
|
|
);
|
|
|
|
expect(result).toBe(updated);
|
|
expect(auditLog.record).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
entityType: 'Inspection',
|
|
entityId: 'inspection-1',
|
|
action: 'UPDATE',
|
|
beforeData: inspection,
|
|
afterData: updated,
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('createTaskFromInspection links the task to the inspection and its colony', async () => {
|
|
prisma.inspection.findUnique.mockResolvedValue(inspection);
|
|
prisma.task.create.mockResolvedValue({ id: 'task-1' });
|
|
|
|
await service.createTaskFromInspection(
|
|
'inspection-1',
|
|
{ title: 'Nachschauen' },
|
|
'user-1',
|
|
);
|
|
|
|
const [[taskArg]] = prisma.task.create.mock.calls as [
|
|
[
|
|
{
|
|
data: {
|
|
sourceInspectionId: string;
|
|
relatedType: string;
|
|
relatedId: string;
|
|
};
|
|
},
|
|
],
|
|
];
|
|
expect(taskArg.data.sourceInspectionId).toBe('inspection-1');
|
|
expect(taskArg.data.relatedType).toBe('COLONY');
|
|
expect(taskArg.data.relatedId).toBe('colony-1');
|
|
});
|
|
});
|