xpeditis2.0/apps/backend/test/integration/booking.repository.spec.ts
David-Henri ARNAUD 1044900e98 feature phase
2025-10-08 16:56:27 +02:00

391 lines
13 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { DataSource } from 'typeorm';
import { faker } from '@faker-js/faker';
import { TypeOrmBookingRepository } from '../../src/infrastructure/persistence/typeorm/repositories/typeorm-booking.repository';
import { BookingOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/booking.orm-entity';
import { ContainerOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/container.orm-entity';
import { OrganizationOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/organization.orm-entity';
import { UserOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/user.orm-entity';
import { RateQuoteOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/rate-quote.orm-entity';
import { PortOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/port.orm-entity';
import { CarrierOrmEntity } from '../../src/infrastructure/persistence/typeorm/entities/carrier.orm-entity';
import { Booking } from '../../src/domain/entities/booking.entity';
import { BookingStatus } from '../../src/domain/value-objects/booking-status.vo';
import { BookingNumber } from '../../src/domain/value-objects/booking-number.vo';
describe('TypeOrmBookingRepository (Integration)', () => {
let module: TestingModule;
let repository: TypeOrmBookingRepository;
let dataSource: DataSource;
let testOrganization: OrganizationOrmEntity;
let testUser: UserOrmEntity;
let testCarrier: CarrierOrmEntity;
let testOriginPort: PortOrmEntity;
let testDestinationPort: PortOrmEntity;
let testRateQuote: RateQuoteOrmEntity;
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
TypeOrmModule.forRoot({
type: 'postgres',
host: process.env.TEST_DB_HOST || 'localhost',
port: parseInt(process.env.TEST_DB_PORT || '5432'),
username: process.env.TEST_DB_USER || 'postgres',
password: process.env.TEST_DB_PASSWORD || 'postgres',
database: process.env.TEST_DB_NAME || 'xpeditis_test',
entities: [
BookingOrmEntity,
ContainerOrmEntity,
OrganizationOrmEntity,
UserOrmEntity,
RateQuoteOrmEntity,
PortOrmEntity,
CarrierOrmEntity,
],
synchronize: true, // Auto-create schema for tests
dropSchema: true, // Clean slate for each test run
logging: false,
}),
TypeOrmModule.forFeature([
BookingOrmEntity,
ContainerOrmEntity,
OrganizationOrmEntity,
UserOrmEntity,
RateQuoteOrmEntity,
PortOrmEntity,
CarrierOrmEntity,
]),
],
providers: [TypeOrmBookingRepository],
}).compile();
repository = module.get<TypeOrmBookingRepository>(TypeOrmBookingRepository);
dataSource = module.get<DataSource>(DataSource);
// Create test data fixtures
await createTestFixtures();
});
afterAll(async () => {
await dataSource.destroy();
await module.close();
});
afterEach(async () => {
// Clean up bookings after each test
await dataSource.getRepository(ContainerOrmEntity).delete({});
await dataSource.getRepository(BookingOrmEntity).delete({});
});
async function createTestFixtures() {
const orgRepo = dataSource.getRepository(OrganizationOrmEntity);
const userRepo = dataSource.getRepository(UserOrmEntity);
const carrierRepo = dataSource.getRepository(CarrierOrmEntity);
const portRepo = dataSource.getRepository(PortOrmEntity);
const rateQuoteRepo = dataSource.getRepository(RateQuoteOrmEntity);
// Create organization
testOrganization = orgRepo.create({
id: faker.string.uuid(),
name: 'Test Freight Forwarder',
type: 'freight_forwarder',
scac: 'TEFF',
address: {
street: '123 Test St',
city: 'Rotterdam',
postalCode: '3000',
country: 'NL',
},
contactEmail: 'test@example.com',
contactPhone: '+31123456789',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
await orgRepo.save(testOrganization);
// Create user
testUser = userRepo.create({
id: faker.string.uuid(),
organizationId: testOrganization.id,
email: 'testuser@example.com',
passwordHash: 'hashed_password',
firstName: 'Test',
lastName: 'User',
role: 'user',
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
await userRepo.save(testUser);
// Create carrier
testCarrier = carrierRepo.create({
id: faker.string.uuid(),
name: 'Test Carrier Line',
code: 'TESTCARRIER',
scac: 'TSTC',
supportsApi: true,
isActive: true,
createdAt: new Date(),
updatedAt: new Date(),
});
await carrierRepo.save(testCarrier);
// Create ports
testOriginPort = portRepo.create({
id: faker.string.uuid(),
name: 'Port of Rotterdam',
code: 'NLRTM',
city: 'Rotterdam',
country: 'Netherlands',
countryCode: 'NL',
timezone: 'Europe/Amsterdam',
latitude: 51.9225,
longitude: 4.47917,
createdAt: new Date(),
updatedAt: new Date(),
});
await portRepo.save(testOriginPort);
testDestinationPort = portRepo.create({
id: faker.string.uuid(),
name: 'Port of Shanghai',
code: 'CNSHA',
city: 'Shanghai',
country: 'China',
countryCode: 'CN',
timezone: 'Asia/Shanghai',
latitude: 31.2304,
longitude: 121.4737,
createdAt: new Date(),
updatedAt: new Date(),
});
await portRepo.save(testDestinationPort);
// Create rate quote
testRateQuote = rateQuoteRepo.create({
id: faker.string.uuid(),
carrierId: testCarrier.id,
originPortId: testOriginPort.id,
destinationPortId: testDestinationPort.id,
baseFreight: 1500.0,
currency: 'USD',
surcharges: [],
totalAmount: 1500.0,
containerType: '40HC',
validFrom: new Date(),
validUntil: new Date(Date.now() + 86400000 * 30), // 30 days
etd: new Date(Date.now() + 86400000 * 7), // 7 days from now
eta: new Date(Date.now() + 86400000 * 37), // 37 days from now
transitDays: 30,
createdAt: new Date(),
updatedAt: new Date(),
});
await rateQuoteRepo.save(testRateQuote);
}
function createTestBookingEntity(): Booking {
return Booking.create({
id: faker.string.uuid(),
bookingNumber: BookingNumber.generate(),
userId: testUser.id,
organizationId: testOrganization.id,
rateQuoteId: testRateQuote.id,
status: BookingStatus.create('draft'),
shipper: {
name: 'Shipper Company Ltd',
address: {
street: '456 Shipper Ave',
city: 'Rotterdam',
postalCode: '3001',
country: 'NL',
},
contactName: 'John Shipper',
contactEmail: 'shipper@example.com',
contactPhone: '+31987654321',
},
consignee: {
name: 'Consignee Corp',
address: {
street: '789 Consignee Rd',
city: 'Shanghai',
postalCode: '200000',
country: 'CN',
},
contactName: 'Jane Consignee',
contactEmail: 'consignee@example.com',
contactPhone: '+86123456789',
},
cargoDescription: 'General cargo - electronics',
containers: [],
specialInstructions: 'Handle with care',
createdAt: new Date(),
updatedAt: new Date(),
});
}
describe('save', () => {
it('should save a new booking', async () => {
const booking = createTestBookingEntity();
const savedBooking = await repository.save(booking);
expect(savedBooking.id).toBe(booking.id);
expect(savedBooking.bookingNumber.value).toBe(booking.bookingNumber.value);
expect(savedBooking.status.value).toBe('draft');
});
it('should update an existing booking', async () => {
const booking = createTestBookingEntity();
await repository.save(booking);
// Update the booking
const updatedBooking = Booking.create({
...booking,
status: BookingStatus.create('pending_confirmation'),
cargoDescription: 'Updated cargo description',
});
const result = await repository.save(updatedBooking);
expect(result.status.value).toBe('pending_confirmation');
expect(result.cargoDescription).toBe('Updated cargo description');
});
});
describe('findById', () => {
it('should find a booking by ID', async () => {
const booking = createTestBookingEntity();
await repository.save(booking);
const found = await repository.findById(booking.id);
expect(found).toBeDefined();
expect(found?.id).toBe(booking.id);
expect(found?.bookingNumber.value).toBe(booking.bookingNumber.value);
});
it('should return null for non-existent ID', async () => {
const nonExistentId = faker.string.uuid();
const found = await repository.findById(nonExistentId);
expect(found).toBeNull();
});
});
describe('findByBookingNumber', () => {
it('should find a booking by booking number', async () => {
const booking = createTestBookingEntity();
await repository.save(booking);
const found = await repository.findByBookingNumber(booking.bookingNumber);
expect(found).toBeDefined();
expect(found?.id).toBe(booking.id);
expect(found?.bookingNumber.value).toBe(booking.bookingNumber.value);
});
it('should return null for non-existent booking number', async () => {
const nonExistentNumber = BookingNumber.generate();
const found = await repository.findByBookingNumber(nonExistentNumber);
expect(found).toBeNull();
});
});
describe('findByOrganization', () => {
it('should find all bookings for an organization', async () => {
const booking1 = createTestBookingEntity();
const booking2 = createTestBookingEntity();
const booking3 = createTestBookingEntity();
await repository.save(booking1);
await repository.save(booking2);
await repository.save(booking3);
const bookings = await repository.findByOrganization(testOrganization.id);
expect(bookings).toHaveLength(3);
expect(bookings.every((b) => b.organizationId === testOrganization.id)).toBe(true);
});
it('should return empty array for organization with no bookings', async () => {
const nonExistentOrgId = faker.string.uuid();
const bookings = await repository.findByOrganization(nonExistentOrgId);
expect(bookings).toEqual([]);
});
});
describe('findByStatus', () => {
it('should find bookings by status', async () => {
const draftBooking1 = createTestBookingEntity();
const draftBooking2 = createTestBookingEntity();
const confirmedBooking = Booking.create({
...createTestBookingEntity(),
status: BookingStatus.create('confirmed'),
});
await repository.save(draftBooking1);
await repository.save(draftBooking2);
await repository.save(confirmedBooking);
const draftBookings = await repository.findByStatus(BookingStatus.create('draft'));
const confirmedBookings = await repository.findByStatus(BookingStatus.create('confirmed'));
expect(draftBookings).toHaveLength(2);
expect(confirmedBookings).toHaveLength(1);
expect(draftBookings.every((b) => b.status.value === 'draft')).toBe(true);
expect(confirmedBookings.every((b) => b.status.value === 'confirmed')).toBe(true);
});
});
describe('delete', () => {
it('should delete a booking', async () => {
const booking = createTestBookingEntity();
await repository.save(booking);
const found = await repository.findById(booking.id);
expect(found).toBeDefined();
await repository.delete(booking.id);
const deletedBooking = await repository.findById(booking.id);
expect(deletedBooking).toBeNull();
});
it('should not throw error when deleting non-existent booking', async () => {
const nonExistentId = faker.string.uuid();
await expect(repository.delete(nonExistentId)).resolves.not.toThrow();
});
});
describe('complex scenarios', () => {
it('should handle bookings with multiple containers', async () => {
const booking = createTestBookingEntity();
// Note: Container handling would be tested separately
// This test ensures the booking can be saved without containers first
await repository.save(booking);
const found = await repository.findById(booking.id);
expect(found).toBeDefined();
});
it('should maintain data integrity for nested shipper and consignee', async () => {
const booking = createTestBookingEntity();
await repository.save(booking);
const found = await repository.findById(booking.id);
expect(found?.shipper.name).toBe(booking.shipper.name);
expect(found?.shipper.contactEmail).toBe(booking.shipper.contactEmail);
expect(found?.consignee.name).toBe(booking.consignee.name);
expect(found?.consignee.contactEmail).toBe(booking.consignee.contactEmail);
});
});
});