Files
TREK/server/tests/unit/nest/atlas.controller.test.ts
T
Konstantinos Thermos 9d72af5238 fix(atlas): stream boundary GeoJSON instead of caching parsed bundles (#1585)
* fix(atlas): stream boundary GeoJSON instead of caching parsed bundles

* fix(atlas): scan admin1 features in one pass to avoid O(n^2) build

createFeatureSplitter re-scanned each partial Feature from the start on
every gunzip chunk, so a large feature (Canada ~5.5MB, ~340 chunks) made
the one-time admin1 store build O(n^2). That pushed the streaming build
(~4.2s) past the 15s test timeout under CI coverage/fork contention and
added the same latency to the first live region request.

Carry scan state (position, brace depth, string state) across chunks so
each character is examined once. Output is byte-identical (3228 features,
197 countries) and peak build memory is unchanged (~192MB RSS under a hard
512MB cap); build time drops to ~1.7s.

* fix(i18n): add packing.quantity to all locales

The packing.quantity key existed only in en (Qty) and de (Menge), so
i18n:parity:strict and the client parity test failed for the other 20
locales. Add the key everywhere; en/de are unchanged.

---------

Co-authored-by: jubnl <jgunther021@gmail.com>
2026-07-18 09:59:55 +02:00

154 lines
6.6 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { HttpException } from '@nestjs/common';
import type { Response } from 'express';
import { AtlasController } from '../../../src/nest/atlas/atlas.controller';
import type { AtlasService } from '../../../src/nest/atlas/atlas.service';
import type { User } from '../../../src/types';
const user = { id: 8 } as User;
function makeController(svc: Partial<AtlasService>) {
return new AtlasController(svc as AtlasService);
}
function makeRes() {
return { setHeader: vi.fn(), send: vi.fn(), json: vi.fn() } as unknown as Response & {
setHeader: ReturnType<typeof vi.fn>;
send: ReturnType<typeof vi.fn>;
json: ReturnType<typeof vi.fn>;
};
}
async function thrown(fn: () => unknown): Promise<{ status: number; body: unknown }> {
try {
await fn();
} catch (err) {
expect(err).toBeInstanceOf(HttpException);
const e = err as HttpException;
return { status: e.getStatus(), body: e.getResponse() };
}
throw new Error('expected the handler to throw');
}
describe('AtlasController (parity with the legacy /api/addons/atlas route)', () => {
it('GET /stats delegates with the user id', () => {
const stats = vi.fn().mockReturnValue({ countries: 3 });
expect(makeController({ stats }).stats(user)).toEqual({ countries: 3 });
expect(stats).toHaveBeenCalledWith(8);
});
describe('GET /regions/geo', () => {
it('returns an empty FeatureCollection without a cache header when no countries given', async () => {
const regionGeo = vi.fn();
const res = makeRes();
const out = await makeController({ regionGeo }).regionGeo(undefined, res);
expect(out).toEqual({ type: 'FeatureCollection', features: [] });
expect(regionGeo).not.toHaveBeenCalled();
expect(res.setHeader).not.toHaveBeenCalled();
});
it('caches a non-empty result for a day', async () => {
const regionGeo = vi.fn().mockResolvedValue({ type: 'FeatureCollection', features: [{ id: 1 }] });
const res = makeRes();
const out = await makeController({ regionGeo }).regionGeo('DE,FR', res);
expect(out).toEqual({ type: 'FeatureCollection', features: [{ id: 1 }] });
expect(regionGeo).toHaveBeenCalledWith(['DE', 'FR']);
expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'public, max-age=86400');
});
});
it('GET /countries/geo serves the gzipped admin-0 bundle with Content-Encoding', () => {
const gz = Buffer.from('gzipped-bytes');
const countryGeoGz = vi.fn().mockReturnValue(gz);
const res = makeRes();
makeController({ countryGeoGz }).countryGeo(res);
expect(res.setHeader).toHaveBeenCalledWith('Content-Encoding', 'gzip');
expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'public, max-age=86400');
expect(res.send).toHaveBeenCalledWith(gz);
});
it('GET /countries/geo falls back to an empty FeatureCollection when the bundle is missing', () => {
const countryGeoGz = vi.fn().mockReturnValue(null);
const res = makeRes();
makeController({ countryGeoGz }).countryGeo(res);
expect(res.json).toHaveBeenCalledWith({ type: 'FeatureCollection', features: [] });
expect(res.setHeader).toHaveBeenCalledWith('Cache-Control', 'no-store');
});
describe('country', () => {
it('GET /country/:code upper-cases the code', () => {
const countryPlaces = vi.fn().mockReturnValue([]);
makeController({ countryPlaces }).countryPlaces(user, 'de');
expect(countryPlaces).toHaveBeenCalledWith(8, 'DE');
});
it('POST mark returns success and upper-cases', () => {
const markCountry = vi.fn();
expect(makeController({ markCountry }).markCountry(user, 'de')).toEqual({ success: true });
expect(markCountry).toHaveBeenCalledWith(8, 'DE');
});
it('DELETE mark returns success', () => {
const unmarkCountry = vi.fn();
expect(makeController({ unmarkCountry }).unmarkCountry(user, 'FR')).toEqual({ success: true });
});
});
describe('region', () => {
it('400 when name or country_code is missing', () => {
const markRegion = vi.fn();
return thrown(() => makeController({ markRegion }).markRegion(user, 'by', undefined, 'DE')).then((r) =>
expect(r).toEqual({ status: 400, body: { error: 'name and country_code are required' } }));
});
it('marks a region, upper-casing both codes', () => {
const markRegion = vi.fn();
expect(makeController({ markRegion }).markRegion(user, 'by', 'Bavaria', 'de')).toEqual({ success: true });
expect(markRegion).toHaveBeenCalledWith(8, 'BY', 'Bavaria', 'DE');
});
});
describe('bucket list', () => {
it('GET wraps the items', () => {
const bucketList = vi.fn().mockReturnValue([{ id: 1 }]);
expect(makeController({ bucketList }).bucketList(user)).toEqual({ items: [{ id: 1 }] });
});
it('400 on create with a blank name', () => {
const createBucketItem = vi.fn();
return thrown(() => makeController({ createBucketItem }).createBucketItem(user, { name: ' ' })).then((r) =>
expect(r).toEqual({ status: 400, body: { error: 'Name is required' } }));
});
it('201-shape create returns { item }', () => {
const createBucketItem = vi.fn().mockReturnValue({ id: 1, name: 'Tokyo' });
expect(makeController({ createBucketItem }).createBucketItem(user, { name: 'Tokyo', lat: 35, lng: 139 }))
.toEqual({ item: { id: 1, name: 'Tokyo' } });
expect(createBucketItem).toHaveBeenCalledWith(8, { name: 'Tokyo', lat: 35, lng: 139, country_code: undefined, notes: undefined, target_date: undefined });
});
it('404 on update of a missing item', () => {
const updateBucketItem = vi.fn().mockReturnValue(null);
return thrown(() => makeController({ updateBucketItem }).updateBucketItem(user, '9', { name: 'X' })).then((r) =>
expect(r).toEqual({ status: 404, body: { error: 'Item not found' } }));
});
it('updates an existing item', () => {
const updateBucketItem = vi.fn().mockReturnValue({ id: 1, name: 'Kyoto' });
expect(makeController({ updateBucketItem }).updateBucketItem(user, '1', { name: 'Kyoto' }))
.toEqual({ item: { id: 1, name: 'Kyoto' } });
});
it('404 on delete of a missing item', () => {
const deleteBucketItem = vi.fn().mockReturnValue(false);
return thrown(() => makeController({ deleteBucketItem }).deleteBucketItem(user, '9')).then((r) =>
expect(r).toEqual({ status: 404, body: { error: 'Item not found' } }));
});
it('deletes an existing item', () => {
const deleteBucketItem = vi.fn().mockReturnValue(true);
expect(makeController({ deleteBucketItem }).deleteBucketItem(user, '1')).toEqual({ success: true });
});
});
});