Files
TREK/server/tests/integration/atlas-geocode-request-context.test.ts
T
Maurice 00cbc5aae3 test(server): src/config takes fixed test values from the setup file, so suites stop mocking it
tests/setup.ts mocks src/config once with tests/helpers/test-config.ts. A suite that needs other values calls overrideTestConfig(), and the setup file restores the defaults after every case. The 191 per-file factories are gone; config.test.ts takes the real module through vi.unmock, and the few suites that signed tokens with their own literal secret read it from TEST_CONFIG.
2026-10-10 20:16:10 +02:00

112 lines
5.1 KiB
TypeScript

/**
* Atlas background-geocode request-context ratchet (L5, task-7-review.md).
*
* `AtlasService`'s `/stats` (AT4) and `/regions` (AT29) handlers start
* `cacheRegionsInBackground` after the response: a detached loop that
* reverse-geocodes a place and writes through
* `PlaceRegionsRepository.upsertRegionWhileUnmoved` (#2527) — a repository
* method, so it validates it is running inside a request context rather than
* the global one. The reviewer traced this with a real `buildApp()` boot, a
* 1.2s-delayed `reverseGeocodeRegion` and a spy on the write catching
* whatever it would otherwise swallow: `GET /stats` writes `FR`/`FR-IDF`
* after the response, `GET /regions` writes two places about 2.4s apart, and
* in both cases `AsyncLocalStorage` carried the request fork with zero
* `cannotUseGlobalContext` errors. The loop now forks its own request context
* with `withRequestContext` all the same, like every other detached chain;
* because the carry would keep an HTTP-driven case green with that wrap
* removed, the load-bearing proof is ATLAS-CTX-001 in `atlas.service.test.ts`,
* which starts the loop from a bare context. This file stays as the parity
* proof for the real routes: the writes still land after the response, with
* the same timing, and nothing reaches the error log.
*/
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import request from 'supertest';
import type { Application } from 'express';
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/websocket', () => ({ broadcast: vi.fn(), broadcastToUser: vi.fn() }));
vi.mock('../../src/nest/atlas/atlas-geo', async (orig) => {
const actual = await orig<typeof import('../../src/nest/atlas/atlas-geo')>();
return {
...actual,
resolveCountryCodeSync: () => null,
reverseGeocodeRegion: async (lat: number) => {
await new Promise((r) => setTimeout(r, 1200)); // Nominatim-rate delay
return { country_code: 'FR', region_code: lat > 45 ? 'FR-IDF' : 'FR-PAC', region_name: 'R' };
},
};
});
import { db as testDb } from '../../src/db/database';
import { buildApp } from '../../src/bootstrap';
import { resetTestDb } from '../helpers/test-db';
import { createUser, createTrip, createPlace } from '../helpers/factories';
import { authCookie } from '../helpers/auth';
import { PlaceRegionsRepository } from '../../src/db/repositories/PlaceRegions.repository';
import { PlaceRegions } from '../../src/db/entities/PlaceRegions.entity';
import { findRow, findRows } from '../helpers/factories/rows';
import type { FactoryOrm } from '../helpers/factories/context';
import { MikroORM } from '@mikro-orm/core';
let nestApp: INestApplication;
let app: Application;
/** The app's own ORM, for reading back what the background geocode wrote. */
const orm = (): FactoryOrm => nestApp.get(MikroORM);
const errors: unknown[] = [];
beforeAll(async () => {
const orig = PlaceRegionsRepository.prototype.upsertRegionWhileUnmoved;
PlaceRegionsRepository.prototype.upsertRegionWhileUnmoved = async function (...a: Parameters<typeof orig>) {
try {
return await orig.apply(this, a);
} catch (e) {
errors.push(e);
throw e;
}
};
nestApp = await buildApp();
app = nestApp.getHttpAdapter().getInstance();
}, 60_000);
afterAll(async () => {
await nestApp.close();
testDb.close();
});
describe('AT4/AT29 background geocode request context', () => {
it('IIFE-CTX-AT4 — /stats IIFE writes place_regions after the response, no cannotUseGlobalContext', async () => {
resetTestDb(testDb);
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const p = createPlace(testDb, trip.id, { lat: 48.85, lng: 2.35 });
const res = await request(app).get('/api/addons/atlas/stats').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
expect(await findRow(orm(), PlaceRegions, { place: p.id })).toBeNull();
await new Promise((r) => setTimeout(r, 2500));
expect(errors).toEqual([]);
expect(await findRow(orm(), PlaceRegions, { place: p.id })).toMatchObject({ country_code: 'FR', region_code: 'FR-IDF' });
}, 15_000);
it('IIFE-CTX-AT29 — /regions IIFE writes two places sequentially (2nd after ~2.4s)', async () => {
resetTestDb(testDb);
const { user } = createUser(testDb);
const trip = createTrip(testDb, user.id);
const p1 = createPlace(testDb, trip.id, { lat: 48.85, lng: 2.35 });
const p2 = createPlace(testDb, trip.id, { lat: 43.3, lng: 5.4 });
const res = await request(app).get('/api/addons/atlas/regions').set('Cookie', authCookie(user.id));
expect(res.status).toBe(200);
await new Promise((r) => setTimeout(r, 3500));
expect(errors).toEqual([]);
const rows = (await findRows(orm(), PlaceRegions, {}, { place: 'asc' })).map((r) => ({
place_id: r.place_id,
region_code: r.region_code,
}));
expect(rows).toEqual([
{ place_id: p1.id, region_code: 'FR-IDF' },
{ place_id: p2.id, region_code: 'FR-PAC' },
]);
}, 15_000);
});