mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 23:43:44 +08:00
a531afd02a
Expose pairing readiness and coordinate adapter hand-offs with Android Auto. Show app pairings separately, support individual removal, and improve pairing controls on both comma screens. Add Galaxy companion endpoints and web asset manifests, preserve the plain camera look, and fix headless speed-limit shapes.
498 lines
26 KiB
Python
498 lines
26 KiB
Python
from typing import NotRequired, TypedDict
|
|
from unittest.mock import Mock
|
|
import http.client
|
|
import json
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
|
|
from openpilot.starpilot.galaxy.android_auto_setup import AndroidAutoSetup
|
|
from openpilot.starpilot.galaxy.server import make_server
|
|
from openpilot.starpilot.system.android_auto.source_verifier import GalaxySourceVerifier
|
|
from openpilot.starpilot.bluetooth.owner import BluetoothUnavailable
|
|
from openpilot.starpilot.system.android_auto import apkpure
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def offline_download(monkeypatch):
|
|
monkeypatch.setattr(apkpure, 'start_background_refresh', lambda: None)
|
|
monkeypatch.setattr(apkpure, '_cache', None)
|
|
|
|
|
|
class ApiState(TypedDict):
|
|
parked: bool
|
|
identity_installed: bool
|
|
bluetooth_enabled: bool
|
|
enabled: bool
|
|
install_ready: bool
|
|
enable_events: list[str]
|
|
bluetooth_failure: NotRequired[str]
|
|
setup_owner: NotRequired[AndroidAutoSetup]
|
|
|
|
|
|
class FakeImport:
|
|
def __init__(self, root):
|
|
self.work_dir = root
|
|
self.calls = []
|
|
self.running = False
|
|
def busy(self): return self.running
|
|
def status(self): return {'state': 'running' if self.running else 'idle'}
|
|
def start(self, *, path, enabled, app_version=None):
|
|
self.calls.append((path.read_bytes(), enabled))
|
|
self.running = True
|
|
|
|
|
|
@pytest.fixture
|
|
def api(tmp_path):
|
|
access = GalaxyAccessOwner(tmp_path / 'access')
|
|
assert access.configure('password123', lambda: True)
|
|
state: ApiState = {'parked': True, 'identity_installed': True, 'bluetooth_enabled': True, 'enabled': True,
|
|
'install_ready': True, 'enable_events': []}
|
|
def set_enabled(value):
|
|
state['enable_events'].append('android_auto')
|
|
state['enabled'] = value
|
|
def enable_bluetooth(_session):
|
|
state['enable_events'].append('bluetooth')
|
|
if state.get('bluetooth_failure') == 'unavailable':
|
|
raise BluetoothUnavailable('Radio unavailable')
|
|
if state.get('bluetooth_failure') != 'unverified':
|
|
state['bluetooth_enabled'] = True
|
|
job = FakeImport(tmp_path / 'imports')
|
|
setup = AndroidAutoSetup(parked=lambda: state['parked'], enabled=lambda: state['enabled'], session_valid=lambda _identity: True,
|
|
import_job=Mock(wraps=job, work_dir=job.work_dir), identity_status=lambda: {'installed': state['identity_installed'],
|
|
'message': 'Package status'},
|
|
bluetooth_enabled=lambda: state['bluetooth_enabled'], install_ready=lambda: state['install_ready'],
|
|
service_ready=lambda: True, set_enabled=set_enabled, enable_bluetooth=enable_bluetooth)
|
|
state['setup_owner'] = setup
|
|
class FakeClient:
|
|
def __init__(self):
|
|
self.calls = []
|
|
self.verifier = None
|
|
self.receiver = ''
|
|
self.running = False
|
|
self.auto_connect = False
|
|
self.connection = 'wireless'
|
|
self.available = True
|
|
self.stop_error = False
|
|
self.stop_completes = True
|
|
def call(self, command, **kwargs):
|
|
assert command in ('status', 'stop') or self.verifier.valid(kwargs['source'])
|
|
self.calls.append((command, dict(kwargs)))
|
|
if command == 'status':
|
|
return {'status': {'receiver_address': self.receiver, 'receiver_name': 'My car', 'state': 'streaming' if self.running else 'idle',
|
|
'running': self.running, 'auto_connect': self.auto_connect, 'pairing_ready': False,
|
|
'connection': self.connection}}
|
|
if command == 'devices':
|
|
return {'devices': [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'Saved car', 'paired': True, 'android_auto': True}]}
|
|
if command == 'select_receiver':
|
|
self.receiver = kwargs['address']
|
|
if command == 'set_auto_connect':
|
|
self.auto_connect = kwargs['enabled']
|
|
if command == 'set_connection':
|
|
self.connection = kwargs['connection']
|
|
if command == 'start':
|
|
self.running = True
|
|
if command == 'stop':
|
|
if self.stop_error:
|
|
raise RuntimeError('Disconnect failed')
|
|
if self.stop_completes:
|
|
self.running = False
|
|
return {'pairing': {'active': True, 'receiver': {'address': 'AA:BB:CC:DD:EE:FF', 'name': 'Car'},
|
|
'prompt': {'id': 'a' * 32, 'kind': 'confirmation', 'value': '123456'},
|
|
'approved': False}} if command == 'pairing_status' else {}
|
|
client = FakeClient()
|
|
server = make_server(port=0, owner=access, parked=lambda: state['parked'], android_auto_setup=setup,
|
|
android_auto_client=client)
|
|
client.verifier = GalaxySourceVerifier(port=server.server_port)
|
|
worker = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': 0.01}, daemon=True)
|
|
worker.start()
|
|
def request(method, path, body=None, *, cookie='', content_type='application/octet-stream'):
|
|
conn = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=3)
|
|
headers = {'Cookie': cookie}
|
|
if method != 'GET':
|
|
headers['Origin'] = f'http://127.0.0.1:{server.server_port}'
|
|
if body is not None:
|
|
headers['Content-Type'] = content_type
|
|
try:
|
|
conn.request(method, path, body=body, headers=headers)
|
|
response = conn.getresponse()
|
|
return response.status, json.loads(response.read()), dict(response.getheaders())
|
|
finally:
|
|
conn.close()
|
|
yield request, job, state, client
|
|
server.shutdown()
|
|
worker.join(timeout=2)
|
|
server.server_close()
|
|
|
|
|
|
def test_galaxy_upload_requires_session_and_exact_binary_body(api):
|
|
request, job, state, _ = api
|
|
assert request('GET', '/api/android-auto/setup')[0] == 401
|
|
assert request('POST', '/api/android-auto/upload', b'fake-apk')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, status, _ = request('GET', '/api/android-auto/setup', cookie=cookie)
|
|
assert code == 200 and status['wiredAvailable'] is True
|
|
assert status['download'] is None
|
|
assert request('POST', '/api/android-auto/upload', b'fake-apk', cookie=cookie,
|
|
content_type='application/json')[0] == 415
|
|
assert request('POST', '/api/android-auto/upload', b'fake-apk', cookie=cookie)[0] == 202
|
|
assert job.calls[0][0] == b'fake-apk'
|
|
state['parked'] = False
|
|
assert job.calls[0][1]() is True
|
|
assert request('POST', '/api/android-auto/upload', b'other', cookie=cookie)[0] == 409
|
|
|
|
|
|
@pytest.mark.parametrize('parked', [True, False])
|
|
@pytest.mark.parametrize('enabled', [True, False])
|
|
def test_galaxy_upload_works_in_either_road_state_without_enabling(api, parked, enabled):
|
|
request, job, state, _ = api
|
|
state.update(parked=parked, enabled=enabled)
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, uploaded, _ = request('POST', '/api/android-auto/upload', b'fake-apk', cookie=cookie)
|
|
assert code == 202 and uploaded['import']['state'] == 'running'
|
|
assert uploaded['parked'] is parked and uploaded['enabled'] is enabled
|
|
assert job.calls[0][0] == b'fake-apk' and job.calls[0][1]()
|
|
assert state['enabled'] is enabled
|
|
|
|
|
|
def test_galaxy_rejects_upload_after_logout(api):
|
|
request, job, _, _ = api
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
assert request('POST', '/api/auth/logout', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert request('POST', '/api/android-auto/upload', b'fake-apk', cookie=cookie)[0] == 401
|
|
assert job.calls == []
|
|
|
|
|
|
@pytest.mark.parametrize('parked', [True, False])
|
|
def test_enable_in_either_road_state_requires_session_and_hardware(api, parked):
|
|
request, _, state, _ = api
|
|
state.update(parked=parked, enabled=False)
|
|
def body(enabled):
|
|
return json.dumps({'enabled': enabled})
|
|
assert request('POST', '/api/android-auto/enable', body(True), content_type='application/json')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
assert request('POST', '/api/android-auto/enable', '{"enabled":1}', cookie=cookie,
|
|
content_type='application/json')[0] == 400
|
|
code, result, _ = request('POST', '/api/android-auto/enable', body(True), cookie=cookie, content_type='application/json')
|
|
assert code == 200 and result['enabled'] is True and result['parked'] is parked
|
|
assert state['enabled'] is True
|
|
state['install_ready'] = False
|
|
assert request('POST', '/api/android-auto/enable', body(True), cookie=cookie,
|
|
content_type='application/json')[0] == 409
|
|
assert request('POST', '/api/android-auto/enable', body(False), cookie=cookie,
|
|
content_type='application/json')[1]['enabled'] is False
|
|
state['install_ready'] = True
|
|
assert request('POST', '/api/android-auto/enable', body(True), cookie=cookie,
|
|
content_type='application/json')[1]['enabled'] is True
|
|
assert request('POST', '/api/auth/logout', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert request('POST', '/api/android-auto/enable', body(False), cookie=cookie, content_type='application/json')[0] == 401
|
|
assert state['enabled'] is True
|
|
|
|
|
|
@pytest.mark.parametrize('parked', [True, False])
|
|
def test_confirmed_enable_powers_bluetooth_first_in_either_road_state(api, parked):
|
|
request, _, state, _ = api
|
|
state.update(parked=parked, enabled=False, bluetooth_enabled=False)
|
|
confirmed = json.dumps({'enabled': True, 'enableBluetooth': True})
|
|
assert request('POST', '/api/android-auto/enable', confirmed, content_type='application/json')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}), content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
assert request('POST', '/api/android-auto/enable', '{"enabled":true}', cookie=cookie, content_type='application/json')[0] == 409
|
|
for payload in ({'enabled': True, 'enableBluetooth': 1}, {'enabled': False, 'enableBluetooth': True}):
|
|
assert request('POST', '/api/android-auto/enable', json.dumps(payload), cookie=cookie, content_type='application/json')[0] == 400
|
|
assert state['enable_events'] == [] and not state['enabled'] and not state['bluetooth_enabled']
|
|
state['install_ready'] = False
|
|
assert request('POST', '/api/android-auto/enable', confirmed, cookie=cookie, content_type='application/json')[0] == 409
|
|
assert state['enable_events'] == []
|
|
state['install_ready'] = True
|
|
code, result, _ = request('POST', '/api/android-auto/enable', confirmed, cookie=cookie, content_type='application/json')
|
|
assert code == 200 and result['enabled'] and result['bluetoothEnabled']
|
|
assert result['parked'] is parked and state['enable_events'] == ['bluetooth', 'android_auto']
|
|
|
|
|
|
@pytest.mark.parametrize('failure', ['unavailable', 'unverified'])
|
|
def test_confirmed_enable_does_not_turn_on_android_auto_when_bluetooth_fails(api, failure):
|
|
request, _, state, _ = api
|
|
state.update(parked=False, enabled=False, bluetooth_enabled=False, bluetooth_failure=failure)
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}), content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, result, _ = request('POST', '/api/android-auto/enable', '{"enabled":true,"enableBluetooth":true}',
|
|
cookie=cookie, content_type='application/json')
|
|
assert code == (503 if failure == 'unavailable' else 409)
|
|
assert 'Bluetooth' in result['error']
|
|
assert state['enable_events'] == ['bluetooth'] and not state['enabled']
|
|
|
|
|
|
@pytest.mark.parametrize('parked', [True, False])
|
|
@pytest.mark.parametrize('enabled', [True, False])
|
|
@pytest.mark.parametrize('connection', ['connected', 'idle', 'unavailable'])
|
|
def test_identity_removal_disconnects_first_in_either_road_state(api, monkeypatch, parked, enabled, connection):
|
|
from openpilot.starpilot.system.android_auto import apk_identity
|
|
request, job, state, client = api
|
|
state.update(parked=parked, enabled=enabled)
|
|
client.running = connection == 'connected'
|
|
client.available = connection != 'unavailable'
|
|
removed = []
|
|
def remove():
|
|
assert not client.running
|
|
commands = [command for command, _kwargs in client.calls]
|
|
assert commands == {'connected': ['status', 'stop', 'status'], 'idle': ['status'], 'unavailable': []}[connection]
|
|
removed.append(True)
|
|
monkeypatch.setattr(apk_identity, 'remove_identity', remove)
|
|
assert request('DELETE', '/api/android-auto/identity')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
job.running = True
|
|
assert request('DELETE', '/api/android-auto/identity', cookie=cookie)[0] == 409
|
|
assert removed == [] and client.calls == []
|
|
job.running = False
|
|
assert request('DELETE', '/api/android-auto/identity', cookie=cookie)[0] == 200
|
|
assert removed == [True]
|
|
assert state['enabled'] is enabled
|
|
assert request('POST', '/api/auth/logout', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert request('DELETE', '/api/android-auto/identity', cookie=cookie)[0] == 401
|
|
assert removed == [True]
|
|
|
|
|
|
@pytest.mark.parametrize('failure', ['stop_error', 'still_running', 'status_error', 'invalid_status'])
|
|
def test_identity_removal_preserves_package_when_disconnect_cannot_be_confirmed(api, monkeypatch, failure):
|
|
from openpilot.starpilot.system.android_auto import apk_identity
|
|
request, _, state, client = api
|
|
state.update(parked=False, enabled=False)
|
|
client.running = True
|
|
client.stop_error = failure == 'stop_error'
|
|
client.stop_completes = failure != 'still_running'
|
|
original_call = client.call
|
|
def call(command, **kwargs):
|
|
if command == 'status' and failure == 'status_error':
|
|
raise RuntimeError('Status unavailable')
|
|
if command == 'status' and failure == 'invalid_status':
|
|
return {'status': {}}
|
|
return original_call(command, **kwargs)
|
|
monkeypatch.setattr(client, 'call', call)
|
|
remove = Mock()
|
|
monkeypatch.setattr(apk_identity, 'remove_identity', remove)
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, result, _ = request('DELETE', '/api/android-auto/identity', cookie=cookie)
|
|
assert code == (409 if failure == 'still_running' else 503)
|
|
assert 'disconnect' in result['error']
|
|
remove.assert_not_called()
|
|
|
|
|
|
def test_pairing_source_is_minted_only_for_authenticated_parked_session_and_revoked_on_logout(api):
|
|
request, _, state, client = api
|
|
assert request('POST', '/api/android-auto/pairing', '{}', content_type='application/json')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
state['parked'] = False
|
|
assert request('POST', '/api/android-auto/pairing', '{}', cookie=cookie, content_type='application/json')[0] == 403
|
|
state['parked'] = True
|
|
state['identity_installed'] = False
|
|
assert request('POST', '/api/android-auto/pairing', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
state['identity_installed'] = True
|
|
state['bluetooth_enabled'] = False
|
|
assert request('POST', '/api/android-auto/pairing', '{}', cookie=cookie, content_type='application/json')[0] == 409
|
|
state['bluetooth_enabled'] = True
|
|
code, result, _ = request('POST', '/api/android-auto/pairing', '{}', cookie=cookie, content_type='application/json')
|
|
assert code == 200 and result == {'pairing': True, 'seconds': 180}
|
|
source = next(kwargs['source'] for command, kwargs in client.calls if command == 'prepare_pairing')
|
|
assert source not in json.dumps(result) and client.verifier.valid(source)
|
|
assert request('POST', '/api/auth/logout', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert not client.verifier.valid(source)
|
|
|
|
|
|
def test_incoming_prompt_requires_same_session_park_and_current_source(api):
|
|
request, _, state, client = api
|
|
assert request('GET', '/api/android-auto/pairing/status')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
assert request('GET', '/api/android-auto/pairing/status', cookie=cookie)[1]['pairing']['active'] is False
|
|
assert request('POST', '/api/android-auto/pairing', '{}', cookie=cookie,
|
|
content_type='application/json')[0] == 200
|
|
code, status, _ = request('GET', '/api/android-auto/pairing/status', cookie=cookie)
|
|
assert code == 200 and status['pairing']['prompt']['kind'] == 'confirmation'
|
|
source = next(kwargs['source'] for command, kwargs in client.calls if command == 'prepare_pairing')
|
|
assert source not in json.dumps(status)
|
|
assert request('POST', '/api/android-auto/pairing/response',
|
|
json.dumps({'prompt_id': 'bad', 'accepted': True, 'value': ''}), cookie=cookie,
|
|
content_type='application/json')[0] == 400
|
|
assert [command for command, _kwargs in client.calls if command != 'status'] == ['prepare_pairing', 'pairing_status']
|
|
payload = {'prompt_id': 'a' * 32, 'accepted': True, 'value': ''}
|
|
assert request('POST', '/api/android-auto/pairing/response', json.dumps(payload), cookie=cookie,
|
|
content_type='application/json')[0] == 200
|
|
assert client.calls[-1][0] == 'pairing_response'
|
|
client.receiver = 'AA:BB:CC:DD:EE:FF'
|
|
code, selected, _ = request('GET', '/api/android-auto/pairing/status', cookie=cookie)
|
|
assert code == 200 and selected['selectedReceiver'] == {'address': client.receiver, 'name': 'My car'}
|
|
state['parked'] = False
|
|
assert request('POST', '/api/android-auto/pairing/response', json.dumps(payload), cookie=cookie,
|
|
content_type='application/json')[0] == 403
|
|
state['parked'] = True
|
|
assert request('POST', '/api/android-auto/pairing/cancel', '{}', cookie=cookie,
|
|
content_type='application/json')[0] == 200
|
|
assert not client.verifier.valid(source)
|
|
assert request('POST', '/api/android-auto/pairing/response', json.dumps(payload), cookie=cookie,
|
|
content_type='application/json')[0] == 409
|
|
|
|
|
|
def test_existing_paired_car_controls_are_local_authenticated_and_source_bound(api):
|
|
request, _, state, client = api
|
|
assert request('GET', '/api/android-auto/receivers')[0] == 401
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}),
|
|
content_type='application/json')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, available, _ = request('GET', '/api/android-auto/receivers', cookie=cookie)
|
|
assert code == 200 and available['receivers'] == [{'address': 'AA:BB:CC:DD:EE:FF', 'name': 'Saved car'}]
|
|
source = next(kwargs['source'] for command, kwargs in client.calls if command == 'devices')
|
|
assert source not in json.dumps(available) and not client.verifier.valid(source)
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'select_receiver', 'address': 'bad'}),
|
|
cookie=cookie, content_type='application/json')[0] == 400
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}),
|
|
cookie=cookie, content_type='application/json')[0] == 409
|
|
code, result, _ = request('POST', '/api/android-auto/control',
|
|
json.dumps({'action': 'select_receiver', 'address': 'AA:BB:CC:DD:EE:FF'}),
|
|
cookie=cookie, content_type='application/json')
|
|
assert code == 200 and result['status']['receiver_address'] == 'AA:BB:CC:DD:EE:FF'
|
|
state['identity_installed'] = False
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}),
|
|
cookie=cookie, content_type='application/json')[0] == 409
|
|
assert not client.running
|
|
state['identity_installed'] = True
|
|
state['parked'] = False
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'auto_connect', 'enabled': True}),
|
|
cookie=cookie, content_type='application/json')[0] == 403
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}),
|
|
cookie=cookie, content_type='application/json')[0] == 200
|
|
assert client.running
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'stop'}),
|
|
cookie=cookie, content_type='application/json')[0] == 200
|
|
assert not client.running
|
|
state['parked'] = True
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'auto_connect', 'enabled': True}),
|
|
cookie=cookie, content_type='application/json')[0] == 200
|
|
assert client.auto_connect
|
|
assert request('POST', '/api/auth/logout', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}),
|
|
cookie=cookie, content_type='application/json')[0] == 401
|
|
|
|
|
|
@pytest.mark.parametrize('connection', ['wired', 'headunitpad', 'openheadunit'])
|
|
def test_wired_connection_requires_park_but_not_a_paired_receiver(api, connection):
|
|
request, _, state, client = api
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
payload = json.dumps({'action': 'connection', 'connection': connection})
|
|
state['parked'] = False
|
|
assert request('POST', '/api/android-auto/control', payload, cookie=cookie, content_type='application/json')[0] == 403
|
|
state['parked'] = True
|
|
code, result, _ = request('POST', '/api/android-auto/control', payload, cookie=cookie, content_type='application/json')
|
|
assert code == 200 and result['status']['connection'] == connection
|
|
assert request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}), cookie=cookie,
|
|
content_type='application/json')[0] == 200
|
|
assert client.running
|
|
|
|
client.running = False
|
|
state['identity_installed'] = False
|
|
code, result, _ = request('POST', '/api/android-auto/control', json.dumps({'action': 'start'}), cookie=cookie,
|
|
content_type='application/json')
|
|
assert code == 409 and result['error'] == 'Verify your Android Auto package first'
|
|
|
|
|
|
def test_local_enable_then_upload_bundle_preserves_binary_bytes(api):
|
|
import io
|
|
import zipfile
|
|
|
|
request, job, state, _ = api
|
|
state['enabled'] = False
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
code, before, _ = request('GET', '/api/android-auto/setup', cookie=cookie)
|
|
assert code == 200 and not before['enabled'] and before['installReady']
|
|
code, enabled, _ = request('POST', '/api/android-auto/enable', json.dumps({'enabled': True}),
|
|
cookie=cookie, content_type='application/json')
|
|
assert code == 200 and enabled['enabled']
|
|
archive = io.BytesIO()
|
|
with zipfile.ZipFile(archive, 'w') as bundle:
|
|
bundle.writestr('base.apk', b'package-content')
|
|
payload = archive.getvalue()
|
|
code, uploaded, _ = request('POST', '/api/android-auto/upload', payload, cookie=cookie)
|
|
assert code == 202 and uploaded['import']['state'] == 'running'
|
|
assert job.calls[0][0] == payload
|
|
assert job.calls[0][1]() is True
|
|
code, status, _ = request('GET', '/api/android-auto/setup', cookie=cookie)
|
|
assert code == 200 and status['import']['state'] == 'running'
|
|
|
|
|
|
def test_outgoing_car_selection_requires_current_pairing_session_and_valid_address(api):
|
|
request, _, state, client = api
|
|
path = '/api/android-auto/pairing/select'
|
|
selected = json.dumps({'address': 'aa:bb:cc:dd:ee:ff'})
|
|
assert request('POST', path, selected, content_type='application/json')[0] == 401
|
|
code, _, headers = request('POST', '/api/auth/login', json.dumps({'password': 'password123'}),
|
|
content_type='application/json')
|
|
assert code == 200
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
def select(body=selected):
|
|
return request('POST', path, body, cookie=cookie, content_type='application/json')[0]
|
|
assert select() == 409
|
|
assert request('POST', '/api/android-auto/pairing', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
for body in ('{}', '{"address":1}', '{"address":"bad"}', '{"address":"AA:BB:CC:DD:EE:FF","force":true}'):
|
|
assert select(body) == 400
|
|
assert select() == 200
|
|
assert client.calls[-1][0] == 'pair_device'
|
|
assert client.calls[-1][1]['address'] == 'AA:BB:CC:DD:EE:FF'
|
|
assert client.verifier.valid(client.calls[-1][1]['source'])
|
|
state['parked'] = False
|
|
assert select() == 403
|
|
state['parked'] = True
|
|
assert request('POST', '/api/android-auto/pairing/cancel', '{}', cookie=cookie, content_type='application/json')[0] == 200
|
|
assert select() == 409
|
|
|
|
|
|
def test_google_sign_in_api_is_gone(api):
|
|
request, _, _, _ = api
|
|
_, _, headers = request('POST', '/api/auth/login', '{"password":"password123"}', content_type='application/json')
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
payload = json.dumps({'email': 'a@example.com', 'token': 'oauth2_4/test', 'remember': False})
|
|
for path in ('/api/android-auto/google-import', '/api/android-auto/google-forget'):
|
|
assert request('POST', path, payload, cookie=cookie, content_type='application/json')[0] == 405
|
|
|
|
|
|
def test_on_device_browser_api_is_gone(api):
|
|
request, _, _, _ = api
|
|
_, _, headers = request('POST', '/api/auth/login', '{"password":"password123"}', content_type='application/json')
|
|
cookie = headers['Set-Cookie'].split(';', 1)[0]
|
|
start = {'operation': 'start', 'email': 'user@example.com', 'remember': False}
|
|
assert request('POST', '/api/android-auto/google-browser', json.dumps(start), cookie=cookie, content_type='application/json')[0] != 200
|