"""Protocol fixtures exercise bounded guest probes, not a real GUI session.""" import base64 import hashlib import http.server import json from pathlib import Path import threading from types import SimpleNamespace import unittest from unittest.mock import patch SOURCE = Path(__file__).resolve().parents[1] / 'crates/supervisor/src/runtime_probe.py' module = {'__name__': 'runtime_probe_test'} exec(compile(SOURCE.read_text(), str(SOURCE), 'exec'), module) class ProbeTests(unittest.TestCase): def setUp(self): fixture = self self.mode = 'valid' self.requests = [] class Handler(http.server.BaseHTTPRequestHandler): protocol_version = 'HTTP/1.1' def log_message(self, *_): pass def do_GET(self): self.close_connection = True fixture.requests.append(self.path) if self.path == '/websockify': self.send_response(200 if fixture.mode == 'no-upgrade' else 101) key = self.headers['Sec-WebSocket-Key'] accepted = base64.b64encode(hashlib.sha1((key + '258EAFA5-E914-47DA-95CA-C5AB0DC85B11').encode()).digest()).decode() self.send_header('Upgrade', 'websocket') self.send_header('Connection', 'Upgrade') self.send_header('Sec-WebSocket-Protocol', 'binary') self.send_header('Sec-WebSocket-Accept', 'wrong' if fixture.mode == 'wrong-accept' else accepted) self.end_headers() data = { 'bad-rfb': b'\x82\x0cNOT A VIEWER', 'oversized-frame': b'\x82\x7e', 'masked-frame': b'\x82\x8c', 'split-frame': b'\x82\x04RFB \x82\x08003.008\n', }.get(fixture.mode, b'\x82\x0cRFB 003.008\n') try: self.wfile.write(data) self.wfile.flush() except (BrokenPipeError, ConnectionResetError): pass self.close_connection = True return if self.path == '/json/version': body = json.dumps({'Browser': 'Chrome/fixture', 'Protocol-Version': '1.3', 'webSocketDebuggerUrl': f'ws://127.0.0.1:{fixture.port}/devtools/browser/test'}).encode() if fixture.mode == 'foreign-browser': body = body.replace(b'127.0.0.1', b'example.com') if fixture.mode == 'bad-browser': body = b'not JSON' if fixture.mode == 'oversized-browser': body = b' ' * 65537 elif self.path == '/vnc_lite.html': body = b'viewer fixture' else: self.send_error(404) return self.send_response(302 if fixture.mode == 'redirect' else 200) self.send_header('Content-Type', 'text/plain' if fixture.mode == 'not-html' else 'text/html') self.send_header('Content-Length', str(len(body))) self.end_headers() try: self.wfile.write(body) except (BrokenPipeError, ConnectionResetError): pass self.server = http.server.ThreadingHTTPServer(('127.0.0.1', 0), Handler) self.port = self.server.server_port threading.Thread(target=self.server.serve_forever, daemon=True).start() self.addCleanup(self.server.server_close) self.addCleanup(self.server.shutdown) def test_browser_requires_bounded_local_devtools_discovery(self): for mode, expected in [('valid', True), ('foreign-browser', False), ('bad-browser', False), ('oversized-browser', False), ('redirect', False)]: self.mode = mode self.assertEqual(module['browser_probe'](self.port), expected, mode) self.assertEqual(self.requests, ['/json/version'] * 5) def test_viewer_requires_websocket_to_rfb_not_just_http(self): for mode, expected in [('valid', True), ('split-frame', True), ('bad-rfb', False), ('wrong-accept', False), ('no-upgrade', False), ('oversized-frame', False), ('masked-frame', False), ('not-html', False), ('redirect', False)]: self.mode = mode self.assertEqual(module['viewer_probe'](self.port), expected, mode) self.assertNotIn('/json/version', self.requests) def test_desktop_needs_a_window_manager_as_well_as_x11(self): run = module['subprocess'].run with patch.object(module['subprocess'], 'run', side_effect=[ SimpleNamespace(returncode=0), SimpleNamespace(returncode=0, stdout=b'_NET_SUPPORTING_WM_CHECK(WINDOW): window id # 0x200001'), SimpleNamespace(returncode=0, stdout=b'_NET_WM_NAME(UTF8_STRING) = "Xfwm4"')]) as call: self.assertTrue(module['desktop_probe'](2)) self.assertEqual(call.call_args_list[0].args[0], ['xdpyinfo', '-display', ':3']) self.assertIn('0x200001', call.call_args_list[2].args[0]) with patch.object(module['subprocess'], 'run', side_effect=[ SimpleNamespace(returncode=0), SimpleNamespace(returncode=0, stdout=b'not found')]): self.assertFalse(module['desktop_probe'](2)) with patch.object(module['subprocess'], 'run', side_effect=FileNotFoundError): self.assertFalse(module['desktop_probe'](2)) self.assertIs(module['subprocess'].run, run) def test_no_assignment_never_probes_a_peer_and_rejects_invalid_slots(self): original = {key: module[key] for key in ('desktop_probe', 'browser_probe', 'viewer_probe')} try: for key in original: module[key] = lambda *_: self.fail('unassigned slot must not be probed') result = module['probe'](None) self.assertIsNone(result['desktop']) self.assertIsNone(result['browser']) self.assertIsNone(result['viewer']) for slot in (True, False, -1, 8, '1', [], {}): with self.assertRaisesRegex(ValueError, 'INVALID_SCREEN_SLOT'): module['probe'](slot) finally: module.update(original) def test_root_timeout_errors_do_not_echo_the_helper_or_create_a_job(self): scope = {'__name__': 'root_health_test'} for filename in ('runner_files.py', 'runner_jobs.py'): source = SOURCE.parent / filename exec(compile(source.read_text(), str(source), 'exec'), scope) scope.update(connect=lambda: object(), RUNTIME_PROBE_SOURCE='PRIVATE_HELPER_CANARY') request = {'action': 'runtime_health', 'identity': {'computer_id': 'computer', 'bot_id': 'bot', 'generation': 1}, 'input': '{"slot":null}'} errors = [scope['subprocess'].TimeoutExpired(['PRIVATE_HELPER_CANARY'], 8), OSError('PRIVATE_HELPER_CANARY')] for error in errors: with patch.object(scope['subprocess'], 'run', side_effect=error) as run: with self.assertRaises(ValueError) as caught: scope['handle'](request) self.assertEqual(str(caught.exception), 'RUNTIME_HEALTH_UNAVAILABLE') self.assertEqual(run.call_args.kwargs['timeout'], 8) self.assertEqual(run.call_args.kwargs['user'], 1000) def test_components_fail_independently_and_ports_follow_the_slot(self): original = {key: module[key] for key in ('desktop_probe', 'browser_probe', 'viewer_probe')} seen = [] try: module['desktop_probe'] = lambda slot: seen.append(('desktop', slot)) or False module['browser_probe'] = lambda port: seen.append(('browser', port)) or True module['viewer_probe'] = lambda port: seen.append(('viewer', port)) or False with patch.object(module['os'], 'getuid', return_value=1000): result = module['probe'](3) self.assertEqual(result, {'version': 1, 'slot': 3, 'runner': True, 'desktop': False, 'browser': True, 'viewer': False}) self.assertEqual(seen, [('desktop', 3), ('browser', 9225), ('viewer', 6083)]) finally: module.update(original) if __name__ == '__main__': unittest.main()