"""ONVIF contract tests against synthetic devices; no real camera is modified.""" import copy import unittest from unittest.mock import patch from xml.etree import ElementTree as ET import camera_settings as cs import onvif def xml(inner): return ET.fromstring(''+inner+'') class SettingsTests(unittest.TestCase): def setUp(self): self.controller=cs.Settings() self.camera=dict(host='192.168.0.2',username='test',password='private',sourceKind='direct',enabled=True, mainPath='/stream?profile=lens2',subPath='/sub?profile=lens2') self.encoder=xml('H264' '192010803' '152048' '60Main' '1PT5S')[0] self.info=dict(device=(80,'/onvif/device_service'),services={'Media':(80,'/media'),'Imaging':(80,'/imaging')}, source='source2',profile='lens2',encoder=self.encoder) self.imaging=xml('50AUTO' 'MANUAL') self.image_options=xml('0100' 'AUTOON') self.video_options=xml('05' '115' '19201080' '2562048') self.calls=[] def call(self,camera,endpoint,ns,operation,body=''): self.calls.append((operation,body)) if operation=='GetDeviceInformation':return xml('') if operation=='GetImagingSettings':return copy.deepcopy(self.imaging) if operation=='GetOptions':return self.image_options if operation=='GetVideoEncoderConfigurationOptions':return self.video_options if operation=='SetImagingSettings': root=xml(body.replace('xmlns:', 'xmlns:')) self.imaging=xml(ET.tostring(root.find(cs.I+'ImagingSettings'),encoding='unicode')) return xml('') return xml('') def test_device_ranges_and_vendor_codec_are_exposed_without_credentials(self): with patch.object(self.controller,'call',self.call):data=self.controller.details(self.camera,self.info) self.assertEqual(data['imaging']['brightness']['max'],100) self.assertEqual(data['imaging']['dayNight']['choices'],['AUTO','ON']) self.assertEqual(data['video']['codec'],'H265') self.assertEqual(data['video']['options']['bitrate']['max'],2048) self.assertNotIn('private',str(data)) self.assertNotIn('sharpness',data['imaging']) def test_rejects_bad_values_and_unsupported_controls(self): rule={'brightness':dict(min=0,max=100)} for value in [True,101,-1,float('nan'),float('inf'),'50']: with self.assertRaises(ValueError):self.controller.validate({'brightness':value},rule) for values in [{},{'unknown':1},None,[]]: with self.assertRaises(ValueError):self.controller.validate(values,rule) with self.assertRaises(ValueError):self.controller.validate({'fps':1.5},{'fps':dict(min=1,max=15)},('fps',)) with self.assertRaises(ValueError):self.controller.validate({'mode':'OFF'},{'mode':dict(choices=['AUTO'])}) def test_imaging_write_preserves_other_fields_and_verifies_readback(self): with patch.object(self.controller,'discover',return_value=self.info),patch.object(self.controller,'call',self.call): data,_=self.controller.read(self.camera) result=self.controller.save(self.camera,dict(revision=data['revision'],section='imaging',values={'brightness':60},quality='main')) self.assertTrue(result['verified']) sent=next(body for op,body in self.calls if op=='SetImagingSettings') self.assertIn('MANUAL',sent);self.assertIn('source2',sent);self.assertIn('ForcePersistence',sent) def test_stale_revision_never_issues_device_write(self): with patch.object(self.controller,'discover',return_value=self.info),patch.object(self.controller,'call',self.call): with self.assertRaises(ValueError):self.controller.save(self.camera,dict(revision='old',section='imaging',values={'brightness':60})) self.assertFalse(any(op.startswith('Set') for op,_ in self.calls)) def test_video_write_retains_codec_extension_multicast_and_other_settings(self): with patch.object(self.controller,'discover',return_value=self.info),patch.object(self.controller,'call',self.call): data,_=self.controller.read(self.camera) result=self.controller.save(self.camera,dict(revision=data['revision'],section='video',values={'fps':10})) sent=next(body for op,body in self.calls if op=='SetVideoEncoderConfiguration') self.assertIn('encoding="H265"',sent);self.assertIn('Multicast',sent);self.assertIn('SessionTimeout',sent) self.assertIn('>10<',sent);self.assertFalse(result['verified']) # Synthetic device deliberately ignores this write. def test_device_timeout_after_save_does_not_falsely_claim_confirmation(self): with patch.object(self.controller,'call',self.call):data=self.controller.details(self.camera,self.info) with patch.object(self.controller,'read',side_effect=[(data,self.info),OSError()]),patch.object(self.controller,'call',return_value=xml('')): result=self.controller.save(self.camera,dict(revision=data['revision'],section='imaging',values={'brightness':60})) self.assertFalse(result['verified']);self.assertIsNone(result['settings']) def test_onvif_requests_do_not_follow_redirects(self): self.assertIsNone(onvif.NoRedirect().redirect_request(None, None, 302, 'Found', {}, 'http://other.example/')) def test_network_endpoints_and_recorder_channels_are_rejected(self): for address in ['http://evil.example/api','https://192.168.0.2/api','http://u:p@192.168.0.2/api','file:///etc/passwd']: with self.assertRaises(ValueError):self.controller.endpoint(self.camera,address) for overrides in [dict(sourceKind='recorder'),dict(enabled=False)]: with self.assertRaises(ValueError):self.controller.discover(dict(self.camera,**overrides),'main') with self.assertRaises(ValueError):self.controller.discover(self.camera,'bad') def test_exact_profile_path_and_query_selects_second_lens(self): selected=[] def call(c,e,ns,op,body=''): if op=='GetCapabilities':return xml('http://192.168.0.2/media') if op=='GetProfiles':return xml(''.join('source'+str(i)+''+ET.tostring(self.encoder,encoding='unicode')+'' for i in [1,2])) if op=='GetStreamUri': token='lens2' if 'lens2' in body else 'lens1';selected.append(token) return xml('rtsp://192.168.0.2/stream?profile='+token+'') with patch.object(self.controller,'call',call):info=self.controller.discover(self.camera,'main') self.assertEqual(info['source'],'source2');self.assertEqual(selected,['lens1','lens2']) def test_failed_options_disable_writes_but_keep_other_section_readable(self): def call(*args): if args[3]=='GetVideoEncoderConfigurationOptions':raise OSError() return self.call(*args) with patch.object(self.controller,'call',call):data=self.controller.details(self.camera,self.info) self.assertEqual(data['video']['options'],{});self.assertIn('brightness',data['imaging']);self.assertTrue(data['notes']) if __name__=='__main__':unittest.main()