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

265 lines
13 KiB
Python

from unittest.mock import Mock
"""Real Galaxy HTTP/auth and file-owner regressions; radio/AA IPC replaced by fakes."""
import copy
import http.client
import json
from pathlib import Path
import tempfile
import threading
from types import SimpleNamespace
import unittest
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
from openpilot.starpilot.galaxy.projection_layout import ProjectionLayoutOwner
from openpilot.starpilot.galaxy.server import make_server
from openpilot.starpilot.system.starport.display_profile import record_screen
from openpilot.starpilot.system.starport.projection_layout import ProjectionLayoutSource
from openpilot.starpilot.ui.onroad_customization import default_document
CAR = '02:00:00:00:00:01'
SCREEN = {'version': 1, 'width': 1280, 'height': 720, 'margin_width': 0,
'margin_height': 240, 'fps': 60, 'config_index': 0}
class Params:
def __init__(self, root): self.root, self.enabled = root, True
def get(self, key):
assert key == 'JetlinkMode'
return 0
def get_param_path(self, key): return str(self.root / key)
def get_bool(self, key):
assert key == 'StarPortEnabled'
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, kwargs))
return self.snapshot()
def cancel_session(self, identity): pass
class AA:
def __init__(self):
self.calls = []
self.result: dict = {'handled': False}
self.error = None
self.http = None
def call(self, command, **kwargs):
self.calls.append((command, kwargs))
assert command in ('bluetooth_action', 'bluetooth_power')
assert set(kwargs) == ({'source', 'operation', 'address'} if command == 'bluetooth_action' else {'source', 'enabled'})
assert self.http is not None
code, data, _ = self.http('/api/starport/source/' + kwargs['source'])
assert code == 200 and data == {'valid': True}, 'Real minted source proof was not live'
if self.error:
raise self.error
return copy.deepcopy(self.result)
class ProjectionBluetoothHttpTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
self.root = Path(temporary.name)
self.params = Params(self.root)
self.native = self.root / 'OnroadCustomizations'
self.native.write_text(json.dumps(default_document()))
self.native_before = self.native.read_bytes()
self.screen = self.root / 'screen.json'
record_screen(SCREEN, self.screen)
self.layout_source = ProjectionLayoutSource(self.root / 'projection/document.json')
self.parked = True
self.layout = ProjectionLayoutOwner(self.params, lambda: self.parked, self.layout_source, self.screen)
self.access = GalaxyAccessOwner(self.root / 'access')
self.assertTrue(self.access.configure('password123', lambda: True))
self.bluetooth, self.starport = Bluetooth(), AA()
self.native_layout = SimpleNamespace(snapshot=lambda: {'document': default_document(), 'editable': True, 'revision': 'native'})
self.server = make_server(port=0, owner=self.access, projection_layout=self.layout, layouts=self.native_layout,
starport_setup=SimpleNamespace(enabled=lambda: self.params.enabled),
starport_client=self.starport, bluetooth=self.bluetooth)
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=''):
connection = http.client.HTTPConnection('127.0.0.1', self.server.server_port, timeout=4)
headers = {'Cookie': cookie}
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 layout_payload(self):
code, value, _ = self.http('/api/starport/layout', cookie=self.cookie)
self.assertEqual(code, 200)
return {'revision': value['revision'], 'document': copy.deepcopy(value['document'])}
def action(self, operation='connect', cookie=None):
return self.http('/api/bluetooth/action', {'operation': operation, 'address': CAR},
self.cookie if cookie is None else cookie)
def power(self, enabled=False):
return self.http('/api/bluetooth/action', {'operation': 'power', 'enabled': enabled}, self.cookie)
def test_power_goes_to_the_daemon_holding_the_adapter_and_falls_back_without_it(self):
snapshot = self.bluetooth.snapshot()
snapshot['powered'] = False
self.starport.result = {'handled': True, 'bluetooth': snapshot}
code, result, _ = self.power(False)
self.assertEqual((code, result), (200, snapshot))
self.assertEqual(self.bluetooth.calls, [])
command, arguments = self.starport.calls[-1]
self.assertEqual((command, arguments['enabled']), ('bluetooth_power', False))
self.assertEqual(self.http('/api/starport/source/' + arguments['source'])[0], 410)
self.starport.result = {'handled': False}
self.assertEqual(self.power(False)[0], 200)
self.assertEqual(self.bluetooth.calls[-1][0], 'power')
self.starport.error = OSError('daemon is not running')
self.assertEqual(self.power(True)[0], 200)
self.assertEqual([call[0] for call in self.bluetooth.calls], ['power', 'power'])
self.params.enabled = False
calls = len(self.starport.calls)
self.assertEqual(self.power(False)[0], 200)
self.assertEqual(len(self.starport.calls), calls, 'Android Auto off: the daemon is never contacted')
def test_power_refusals_from_the_daemon_never_fall_back(self):
for error, code in [('projection_active', 409), ('busy', 409), ('park_required', 409), ('service_unavailable', 503)]:
self.starport.result = {'handled': True, 'error_code': error}
actual, result, _ = self.power(False)
self.assertEqual((actual, result['code']), (code, error))
self.assertEqual(self.bluetooth.calls, [])
def test_layout_requires_actual_auth_then_saves_only_projection_document(self):
self.assertEqual(self.http('/api/starport/layout')[0], 401)
payload = self.layout_payload()
payload['document']['widgets']['current_speed']['x'] += 10
self.assertEqual(self.http('/api/starport/layout', payload)[0], 401)
code, result, _ = self.http('/api/starport/layout', payload, self.cookie)
self.assertEqual(code, 200)
self.assertTrue(result['valid'])
self.assertEqual(json.loads(self.layout_source.path.read_bytes()), payload['document'])
self.assertEqual(self.native.read_bytes(), self.native_before)
def test_layout_stale_screen_revision_and_unpark_do_not_replace_saved_document(self):
payload = self.layout_payload()
self.assertEqual(self.http('/api/starport/layout', payload, self.cookie)[0], 200)
before = self.layout_source.path.read_bytes()
stale = self.layout_payload()
record_screen(SCREEN | {'config_index': 1}, self.screen)
self.assertEqual(self.http('/api/starport/layout', stale, self.cookie)[0], 409)
fresh = self.layout_payload()
self.parked = False
self.assertEqual(self.http('/api/starport/layout', fresh, self.cookie)[0], 409)
self.assertEqual(self.layout_source.path.read_bytes(), before)
self.assertEqual(self.native.read_bytes(), self.native_before)
def test_logged_out_session_cannot_save_layout_or_delegate_bluetooth(self):
payload = self.layout_payload()
self.assertEqual(self.http('/api/auth/logout', {}, self.cookie)[0], 200)
self.assertEqual(self.http('/api/starport/layout', payload, self.cookie)[0], 401)
self.assertEqual(self.action()[0], 401)
self.assertEqual(self.starport.calls, [])
self.assertEqual(self.bluetooth.calls, [])
self.assertFalse(self.layout_source.path.exists())
self.assertEqual(self.native.read_bytes(), self.native_before)
def test_active_starport_handled_snapshot_never_calls_generic_controller(self):
snapshot = self.bluetooth.snapshot()
snapshot['devices'] = [{'address': CAR, 'name': 'IONIQ 6',
'paired': True, 'trusted': True, 'connected': True}]
self.starport.result = {'handled': True, 'bluetooth': snapshot}
code, result, _ = self.action()
self.assertEqual(code, 200)
self.assertEqual(result, snapshot)
self.assertEqual(self.bluetooth.calls, [])
command, arguments = self.starport.calls[0]
self.assertEqual((command, arguments['operation'], arguments['address']), ('bluetooth_action', 'connect', CAR))
self.assertEqual(self.http('/api/starport/source/' + arguments['source'])[0], 410)
def test_no_starport_lease_falls_back_once_and_disabled_starport_does_not_contact_daemon(self):
self.assertEqual(self.action()[0], 200)
self.assertEqual(len(self.starport.calls), 1)
self.assertEqual(len(self.bluetooth.calls), 1)
self.params.enabled = False
self.assertEqual(self.action('disconnect')[0], 200)
self.assertEqual(len(self.starport.calls), 1)
self.assertEqual(len(self.bluetooth.calls), 2)
self.assertEqual(self.bluetooth.calls[-1][0], 'disconnect')
def test_forget_is_delegated_to_a_running_projection_and_falls_back_without_the_daemon(self):
snapshot = self.bluetooth.snapshot()
self.starport.result = {'handled': True, 'bluetooth': snapshot}
code, result, _ = self.action('forget')
self.assertEqual((code, result), (200, snapshot))
self.assertEqual(self.starport.calls[0][1]['operation'], 'forget')
self.assertEqual(self.bluetooth.calls, [])
self.starport.result = {'handled': False}
self.assertEqual(self.action('forget')[0], 200)
self.assertEqual(self.bluetooth.calls[-1][0], 'forget')
self.starport.error = OSError('daemon is not running')
self.assertEqual(self.action('forget')[0], 200)
self.assertEqual([call[0] for call in self.bluetooth.calls], ['forget', 'forget'])
self.assertEqual(self.action('connect')[0], 503)
def test_owner_busy_and_error_codes_never_fall_back(self):
for error, code in [('busy', 409), ('park_required', 409), ('changed', 409), ('service_unavailable', 503)]:
self.starport.result = {'handled': True, 'error_code': error}
actual, result, _ = self.action()
self.assertEqual(actual, code)
self.assertEqual(result['code'], error)
self.assertEqual(self.bluetooth.calls, [])
def test_unknown_or_malformed_delegated_error_fails_closed_without_fallback(self):
for error in ('unexpected_internal_error', '', None, 4, {'private': 'detail'}):
self.starport.result = {'handled': True, 'error_code': error}
code, result, _ = self.action()
self.assertEqual(code, 503)
self.assertEqual(result['code'], 'service_unavailable')
self.assertNotIn('unexpected_internal_error', json.dumps(result))
self.assertEqual(self.bluetooth.calls, [])
def test_native_layout_optional_projection_failure_preserves_native_snapshot(self):
self.assertEqual(self.http('/api/ui/layout')[0], 401)
for error in (OSError('saved screen unavailable'), ValueError('invalid screen'), RuntimeError('projection owner unavailable')):
def unavailable(error=error):
raise error
self.layout.snapshot = Mock(side_effect=unavailable)
code, result, _ = self.http('/api/ui/layout', cookie=self.cookie)
self.assertEqual(code, 200)
self.assertFalse(result['projectionAvailable'])
self.assertEqual(result['document'], default_document())
self.assertEqual(result['revision'], 'native')
self.assertEqual(self.native.read_bytes(), self.native_before)
self.assertEqual(self.starport.calls, [])
def test_daemon_transport_failure_and_malformed_reply_never_fall_back(self):
self.starport.error = TimeoutError('private IPC failure')
code, result, _ = self.action()
self.assertEqual(code, 503)
self.assertEqual(result['code'], 'service_unavailable')
self.assertNotIn('private', json.dumps(result))
self.assertEqual(self.bluetooth.calls, [])
self.starport.error = None
for malformed in ({}, {'handled': 'yes'}, {'handled': True}, {'handled': True, 'bluetooth': []}):
self.starport.result = malformed
self.assertEqual(self.action()[0], 503)
self.assertEqual(self.bluetooth.calls, [])
if __name__ == '__main__':
unittest.main()