import { describe, it, expect } from 'vitest' import { validateDevice, validateUpdateDevice, validateUser, validateUpdateUser, validatePoi, validateUpdatePoi, } from '../../server/utils/validation.js' describe('validation', () => { describe('validateDevice', () => { it('validates valid device data', () => { const result = validateDevice({ name: 'Test Device', device_type: 'traffic', lat: 40.7128, lng: -74.0060, stream_url: 'https://example.com/stream', source_type: 'mjpeg', }) expect(result.valid).toBe(true) expect(result.data.device_type).toBe('traffic') }) it.each([ [{ name: 'Test', lat: 'invalid', lng: -74.0060 }, 'lat and lng required as finite numbers'], [null, 'body required'], ])('rejects invalid input: %j', (input, errorMsg) => { const result = validateDevice(input) expect(result.valid).toBe(false) expect(result.errors).toContain(errorMsg) }) it('defaults device_type to feed', () => { const result = validateDevice({ name: 'Test', lat: 40.7128, lng: -74.0060 }) expect(result.valid).toBe(true) expect(result.data.device_type).toBe('feed') }) it('defaults stream_url to empty string', () => { const result = validateDevice({ name: 'Test', lat: 40.7128, lng: -74.0060 }) expect(result.valid).toBe(true) expect(result.data.stream_url).toBe('') }) it('defaults invalid source_type to mjpeg', () => { const result = validateDevice({ name: 'Test', lat: 40.7128, lng: -74.0060, source_type: 'invalid', }) expect(result.valid).toBe(true) expect(result.data.source_type).toBe('mjpeg') }) it.each([ [{ name: 'Test', lat: 40.7128, lng: -74.0060 }, null], [{ name: 'Test', lat: 40.7128, lng: -74.0060, config: { key: 'value' } }, '{"key":"value"}'], [{ name: 'Test', lat: 40.7128, lng: -74.0060, config: '{"key":"value"}' }, '{"key":"value"}'], [{ name: 'Test', lat: 40.7128, lng: -74.0060, config: null }, null], ])('handles config: %j -> %s', (input, expected) => { const result = validateDevice(input) expect(result.valid).toBe(true) expect(result.data.config).toBe(expected) }) it('defaults vendor to null', () => { const result = validateDevice({ name: 'Test', lat: 40.7128, lng: -74.0060 }) expect(result.valid).toBe(true) expect(result.data.vendor).toBeNull() }) }) describe('validateUpdateDevice', () => { it('validates partial updates', () => { const result = validateUpdateDevice({ name: 'Updated', lat: 40.7128 }) expect(result.valid).toBe(true) expect(result.data).toMatchObject({ name: 'Updated', lat: 40.7128 }) }) it('allows empty updates', () => { const result = validateUpdateDevice({}) expect(result.valid).toBe(true) expect(Object.keys(result.data).length).toBe(0) }) it.each([ [{ device_type: 'invalid' }, 'Invalid device_type'], ])('rejects invalid input: %j', (input, errorMsg) => { const result = validateUpdateDevice(input) expect(result.valid).toBe(false) expect(result.errors).toContain(errorMsg) }) it.each([ [{ name: 'Test' }, undefined], [{ device_type: 'traffic' }, 'traffic'], ])('handles device_type: %j -> %s', (input, expected) => { const result = validateUpdateDevice(input) expect(result.valid).toBe(true) expect(result.data.device_type).toBe(expected) }) it.each([ [{ vendor: null }, null], [{ vendor: '' }, null], [{ vendor: 'Test Vendor' }, 'Test Vendor'], ])('handles vendor: %j -> %s', (input, expected) => { const result = validateUpdateDevice(input) expect(result.valid).toBe(true) expect(result.data.vendor).toBe(expected) }) it.each([ [{ config: { key: 'value' } }, '{"key":"value"}'], [{ config: '{"key":"value"}' }, '{"key":"value"}'], [{ config: null }, null], [{ config: undefined }, undefined], [{ name: 'Test' }, undefined], ])('handles config: %j', (input, expected) => { const result = validateUpdateDevice(input) expect(result.valid).toBe(true) expect(result.data.config).toBe(expected) }) it('handles all field types', () => { const result = validateUpdateDevice({ name: 'Test', device_type: 'traffic', vendor: 'Vendor', lat: 40.7128, lng: -74.0060, stream_url: 'https://example.com', source_type: 'hls', config: { key: 'value' }, }) expect(result.valid).toBe(true) expect(result.data).toMatchObject({ name: 'Test', device_type: 'traffic', vendor: 'Vendor', lat: 40.7128, lng: -74.0060, stream_url: 'https://example.com', source_type: 'hls', config: '{"key":"value"}', }) }) it.each([ ['source_type'], ['lat'], ['lng'], ['stream_url'], ])('handles %s undefined in updates', (field) => { const result = validateUpdateDevice({ name: 'Test' }) expect(result.valid).toBe(true) expect(result.data[field]).toBeUndefined() }) }) describe('validateUser', () => { it('validates valid user data', () => { const result = validateUser({ identifier: 'testuser', password: 'password123', role: 'admin', }) expect(result.valid).toBe(true) expect(result.data.identifier).toBe('testuser') }) it.each([ [{ password: 'password123', role: 'admin' }, 'identifier required'], [{ identifier: 'testuser', password: 'password123', role: 'invalid' }, 'role must be admin, leader, or member'], ])('rejects invalid input: %j', (input, errorMsg) => { const result = validateUser(input) expect(result.valid).toBe(false) expect(result.errors).toContain(errorMsg) }) }) describe('validateUpdateUser', () => { it('validates partial updates', () => { const result = validateUpdateUser({ role: 'leader' }) expect(result.valid).toBe(true) expect(result.data.role).toBe('leader') }) it('rejects empty identifier', () => { const result = validateUpdateUser({ identifier: '' }) expect(result.valid).toBe(false) expect(result.errors).toContain('identifier cannot be empty') }) it.each([ [{ password: '' }, undefined], [{ password: undefined }, undefined], [{ password: 'newpassword' }, 'newpassword'], ])('handles password: %j -> %s', (input, expected) => { const result = validateUpdateUser(input) expect(result.valid).toBe(true) expect(result.data.password).toBe(expected) }) it.each([ ['role'], ['identifier'], ['password'], ])('handles %s undefined', (field) => { const result = validateUpdateUser({}) expect(result.valid).toBe(true) expect(result.data[field]).toBeUndefined() }) }) describe('validatePoi', () => { it('validates valid POI data', () => { const result = validatePoi({ lat: 40.7128, lng: -74.0060, label: 'Test POI', iconType: 'flag', }) expect(result.valid).toBe(true) expect(result.data).toMatchObject({ lat: 40.7128, lng: -74.0060, label: 'Test POI', icon_type: 'flag', }) }) it('rejects invalid coordinates', () => { const result = validatePoi({ lat: 'invalid', lng: -74.0060 }) expect(result.valid).toBe(false) expect(result.errors).toContain('lat and lng required as finite numbers') }) it.each([ [{ lat: 40.7128, lng: -74.0060 }, 'pin'], [{ lat: 40.7128, lng: -74.0060, iconType: 'invalid' }, 'pin'], ])('defaults iconType to pin: %j -> %s', (input, expected) => { const result = validatePoi(input) expect(result.valid).toBe(true) expect(result.data.icon_type).toBe(expected) }) }) describe('validateUpdatePoi', () => { it('validates partial updates', () => { const result = validateUpdatePoi({ label: 'Updated', lat: 40.7128 }) expect(result.valid).toBe(true) expect(result.data).toMatchObject({ label: 'Updated', lat: 40.7128 }) }) it('allows empty updates', () => { const result = validateUpdatePoi({}) expect(result.valid).toBe(true) expect(Object.keys(result.data).length).toBe(0) }) it.each([ [{ iconType: 'invalid' }, 'Invalid iconType'], [{ lat: 'invalid' }, 'lat must be a finite number'], [{ lng: 'invalid' }, 'lng must be a finite number'], ])('rejects invalid input: %j', (input, errorMsg) => { const result = validateUpdatePoi(input) expect(result.valid).toBe(false) expect(result.errors).toContain(errorMsg) }) it('handles all field types', () => { const result = validateUpdatePoi({ label: 'Updated', iconType: 'waypoint', lat: 41.7128, lng: -75.0060, }) expect(result.valid).toBe(true) expect(result.data).toMatchObject({ label: 'Updated', icon_type: 'waypoint', lat: 41.7128, lng: -75.0060, }) }) it.each([ ['label'], ['icon_type'], ['lat'], ['lng'], ])('handles %s undefined', (field) => { const result = validateUpdatePoi({}) expect(result.valid).toBe(true) expect(result.data[field]).toBeUndefined() }) }) })