import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import { rowToDevice, sanitizeDeviceForResponse, validateDeviceBody, DEVICE_TYPES, SOURCE_TYPES } from '../../server/utils/deviceUtils.js' describe('deviceUtils', () => { describe('rowToDevice', () => { it('returns device row for valid db row', () => { const row = { id: 'd1', name: 'Cam 1', device_type: 'feed', vendor: null, lat: 37.7, lng: -122.4, stream_url: 'https://example.com/mjpeg', source_type: 'mjpeg', config: null, } expect(rowToDevice(row)).toEqual({ ...row, lat: 37.7, lng: -122.4 }) }) it('normalizes source_type to mjpeg when invalid', () => { const row = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: '', source_type: 'invalid', config: null } expect(rowToDevice(row)?.source_type).toBe('mjpeg') }) it('accepts hls source_type', () => { const row = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: '', source_type: 'hls', config: null } expect(rowToDevice(row)?.source_type).toBe('hls') }) it('returns null for null row', () => { expect(rowToDevice(null)).toBe(null) }) it('returns null for row missing id', () => { expect(rowToDevice({ name: 'x', device_type: 'feed', lat: 0, lng: 0 })).toBe(null) }) it('returns null for invalid lat/lng', () => { expect(rowToDevice({ id: 'd1', name: 'x', device_type: 'feed', lat: Number.NaN, lng: 0, stream_url: '', source_type: 'mjpeg' })).toBe(null) }) it('coerces string lat/lng to numbers', () => { const row = { id: 'd1', name: 'x', device_type: 'feed', lat: '37.5', lng: '-122.0', stream_url: '', source_type: 'mjpeg', config: null } const out = rowToDevice(row) expect(out?.lat).toBe(37.5) expect(out?.lng).toBe(-122) }) it('coerces non-string vendor, stream_url, config to null or empty', () => { const row = { id: 'd1', name: 'x', device_type: 'feed', vendor: 123, lat: 0, lng: 0, stream_url: null, source_type: 'mjpeg', config: 456, } const out = rowToDevice(row) expect(out?.vendor).toBe(null) expect(out?.stream_url).toBe('') expect(out?.config).toBe(null) }) }) describe('sanitizeDeviceForResponse', () => { it('returns camelCase streamUrl and sourceType', () => { const device = { id: 'd1', name: 'Cam', device_type: 'traffic', vendor: null, lat: 1, lng: 2, stream_url: 'https://example.com/stream', source_type: 'mjpeg', config: null, } const out = sanitizeDeviceForResponse(device) expect(out.streamUrl).toBe('https://example.com/stream') expect(out.sourceType).toBe('mjpeg') expect(out).not.toHaveProperty('stream_url') }) it('sanitizes stream_url to empty for javascript:', () => { const device = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: 'javascript:alert(1)', source_type: 'mjpeg', config: null, } expect(sanitizeDeviceForResponse(device).streamUrl).toBe('') }) it('sanitizes stream_url to empty when not http(s)', () => { const device = { id: 'd1', name: 'x', device_type: 'feed', vendor: null, lat: 0, lng: 0, stream_url: 'ftp://example.com', source_type: 'mjpeg', config: null, } expect(sanitizeDeviceForResponse(device).streamUrl).toBe('') }) }) describe('validateDeviceBody', () => { beforeEach(() => { vi.stubGlobal('createError', (opts) => { const err = new Error(opts?.message || 'Error') err.statusCode = opts?.statusCode throw err }) }) afterEach(() => { vi.unstubAllGlobals() }) it('returns normalized body for valid input', () => { const body = { name: ' My Cam ', device_type: 'alpr', lat: 37, lng: -122, stream_url: 'https://x.com/s', source_type: 'hls', } const out = validateDeviceBody(body) expect(out.name).toBe('My Cam') expect(out.device_type).toBe('alpr') expect(out.lat).toBe(37) expect(out.lng).toBe(-122) expect(out.stream_url).toBe('https://x.com/s') expect(out.source_type).toBe('hls') expect(out.vendor).toBe(null) expect(out.config).toBe(null) }) it('defaults device_type to feed when invalid', () => { const out = validateDeviceBody({ lat: 0, lng: 0, device_type: 'invalid' }) expect(out.device_type).toBe('feed') }) it('defaults device_type to feed when not a string', () => { const out = validateDeviceBody({ lat: 0, lng: 0, device_type: 99 }) expect(out.device_type).toBe('feed') }) it('throws when lat/lng missing or non-finite', () => { expect(() => validateDeviceBody({})).toThrow() expect(() => validateDeviceBody({ lat: 0, lng: Number.NaN })).toThrow() }) it('accepts vendor string and config object', () => { const out = validateDeviceBody({ name: 'Cam', lat: 0, lng: 0, vendor: ' Acme Corp ', config: { resolution: '1080p' }, }) expect(out.vendor).toBe('Acme Corp') expect(out.config).toBe('{"resolution":"1080p"}') }) }) describe('constants', () => { it('DEVICE_TYPES includes expected types', () => { expect(DEVICE_TYPES).toContain('alpr') expect(DEVICE_TYPES).toContain('feed') expect(DEVICE_TYPES).toContain('traffic') expect(DEVICE_TYPES).toContain('ip') expect(DEVICE_TYPES).toContain('drone') }) it('SOURCE_TYPES includes mjpeg and hls', () => { expect(SOURCE_TYPES).toEqual(['mjpeg', 'hls']) }) }) })