Files
StarPilot/openpilot/starpilot/galaxy/test_projection_phone_window_http.py
T

148 lines
6.9 KiB
Python

"""Galaxy routes a phone pairing window to Android Auto while it projects; radio and AA IPC are fakes."""
import http.client
import json
from pathlib import Path
import tempfile
import threading
from types import SimpleNamespace
import unittest
from unittest import mock
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
from openpilot.starpilot.galaxy.server import make_server
PROMPT = '0123456789abcdef0123456789abcdef'
PHONE = '11:22:33:44:55:66'
class Params:
def __init__(self, root): self.root, self.enabled = root, True
def get(self, key): return None
def get_param_path(self, key): return str(self.root / key)
def get_bool(self, key): return self.enabled
class Bluetooth:
def __init__(self): self.calls = []
def snapshot(self, **kwargs):
return {'version': 1, 'available': True, 'parked': True, 'powered': True,
'discovering': False, 'devices': [], 'errorCode': None, 'pairing': None}
def request(self, operation, **kwargs):
self.calls.append(operation)
return self.snapshot()
def cancel_session(self, identity): pass
class AA:
def __init__(self):
self.calls, self.window, self.http, self.paired = [], True, None, None
def call(self, command, **kwargs):
assert command == 'phone_window'
self.calls.append(kwargs)
code, data, _ = self.http('/api/starport/source/' + kwargs['source']) # ty: ignore[call-non-callable] # set by the test
assert code == 200 and data == {'valid': True}, 'Source proof was not live'
if not self.window:
return {'handled': False}
pairing = {'address': PHONE, 'state': 'pairing', 'incoming': True, 'name': 'iPhone',
'prompt': {'id': PROMPT, 'kind': 'confirmation', 'value': '123456'}}
return {'handled': True, 'bluetooth': {'version': 1, 'devices': [], 'pairing': pairing, 'windowPaired': self.paired}}
class ProjectionPhoneWindowHttpTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
claim_path = mock.patch('openpilot.starpilot.galaxy_bluetooth.bridge.claim.CLAIM_PATH', Path(temporary.name) / 'claim.json')
claim_path.start()
self.addCleanup(claim_path.stop)
root = Path(temporary.name)
self.params = Params(root)
self.access = GalaxyAccessOwner(root / 'access')
self.assertTrue(self.access.configure('password123', lambda: True))
self.bluetooth, self.starport = Bluetooth(), AA()
self.spotify = SimpleNamespace(prepared=[], adopted=[])
self.spotify.prepare_phone_pairing = lambda: self.spotify.prepared.append(True)
self.spotify.adopt_phone = lambda *args: self.spotify.adopted.append(args)
self.server = make_server(port=0, owner=self.access, parked=lambda: True,
starport_setup=SimpleNamespace(enabled=lambda: self.params.enabled),
starport_client=self.starport, bluetooth=self.bluetooth, spotify_setup=self.spotify)
self.worker = threading.Thread(target=self.server.serve_forever, kwargs={'poll_interval': .01}, daemon=True)
self.worker.start()
self.addCleanup(self.stop)
self.starport.http = self.http
code, _, headers = self.http('/api/auth/login', {'password': 'password123'})
self.assertEqual(code, 200)
self.cookie = headers['Set-Cookie'].split(';', 1)[0]
def stop(self):
self.server.shutdown()
self.worker.join(timeout=2)
self.server.server_close()
def http(self, path, payload=None, cookie=None):
connection = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=4)
headers = {'Cookie': self.cookie if cookie is None and hasattr(self, 'cookie') else cookie or ''}
if payload is not None:
headers.update({'Content-Type': 'application/json', 'Origin': f'http://127.0.0.1:{self.server.server_port}'})
try:
connection.request('POST' if payload is not None else 'GET', path,
body=json.dumps(payload) if payload is not None else None, headers=headers)
response = connection.getresponse()
return response.status, json.loads(response.read()), dict(response.getheaders())
finally:
connection.close()
def action(self, payload):
return self.http('/api/bluetooth/action', payload)
def test_window_opens_answers_and_reports_through_starport_while_projecting(self):
code, result, _ = self.action({'operation': 'accept_pair'})
self.assertEqual(code, 200)
self.assertTrue(result['pairing']['incoming'])
self.assertNotIn('windowPaired', result)
self.assertEqual(self.bluetooth.calls, [])
self.assertEqual(self.spotify.prepared, [True])
code, status, _ = self.http('/api/bluetooth/status')
self.assertEqual((code, status['pairing']['prompt']['value']), (200, '123456'))
self.starport.paired = {'address': PHONE, 'name': 'iPhone', 'uuids': ['0000110a-0000-1000-8000-00805f9b34fb']}
code, _, _ = self.action({'operation': 'pairing_response', 'promptId': PROMPT, 'accepted': True})
self.assertEqual(code, 200)
self.assertEqual(self.spotify.adopted, [(PHONE, 'iPhone', ('0000110a-0000-1000-8000-00805f9b34fb',))])
sources = {call['source'] for call in self.starport.calls}
self.assertEqual(len(sources), 1, 'every window request must reuse one source')
self.assertEqual(self.starport.calls[2]['prompt_id'], PROMPT)
self.assertEqual(self.bluetooth.calls, [])
def test_without_projection_the_comma_owner_opens_the_window(self):
self.starport.window = False
code, _, _ = self.action({'operation': 'accept_pair'})
self.assertEqual(code, 200)
self.assertEqual(self.bluetooth.calls, ['accept_pair'])
self.starport.calls.clear()
self.http('/api/bluetooth/status')
self.assertEqual(self.starport.calls, [], 'an unhandled window must not keep a source alive')
def test_starport_off_never_contacts_the_daemon(self):
self.params.enabled = False
self.assertEqual(self.action({'operation': 'accept_pair'})[0], 200)
self.assertEqual(self.starport.calls, [])
self.assertEqual(self.bluetooth.calls, ['accept_pair'])
def test_accessory_scan_pair_prompt_and_cancel_are_delegated(self):
for operation in ('scan', 'stop_scan'):
self.assertEqual(self.action({'operation': operation})[0], 200)
self.assertNotEqual(self.http('/api/starport/source/' + self.starport.calls[-1]['source'])[1], {'valid': True})
self.assertEqual(self.action({'operation': 'pair', 'address': PHONE})[0], 200)
source = self.starport.calls[-1]['source']
self.assertEqual(self.starport.calls[-1]['address'], PHONE)
self.assertEqual(self.http('/api/bluetooth/status')[0], 200)
self.assertEqual(self.action({'operation': 'pairing_response', 'promptId': PROMPT, 'accepted': True})[0], 200)
self.assertEqual(self.action({'operation': 'cancel_pair'})[0], 200)
self.assertEqual({c['source'] for c in self.starport.calls[-4:]}, {source})
self.assertEqual(self.bluetooth.calls, [])
self.assertEqual(self.spotify.prepared, [])
if __name__ == '__main__':
unittest.main()