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 PublicBatchBody { lotNumber: string; productName: string | null; honeyType: string | null; waterContent: number | null; origin: { regions: string[]; country: string }; harvestDates: string[]; } describe('Public batch origin page (e2e)', () => { let app: INestApplication; let prisma: PrismaService; const beekeeperEmail = `public-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; let productId: string; let bottlingBatchId: string; const lotNumber = `WH-E2E-${Date.now()}`; const apiaryName = `Stand Wald ${Date.now()}`; const legalName = `Rechtsname-${Date.now()}-honig`; const foodBusinessAddress = `Geheimweg ${Date.now()}, 86450 Altenmünster`; 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: apiaryName, location: 'Waldweg 3', address: 'Waldweg 3, 12345 Ort', latitude: 51.2, longitude: 9.4, publicLocationLabel: 'Nordhessen', }, }); 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('2026-08-12'), userId: beekeeperId }, }); harvestId = harvest.id; await prisma.harvestColony.create({ data: { harvestId, colonyId } }); const extraction = await prisma.extraction.create({ data: { date: new Date('2026-08-13'), 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: 25, honeyType: 'Sommerhonig', waterContent: 16.8, }, }); honeyBatchId = honeyBatch.id; const product = await prisma.product.create({ data: { name: 'Sommerhonig 500g', legalName: legalName }, }); productId = product.id; const bottlingBatch = await prisma.bottlingBatch.create({ data: { honeyBatchId, productId, date: new Date('2026-08-20'), jarSize: 500, jarCount: 10, totalAmount: 5, bestBeforeDate: new Date('2028-08-20'), lotNumber, userId: beekeeperId, }, }); bottlingBatchId = bottlingBatch.id; }); afterAll(async () => { await prisma.bottlingBatch.deleteMany({ where: { id: bottlingBatchId } }); await prisma.product.deleteMany({ where: { id: productId } }); 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('returns public-safe origin data for a lot without any authentication', async () => { const response = await request(app.getHttpServer()) .get(`/api/v1/public/batches/${lotNumber}`) .expect(200); const body = response.body as PublicBatchBody; expect(body.lotNumber).toBe(lotNumber); expect(body.productName).toBe('Sommerhonig 500g'); expect(body.honeyType).toBe('Sommerhonig'); expect(body.waterContent).toBe(16.8); expect(body.origin.regions).toEqual(['Nordhessen']); expect(body.origin.country).toEqual(expect.any(String)); expect(body.harvestDates).toHaveLength(1); }); it('the public origin page exposes neither foodBusinessAddress nor legalName', async () => { const before = await prisma.siteSettings.findUnique({ where: { id: 'singleton' }, }); await prisma.siteSettings.upsert({ where: { id: 'singleton' }, update: { foodBusinessName: 'Imkerei Geheim', foodBusinessAddress }, create: { id: 'singleton', foodBusinessName: 'Imkerei Geheim', foodBusinessAddress, }, }); try { const response = await request(app.getHttpServer()) .get(`/api/v1/public/batches/${lotNumber}`) .expect(200); const text = JSON.stringify(response.body); expect(text).not.toContain(foodBusinessAddress); expect(text).not.toContain('Imkerei Geheim'); expect(text).not.toContain(legalName); expect(response.body).not.toHaveProperty('legalName'); expect(response.body).not.toHaveProperty('foodBusinessAddress'); } finally { await prisma.siteSettings.update({ where: { id: 'singleton' }, data: { foodBusinessName: before?.foodBusinessName ?? null, foodBusinessAddress: before?.foodBusinessAddress ?? null, }, }); } }); it('never leaks the apiary name, location, address or coordinates', async () => { const response = await request(app.getHttpServer()) .get(`/api/v1/public/batches/${lotNumber}`) .expect(200); const text = JSON.stringify(response.body); for (const secret of [apiaryName, 'Waldweg 3', '51.2', '9.4', 'Ort']) { expect(text).not.toContain(secret); } expect(response.body).not.toHaveProperty('apiaries'); }); it('sends the public cache header on 200 and no-store on 404', async () => { const ok = await request(app.getHttpServer()) .get(`/api/v1/public/batches/${lotNumber}`) .expect(200) .expect('Cache-Control', 'public, max-age=300, s-maxage=3600'); expect(ok.headers['pragma']).toBeUndefined(); await request(app.getHttpServer()) .get('/api/v1/public/batches/NONE') .expect(404) .expect('Cache-Control', 'no-store'); }); it('generates a QR code for the bottling batch that deep-links to the public page', async () => { const response = await request(app.getHttpServer()) .get(`/api/v1/qr/bottling-batches/${bottlingBatchId}.svg`) .set('Authorization', `Bearer ${beekeeperToken}`) .expect(200); expect(response.headers['content-type']).toContain('image/svg+xml'); }); });