"""Exercise the PC client against a real local HTTP socket, without Android.""" import importlib.util import json from pathlib import Path import tempfile import threading import unittest from unittest.mock import patch from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer spec = importlib.util.spec_from_file_location('bridge', Path(__file__).resolve().parents[1] / 'bridge.py') module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) class Handler(BaseHTTPRequestHandler): def log_message(self, *_): pass def handle_request(self): length = int(self.headers.get('Content-Length', 0)) data = self.rfile.read(length) self.server.requests.append((self.command, self.path, dict(self.headers), data)) if self.path == '/reject': status, result = 401, {'error': 'Unauthorized'} elif self.command == 'PUT': status, result = 200, {'file': self.path.split('/')[-1], 'sha256': self.headers['X-SHA256']} elif self.path == '/command': status, result = 202, {'id': 'test-job'} elif self.path == '/jobs/test-job': status, result = 200, self.server.job_result else: status, result = 200, {'state': 'bound'} body = json.dumps(result).encode() self.send_response(status) self.send_header('Content-Length', str(len(body))) self.end_headers() self.wfile.write(body) do_GET = do_POST = do_PUT = handle_request class ClientTests(unittest.TestCase): @classmethod def setUpClass(cls): cls.server = ThreadingHTTPServer(('127.0.0.1', 0), Handler) cls.server.requests = [] threading.Thread(target=cls.server.serve_forever, daemon=True).start() @classmethod def tearDownClass(cls): cls.server.shutdown() cls.server.server_close() def setUp(self): self.client = module.Bridge(port=self.server.server_port) self.client.token = 'test-token' self.server.requests.clear() def test_unicode_message_survives_http(self): self.server.job_result = {'state': 'success', 'result': {'code': 200}} self.client.command({'command': 'message', 'message': 'S2 真机 πŸ™‚'}) method, path, headers, body = self.server.requests[0] self.assertEqual(json.loads(body)['message'], 'S2 真机 πŸ™‚') self.assertEqual(headers['Authorization'], 'Bearer test-token') self.assertEqual((method, path), ('POST', '/command')) def test_watch_failure_is_preserved(self): self.server.job_result = {'state': 'failed', 'result': {'code': 5005, 'msg': 'invalid package'}} result = self.client.command({'command': 'install', 'file': 'test.pkg'}) self.assertEqual(result['state'], 'failed') self.assertEqual(result['result']['code'], 5005) def test_timeout_keeps_job_for_inspection(self): result = self.client.command({'command': 'info'}, timeout=0) self.assertEqual(result['state'], 'timeout') self.assertEqual(result['id'], 'test-job') def test_binary_upload_hash_and_safe_name(self): with tempfile.TemporaryDirectory() as tmp: file = Path(tmp) / 'δΈ­ζ–‡ package.pkg' data = bytes(range(256)) * 32 file.write_bytes(data) name = self.client.upload(file) request = self.server.requests[0] self.assertEqual(request[3], data) self.assertRegex(name, r'^[a-f0-9]{24}\.pkg$') self.assertEqual(request[2]['X-Sha256'], module.hashlib.sha256(data).hexdigest()) def test_rejected_request_does_not_leak_token(self): with self.assertRaises(RuntimeError) as ctx: self.client.request('GET', '/reject') self.assertIn('401', str(ctx.exception)) self.assertNotIn('test-token', str(ctx.exception)) def test_empty_upload_rejected_before_network(self): with tempfile.TemporaryDirectory() as tmp: file = Path(tmp) / 'empty.pkg' file.touch() with self.assertRaises(ValueError): self.client.upload(file) self.assertEqual(self.server.requests, []) def test_wait_link_allows_health_to_reconnect(self): offline = {'state': 'success', 'result': {'connected': False}} online = {'state': 'success', 'result': {'connected': True}} with patch.object(self.client, 'command', side_effect=[offline, online]) as command, patch.object(module.time, 'sleep'): self.assertEqual(self.client.wait_link(5), online) self.assertEqual(command.call_count, 2) self.assertTrue(all(call.args[0] == {'command': 'link'} for call in command.call_args_list)) def test_wait_link_does_not_repeat_failed_calls(self): failed = {'state': 'failed', 'result': 'disconnected'} with patch.object(self.client, 'command', return_value=failed) as command: self.assertEqual(self.client.wait_link(5), failed) command.assert_called_once() if __name__ == '__main__': unittest.main()