Files
ApiCore/ApiCore-Backend/test/export.e2e-spec.ts
T
Nick SzaunigandClaude Opus 5.5 f263ec79f2 feat(export): Stockkarte - a colony's complete record as PDF
New report colony-record-card (button "Stockkarte (PDF)" on the colony
page): master data, current queen and queen history, every inspection as
a table row (strength, eggs, open/capped brood, queen seen, queen cells,
gentleness, swarm drive, feed, measures, notes), treatments with batch
number and withdrawal period, feedings, varroa counts and harvests.
Landscape A4, table header repeated on each page. Whole history by
default, one calendar year with ?year=.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-28 06:27:55 +02:00

205 lines
7.5 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';
describe('Export (e2e)', () => {
let app: INestApplication<App>;
let prisma: PrismaService;
const beekeeperEmail = `export-e2e-beekeeper-${Date.now()}@example.com`;
const password = 'SuperSecret123!';
let beekeeperId: string;
let beekeeperToken: string;
let apiaryId: string;
let colonyId: string;
let harvestId: string;
let extractionId: string;
let honeyBatchId: 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: { connect: { key: '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;
const harvest = await prisma.harvest.create({
data: { apiaryId, date: new Date(), userId: beekeeperId },
});
harvestId = harvest.id;
await prisma.harvestColony.create({ data: { harvestId, colonyId } });
const extraction = await prisma.extraction.create({
data: { date: new Date(), userId: beekeeperId },
});
extractionId = extraction.id;
await prisma.extractionHarvest.create({
data: { extractionId, harvestId },
});
const honeyBatch = await prisma.honeyBatch.create({
data: { batchNumber: `HC-${Date.now()}`, extractionId, amount: 20 },
});
honeyBatchId = honeyBatch.id;
});
afterAll(async () => {
await prisma.honeyBatch.deleteMany({ where: { id: honeyBatchId } });
await prisma.extractionHarvest.deleteMany({ where: { extractionId } });
await prisma.extraction.deleteMany({ where: { id: extractionId } });
await prisma.harvestColony.deleteMany({ where: { harvestId } });
await prisma.harvest.deleteMany({ where: { id: harvestId } });
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('exports colonies as CSV', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/export/colonies.csv')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.headers['content-type']).toContain('text/csv');
expect(response.text.split('\n')[0]).toContain('code');
expect(response.text).toContain(colonyId);
});
it('exports honey batches as CSV', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/export/honey-batches.csv')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.text).toContain(honeyBatchId);
});
it('exports colonies as JSON', async () => {
const response = await request(app.getHttpServer())
.get('/api/v1/export/colonies.json')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
const body = response.body as { id: string }[];
expect(body.some((colony) => colony.id === colonyId)).toBe(true);
});
it('rejects an unsupported JSON entity with 400', () => {
return request(app.getHttpServer())
.get('/api/v1/export/bogus.json')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(400);
});
it('exports the honey batch traceability report as PDF', async () => {
const response = await request(app.getHttpServer())
.get(
`/api/v1/export/honey-batch-traceability.pdf?honeyBatchId=${honeyBatchId}`,
)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.headers['content-type']).toContain('application/pdf');
const pdfBody = response.body as Buffer;
expect(pdfBody.subarray(0, 4).toString('latin1')).toBe('%PDF');
});
it('rejects the traceability report without honeyBatchId with 400', () => {
return request(app.getHttpServer())
.get('/api/v1/export/honey-batch-traceability.pdf')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(400);
});
it('rejects the colony annual report without a colonyId', () => {
return request(app.getHttpServer())
.get('/api/v1/export/colony-annual-report.pdf')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(400);
});
it('rejects the colony annual report for an unknown colony', () => {
return request(app.getHttpServer())
.get('/api/v1/export/colony-annual-report.pdf?colonyId=does-not-exist')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(404);
});
it('renders a colony annual report PDF', async () => {
const response = await request(app.getHttpServer())
.get(`/api/v1/export/colony-annual-report.pdf?colonyId=${colonyId}`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.headers['content-type']).toContain('application/pdf');
expect((response.body as Buffer).subarray(0, 4).toString('latin1')).toBe(
'%PDF',
);
});
it('renders the record card (Stockkarte) for the whole history and for one year', async () => {
for (const query of [
`colonyId=${colonyId}`,
`colonyId=${colonyId}&year=2026`,
]) {
const response = await request(app.getHttpServer())
.get(`/api/v1/export/colony-record-card.pdf?${query}`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.headers['content-type']).toContain('application/pdf');
expect((response.body as Buffer).subarray(0, 4).toString('latin1')).toBe(
'%PDF',
);
}
});
it('rejects the record card without a colonyId and for an unknown colony', async () => {
await request(app.getHttpServer())
.get('/api/v1/export/colony-record-card.pdf')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(400);
await request(app.getHttpServer())
.get('/api/v1/export/colony-record-card.pdf?colonyId=does-not-exist')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(404);
});
it('rejects the apiary annual report without an apiaryId', () => {
return request(app.getHttpServer())
.get('/api/v1/export/apiary-annual-report.pdf')
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(400);
});
it('renders an apiary annual report PDF', async () => {
const response = await request(app.getHttpServer())
.get(`/api/v1/export/apiary-annual-report.pdf?apiaryId=${apiaryId}`)
.set('Authorization', `Bearer ${beekeeperToken}`)
.expect(200);
expect(response.headers['content-type']).toContain('application/pdf');
expect((response.body as Buffer).subarray(0, 4).toString('latin1')).toBe(
'%PDF',
);
});
});