fix: block creating or moving a colony into an inactive/missing apiary
Colony.apiaryId was never checked against the target apiary at all - create() would hit a raw Prisma FK-constraint error for a nonexistent apiary instead of a clean 404, and both create() and move() would silently succeed against a deactivated apiary, orphaning the colony the same way (just from the opposite direction) as the apiary-side gap fixed in the previous commit. Adds a shared assertApiaryActive() check, following the same findUnique-then-status pattern already used for cross-entity checks elsewhere (e.g. QueensService.assertMotherQueenExists), used by both create() and move(). Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
961a237335
commit
5c5c7b756f
2 files changed
+66
No files matched your search
@@ -20,6 +20,7 @@ describe('ColoniesService', () => {
|
|||||||
create: jest.Mock;
|
create: jest.Mock;
|
||||||
update: jest.Mock;
|
update: jest.Mock;
|
||||||
};
|
};
|
||||||
|
apiary: { findUnique: jest.Mock };
|
||||||
colonyApiaryHistory: { findMany: jest.Mock };
|
colonyApiaryHistory: { findMany: jest.Mock };
|
||||||
queenColonyHistory: { findMany: jest.Mock };
|
queenColonyHistory: { findMany: jest.Mock };
|
||||||
colonyLineage: { findMany: jest.Mock };
|
colonyLineage: { findMany: jest.Mock };
|
||||||
@@ -49,6 +50,11 @@ describe('ColoniesService', () => {
|
|||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
},
|
},
|
||||||
|
apiary: {
|
||||||
|
findUnique: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'apiary-2', status: 'ACTIVE' }),
|
||||||
|
},
|
||||||
colonyApiaryHistory: { findMany: jest.fn() },
|
colonyApiaryHistory: { findMany: jest.fn() },
|
||||||
queenColonyHistory: { findMany: jest.fn() },
|
queenColonyHistory: { findMany: jest.fn() },
|
||||||
colonyLineage: { findMany: jest.fn() },
|
colonyLineage: { findMany: jest.fn() },
|
||||||
@@ -185,6 +191,45 @@ describe('ColoniesService', () => {
|
|||||||
).rejects.toBeInstanceOf(ConflictException);
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('move rejects a nonexistent destination apiary', async () => {
|
||||||
|
prisma.colony.findUnique.mockResolvedValue(parentColony);
|
||||||
|
prisma.apiary.findUnique.mockResolvedValue(null);
|
||||||
|
await expect(
|
||||||
|
service.move('colony-1', { apiaryId: 'missing' }, 'user-1'),
|
||||||
|
).rejects.toBeInstanceOf(NotFoundException);
|
||||||
|
expect(tx.colony.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('move rejects an inactive destination apiary', async () => {
|
||||||
|
prisma.colony.findUnique.mockResolvedValue(parentColony);
|
||||||
|
prisma.apiary.findUnique.mockResolvedValue({
|
||||||
|
id: 'apiary-2',
|
||||||
|
status: 'INACTIVE',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
service.move('colony-1', { apiaryId: 'apiary-2' }, 'user-1'),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
expect(tx.colony.update).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('create rejects an inactive apiary', async () => {
|
||||||
|
prisma.apiary.findUnique.mockResolvedValue({
|
||||||
|
id: 'apiary-1',
|
||||||
|
status: 'INACTIVE',
|
||||||
|
});
|
||||||
|
await expect(
|
||||||
|
service.create(
|
||||||
|
{
|
||||||
|
code: 'V-002',
|
||||||
|
apiaryId: 'apiary-1',
|
||||||
|
colonyType: 'PRODUCTION_COLONY',
|
||||||
|
},
|
||||||
|
'user-1',
|
||||||
|
),
|
||||||
|
).rejects.toBeInstanceOf(ConflictException);
|
||||||
|
expect(prisma.colony.create).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('dissolve sets status DISSOLVED and records DEACTIVATE', async () => {
|
it('dissolve sets status DISSOLVED and records DEACTIVATE', async () => {
|
||||||
prisma.colony.findUnique.mockResolvedValue(parentColony);
|
prisma.colony.findUnique.mockResolvedValue(parentColony);
|
||||||
tx.colony.update.mockResolvedValue({
|
tx.colony.update.mockResolvedValue({
|
||||||
|
|||||||
@@ -51,7 +51,27 @@ export class ColoniesService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Colony.apiaryId is a plain FK with no built-in status check - without
|
||||||
|
// this, a colony could be created or moved into an apiary that either
|
||||||
|
// doesn't exist (a raw Prisma FK-constraint error instead of a clean
|
||||||
|
// 404) or is deactivated (silently orphaning the colony the same way an
|
||||||
|
// apiary deactivated out from under an existing colony would).
|
||||||
|
private async assertApiaryActive(apiaryId: string): Promise<void> {
|
||||||
|
const apiary = await this.prisma.apiary.findUnique({
|
||||||
|
where: { id: apiaryId },
|
||||||
|
});
|
||||||
|
if (!apiary) {
|
||||||
|
throw new NotFoundException(`Apiary ${apiaryId} not found`);
|
||||||
|
}
|
||||||
|
if (apiary.status !== 'ACTIVE') {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Apiary ${apiaryId} is not active (status: ${apiary.status})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async create(dto: CreateColonyDto, actingUserId: string): Promise<Colony> {
|
async create(dto: CreateColonyDto, actingUserId: string): Promise<Colony> {
|
||||||
|
await this.assertApiaryActive(dto.apiaryId);
|
||||||
const colony = await this.prisma.colony.create({ data: { ...dto } });
|
const colony = await this.prisma.colony.create({ data: { ...dto } });
|
||||||
await this.auditLog.record({
|
await this.auditLog.record({
|
||||||
entityType: 'Colony',
|
entityType: 'Colony',
|
||||||
@@ -93,6 +113,7 @@ export class ColoniesService {
|
|||||||
): Promise<Colony> {
|
): Promise<Colony> {
|
||||||
const before = await this.findById(id);
|
const before = await this.findById(id);
|
||||||
this.assertActive(before);
|
this.assertActive(before);
|
||||||
|
await this.assertApiaryActive(dto.apiaryId);
|
||||||
const colony = await this.prisma.$transaction(async (tx) => {
|
const colony = await this.prisma.$transaction(async (tx) => {
|
||||||
const updated = await tx.colony.update({
|
const updated = await tx.colony.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
|
|||||||
Reference in new issue
Block a user