Colony.apiaryId is a plain FK, never checked against the apiary's own status. Without a guard, deactivating an apiary silently orphaned any colonies still living there: they kept showing as ACTIVE, but their apiary had disappeared from every "active apiaries" list, dashboard count, and colony-creation picker - a confusing half-state with no warning at all. ApiariesService.deactivate() now throws ConflictException when active colonies remain, so the beekeeper has to move or dissolve them first - matching the same assertActive-style guard pattern already used elsewhere (colonies, e.g. move()/merge()/split()). Co-Authored-By: Claude Sonnet 5 <[email protected]>
122 lines
3.6 KiB
TypeScript
122 lines
3.6 KiB
TypeScript
import { ConflictException, NotFoundException } from '@nestjs/common';
|
|
import { ApiariesService } from './apiaries.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { AuditLogService } from '../audit/audit-log.service';
|
|
|
|
describe('ApiariesService', () => {
|
|
let service: ApiariesService;
|
|
let prisma: {
|
|
apiary: {
|
|
findMany: jest.Mock;
|
|
findUnique: jest.Mock;
|
|
create: jest.Mock;
|
|
update: jest.Mock;
|
|
};
|
|
colony: { count: jest.Mock };
|
|
};
|
|
let auditLog: { record: jest.Mock };
|
|
|
|
const existingApiary = {
|
|
id: 'apiary-1',
|
|
name: 'Heimstand',
|
|
status: 'ACTIVE',
|
|
deactivatedAt: null,
|
|
};
|
|
|
|
beforeEach(() => {
|
|
prisma = {
|
|
apiary: {
|
|
findMany: jest.fn(),
|
|
findUnique: jest.fn(),
|
|
create: jest.fn(),
|
|
update: jest.fn(),
|
|
},
|
|
colony: { count: jest.fn().mockResolvedValue(0) },
|
|
};
|
|
auditLog = { record: jest.fn().mockResolvedValue(undefined) };
|
|
service = new ApiariesService(
|
|
prisma as unknown as PrismaService,
|
|
auditLog as unknown as AuditLogService,
|
|
);
|
|
});
|
|
|
|
it('findById throws NotFoundException when missing', async () => {
|
|
prisma.apiary.findUnique.mockResolvedValue(null);
|
|
await expect(service.findById('missing')).rejects.toBeInstanceOf(
|
|
NotFoundException,
|
|
);
|
|
});
|
|
|
|
it('create persists the apiary and records an audit entry', async () => {
|
|
prisma.apiary.create.mockResolvedValue(existingApiary);
|
|
|
|
const result = await service.create({ name: 'Heimstand' }, 'user-1');
|
|
|
|
expect(result).toEqual(existingApiary);
|
|
expect(auditLog.record).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
entityType: 'Apiary',
|
|
action: 'CREATE',
|
|
userId: 'user-1',
|
|
}),
|
|
);
|
|
});
|
|
|
|
it('deactivate sets status to INACTIVE and records an audit entry', async () => {
|
|
prisma.apiary.findUnique.mockResolvedValue(existingApiary);
|
|
prisma.apiary.update.mockResolvedValue({
|
|
...existingApiary,
|
|
status: 'INACTIVE',
|
|
deactivatedAt: new Date(),
|
|
});
|
|
|
|
const result = await service.deactivate('apiary-1', 'user-1');
|
|
|
|
expect(result.status).toBe('INACTIVE');
|
|
expect(auditLog.record).toHaveBeenCalledWith(
|
|
expect.objectContaining({ entityType: 'Apiary', action: 'DEACTIVATE' }),
|
|
);
|
|
});
|
|
|
|
it('deactivate throws ConflictException when active colonies remain', async () => {
|
|
prisma.apiary.findUnique.mockResolvedValue(existingApiary);
|
|
prisma.colony.count.mockResolvedValue(2);
|
|
|
|
await expect(
|
|
service.deactivate('apiary-1', 'user-1'),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
expect(prisma.apiary.update).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it('reactivate throws ConflictException when already active', async () => {
|
|
prisma.apiary.findUnique.mockResolvedValue(existingApiary);
|
|
await expect(
|
|
service.reactivate('apiary-1', 'user-1'),
|
|
).rejects.toBeInstanceOf(ConflictException);
|
|
});
|
|
|
|
it('reactivate sets status to ACTIVE and clears deactivatedAt', async () => {
|
|
prisma.apiary.findUnique.mockResolvedValue({
|
|
...existingApiary,
|
|
status: 'INACTIVE',
|
|
deactivatedAt: new Date(),
|
|
});
|
|
prisma.apiary.update.mockResolvedValue({
|
|
...existingApiary,
|
|
status: 'ACTIVE',
|
|
deactivatedAt: null,
|
|
});
|
|
|
|
const result = await service.reactivate('apiary-1', 'user-1');
|
|
|
|
expect(result.status).toBe('ACTIVE');
|
|
expect(prisma.apiary.update).toHaveBeenCalledWith({
|
|
where: { id: 'apiary-1' },
|
|
data: { status: 'ACTIVE', deactivatedAt: null },
|
|
});
|
|
expect(auditLog.record).toHaveBeenCalledWith(
|
|
expect.objectContaining({ entityType: 'Apiary', action: 'UPDATE' }),
|
|
);
|
|
});
|
|
});
|