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]>
This commit is contained in:
1 parent
d99a697739
commit
f468dc5f75
87 files changed
+6022
-510
No files matched your search
@@ -5,6 +5,8 @@ import { App } from 'supertest/types';
|
||||
import { PrismaService } from '../src/prisma/prisma.service';
|
||||
import { createTestApp } from './utils/create-test-app';
|
||||
import { login } from './utils/api';
|
||||
import { drainDomainEvents } from './utils/domain-events';
|
||||
import { CalendarSyncSettingsService } from '../src/calendar-sync/calendar-sync-settings.service';
|
||||
|
||||
interface QueenBody {
|
||||
id: string;
|
||||
@@ -22,6 +24,8 @@ describe('Queens (e2e)', () => {
|
||||
let colonyId: string;
|
||||
let queenId: string;
|
||||
let daughterQueenId: string;
|
||||
let secondColonyId: string;
|
||||
let subscriptionId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
app = await createTestApp();
|
||||
@@ -53,7 +57,43 @@ describe('Queens (e2e)', () => {
|
||||
colonyId = colony.id;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
if (subscriptionId) {
|
||||
// Deliveries go with their subscription (ON DELETE CASCADE).
|
||||
await prisma.webhookSubscription.deleteMany({
|
||||
where: { id: subscriptionId },
|
||||
});
|
||||
}
|
||||
if (queenId) {
|
||||
const historyIds = (
|
||||
await prisma.queenColonyHistory.findMany({
|
||||
where: { queenId },
|
||||
select: { id: true },
|
||||
})
|
||||
).map((history) => history.id);
|
||||
await prisma.calendarSyncOutboxEntry.deleteMany({
|
||||
where: { sourceType: 'QUEEN_ASSIGNMENT', sourceId: { in: historyIds } },
|
||||
});
|
||||
}
|
||||
if (secondColonyId) {
|
||||
await prisma.queenColonyHistory.deleteMany({
|
||||
where: { colonyId: secondColonyId },
|
||||
});
|
||||
await prisma.colony.update({
|
||||
where: { id: secondColonyId },
|
||||
data: { currentQueenId: null },
|
||||
});
|
||||
}
|
||||
if (queenId) {
|
||||
// Events go with their deliveries (ON DELETE CASCADE).
|
||||
await prisma.domainEvent.deleteMany({
|
||||
where: { aggregateType: 'Queen', aggregateId: queenId },
|
||||
});
|
||||
}
|
||||
if (queenId) {
|
||||
await prisma.queenEvaluation.deleteMany({ where: { queenId } });
|
||||
}
|
||||
@@ -70,7 +110,9 @@ describe('Queens (e2e)', () => {
|
||||
if (queenId) {
|
||||
await prisma.queen.delete({ where: { id: queenId } });
|
||||
}
|
||||
await prisma.colony.deleteMany({ where: { id: colonyId } });
|
||||
await prisma.colony.deleteMany({
|
||||
where: { id: { in: [colonyId, secondColonyId].filter(Boolean) } },
|
||||
});
|
||||
await prisma.apiary.deleteMany({ where: { id: apiaryId } });
|
||||
await prisma.refreshToken.deleteMany({ where: { userId: beekeeperId } });
|
||||
await prisma.auditLog.deleteMany({ where: { userId: beekeeperId } });
|
||||
@@ -94,6 +136,8 @@ describe('Queens (e2e)', () => {
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.send({ colonyId })
|
||||
.expect(201);
|
||||
// Settle the first assignment's event, so later tests see only their own.
|
||||
await drainDomainEvents(app, [queenId]);
|
||||
|
||||
const colony = await request(app.getHttpServer())
|
||||
.get(`/api/v1/colonies/${colonyId}`)
|
||||
@@ -110,6 +154,86 @@ describe('Queens (e2e)', () => {
|
||||
expect(history.body).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('assigning a queen leaves a calendar entry and a queen.replaced delivery after draining', async () => {
|
||||
jest.spyOn(app.get(CalendarSyncSettingsService), 'get').mockResolvedValue({
|
||||
id: 'singleton',
|
||||
enabled: true,
|
||||
groupId: 'group-e2e',
|
||||
syncTask: true,
|
||||
syncInspection: true,
|
||||
syncTreatment: true,
|
||||
syncFeeding: true,
|
||||
syncHarvest: true,
|
||||
syncQueenAssignment: true,
|
||||
syncWithdrawal: true,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
subscriptionId = (
|
||||
await prisma.webhookSubscription.create({
|
||||
data: {
|
||||
url: 'https://example.com/hook',
|
||||
events: ['queen.replaced'],
|
||||
secret: 'a'.repeat(64),
|
||||
createdByUserId: beekeeperId,
|
||||
},
|
||||
})
|
||||
).id;
|
||||
secondColonyId = (
|
||||
await prisma.colony.create({
|
||||
data: {
|
||||
code: `V2-${Date.now()}`,
|
||||
apiaryId,
|
||||
colonyType: 'PRODUCTION_COLONY',
|
||||
},
|
||||
})
|
||||
).id;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post(`/api/v1/queens/${queenId}/assign-colony`)
|
||||
.set('Authorization', `Bearer ${beekeeperToken}`)
|
||||
.send({ colonyId: secondColonyId })
|
||||
.expect(201);
|
||||
|
||||
// Nothing but the event is written in the request.
|
||||
const history = await prisma.queenColonyHistory.findFirstOrThrow({
|
||||
where: { queenId, colonyId: secondColonyId },
|
||||
});
|
||||
expect(
|
||||
await prisma.calendarSyncOutboxEntry.count({
|
||||
where: { sourceType: 'QUEEN_ASSIGNMENT', sourceId: history.id },
|
||||
}),
|
||||
).toBe(0);
|
||||
expect(
|
||||
await prisma.webhookDelivery.count({
|
||||
where: { subscriptionId },
|
||||
}),
|
||||
).toBe(0);
|
||||
const event = await prisma.domainEvent.findFirstOrThrow({
|
||||
where: { type: 'QueenAssigned', aggregateId: queenId },
|
||||
orderBy: { occurredAt: 'desc' },
|
||||
});
|
||||
expect(event.payload).toEqual(
|
||||
expect.objectContaining({
|
||||
colonyId: secondColonyId,
|
||||
historyId: history.id,
|
||||
previousColonyId: colonyId,
|
||||
}),
|
||||
);
|
||||
|
||||
await drainDomainEvents(app, [queenId]);
|
||||
|
||||
expect(
|
||||
await prisma.calendarSyncOutboxEntry.count({
|
||||
where: { sourceType: 'QUEEN_ASSIGNMENT', sourceId: history.id },
|
||||
}),
|
||||
).toBe(1);
|
||||
const deliveries = await prisma.webhookDelivery.findMany({
|
||||
where: { subscriptionId, event: 'queen.replaced' },
|
||||
});
|
||||
expect(deliveries).toHaveLength(1);
|
||||
expect(deliveries[0]?.body).toContain(queenId);
|
||||
});
|
||||
|
||||
it('adds a queen evaluation and lists it back', async () => {
|
||||
await request(app.getHttpServer())
|
||||
.post(`/api/v1/queens/${queenId}/evaluations`)
|
||||
|
||||
Reference in new issue
Block a user