Initial proxy port monitor TUI
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,166 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
from typing import Any
|
||||
import unittest
|
||||
import urllib.parse
|
||||
|
||||
|
||||
def load_module():
|
||||
module_path = Path(__file__).resolve().parents[1] / "proxy_monitor_tui.py"
|
||||
spec = importlib.util.spec_from_file_location("proxy_monitor_tui", module_path)
|
||||
assert spec and spec.loader
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules[spec.name] = module
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
class ApiFixtureHandler(BaseHTTPRequestHandler):
|
||||
requests: list[dict[str, Any]] = []
|
||||
|
||||
def _json(self, status: int, payload: object) -> None:
|
||||
body = json.dumps(payload).encode("utf-8")
|
||||
self.send_response(status)
|
||||
self.send_header("Content-Type", "application/json")
|
||||
self.send_header("Content-Length", str(len(body)))
|
||||
self.end_headers()
|
||||
self.wfile.write(body)
|
||||
|
||||
def do_GET(self) -> None: # noqa: N802
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
type(self).requests.append({"method": "GET", "path": parsed.path, "query": urllib.parse.parse_qs(parsed.query)})
|
||||
if parsed.path == "/base/status.json":
|
||||
self._json(200, {"targets_up": 1, "targets_total": 1, "targets": [{"name": "pc", "ok": True}]})
|
||||
return
|
||||
if parsed.path == "/base/admin/pc%20win/api/proxies":
|
||||
self._json(
|
||||
200,
|
||||
{
|
||||
"proxies": {
|
||||
"Selector A": {
|
||||
"type": "Selector",
|
||||
"now": "Node A",
|
||||
"all": ["Node A", "Node B"],
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
if parsed.path == "/base/admin/pc%20win/api/proxies/Node%20A/delay":
|
||||
self._json(200, {"delay": 42})
|
||||
return
|
||||
self._json(404, {"error": "not found"})
|
||||
|
||||
def do_PUT(self) -> None: # noqa: N802
|
||||
parsed = urllib.parse.urlparse(self.path)
|
||||
length = int(self.headers.get("Content-Length", "0"))
|
||||
payload = json.loads(self.rfile.read(length).decode("utf-8"))
|
||||
type(self).requests.append({"method": "PUT", "path": parsed.path, "payload": payload})
|
||||
if parsed.path == "/base/admin/pc%20win/api/proxies/Selector%20A":
|
||||
self.send_response(204)
|
||||
self.send_header("Content-Length", "0")
|
||||
self.end_headers()
|
||||
return
|
||||
self._json(404, {"error": "not found"})
|
||||
|
||||
def log_message(self, format: str, *args: object) -> None: # noqa: A003
|
||||
return
|
||||
|
||||
|
||||
class ProxyMonitorTUITests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.server = ThreadingHTTPServer(("127.0.0.1", 0), ApiFixtureHandler)
|
||||
cls.thread = threading.Thread(target=cls.server.serve_forever, daemon=True)
|
||||
cls.thread.start()
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.server.shutdown()
|
||||
cls.thread.join(timeout=5)
|
||||
cls.server.server_close()
|
||||
|
||||
def setUp(self) -> None:
|
||||
ApiFixtureHandler.requests = []
|
||||
self.mod = load_module()
|
||||
self.base_url = f"http://127.0.0.1:{self.server.server_port}/base"
|
||||
self.client = self.mod.ProxyMonitorClient(self.base_url, 2)
|
||||
|
||||
def test_status_and_controller_paths_keep_the_base_prefix_and_quote_names(self) -> None:
|
||||
status = self.client.fetch_status()
|
||||
proxies = self.client.fetch_proxies("pc win")
|
||||
delay = self.client.measure_delay(
|
||||
"pc win",
|
||||
"Node A",
|
||||
test_url="https://www.gstatic.com/generate_204",
|
||||
timeout_ms=5000,
|
||||
)
|
||||
self.client.select_proxy("pc win", "Selector A", "Node B")
|
||||
|
||||
self.assertEqual(status["targets_up"], 1)
|
||||
self.assertIn("Selector A", proxies["proxies"])
|
||||
self.assertEqual(delay, 42)
|
||||
self.assertEqual(
|
||||
ApiFixtureHandler.requests,
|
||||
[
|
||||
{"method": "GET", "path": "/base/status.json", "query": {}},
|
||||
{"method": "GET", "path": "/base/admin/pc%20win/api/proxies", "query": {}},
|
||||
{
|
||||
"method": "GET",
|
||||
"path": "/base/admin/pc%20win/api/proxies/Node%20A/delay",
|
||||
"query": {"url": ["https://www.gstatic.com/generate_204"], "timeout": ["5000"]},
|
||||
},
|
||||
{
|
||||
"method": "PUT",
|
||||
"path": "/base/admin/pc%20win/api/proxies/Selector%20A",
|
||||
"payload": {"name": "Node B"},
|
||||
},
|
||||
],
|
||||
)
|
||||
|
||||
def test_selector_groups_only_include_switchable_groups(self) -> None:
|
||||
groups = self.mod.normalize_selector_groups(
|
||||
{
|
||||
"proxies": {
|
||||
"url-test": {"type": "URLTest", "all": ["A"]},
|
||||
"selector": {"type": "Selector", "now": "A", "all": ["A", "B", "A"]},
|
||||
"empty": {"type": "Selector", "now": "", "all": []},
|
||||
}
|
||||
}
|
||||
)
|
||||
self.assertEqual(groups, [{"name": "selector", "now": "A", "candidates": ["A", "B"]}])
|
||||
|
||||
def test_recommendation_ignores_failed_or_zero_delay_candidates(self) -> None:
|
||||
recommended = self.mod.recommend_fastest(
|
||||
{
|
||||
"failure": (None, "timed out"),
|
||||
"zero": (0, "no delay result"),
|
||||
"slow": (340, "340 ms"),
|
||||
"fast": (81, "81 ms"),
|
||||
}
|
||||
)
|
||||
self.assertEqual(recommended, "fast")
|
||||
|
||||
def test_machine_rows_preserve_monitor_order_and_status(self) -> None:
|
||||
rows = self.mod.normalize_machine_rows(
|
||||
{
|
||||
"targets": [
|
||||
{"name": "first", "ok": True, "detail": "latency=1ms", "exit_country": "Japan"},
|
||||
{"name": "second", "ok": False, "consecutive_failures": 3},
|
||||
]
|
||||
}
|
||||
)
|
||||
self.assertEqual([row["name"] for row in rows], ["first", "second"])
|
||||
self.assertEqual(rows[0]["state"], "online")
|
||||
self.assertEqual(rows[1]["state"], "offline")
|
||||
self.assertEqual(rows[1]["consecutive_failures"], 3)
|
||||
|
||||
def test_invalid_api_url_is_rejected_before_network_access(self) -> None:
|
||||
with self.assertRaises(ValueError):
|
||||
self.mod.ProxyMonitorClient("proxy.tailbeb9ad.ts.net", 2)
|
||||
Reference in New Issue
Block a user