mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
f73f395817
The buildApp() suites for auth, bookings, budget, journeys, assignments, passkeys, profiles, OAuth, the AirTrail tick and the plugin boot order, plus the storage restore suite, seeded settings, addons and fixture rows and read back what a request left behind through raw better-sqlite3 statements. They now write and read through the ORM factories and readers, against the app's own MikroORM (or the suite's test ORM where one already existed). The account deletion case keeps every one of its foreign key checks; a row that used to read back as undefined now reads back as null.
171 lines
7.7 KiB
TypeScript
171 lines
7.7 KiB
TypeScript
/**
|
|
* `airtrail-sync` cron tick request-context ratchet (Plan 3d Task 0,
|
|
* deliverable 4b — inventory §12, the Plan 3a `*-003` shape
|
|
* `orm-request-context-seams.test.ts`'s SEAM-003 established for
|
|
* `PlaceShadowRetentionJob`).
|
|
*
|
|
* `AirtrailSyncJob.tick` → `AirtrailSyncService.runAirtrailSync` →
|
|
* `syncOwner` is the ONLY cron in the program that reaches a 3d service
|
|
* (`ReservationsService.getReservation`/`.update`, inventory §12's boot/cron
|
|
* table) — `airtrail-sync-boot` is already in BOOT-SWEEP-001's name list
|
|
* (it reads `app_settings` only, raw), but nothing before this file drove
|
|
* the TICK itself (not the boot sweep) through the real, production-wired
|
|
* `CronRegistrarService` into `ReservationsService`. `register()` already
|
|
* wraps every registered tick in `withRequestContext` unconditionally, fail
|
|
* closed on a missing ORM (`cron-registrar.service.ts`) — this pins that the
|
|
* wrap is actually reached from THIS job's tick, the same way SEAM-003 pins
|
|
* it for `PlaceShadowRetentionJob`'s.
|
|
*
|
|
* `ReservationsService` is not yet repository-backed (Plan 3d Tasks 2/4
|
|
* convert it) — the wrap around `getReservation` below performs a REAL
|
|
* repository read from exactly the point in the tick where the real,
|
|
* converted implementation will sit, proving the context is live now, ready
|
|
* for that conversion, the same technique `feeds-request-context.test.ts`'s
|
|
* SEAM-FEED-001 uses for the anonymous ICS routes.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
vi.mock('../../src/config', () => ({
|
|
JWT_SECRET: 'test-jwt-secret-for-trek-testing-only',
|
|
ENCRYPTION_KEY: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2a3b4c5d6a7b8c9d0e1f2',
|
|
updateJwtSecret: () => {},
|
|
SESSION_DURATION: '24h',
|
|
SESSION_DURATION_MS: 86400000,
|
|
SESSION_DURATION_SECONDS: 86400,
|
|
DEFAULT_LANGUAGE: 'en',
|
|
}));
|
|
vi.mock('../../src/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn(), getOnlineUserIds: vi.fn(() => []) }));
|
|
|
|
import { MikroORM } from '@mikro-orm/core';
|
|
import { SchedulerRegistry } from '@nestjs/schedule';
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { buildApp } from '../../src/bootstrap';
|
|
import { CronRegistrarService } from '../../src/nest/scheduling/cron-registrar.service';
|
|
import { AirtrailSyncJob } from '../../src/nest/integrations/airtrail-sync.job';
|
|
import { AirtrailLinkService } from '../../src/nest/integrations/airtrail-link.service';
|
|
import { AirtrailService } from '../../src/nest/integrations/airtrail.service';
|
|
import { AirtrailClient, type AirtrailFlightRaw } from '../../src/nest/integrations/airtrail.client';
|
|
import { ReservationsService } from '../../src/nest/reservations/reservations.service';
|
|
import { Users } from '../../src/db/entities/Users.entity';
|
|
import { createTrip, createUser } from '../helpers/factories';
|
|
import { deleteRows, insertRow } from '../helpers/factories/rows';
|
|
import { Reservations } from '../../src/db/entities/Reservations.entity';
|
|
|
|
describe('airtrail-sync cron tick runs inside a request context', () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
app = await buildApp();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
testDb.close();
|
|
});
|
|
|
|
it('AIRTRAIL-CTX-001: firing the real registered tick reaches ReservationsService.getReservation with a live request context, against the SAME MikroORM app.get(MikroORM) does', async () => {
|
|
const orm = app.get(MikroORM);
|
|
const registrar = app.get(CronRegistrarService);
|
|
const { user } = createUser(testDb);
|
|
const trip = createTrip(testDb, user.id);
|
|
|
|
const reservationId = await insertRow(orm, Reservations, {
|
|
trip: trip.id, title: 'AirTrail Flight', type: 'flight', status: 'confirmed', external_source: 'airtrail',
|
|
external_id: '999', external_owner_user_id: user.id, sync_enabled: 1, external_hash: 'stale-hash',
|
|
});
|
|
|
|
const flight: AirtrailFlightRaw = {
|
|
id: 999,
|
|
from: null,
|
|
to: null,
|
|
date: '2026-01-01',
|
|
datePrecision: 'day',
|
|
departure: null,
|
|
arrival: null,
|
|
departureScheduled: null,
|
|
arrivalScheduled: null,
|
|
airline: null,
|
|
flightNumber: null,
|
|
aircraft: null,
|
|
aircraftReg: null,
|
|
note: null,
|
|
};
|
|
|
|
const link = app.get(AirtrailLinkService);
|
|
const airtrail = app.get(AirtrailService);
|
|
const client = app.get(AirtrailClient);
|
|
const reservations = app.get(ReservationsService);
|
|
|
|
const syncEnabledSpy = vi.spyOn(link, 'syncGloballyEnabled').mockResolvedValue(true);
|
|
const credsSpy = vi.spyOn(airtrail, 'getAirtrailCredentials').mockResolvedValue({
|
|
baseUrl: 'https://airtrail.test',
|
|
apiKey: 'test-key',
|
|
allowInsecureTls: false,
|
|
});
|
|
const flightsSpy = vi.spyOn(client, 'listFlights').mockResolvedValue([flight]);
|
|
|
|
let repoRead: unknown;
|
|
let caught: unknown;
|
|
const originalGetReservation = reservations.getReservation.bind(reservations);
|
|
const getReservationSpy = vi.spyOn(reservations, 'getReservation').mockImplementation(async (id, tripId) => {
|
|
try {
|
|
repoRead = await orm.em.getRepository(Users).findOne({ id: user.id });
|
|
} catch (e) {
|
|
caught = e;
|
|
}
|
|
return originalGetReservation(id, tripId);
|
|
});
|
|
// Item 10 (Plan 3d Task 7 review): the tick's own request-context proof
|
|
// stopped at `getReservation` — `syncOwner` only calls `update` AFTER
|
|
// that read, when the incoming flight's hash actually differs from the
|
|
// stored `external_hash` (it does here, 'stale-hash' vs anything real),
|
|
// so a context regression that only showed up on the WRITE half of this
|
|
// same tick was invisible to this test. `update` is real here (not
|
|
// re-mocked), so this also proves it runs to completion without itself
|
|
// throwing `cannotUseGlobalContext`.
|
|
const updateSpy = vi.spyOn(reservations, 'update');
|
|
|
|
const isEnabledSpy = vi.spyOn(registrar, 'isEnabled').mockReturnValue(true);
|
|
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
try {
|
|
// Real onApplicationBootstrap: registers the tick for real through
|
|
// CronRegistrarService.register (task-6's withRequestContext wrap),
|
|
// the same choke point SEAM-003 pins for PlaceShadowRetentionJob.
|
|
await app.get(AirtrailSyncJob).onApplicationBootstrap();
|
|
const job = app.get(SchedulerRegistry).getCronJob('airtrail-sync');
|
|
await job.fireOnTick();
|
|
// cron's CronJob.fireOnTick() only awaits its callback with
|
|
// `waitForCompletion: true` (SEAM-003's own docstring) — register()
|
|
// does not set that option either, so give the awaited
|
|
// withRequestContext(orm, tick) promise a chance to actually settle.
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
await new Promise((resolve) => setImmediate(resolve));
|
|
|
|
expect(caught).toBeUndefined();
|
|
expect(repoRead).toBeTruthy();
|
|
expect(getReservationSpy).toHaveBeenCalled();
|
|
expect(updateSpy).toHaveBeenCalled();
|
|
|
|
const suspicious = errSpy.mock.calls
|
|
.map((args) => args.map(String).join(' '))
|
|
.filter((line) => /cannotUseGlobalContext|global EntityManager/i.test(line));
|
|
expect(suspicious).toEqual([]);
|
|
} finally {
|
|
registrar.unregister('airtrail-sync');
|
|
isEnabledSpy.mockRestore();
|
|
errSpy.mockRestore();
|
|
getReservationSpy.mockRestore();
|
|
updateSpy.mockRestore();
|
|
flightsSpy.mockRestore();
|
|
credsSpy.mockRestore();
|
|
syncEnabledSpy.mockRestore();
|
|
await deleteRows(orm, Reservations, { id: reservationId });
|
|
}
|
|
});
|
|
});
|