import importlib.util from pathlib import Path import sys import unittest from unittest.mock import patch sys.path.insert(0,str(Path(__file__).parents[1]/'scripts')) import input_controls as a import web_server as w class InputTests(unittest.TestCase): def test_bus_property_scalar_and_method_tuple(self): with patch.object(a,'command',return_value='{"type":"d","data":-15.0}'): self.assertEqual(a.bus('get-property','service','path','interface','Volume'),-15.0) with patch.object(a,'command',return_value='{"type":"a{sv}","data":[{"Volume":-15.0}]}'): self.assertEqual(a.bus('call','service','path','interface','GetAll'),{'Volume':-15.0}) def test_headphone_volume_write_is_rejected(self): props={'Device':'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF','Mode':'sink','Transport':'A2DP-source','Volume':0x7f7f} with patch.object(a,'pcms',return_value={'/output':props}), patch.object(a,'command') as command: with self.assertRaises(ValueError):a.set_input('AA:BB:CC:DD:EE:FF',volume=20) command.assert_not_called() def test_bluetooth_only_writes_input_pcm(self): source={'Device':'/org/bluez/hci0/dev_AA_BB_CC_DD_EE_FF','Mode':'source','Transport':'A2DP-sink','Volume':0x7f7f} sink={**source,'Mode':'sink','Transport':'A2DP-source'} with patch.object(a,'pcms',return_value={'/input':source,'/output':sink}),patch.object(a,'command') as command: a.set_input('AA:BB:CC:DD:EE:FF',volume=40) self.assertEqual(command.call_count,2) self.assertTrue(all('/input' in call.args for call in command.call_args_list)) self.assertTrue(all('/output' not in call.args for call in command.call_args_list)) def test_airplay_uses_existing_native_volume(self): with patch.object(a,'airplay',return_value={'volume':80,'muted':False}),patch.object(a,'remember'),patch.object(a,'command') as command: a.set_input('airplay',volume=50) self.assertEqual(command.call_args.args,('busctl','--system','--','set-property',a.SPS,a.SPS_PATH,a.SPS,'Volume','d','-15.0')) a.set_input('airplay',mute=True) self.assertEqual(command.call_args.args[-1],'-144.0') def test_volume_validation_rejects_outside_range_and_bool(self): for level in (-1,101,True,'50'): with self.subTest(level=level), self.assertRaises(ValueError): a.set_input('airplay',volume=level) def test_packed_bluetooth_stereo_mute(self): value=a.encode_volume(60,True) level,muted=a.decode_volume(value) self.assertEqual(level,60);self.assertTrue(muted) self.assertEqual((value>>8)&255,value&255) class WebTests(unittest.TestCase): def test_dac_selection_is_one_atomic_cli_command(self): self.assertEqual(w.validate_action({'action':'output','mode':'dac','pcm':'hw:CARD=Audio,DEV=0'}), ['output','dac','--pcm','hw:CARD=Audio,DEV=0']) def test_no_output_volume_api(self): for data in [{'action':'output-volume','volume':20},{'action':'volume','input':'dac','volume':20}, {'action':'volume','input':'airplay','volume':101}, {'action':'output','mode':'bluetooth','address':'AA:BB:CC:DD:EE:FF;reboot'}]: with self.subTest(data=data),self.assertRaises(ValueError):w.validate_action(data) def test_http_requires_token_and_same_origin(self): import threading,json,urllib.request,urllib.error server=w.ThreadingHTTPServer(('127.0.0.1',0),w.Handler) thread=threading.Thread(target=server.serve_forever,daemon=True);thread.start() url='http://127.0.0.1:'+str(server.server_address[1])+'/api/action' try: with patch.object(w,'allowed_host',return_value=True),patch.object(w,'launch',return_value='mockjob'): body=json.dumps({'action':'scan'}).encode() for headers in [{'Content-Type':'application/json'}, {'Content-Type':'application/json','X-Mixer-Token':w.TOKEN,'Origin':'http://other.site'}]: req=urllib.request.Request(url,body,headers=headers) with self.assertRaises(urllib.error.HTTPError) as error:urllib.request.urlopen(req,timeout=5) self.assertEqual(error.exception.code,403);error.exception.close() req=urllib.request.Request(url,body,headers={'Content-Type':'application/json','X-Mixer-Token':w.TOKEN}) with urllib.request.urlopen(req,timeout=5) as response:self.assertEqual(response.status,202) finally:server.shutdown();server.server_close();thread.join() if __name__=='__main__':unittest.main()