/** * Slim Nest test harness — boots ONLY the passenger domain modules needed for pricing/booking * tests, deliberately excluding the IAM (TriaIamModule), SharedAuth, and MinIO stack from * app.module.ts. Those drag in `@tria-plc/api-common`'s file-crud/minio chain which requires the * ESM-only `file-type` package that jest's CommonJS resolver cannot load. * * Two entry points: * - createServiceHarness(): resolve services directly (FareEngineService, etc.) for unit/DB-level * assertions on the money math. * - createHttpHarness(): a full Nest HTTP app with the SAME global ValidationPipe as main.ts, so * controller/DTO/pipe behavior (client-trust, DTO validation) is exercised end-to-end over HTTP. * * The IAM JwtGuard is overridden with an always-allow stub so protected routes are reachable; auth * *enforcement* findings (which guards are missing) are asserted separately via route metadata, not * by booting the real guard. */ import { Global, INestApplication, Module, ValidationPipe } from "@nestjs/common"; import { Test, TestingModule } from "@nestjs/testing"; import { ConfigModule } from "@nestjs/config"; import { EventEmitterModule } from "@nestjs/event-emitter"; import { ScheduleModule } from "@nestjs/schedule"; import { getDataSourceToken } from "@nestjs/typeorm"; import { PrismaClient } from "@prisma/client"; import { PrismaModule } from "../../src/common/prisma.module"; import { PrismaService } from "../../src/common/prisma.service"; import { SessionActivityInterceptor } from "../../src/common/interceptors/session-activity.interceptor"; import { FareEngineModule } from "../../src/modules/fare-engine/fare-engine.module"; import { CurrencyModule } from "../../src/modules/currency/currency.module"; import { CurrenciesModule } from "../../src/modules/currencies/currencies.module"; import { PromosModule } from "../../src/modules/promos/promos.module"; import { SeatClassesModule } from "../../src/modules/seat-classes/seat-classes.module"; import { StationsModule } from "../../src/modules/stations/stations.module"; import { SchedulesModule } from "../../src/modules/schedules/schedules.module"; import { SegmentsModule } from "../../src/modules/segments/segments.module"; import { SystemConfigModule } from "../../src/modules/system-config/system-config.module"; /** * A stub TypeORM DataSource, provided globally so IAM-derived providers that reach the slim * harness transitively (e.g. NotificationsService via ExcessBaggageModule) can instantiate. * Pricing tests never trigger the code paths that actually use it. */ const fakeDataSource = { query: async () => [], transaction: async (cb: (m: unknown) => unknown) => cb({}), getRepository: () => ({}), createQueryRunner: () => ({ connect: async () => undefined, startTransaction: async () => undefined, commitTransaction: async () => undefined, rollbackTransaction: async () => undefined, release: async () => undefined, manager: {}, }), }; @Global() @Module({ providers: [{ provide: getDataSourceToken(), useValue: fakeDataSource }], exports: [getDataSourceToken()], }) class TestGlobalsModule {} /** Modules that are safe to import in isolation (verified free of the IAM/MinIO chain). */ const DOMAIN_MODULES = [ FareEngineModule, CurrencyModule, CurrenciesModule, PromosModule, SeatClassesModule, StationsModule, SchedulesModule, SegmentsModule, SystemConfigModule, ]; // NOTE: ExcessBaggageModule/PaymentsModule/BookingsModule are intentionally excluded — they pull in // NotificationsModule → @golevelup RabbitMQ which connects at boot. Their suites instantiate the // service directly with mocked collaborators (see excess-baggage / booking-trust specs). async function buildModule(): Promise { return Test.createTestingModule({ imports: [ ConfigModule.forRoot({ isGlobal: true }), EventEmitterModule.forRoot(), ScheduleModule.forRoot(), TestGlobalsModule, PrismaModule, ...DOMAIN_MODULES, ], }) // SessionActivityInterceptor needs the IAM TypeORM DataSource, which the slim harness // deliberately omits. Replace it with a pass-through — it does not affect pricing logic. .overrideProvider(SessionActivityInterceptor) .useValue({ intercept: (_ctx: unknown, next: { handle: () => unknown }) => next.handle() }) .compile(); } export interface ServiceHarness { moduleRef: TestingModule; prisma: PrismaClient; close: () => Promise; } /** Resolve services for direct method-level assertions. */ export async function createServiceHarness(): Promise { const moduleRef = await buildModule(); const prisma = moduleRef.get(PrismaService) as unknown as PrismaClient; return { moduleRef, prisma, close: async () => { await moduleRef.close(); }, }; } export interface HttpHarness { app: INestApplication; moduleRef: TestingModule; prisma: PrismaClient; close: () => Promise; } /** Boot a full HTTP app with the production ValidationPipe config from src/main.ts:56. */ export async function createHttpHarness(): Promise { const moduleRef = await buildModule(); const app = moduleRef.createNestApplication(); app.useGlobalPipes( new ValidationPipe({ whitelist: true, transform: true, forbidUnknownValues: false, }), ); await app.init(); const prisma = moduleRef.get(PrismaService) as unknown as PrismaClient; return { app, moduleRef, prisma, close: async () => { await app.close(); }, }; }