import { INestApplication, UnauthorizedException, ValidationPipe, VersioningType, } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { Test } from '@nestjs/testing'; import * as request from 'supertest'; import { RecurringTransactionIntervalEnum } from '../recurring-transactions/recurring-transaction-interval.enum'; import validationOptions from '../utils/validation-options'; import { CashboxExportController } from './cashbox-export.controller'; import { CashboxExportScheduler } from './cashbox-export.scheduler'; import { CashboxExportService } from './cashbox-export.service'; import { CashboxExportSubscriptionService } from './cashbox-export-subscription.service'; describe('cashbox export HTTP boundary', () => { let app: INestApplication; const service = { exportForUser: jest.fn(), }; const subscriptionService = { getSubscription: jest.fn(), upsertSubscription: jest.fn(), }; const scheduler = { runDueSubscriptions: jest.fn() }; beforeAll(async () => { const module = await Test.createTestingModule({ controllers: [CashboxExportController], providers: [ { provide: CashboxExportService, useValue: service }, { provide: CashboxExportSubscriptionService, useValue: subscriptionService }, { provide: CashboxExportScheduler, useValue: scheduler }, ], }) .overrideGuard(AuthGuard('jwt')) .useValue({ canActivate(context) { const httpRequest = context.switchToHttp().getRequest(); if (httpRequest.headers.authorization !== 'Bearer user') { throw new UnauthorizedException(); } httpRequest.user = { id: 42, role: { id: 2 } }; return true; }, }) .compile(); app = module.createNestApplication(); app.setGlobalPrefix('api'); app.enableVersioning({ type: VersioningType.URI }); app.useGlobalPipes(new ValidationPipe(validationOptions)); await app.init(); }); afterAll(() => app.close()); beforeEach(() => jest.clearAllMocks()); it('requires authentication', async () => { await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5?from=2026-08-01&to=2026-08-31&format=csv') .expect(401); }); it('rejects an invalid format', async () => { await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5?from=2026-08-01&to=2026-08-31&format=xls') .set('Authorization', 'Bearer user') .expect(422); expect(service.exportForUser).not.toHaveBeenCalled(); }); it('rejects a full ISO datetime instead of a plain YYYY-MM-DD date for from', async () => { await request(app.getHttpServer()) .get( '/api/v1/cashbox-export/5?from=2026-08-01T12:00:00Z&to=2026-08-31&format=csv', ) .set('Authorization', 'Bearer user') .expect(422); expect(service.exportForUser).not.toHaveBeenCalled(); }); it('rejects a malformed date string for to', async () => { await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5?from=2026-08-01&to=not-a-date&format=csv') .set('Authorization', 'Bearer user') .expect(422); expect(service.exportForUser).not.toHaveBeenCalled(); }); it('returns a CSV file with correct headers and content', async () => { const csvBuffer = Buffer.from('Datum;Typ;Wer;Notiz;Betrag;Periodensaldo', 'utf-8'); service.exportForUser.mockResolvedValue({ buffer: csvBuffer, contentType: 'text/csv; charset=utf-8', filename: 'kassenbuch_team-a_2026-08-01_2026-08-31.csv', }); const response = await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5?from=2026-08-01&to=2026-08-31&format=csv') .set('Authorization', 'Bearer user') .expect(200); expect(response.headers['content-type']).toContain('text/csv'); expect(response.headers['content-disposition']).toContain( 'kassenbuch_team-a_2026-08-01_2026-08-31.csv', ); expect(response.text).toBe(csvBuffer.toString('utf-8')); expect(service.exportForUser).toHaveBeenCalledWith(5, 42, '2026-08-01', '2026-08-31', 'csv'); }); it('returns a PDF file with correct headers and binary content', async () => { const pdfBuffer = Buffer.from('%PDF-1.4 fake content'); service.exportForUser.mockResolvedValue({ buffer: pdfBuffer, contentType: 'application/pdf', filename: 'kassenbuch_team-a_2026-08-01_2026-08-31.pdf', }); const response = await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5?from=2026-08-01&to=2026-08-31&format=pdf') .set('Authorization', 'Bearer user') .expect(200); expect(response.headers['content-type']).toBe('application/pdf'); expect(Buffer.from(response.body).equals(pdfBuffer)).toBe(true); }); it('reads the current subscription', async () => { const subscription = { recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, nextRunDate: '2026-09-01T00:00:00.000Z', }; subscriptionService.getSubscription.mockResolvedValue(subscription); await request(app.getHttpServer()) .get('/api/v1/cashbox-export/5/subscription') .set('Authorization', 'Bearer user') .expect(200) .expect(subscription); expect(subscriptionService.getSubscription).toHaveBeenCalledWith(5, 42); }); it('rejects an invalid recipient email on upsert', async () => { await request(app.getHttpServer()) .put('/api/v1/cashbox-export/5/subscription') .set('Authorization', 'Bearer user') .send({ recipients: ['not-an-email'], interval: 'monthly', active: true }) .expect(422); expect(subscriptionService.upsertSubscription).not.toHaveBeenCalled(); }); it('accepts a valid subscription upsert', async () => { const subscription = { recipients: ['vorstand@example.com'], interval: RecurringTransactionIntervalEnum.monthly, active: true, nextRunDate: '2026-09-01T00:00:00.000Z', }; subscriptionService.upsertSubscription.mockResolvedValue(subscription); await request(app.getHttpServer()) .put('/api/v1/cashbox-export/5/subscription') .set('Authorization', 'Bearer user') .send({ recipients: ['vorstand@example.com'], interval: 'monthly', active: true }) .expect(200) .expect(subscription); expect(subscriptionService.upsertSubscription).toHaveBeenCalledWith(5, 42, { recipients: ['vorstand@example.com'], interval: 'monthly', active: true, }); }); });