391 lines
13 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|