import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { ConfigService } from '@nestjs/config'; import request from 'supertest'; import { Subscription } from '@domain/entities/subscription.entity'; import { SubscriptionPlan } from '@domain/value-objects/subscription-plan.vo'; import { ShipmentLimitExceededException } from '@domain/exceptions/shipment-limit-exceeded.exception'; import { CreateCsvBookingDto } from '../dto/csv-booking.dto'; import { SubscriptionStatus } from '@domain/value-objects/subscription-status.vo'; import { CsvBookingsController } from './csv-bookings.controller'; import { JwtAuthGuard } from '../guards/jwt-auth.guard'; import { CsvBookingService } from '../services/csv-booking.service'; import { SubscriptionService } from '../services/subscription.service'; import { SHIPMENT_COUNTER_PORT } from '@domain/ports/out/shipment-counter.port'; import { ORGANIZATION_REPOSITORY } from '@domain/ports/out/organization.repository'; describe('CSV booking HTTP security', () => { let app: INestApplication; let subscription: Subscription; const countPaidShipmentsForOrganizationInYear = jest.fn().mockResolvedValue(0); const createBooking = jest.fn(async () => ({ id: 'booking' })); const getUserBookings = jest.fn(async () => ({ bookings: [] })); beforeAll(async () => { const module = await Test.createTestingModule({ controllers: [CsvBookingsController], providers: [ { provide: CsvBookingService, useValue: { createBooking, getUserBookings } }, { provide: SubscriptionService, useValue: { getOrCreateSubscription: async () => subscription, }, }, { provide: ConfigService, useValue: {} }, { provide: SHIPMENT_COUNTER_PORT, useValue: { countPaidShipmentsForOrganizationInYear } }, { provide: ORGANIZATION_REPOSITORY, useValue: {} }, ], }) .overrideGuard(JwtAuthGuard) .useValue({ canActivate: (context: ExecutionContext) => { const req = context.switchToHttp().getRequest(); req.user = { id: 'user', organizationId: 'org', role: req.headers['x-test-role'] || 'USER', }; return true; }, }) .compile(); app = module.createNestApplication({ logger: false }); await app.init(); await app.listen(0, '127.0.0.1'); }); afterAll(async () => { await app?.close(); }); beforeEach(() => { jest.clearAllMocks(); subscription = Subscription.create({ id: 'sub', organizationId: 'org', plan: SubscriptionPlan.gold(), }); countPaidShipmentsForOrganizationInYear.mockResolvedValue(0); }); it('rejects VIEWER mutations before invoking the booking service', async () => { await request(app.getHttpServer()) .post('/csv-bookings') .set('x-test-role', 'VIEWER') .attach('documents', Buffer.from('document'), 'test.pdf') .expect(403); expect(createBooking).not.toHaveBeenCalled(); }); it('preserves VIEWER reads', async () => { await request(app.getHttpServer()) .get('/csv-bookings') .set('x-test-role', 'VIEWER') .expect(200); expect(getUserBookings).toHaveBeenCalled(); }); it('rejects organization-wide reads for an ordinary member', async () => { await request(app.getHttpServer()).get('/csv-bookings/organization/all').expect(403); }); it('rejects oversized documents before invoking the service', async () => { await request(app.getHttpServer()) .post('/csv-bookings') .attach('documents', Buffer.alloc(10 * 1024 * 1024 + 1), 'large.pdf') .expect(413); expect(createBooking).not.toHaveBeenCalled(); }); it('applies the Bronze quota after a paid subscription is suspended', async () => { subscription = subscription.updateStatus(SubscriptionStatus.create('UNPAID')); countPaidShipmentsForOrganizationInYear.mockResolvedValue( SubscriptionPlan.bronze().maxShipmentsPerYear ); await expect( app .get(CsvBookingsController) .createBooking({} as CreateCsvBookingDto, [{} as Express.Multer.File], { user: { id: 'user', organizationId: 'org', role: 'USER' }, }) ).rejects.toBeInstanceOf(ShipmentLimitExceededException); expect(createBooking).not.toHaveBeenCalled(); expect(countPaidShipmentsForOrganizationInYear).toHaveBeenCalledWith( 'org', new Date().getFullYear() ); }); it('preserves permitted uploads', async () => { await request(app.getHttpServer()) .post('/csv-bookings') .attach('documents', Buffer.from('document'), 'test.pdf') .expect(201); expect(createBooking).toHaveBeenCalledTimes(1); }); });