Files
ApiCore/ApiCore-Backend/test/queens.e2e-spec.ts
T
Nick Szaunig e458a7cf07 feat: Königinnen-Abstammungsbaum (Mutterlinie)
Neue Funktion jenseits der Lastenheft-Checkliste, explizit im Text als
spätere Erweiterung angedacht (§4.4: "ApiCore SOLL später
Abstammungsbäume darstellen können"). Königin.motherQueenId ist eine
neue optionale Selbstreferenz (additive Migration), zusätzlich zu den
bestehenden freien Textfeldern motherLine/fatherLine – letztere
bleiben für Fälle, in denen die Mutter nicht selbst in ApiCore erfasst
ist. Die Vaterlinie bleibt bewusst Freitext, da eine Königin mit vielen
Drohnen aus unterschiedlichen Völkern begattet wird und es keine
einzelne "Vater-Königin" gibt, die referenzierbar wäre.

Backend: GET /queens/:id/lineage baut die mütterliche Ahnenkette
(rekursiv über motherQueenId, Tiefe 8) sowie den Töchterbaum
(rekursiv über die neue daughterQueens-Relation) in einer Antwort;
create/update lehnen eine nicht existierende oder auf sich selbst
zeigende motherQueenId ab.

Frontend: neue "Abstammungsbaum"-Karte auf der Königin-Detailseite
zeigt die mütterliche Linie als Liste sowie die Töchter als
eingerückten Baum, beide mit Links zu den jeweiligen Königinnen.
Anlage- und Bearbeiten-Formular bekommen ein "Mutter"-Auswahlfeld
(Bearbeiten schließt die Königin selbst aus der Liste aus).

Live in Chrome verifiziert: dreistufige Kette (Großmutter → Mutter →
Tochter) korrekt in beide Richtungen dargestellt; dabei einen echten
Anzeigefehler gefunden und behoben – das Mutter-Auswahlfeld zeigte
nach dem Speichern die rohe Datenbank-ID statt des Königin-Codes an
(derselbe SelectValue-ohne-children-Fehler wie zuvor bei der
Import-Entitätsauswahl).
2026-08-25 05:13:45 +02:00

187 lines
6.2 KiB
TypeScript

import { INestApplication } from '@nestjs/common';
import * as bcrypt from 'bcryptjs';
import request from 'supertest';
import { App } from 'supertest/types';
import { PrismaService } from '../src/prisma/prisma.service';
import { createTestApp } from './utils/create-test-app';
import { login } from './utils/api';
interface QueenBody {
id: string;
code: string;
}
describe('Queens (e2e)', () => {
let app: INestApplication<App>;
let prisma: PrismaService;
const beekeeperEmail = `queens-e2e-beekeeper-${Date.now()}@example.com`;
const password = 'SuperSecret123!';
let beekeeperId: string;
let beekeeperToken: string;
let apiaryId: string;
let colonyId: string;
let queenId: string;
let daughterQueenId: string;
beforeAll(async () => {
app = await createTestApp();
prisma = app.get(PrismaService);
const passwordHash = await bcrypt.hash(password, 12);
const beekeeper = await prisma.user.create({
data: {
email: beekeeperEmail,
passwordHash,
name: 'Beekeeper',
role: 'BEEKEEPER',
},
});
beekeeperId = beekeeper.id;
beekeeperToken = (await login(app, beekeeperEmail, password)).accessToken;
const apiary = await prisma.apiary.create({
data: { name: `Stand ${Date.now()}` },
});
apiaryId = apiary.id;
const colony = await prisma.colony.create({
data: {
code: `V-${Date.now()}`,
apiaryId,
colonyType: 'PRODUCTION_COLONY',
},
});
colonyId = colony.id;
});
afterAll(async () => {
if (queenId) {
await prisma.queenEvaluation.deleteMany({ where: { queenId } });
}
await prisma.queenColonyHistory.deleteMany({ where: { colonyId } });
if (colonyId) {
await prisma.colony.update({
where: { id: colonyId },
data: { currentQueenId: null },
});
}
if (daughterQueenId) {
await prisma.queen.delete({ where: { id: daughterQueenId } });
}
if (queenId) {
await prisma.queen.delete({ where: { id: queenId } });
}
await prisma.colony.deleteMany({ where: { id: colonyId } });
await prisma.apiary.deleteMany({ where: { id: apiaryId } });
await prisma.refreshToken.deleteMany({ where: { userId: beekeeperId } });
await prisma.auditLog.deleteMany({ where: { userId: beekeeperId } });
await prisma.user.deleteMany({ where: { id: beekeeperId } });
await app.close();
});
it('lets a BEEKEEPER create a queen', async () => {
const response = await request(app.getHttpServer())
.post('/api/v1/queens')
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({ code: `Q-${Date.now()}`, breed: 'Carnica' })
.expect(201);
queenId = (response.body as QueenBody).id;
expect(queenId).toEqual(expect.any(String));
});
it('assigns the queen to a colony and records history', async () => {
await request(app.getHttpServer())
.post(`/api/v1/queens/${queenId}/assign-colony`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({ colonyId })
.expect(201);
const colony = await request(app.getHttpServer())
.get(`/api/v1/colonies/${colonyId}`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect((colony.body as { currentQueenId: string }).currentQueenId).toBe(
queenId,
);
const history = await request(app.getHttpServer())
.get(`/api/v1/queens/${queenId}/history`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(history.body).toHaveLength(1);
});
it('adds a queen evaluation and lists it back', async () => {
await request(app.getHttpServer())
.post(`/api/v1/queens/${queenId}/evaluations`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({
colonyId,
date: new Date().toISOString(),
colonyStrength: 4,
gentleness: 5,
combBuild: 3,
swarmingTendency: 2,
broodPattern: 4,
honeyYield: 5,
})
.expect(201);
const response = await request(app.getHttpServer())
.get(`/api/v1/queens/${queenId}/evaluations`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
const evaluations = response.body as { colonyStrength: number }[];
expect(evaluations).toHaveLength(1);
expect(evaluations[0]?.colonyStrength).toBe(4);
});
it('rejects creating a queen with a motherQueenId that does not exist', () => {
return request(app.getHttpServer())
.post('/api/v1/queens')
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({ code: `Q-orphan-${Date.now()}`, motherQueenId: 'does-not-exist' })
.expect(404);
});
it('rejects a queen set as her own mother', () => {
return request(app.getHttpServer())
.patch(`/api/v1/queens/${queenId}`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({ motherQueenId: queenId })
.expect(400);
});
it('creates a daughter queen and returns the ancestor/descendant lineage', async () => {
const daughterResponse = await request(app.getHttpServer())
.post('/api/v1/queens')
.set('Authorization', `Bearer ${beekeeperToken}`)
.send({ code: `Q-daughter-${Date.now()}`, motherQueenId: queenId })
.expect(201);
daughterQueenId = (daughterResponse.body as QueenBody).id;
const motherLineage = await request(app.getHttpServer())
.get(`/api/v1/queens/${queenId}/lineage`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
const motherBody = motherLineage.body as {
queen: { id: string; mother: unknown };
descendants: { id: string }[];
};
expect(motherBody.queen.id).toBe(queenId);
expect(motherBody.queen.mother).toBeNull();
expect(motherBody.descendants).toHaveLength(1);
expect(motherBody.descendants[0]?.id).toBe(daughterQueenId);
const daughterLineage = await request(app.getHttpServer())
.get(`/api/v1/queens/${daughterQueenId}/lineage`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
const daughterBody = daughterLineage.body as {
queen: { id: string; mother: { id: string } | null };
descendants: unknown[];
};
expect(daughterBody.queen.mother?.id).toBe(queenId);
expect(daughterBody.descendants).toHaveLength(0);
});
});