mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
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>
146 lines
5.5 KiB
TypeScript
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();
|
|
},
|
|
};
|
|
}
|