118 行
4.9 KiB
Python
118 行
4.9 KiB
Python
"""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()
|