xpeditis2.0/apps/backend/src/application/controllers/csv-bookings.security.spec.ts

120 lines
4.7 KiB
TypeScript

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);
});
});