mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-23 15:36:33 +00:00
4e6b45016a
Two of the three files in be-places-helpers-fold. placePhotoCache is left for its own PR — it holds three module-level caches and its sweep hangs off the cron, so it needs both a provider with real state and a second entry in the scheduler contract. kmlImport.ts is 212 lines with no state, no database and no I/O — parsing, sanitising and coordinate arithmetic. It is nest/places/kml-import.helpers.ts now, unchanged, next to the places.helpers.ts that already consumes it. placeImage.ts moves to nest/places/place-image.ts as free functions. Making reclaimPlaceImage a provider would have split it from PLACE_IMAGES_DIR, which cannot be injected: nest/common/place-image-upload.ts reads it at module scope to configure multer's storage, before any container exists. That constant is the one thing here that could have broken quietly: path.resolve(__dirname, '../../uploads/places') __dirname moves with the file. From src/services that resolved to <server>/uploads/places; from src/nest/places the same string would have resolved to <server>/src/uploads/places — a directory multer would have happily created, leaving every uploaded place image in the wrong place and every existing one unreachable. The path is now '../../../uploads/places', verified to resolve to the identical absolute directory, and uploads-dirs.test.ts covers it.
97 lines
3.3 KiB
TypeScript
97 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buildCategoryNameLookup,
|
|
decodeUtf8WithWarning,
|
|
extractKmlPlacemarkNodes,
|
|
parseKmlPointCoordinates,
|
|
parsePlacemarkNode,
|
|
resolveCategoryIdForFolder,
|
|
sanitizeKmlDescription,
|
|
} from '../../../../src/nest/places/kml-import.helpers';
|
|
|
|
describe('kmlImportUtils', () => {
|
|
it('sanitizes HTML descriptions with br to newline', () => {
|
|
const input = 'Line 1<br>Line <b>2</b> & more';
|
|
const output = sanitizeKmlDescription(input);
|
|
expect(output).toBe('Line 1\nLine 2 & more');
|
|
});
|
|
|
|
it('unwraps CDATA sections before stripping tags', () => {
|
|
const input = '<![CDATA[Great spot<br>for photos <b>and</b> skyline.]]>';
|
|
expect(sanitizeKmlDescription(input)).toBe('Great spot\nfor photos and skyline.');
|
|
});
|
|
|
|
it('parses KML coordinate order lng,lat,alt', () => {
|
|
const parsed = parseKmlPointCoordinates('13.4050,52.5200,15');
|
|
expect(parsed).toEqual({ lat: 52.52, lng: 13.405 });
|
|
});
|
|
|
|
it('extracts placemarks from nested folders', () => {
|
|
const root = {
|
|
Document: {
|
|
Folder: {
|
|
name: 'Parent',
|
|
Folder: {
|
|
name: 'Child',
|
|
Placemark: { name: 'Nested', Point: { coordinates: '13.4,52.5,0' } },
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const nodes = extractKmlPlacemarkNodes(root);
|
|
expect(nodes).toHaveLength(1);
|
|
expect(nodes[0].folderName).toBe('Child');
|
|
|
|
const parsed = parsePlacemarkNode(nodes[0]);
|
|
expect(parsed.name).toBe('Nested');
|
|
expect(parsed.lat).toBe(52.5);
|
|
expect(parsed.lng).toBe(13.4);
|
|
});
|
|
|
|
it('builds exact case-insensitive category lookup', () => {
|
|
const lookup = buildCategoryNameLookup([
|
|
{ id: 3, name: 'Museums' },
|
|
{ id: 4, name: 'Parks' },
|
|
]);
|
|
|
|
expect(resolveCategoryIdForFolder('museums', lookup)).toBe(3);
|
|
expect(resolveCategoryIdForFolder('Museum', lookup)).toBeNull();
|
|
expect(resolveCategoryIdForFolder('parks', lookup)).toBe(4);
|
|
});
|
|
|
|
it('decodes non-BMP decimal HTML entities (emoji)', () => {
|
|
// 😀 = U+1F600 = 😀 — requires String.fromCodePoint, not fromCharCode
|
|
expect(sanitizeKmlDescription('😀')).toBe('😀');
|
|
});
|
|
|
|
it('decodes non-BMP hex HTML entities (emoji)', () => {
|
|
// 😀 = U+1F600 = 😀
|
|
expect(sanitizeKmlDescription('😀')).toBe('😀');
|
|
});
|
|
|
|
it('does not produce [object Object] when description is a parsed object with #text', () => {
|
|
// fast-xml-parser can return an object for mixed-content nodes when stopNodes
|
|
// is not configured; the fallback in asTrimmedString must extract #text.
|
|
const result = sanitizeKmlDescription({ '#text': 'Hello <b>world</b>' } as any);
|
|
expect(result).not.toBe('[object Object]');
|
|
expect(result).toBe('Hello world');
|
|
});
|
|
|
|
it('returns null when description object has no #text', () => {
|
|
expect(sanitizeKmlDescription({ i: 'bold' } as any)).toBeNull();
|
|
});
|
|
|
|
it('returns warning for non-UTF8 payload', () => {
|
|
const buffer = Buffer.concat([
|
|
Buffer.from('<?xml version="1.0"?><kml><Document><Placemark><name>Caf'),
|
|
Buffer.from([0xe9]),
|
|
Buffer.from('</name></Placemark></Document></kml>'),
|
|
]);
|
|
|
|
const decoded = decodeUtf8WithWarning(buffer);
|
|
expect(decoded.warning).toContain('not valid UTF-8');
|
|
expect(decoded.text).toContain('<kml>');
|
|
});
|
|
});
|