Files
StarPilot/openpilot/starpilot/galaxy/tests/test_software_http.py
T
firestar5683 80fbb4f6db Feature registration
Register vehicle controls, planner modes, UI actions and Galaxy services after their providers exist. Wire the shared runtime entrypoints and cover their combined behavior.

Co-authored-by: Adeeb Shihadeh <adeebshihadeh@gmail.com>
Co-authored-by: Daniel Koepping <elkoled@gmail.com>
Co-authored-by: Danny <dannyirwin31@yahoo.com>
Co-authored-by: DimitriChrysafis <Dimitri.chrysafis@gmail.com>
Co-authored-by: Dom <168790843+firestar5683@users.noreply.github.com>
Co-authored-by: Harald Schäfer <harald.the.engineer@gmail.com>
Co-authored-by: Kacper Rączy <gfw.kra@gmail.com>
Co-authored-by: Prabhaav Pillai <prabhaav@msu.edu>
Co-authored-by: dirwin31 <83434411+dirwin31@users.noreply.github.com>
Co-authored-by: firestarsdog <229254897+firestarsdog@users.noreply.github.com>
Co-authored-by: stef <19478336+stefpi@users.noreply.github.com>
Co-authored-by: whoisdomi <dcantu83@gmail.com>

Co-authored-by: Intelli <contact@intelli.software>
Co-authored-by: haraschax <6804392+haraschax@users.noreply.github.com>
Co-authored-by: prabhaavp <prabhaav@msu.edu>
2026-10-14 20:02:39 -05:00

116 lines
6.4 KiB
Python

import base64
import hashlib
import http.client
import json
from pathlib import Path
import subprocess
import tempfile
import threading
import unittest
from openpilot.common.params import Params
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner
from openpilot.starpilot.galaxy.remote import RemotePairing
from openpilot.starpilot.galaxy.server import make_remote_server, make_server
from openpilot.starpilot.galaxy.software_operations import SoftwareOperations
from openpilot.starpilot.galaxy.software_status import SoftwareStatus
from openpilot.starpilot.galaxy.tests.test_software_operations import FakeProcess
class SoftwareHttpTest(unittest.TestCase):
def setUp(self):
temporary = tempfile.TemporaryDirectory()
self.addCleanup(temporary.cleanup)
root = Path(temporary.name)
installed = root / 'installed'
installed.mkdir()
subprocess.run(['git', '-C', str(installed), '-c', 'init.defaultBranch=main', 'init', '-q'], check=True)
subprocess.run(['git', '-C', str(installed), '-c', 'core.hooksPath=/dev/null', '-c', 'user.name=Test',
'-c', 'user.email=test@example.com', 'commit', '--allow-empty', '-qm', 'Software history test'], check=True)
commit = subprocess.check_output(['git', '-C', str(installed), 'rev-parse', 'HEAD'], text=True).strip()
self.params = Params(str(root / 'params'))
for key, value in {'Version': 'test', 'GitBranch': 'main', 'GitCommit': commit,
'UpdaterState': 'idle', 'UpdaterTargetBranch': 'main', 'UpdaterAvailableBranches': 'main'}.items():
self.params.put(key, value, block=True)
self.params.put_bool('IsOffroad', True, block=True)
self.params.put('UpdaterCurrentReleaseNotes', b'<h1>Release notes</h1><li>Useful changes</li>', block=True)
self.process = FakeProcess()
self.parked = True
self.operations = SoftwareOperations(self.params, parked=lambda: self.parked, process=self.process,
installed=installed, finalized=root / 'finalized')
self.access = GalaxyAccessOwner(root / 'access')
self.pairing = RemotePairing(root / 'pairing')
self.local = make_server(port=0, owner=self.access, remote_pairing=self.pairing,
software=SoftwareStatus(self.params), software_operations=self.operations)
self.remote = make_remote_server(self.local, port=0)
self.servers = []
for server in (self.local, self.remote):
thread = threading.Thread(target=server.serve_forever, kwargs={'poll_interval': .01}, daemon=True)
thread.start()
self.servers.append((server, thread))
self.addCleanup(self.close)
self.local_cookie = self.request('/api/auth/session')[2]['Set-Cookie'].split(';', 1)[0]
self.access.configure('password123', lambda: True)
self.slug = self.pairing.pair(hashlib.sha256(b'password123').hexdigest())
record = self.pairing.read()
assert record is not None
token = base64.urlsafe_b64encode(json.dumps({self.slug: record['session']}).encode()).decode().rstrip('=')
self.remote_cookie = 'galaxy_session=' + token
def close(self):
for server, thread in reversed(self.servers):
server.shutdown()
thread.join(timeout=2)
server.server_close()
def request(self, path, *, payload=None, remote=False, cookie=None, origin=None):
server = self.remote if remote else self.local
host = f'{self.slug}.devices.local' if remote else f'127.0.0.1:{server.server_port}'
headers = {'Host': host}
if cookie is not None:
headers['Cookie'] = cookie
if payload is not None:
headers.update({'Content-Type': 'application/json', 'Origin': origin or
('https://galaxy.firestar.link' if remote else f'http://{host}')})
connection = http.client.HTTPConnection('127.0.0.1', server.server_port, timeout=5)
try:
connection.request('POST' if payload is not None else 'GET', path,
json.dumps(payload) if payload is not None else None, headers)
response = connection.getresponse()
return response.status, json.loads(response.read()), dict(response.getheaders())
finally:
connection.close()
def test_local_and_remote_history_and_immediate_preference_roundtrip(self):
for remote in (False, True):
with self.subTest(remote=remote):
cookie = self.remote_cookie if remote else self.local_cookie
status, light, _ = self.request('/api/software/status?history=0', remote=remote, cookie=cookie)
self.assertEqual(status, 200)
self.assertNotIn('history', light['operations'])
status, value, _ = self.request('/api/software/status', remote=remote, cookie=cookie)
self.assertEqual(light['operations'], {key: entry for key, entry in value['operations'].items() if key != 'history'})
self.assertEqual(status, 200)
view = value['operations']
self.assertEqual(view['history']['installed'][0]['subject'], 'Software history test')
self.assertEqual(view['history']['currentReleaseNotes'], 'Release notes\n\nUseful changes')
expected = view['automaticDownloads']
payload = {'action': 'preferences', 'automaticDownloads': not expected, 'expectedAutomaticDownloads': expected}
status, saved, _ = self.request('/api/software/action', payload=payload, remote=remote, cookie=cookie)
self.assertEqual(status, 200)
self.assertIs(saved['operations']['automaticDownloads'], not expected)
self.assertIsNone(saved['operations']['request'])
self.assertEqual(self.request('/api/software/action', payload=payload, remote=remote, cookie=cookie)[0], 409)
self.assertEqual(self.process.sent, [])
def test_remote_auth_origin_and_parked_rules_cannot_be_bypassed(self):
payload = {'action': 'preferences', 'automaticDownloads': False, 'expectedAutomaticDownloads': True}
self.assertEqual(self.request('/api/software/status', remote=True)[0], 401)
self.assertEqual(self.request('/api/software/status', remote=True, cookie=self.local_cookie)[0], 401)
self.assertEqual(self.request('/api/software/action', payload=payload, remote=True,
cookie=self.remote_cookie, origin='https://wrong.example')[0], 403)
self.parked = False
self.assertEqual(self.request('/api/software/action', payload=payload, remote=True, cookie=self.remote_cookie)[0], 409)
self.assertFalse(Path(self.params.get_param_path('UpdaterAutomaticDownloads')).exists())
self.assertEqual(self.process.sent, [])