test: Tor controller and API endpoint tests
Covers: password/cookie/bare auth, auth failure, connect failure, NEWNYM success/rate-limiting/reconnect, GETINFO multi-line parsing, start/stop lifecycle, GET /tor status, POST /tor/newnym dispatch, and TorConfig YAML parsing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ from s5p.api import (
|
||||
_handle_metrics,
|
||||
_handle_pool,
|
||||
_handle_status,
|
||||
_handle_tor,
|
||||
_handle_tor_newnym,
|
||||
_json_response,
|
||||
_parse_request,
|
||||
_route,
|
||||
@@ -80,6 +82,7 @@ class TestJsonResponse:
|
||||
def _make_ctx(
|
||||
config: Config | None = None,
|
||||
pool: MagicMock | None = None,
|
||||
tor: MagicMock | None = None,
|
||||
) -> dict:
|
||||
"""Build a mock context dict."""
|
||||
return {
|
||||
@@ -87,6 +90,7 @@ def _make_ctx(
|
||||
"metrics": Metrics(),
|
||||
"pool": pool,
|
||||
"hop_pool": None,
|
||||
"tor": tor,
|
||||
}
|
||||
|
||||
|
||||
@@ -284,3 +288,82 @@ class TestRouting:
|
||||
status, body = asyncio.run(_route("GET", "/reload", ctx))
|
||||
assert status == 405
|
||||
assert "POST" in body["error"]
|
||||
|
||||
def test_get_tor(self):
|
||||
ctx = _make_ctx()
|
||||
status, body = asyncio.run(_route("GET", "/tor", ctx))
|
||||
assert status == 200
|
||||
assert body == {"enabled": False}
|
||||
|
||||
def test_post_tor_newnym(self):
|
||||
tor = MagicMock()
|
||||
tor.newnym = AsyncMock(return_value=True)
|
||||
ctx = _make_ctx(tor=tor)
|
||||
status, body = asyncio.run(_route("POST", "/tor/newnym", ctx))
|
||||
assert status == 200
|
||||
assert body == {"ok": True}
|
||||
|
||||
|
||||
# -- Tor endpoints ----------------------------------------------------------
|
||||
|
||||
|
||||
class TestHandleTor:
|
||||
"""Test GET /tor handler."""
|
||||
|
||||
def test_disabled(self):
|
||||
ctx = _make_ctx()
|
||||
status, body = _handle_tor(ctx)
|
||||
assert status == 200
|
||||
assert body == {"enabled": False}
|
||||
|
||||
def test_connected(self):
|
||||
tor = MagicMock()
|
||||
tor.connected = True
|
||||
tor.last_newnym = 0.0
|
||||
tor.newnym_interval = 60.0
|
||||
ctx = _make_ctx(tor=tor)
|
||||
status, body = _handle_tor(ctx)
|
||||
assert status == 200
|
||||
assert body["enabled"] is True
|
||||
assert body["connected"] is True
|
||||
assert body["last_newnym"] is None
|
||||
assert body["newnym_interval"] == 60.0
|
||||
|
||||
def test_with_last_newnym(self):
|
||||
import time
|
||||
tor = MagicMock()
|
||||
tor.connected = True
|
||||
tor.last_newnym = time.monotonic() - 42.0
|
||||
tor.newnym_interval = 0.0
|
||||
ctx = _make_ctx(tor=tor)
|
||||
status, body = _handle_tor(ctx)
|
||||
assert status == 200
|
||||
assert body["last_newnym"] is not None
|
||||
assert body["last_newnym"] >= 42.0
|
||||
|
||||
|
||||
class TestHandleTorNewnym:
|
||||
"""Test POST /tor/newnym handler."""
|
||||
|
||||
def test_success(self):
|
||||
tor = MagicMock()
|
||||
tor.newnym = AsyncMock(return_value=True)
|
||||
ctx = _make_ctx(tor=tor)
|
||||
status, body = asyncio.run(_handle_tor_newnym(ctx))
|
||||
assert status == 200
|
||||
assert body == {"ok": True}
|
||||
|
||||
def test_rate_limited(self):
|
||||
tor = MagicMock()
|
||||
tor.newnym = AsyncMock(return_value=False)
|
||||
ctx = _make_ctx(tor=tor)
|
||||
status, body = asyncio.run(_handle_tor_newnym(ctx))
|
||||
assert status == 200
|
||||
assert body["ok"] is False
|
||||
assert "reason" in body
|
||||
|
||||
def test_not_configured(self):
|
||||
ctx = _make_ctx()
|
||||
status, body = asyncio.run(_handle_tor_newnym(ctx))
|
||||
assert status == 400
|
||||
assert "error" in body
|
||||
|
||||
Reference in New Issue
Block a user