mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-19 13:36:48 +00:00
724c88c201
A mechanical prettier --write over server/src, server/tests, server/scripts and shared/src with the workspaces' .prettierrc; nothing but layout and import order changes, and every file keeps the same set of imports. shared is formatted throughout and its format baseline is empty. The server baseline keeps the 35 files that Prettier would grow past their lint:size limit (they need a split first, which is not a formatting change), and the generated entity files stay as their generator writes them.
114 lines
5.1 KiB
TypeScript
114 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 { buildApp } from '../../src/bootstrap';
|
|
import { db as testDb } from '../../src/db/database';
|
|
import { PlaceRegions } from '../../src/db/entities/PlaceRegions.entity';
|
|
import { PlaceRegionsRepository } from '../../src/db/repositories/PlaceRegions.repository';
|
|
import { authCookie } from '../helpers/auth';
|
|
import { createUser, createTrip, createPlace } from '../helpers/factories';
|
|
import type { FactoryOrm } from '../helpers/factories/context';
|
|
import { findRow, findRows } from '../helpers/factories/rows';
|
|
import { resetTestDb } from '../helpers/test-db';
|
|
import { MikroORM } from '@mikro-orm/core';
|
|
import type { INestApplication } from '@nestjs/common';
|
|
|
|
import type { Application } from 'express';
|
|
import request from 'supertest';
|
|
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
|
|
|
vi.mock('../../src/db/database', async () => {
|
|
const { createSnapshotTestDb, buildDbMock } = await import('../helpers/db-mock');
|
|
return buildDbMock(createSnapshotTestDb());
|
|
});
|
|
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' };
|
|
},
|
|
};
|
|
});
|
|
|
|
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);
|
|
});
|