import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { parseOverpassElement, parseBbox, tileKeysForBbox, getAlprCameras, markTilesFetched, } from '../../server/utils/alpr.js' import { cameraToFeature, identifyingProperties, inferModelFromTags, isGenericCameraName } from '../../server/utils/alprGeo.js' import { getDb, setDbPathForTest, closeDb } from '../../server/utils/db.js' import { join } from 'node:path' import { tmpdir } from 'node:os' import { randomUUID } from 'node:crypto' describe('alpr utils', () => { describe('parseOverpassElement', () => { it('parses a valid OSM node', () => { const out = parseOverpassElement({ type: 'node', id: 123, lat: 33.75, lon: -84.39, tags: { 'manufacturer': 'Flock Safety', 'camera:direction': '90' }, }) expect(out?.osmId).toBe(123) expect(out?.manufacturer).toBe('Flock Safety') }) }) describe('cameraToFeature', () => { it('returns GeoJSON Point feature with identifying tags', () => { const feature = cameraToFeature({ osmId: 1, lat: 33.5, lng: -84.5, manufacturer: 'Flock Safety', direction: 90, fov: 45, tags: { 'model': 'Falcon', 'operator': 'City PD', 'ref': 'CAM-12', 'operator:wikidata': 'Q123', 'fixme': 'verify mount', }, }) expect(feature.properties.manufacturer).toBe('Flock Safety') expect(feature.properties.model).toBe('Falcon') expect(feature.properties.operator).toBe('City PD') expect(feature.properties.ref).toBe('CAM-12') expect(feature.properties.operatorWikidata).toBe('Q123') expect(feature.properties.tags).toEqual({ fixme: 'verify mount' }) }) }) describe('identifyingProperties', () => { it('omits brand when same as manufacturer', () => { const props = identifyingProperties({ manufacturer: 'Flock Safety', brand: 'Flock Safety', model: 'Falcon' }) expect(props.manufacturer).toBe('Flock Safety') expect(props.model).toBe('Falcon') expect(props.brand).toBeUndefined() }) it('infers model from lowercase name tag', () => { const props = identifyingProperties({ manufacturer: 'Flock Safety', name: 'falcon', }) expect(props.model).toBe('Falcon') expect(props.name).toBeUndefined() expect(props.modelUnknown).toBeUndefined() }) it('drops generic camera names and flags unknown model', () => { const props = identifyingProperties({ manufacturer: 'Flock Safety', name: 'Flock ALPR camera', }) expect(props.name).toBeUndefined() expect(props.model).toBeUndefined() expect(props.modelUnknown).toBe(true) }) }) describe('inferModelFromTags', () => { it('reads model tag and mis-tagged Flock Falcon name', () => { expect(inferModelFromTags({ model: 'Sparrow' })).toBe('Sparrow') expect(inferModelFromTags({ name: 'Flock Falcon' })).toBe('Falcon') expect(isGenericCameraName('Flock ALPR camera')).toBe(true) expect(isGenericCameraName('Peachtree & 5th')).toBe(false) }) }) describe('parseBbox', () => { it('parses valid bbox', () => { expect(parseBbox({ south: 33.0, west: -85.0, north: 33.4, east: -84.6 })).toEqual({ south: 33.0, west: -85.0, north: 33.4, east: -84.6, }) }) it('rejects oversized bbox', () => { expect(() => parseBbox({ south: 0, west: 0, north: 2, east: 1 })).toThrow(/bbox exceeds/) }) }) describe('cache and fetch', () => { let dbPath beforeEach(async () => { dbPath = join(tmpdir(), `kestrelos-alpr-${randomUUID()}.db`) setDbPathForTest(dbPath) await getDb() }) afterEach(() => { closeDb() setDbPathForTest(null) }) it('returns GeoJSON FeatureCollection from cache', async () => { const db = await getDb() const bbox = { south: 33.4, west: -85.0, north: 33.6, east: -84.0 } await db.run( `INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [99, 33.5, -84.5, 'Acme', 180, '{}', new Date().toISOString()], ) await markTilesFetched(db, tileKeysForBbox(bbox)) vi.stubGlobal('fetch', vi.fn()) const result = await getAlprCameras(db, bbox) expect(result.type).toBe('FeatureCollection') expect(result.features).toHaveLength(1) expect(result.source).toBe('cache') vi.unstubAllGlobals() }) it('falls back to cache when Overpass fails', async () => { const db = await getDb() await db.run( `INSERT INTO alpr_nodes (osm_id, lat, lng, manufacturer, direction, tags, fetched_at) VALUES (?, ?, ?, ?, ?, ?, ?)`, [42, 33.5, -84.5, null, null, '{}', new Date().toISOString()], ) vi.stubGlobal('fetch', vi.fn().mockRejectedValue(new Error('network down'))) const result = await getAlprCameras(db, { south: 33.4, west: -85.0, north: 33.6, east: -84.0 }) expect(result.type).toBe('FeatureCollection') expect(result.features).toHaveLength(1) vi.unstubAllGlobals() }) }) })