blob: 1ded7d4237d466a11250a1f67d2445351c100686 [file]
#!/usr/bin/env python3
# Copyright 2026 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Unit tests for run_server.py."""
import io
import subprocess
import sys
import unittest
from http import HTTPStatus
from unittest.mock import MagicMock, patch
# Prevent Python from creating a __pycache__ directory in the source tree.
sys.dont_write_bytecode = True
import run_server
class RunServerTest(unittest.TestCase):
def setUp(self):
# Suppress print statements during tests.
self.held_stdout = io.StringIO()
self.original_stdout = sys.stdout
sys.stdout = self.held_stdout
# Reset the global configuration before each test.
run_server._config = run_server.ServerConfig()
run_server._config.adb = MagicMock()
run_server._config.require_imprint = False
def tearDown(self):
# Restore original stdout.
sys.stdout = self.original_stdout
def create_mock_handler(self, path, client_ip='127.0.0.1'):
"""Creates a partially initialized handler for testing do_GET."""
handler = run_server.MainRequestHandler.__new__(
run_server.MainRequestHandler
)
handler.path = path
handler.client_address = (client_ip, 12345)
handler.send_error = MagicMock()
handler.send_response = MagicMock()
handler.send_header = MagicMock()
handler.end_headers = MagicMock()
handler.wfile = MagicMock()
return handler
# --- Data Parsing Tests ---
def test_density_parsing_standard(self):
mock_result = MagicMock()
mock_result.stdout = "Physical density: 480\n"
run_server._config.adb.run.return_value = mock_result
handler = self.create_mock_handler('/api/density.json')
# 480 / 160.0 = 3.0
self.assertEqual(handler._get_density_data(), 3.0)
def test_density_parsing_override(self):
mock_result = MagicMock()
mock_result.stdout = "Physical density: 480\nOverride density: 560\n"
run_server._config.adb.run.return_value = mock_result
handler = self.create_mock_handler('/api/density.json')
# 560 / 160.0 = 3.5
self.assertEqual(handler._get_density_data(), 3.5)
def test_screenshot_trimming(self):
# Simulate a warning message prepended to the binary PNG output.
fake_binary = b'WARNING: Display 1 is off.\n\x89PNG\r\n\x1a\nDATA'
mock_result = MagicMock()
mock_result.stdout = fake_binary
run_server._config.adb.run.return_value = mock_result
handler = self.create_mock_handler('/api/screenshot.png')
png_data = handler._get_screenshot_data()
self.assertEqual(png_data, b'\x89PNG\r\n\x1a\nDATA')
def test_dump_ui_standard(self):
handler = self.create_mock_handler('/api/ui-dump.json')
handler._dump_ui('/sdcard/temp.xml')
run_server._config.adb.run.assert_called_once_with(
['shell', 'uiautomator', 'dump', '--windows', '/sdcard/temp.xml']
)
def test_dump_ui_legacy_flag(self):
run_server._config.legacy = True
handler = self.create_mock_handler('/api/ui-dump.json')
handler._dump_ui('/sdcard/temp.xml')
run_server._config.adb.run.assert_called_once_with(
['shell', 'uiautomator', 'dump', '/sdcard/temp.xml']
)
def test_dump_ui_fallback_on_failure(self):
run_server._config.adb.run.side_effect = [
subprocess.CalledProcessError(
1, ['uiautomator', 'dump', '--windows']
),
MagicMock(),
]
handler = self.create_mock_handler('/api/ui-dump.json')
handler._dump_ui('/sdcard/temp.xml')
self.assertTrue(run_server._config.legacy)
self.assertEqual(run_server._config.adb.run.call_count, 2)
self.assertEqual(
run_server._config.adb.run.call_args_list[0][0][0],
['shell', 'uiautomator', 'dump', '--windows', '/sdcard/temp.xml'],
)
self.assertEqual(
run_server._config.adb.run.call_args_list[1][0][0],
['shell', 'uiautomator', 'dump', '/sdcard/temp.xml'],
)
# --- Security & Routing Tests ---
@patch('http.server.SimpleHTTPRequestHandler.do_GET')
def test_allowed_extensions(self, mock_super_do_get):
for path in ['/index.html', '/style.css', '/app.js', '/model.js']:
handler = self.create_mock_handler(path)
handler.do_GET()
handler.send_error.assert_not_called()
mock_super_do_get.assert_called()
def test_root_redirects_to_index(self):
handler = self.create_mock_handler('/')
# Prevent it from actually trying to serve the file
with patch('http.server.SimpleHTTPRequestHandler.do_GET'):
handler.do_GET()
self.assertEqual(handler.path, '/index.html')
def test_security_imprinting(self):
run_server._config.require_imprint = True
# First request sets the imprinted IP.
handler1 = self.create_mock_handler('/index.html', client_ip='10.0.0.1')
with patch('http.server.SimpleHTTPRequestHandler.do_GET'):
handler1.do_GET()
self.assertEqual(run_server._config.allowed_client_ip, '10.0.0.1')
handler1.send_error.assert_not_called()
# Request from the same IP is allowed.
handler2 = self.create_mock_handler('/style.css', client_ip='10.0.0.1')
with patch('http.server.SimpleHTTPRequestHandler.do_GET'):
handler2.do_GET()
handler2.send_error.assert_not_called()
# Request from a different IP is blocked.
handler3 = self.create_mock_handler('/index.html', client_ip='10.0.0.2')
handler3.do_GET()
handler3.send_error.assert_called_with(
HTTPStatus.FORBIDDEN,
"Forbidden: Server is imprinted to a different IP.",
)
if __name__ == '__main__':
unittest.main()