Files
socks5-monitor/tests/test_log_retention.py
T
Hermes ce0aa22672 Fix node id collision, host normalization and blank error text
Three defects made the panel look broken to the operator:

- POST /api/proxies with a Chinese name slugged the id down to its ASCII
  digits ("香港01" -> "01"), collided with an existing node and returned
  HTTP 500 "proxy id already exists". Non-ASCII names now get sequential
  node-N ids; ASCII names keep a readable slug. Both paths de-duplicate.
- A host pasted as a URL ("http://192.168.3.71") went straight to DNS, so
  every check failed with "[Errno -2] Name does not resolve". normalize_host
  accepts host, host:port, scheme://user:pass@host:port and [IPv6]:port, and
  existing configs are healed at startup.
- str(asyncio.TimeoutError()) is empty, so a black-holed proxy showed a blank
  error in the UI and looked like an internal bug. describe_exc always
  produces a reason.

Also:
- Expected validation failures return 400 with an error message instead of
  500 + traceback; the frontend surfaces every API error as a toast.
- PUT/DELETE on an unknown proxy id return 404 instead of silently creating
  or reporting ok=false with 200.
- POST /api/check/<id> forces a check on disabled nodes instead of returning
  ok=true without doing anything.
- Port range is validated (1..65535).
- Store.close() lets tests release SQLite handles.
- 33 unittest cases cover id generation, host normalization, error text,
  startup migration and manual checks.
2026-08-23 14:07:37 +08:00

74 lines
2.9 KiB
Python

#!/usr/bin/env python3
from __future__ import annotations
import asyncio
import importlib.util
import sys
import tempfile
from pathlib import Path
import unittest
ROOT = Path(__file__).resolve().parents[1]
spec = importlib.util.spec_from_file_location("socks5_app", ROOT / "app.py")
assert spec is not None and spec.loader is not None
app = importlib.util.module_from_spec(spec)
sys.modules["socks5_app"] = app
spec.loader.exec_module(app)
class LogRetentionTest(unittest.TestCase):
def make_store(self, path: Path):
store = app.Store(path)
self.addCleanup(store.close)
return store
def test_check_records_are_pruned_per_proxy(self):
with tempfile.TemporaryDirectory() as td:
store = self.make_store(Path(td) / "monitor.db")
async def run():
for i in range(5):
await store.add_check("node-a", True, i, None, f"203.0.113.{i}", max_per_proxy=2)
await store.add_check("node-b", True, 99, None, "198.51.100.1", max_per_proxy=2)
asyncio.run(run())
node_a = store.recent_checks("node-a", 10)
node_b = store.recent_checks("node-b", 10)
self.assertEqual(len(node_a), 2)
self.assertEqual([r["exit_ip"] for r in node_a], ["203.0.113.4", "203.0.113.3"])
self.assertEqual(len(node_b), 1)
def test_clear_logs_removes_checks_events_and_notification_logs(self):
with tempfile.TemporaryDirectory() as td:
store = self.make_store(Path(td) / "monitor.db")
async def seed_and_clear():
await store.add_check("node-a", True, 1, None, "203.0.113.1")
await store.add_event("node-a", "unknown", "up", "ok")
await store.add_notification({"id": "ch-1", "name": "test"}, True, "title", "body", "ok")
removed = await store.clear_logs()
return removed
removed = asyncio.run(seed_and_clear())
self.assertEqual(removed["checks"], 1)
self.assertEqual(removed["events"], 1)
self.assertEqual(removed["notification_logs"], 1)
self.assertEqual(store.recent_checks("node-a", 10), [])
self.assertEqual(store.recent_events(10), [])
self.assertEqual(store.recent_notifications(10), [])
def test_settings_accept_max_checks_per_proxy(self):
with tempfile.TemporaryDirectory() as td:
cfg = Path(td) / "config.json"
cfg.write_text('{"proxies": [], "notifications": {"channels": []}}', encoding="utf-8")
application = app.App(cfg)
self.addCleanup(application.store.close)
settings = asyncio.run(application.update_settings({"max_checks_per_proxy": 7}))
self.assertEqual(settings["max_checks_per_proxy"], 7)
saved = app.load_json(cfg)
self.assertEqual(saved["max_checks_per_proxy"], 7)
if __name__ == "__main__":
unittest.main()