Every POST endpoint (approve/reject/exit/watch) crashed with NameError: do_POST references os.environ + hmac.compare_digest but neither was imported. Adds the imports, a non-loopback regression test for the token gate (no token -> routes; token set -> 401 without/with-wrong header, 200 with correct), and plumbs DASHBOARD_TOKEN + PG_SKIP_SCHEMA=1 into both compose dashboard services (+ .env.example docs for DASHBOARD_TOKEN / COMMAND_BUS_ENABLED). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
79 lines
2.8 KiB
Python
79 lines
2.8 KiB
Python
"""POST auth + no-NameError regression for the dashboard handler (plan 007 step 1).
|
|
|
|
These drive ``DashboardHandler.do_POST`` DIRECTLY — the handler is constructed
|
|
without a socket and its ``_send_json`` is captured — so the test never binds a
|
|
loopback port (which is refused in this WSL sandbox) yet still exercises the real
|
|
token gate that used to ``NameError`` on missing ``os``/``hmac`` imports.
|
|
"""
|
|
|
|
import io
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from api.main import DashboardHandler, DashboardState
|
|
|
|
|
|
class CaptureHandler(DashboardHandler):
|
|
"""DashboardHandler wired to in-memory buffers instead of a live socket."""
|
|
|
|
def __init__(self, state, path, body=b"", headers=None): # noqa: D401 — no super().__init__
|
|
self.state = state
|
|
self.path = path
|
|
self.rfile = io.BytesIO(body)
|
|
self.headers = headers or {}
|
|
self.captured = [] # list of (status, payload)
|
|
|
|
def _send_json(self, payload, status=200):
|
|
self.captured.append((status, payload))
|
|
|
|
|
|
def _state():
|
|
# add_watch needs no DB, so this routes without opening a connection.
|
|
return DashboardState("momentum", watch_inject=lambda s: None)
|
|
|
|
|
|
def _post(handler_state, path, body, headers=None):
|
|
body_bytes = json.dumps(body).encode()
|
|
hdrs = {"Content-Length": str(len(body_bytes))}
|
|
if headers:
|
|
hdrs.update(headers)
|
|
h = CaptureHandler(handler_state, path, body_bytes, hdrs)
|
|
h.do_POST()
|
|
return h.captured[-1] if h.captured else None
|
|
|
|
|
|
def test_post_without_token_configured_routes(monkeypatch):
|
|
# No DASHBOARD_TOKEN -> the token gate is skipped; the POST must ROUTE, not
|
|
# crash with NameError (the os/hmac import bug this plan fixes).
|
|
monkeypatch.delenv("DASHBOARD_TOKEN", raising=False)
|
|
status, payload = _post(_state(), "/api/watch/add", {"symbol": "aapl"})
|
|
assert status == 200
|
|
assert payload == {"ok": True, "symbol": "AAPL"}
|
|
|
|
|
|
def test_post_with_token_missing_header_401(monkeypatch):
|
|
monkeypatch.setenv("DASHBOARD_TOKEN", "s3cret")
|
|
status, payload = _post(_state(), "/api/watch/add", {"symbol": "aapl"})
|
|
assert status == 401
|
|
assert "X-Dashboard-Token" in payload["error"]
|
|
|
|
|
|
def test_post_with_token_wrong_header_401(monkeypatch):
|
|
monkeypatch.setenv("DASHBOARD_TOKEN", "s3cret")
|
|
status, _ = _post(_state(), "/api/watch/add", {"symbol": "aapl"},
|
|
headers={"X-Dashboard-Token": "nope"})
|
|
assert status == 401
|
|
|
|
|
|
def test_post_with_token_correct_header_routes(monkeypatch):
|
|
monkeypatch.setenv("DASHBOARD_TOKEN", "s3cret")
|
|
status, payload = _post(_state(), "/api/watch/add", {"symbol": "aapl"},
|
|
headers={"X-Dashboard-Token": "s3cret"})
|
|
assert status == 200
|
|
assert payload["ok"] is True
|
|
|
|
|
|
if __name__ == "__main__": # pragma: no cover
|
|
pytest.main([__file__, "-q"])
|