Files
edr-platform/apps/edr-passenger-api/test/setup/slim-app.ts
Muluhabt c4f54a666b test: add EDR passenger pricing/config E2E bug-hunt harness
Hermetic E2E harness targeting pricing integrity and backoffice config:
- e2e/ docker Postgres (5544) + prepare.sh/run.sh one-command runner + HTML report
- 6 suites / 23 tests reproducing pricing, FX, wallet, refund, config and auth
  defects (see docs/ISSUES.md); docs/e2e-test-matrix.md documents the matrix
- two-tier harness (slim module boot + direct service instantiation) to work
  around the IAM/RabbitMQ/file-type boot wall
- .env.test.example tracked; loader falls back to it for fresh checkouts

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 16:22:40 +03:00

146 lines
5.5 KiB
TypeScript

/**
* 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<TestingModule> {
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<void>;
}
/** Resolve services for direct method-level assertions. */
export async function createServiceHarness(): Promise<ServiceHarness> {
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<void>;
}
/** Boot a full HTTP app with the production ValidationPipe config from src/main.ts:56. */
export async function createHttpHarness(): Promise<HttpHarness> {
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();
},
};
}