diff --git a/pilot/dashboard.py b/pilot/dashboard.py index 2507181..1dbc363 100644 --- a/pilot/dashboard.py +++ b/pilot/dashboard.py @@ -9,13 +9,13 @@ ThreadingHTTPServer shape) that renders three views off the feedback SQLite: GET /r/// one PR's findings GET /r////raw raw Markdown body (via Gitea) GET /static/style.css CSS - GET /login login form - POST /login check token, set cookie POST /r///edit mutate .pr-review.json (Tasks C+D) -Auth: when `PRAGENT_DASHBOARD_TOKEN` is set, every route except `/login`, -`/static/*`, and `POST /login` requires `Cookie: pragent_dash=`. -Unset → tailnet-only mode, no auth. +Auth: oauth2-proxy fronts this service in-cluster. Every route except +`/static/*` requires the `X-Forwarded-User` header (set by oauth2-proxy +once the user has logged in via Logto). Missing header → 401 + +`WWW-Authenticate: Basic realm="pragent-dashboard"` so oauth2-proxy +intercepts the response. DB: `PRAGENT_FEEDBACK_DB` points at the SQLite file the webhook server also writes. Per-request open (SQLite is cheap, no concurrency hazard, @@ -33,6 +33,7 @@ import datetime import html import json import os +import secrets import string import urllib.error import urllib.parse @@ -47,13 +48,15 @@ from pilot import dashboard_data # --------------------------------------------------------------------------- FEEDBACK_DB = "" # legacy; readers should call _feedback_db() -DASHBOARD_TOKEN = "" # legacy; readers should call _dashboard_token() PORT = int(os.environ.get("DASHBOARD_PORT", "8081")) GITEA_API = "" # legacy; readers should call _gitea_api() BOT_TOKEN = "" # legacy; readers should call _bot_token() -COOKIE_NAME = "pragent_dash" +# CSRF secret for the edit form. Regenerated per process (each Python +# interpreter launch). Behind oauth2-proxy this is enough — only an +# already-authenticated same-tab request can read this and echo it back. +_CSRF_SECRET: str = secrets.token_urlsafe(24) # --------------------------------------------------------------------------- @@ -67,10 +70,6 @@ def _feedback_db() -> str: return os.environ.get("PRAGENT_FEEDBACK_DB", "") -def _dashboard_token() -> str: - return os.environ.get("PRAGENT_DASHBOARD_TOKEN", "") - - def _bot_token() -> str: return os.environ.get("PRAGENT_BOT_TOKEN", "") @@ -231,18 +230,6 @@ ${findings_table} """) -_LOGIN = string.Template("""

Login

-${flash} -
- - -
- -
-
-""") - - # --------------------------------------------------------------------------- # Small helpers # --------------------------------------------------------------------------- @@ -418,10 +405,6 @@ def _pr_body(data: dict, *, repo_url: str) -> str: ) -def _login_body(flash: str = "") -> str: - return _LOGIN.substitute(flash=_esc(flash)) - - def _page(title: str, body: str, *, repos_first: str = "") -> str: db_status = _feedback_db() or "(no DB configured)" return _BASE.substitute( @@ -467,16 +450,14 @@ def _http(method: str, url: str, *, token: str = "", body: dict | None = None, def _is_authed(headers) -> bool: - """True when token matches; false when token unset OR cookie absent/wrong.""" - tok = _dashboard_token() - if not tok: - return True # no token → tailnet-only mode - cookie = headers.get("Cookie") or "" - for part in cookie.split(";"): - k, _, v = part.strip().partition("=") - if k == COOKIE_NAME and v == tok: - return True - return False + """True when oauth2-proxy forwarded a verified user. + + oauth2-proxy sets `X-Forwarded-User` (and friends) only after a + successful Logto login + email allowlist check. Unauthenticated + requests never see the header, so the dashboard never has to know + about cookies, secrets, or Logto's token shape. + """ + return bool((headers.get("X-Forwarded-User") or "").strip()) # --------------------------------------------------------------------------- @@ -505,11 +486,10 @@ def _route_repo(owner: str, name: str) -> bytes: current_model = cfg.get("model", "") elif err and err != "404": flash = f"could not read .pr-review.json: {err}" - csrf = _dashboard_token() or "noauth" body = _repo_body( data, repo_url=repo_url, - csrf=csrf, + csrf=_CSRF_SECRET, current_model=current_model, current_static_message=current_static_message, flash=flash, @@ -549,25 +529,11 @@ def _route_static_css() -> bytes: return STYLE_CSS.encode() -def _route_login_get() -> bytes: - return _page("login", _login_body()).encode() - - -def _route_login_post(token: str) -> tuple[int, dict, bytes]: - """Compare token to env. Sets cookie on match, 401 otherwise.""" - expected = _dashboard_token() - if not expected or token != expected: - return 401, {}, b"invalid token" - cookie = f"{COOKIE_NAME}={expected}; HttpOnly; SameSite=Strict; Path=/" - return 302, {"Set-Cookie": cookie, "Location": "/"}, b"" - - def _route_edit(owner: str, name: str, form: dict) -> tuple[int, dict, bytes]: """Mutate .pr-review.json via the Gitea contents API (Tasks C+D).""" repo_url = f"{owner}/{name}" csrf = form.get("_csrf", "") - expected_csrf = _dashboard_token() or "noauth" - if csrf != expected_csrf: + if csrf != _CSRF_SECRET: return 302, {"Location": f"/r/{repo_url}"}, b"" static_message = (form.get("static_message") or "").strip()[:400] model = (form.get("model") or "").strip() @@ -682,19 +648,27 @@ class Handler(BaseHTTPRequestHandler): self.end_headers() self.wfile.write(body) + def _unauthorized(self) -> None: + """401 + Basic challenge so oauth2-proxy intercepts and redirects to Logto.""" + body = b"unauthorized\n" + self.send_response(401) + self.send_header("Content-Type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.send_header("WWW-Authenticate", 'Basic realm="pragent-dashboard"') + self.end_headers() + self.wfile.write(body) + # --- GET ----------------------------------------------------------------- def do_GET(self): path = self.path - # Static is exempt from auth so the login page can load its CSS. + # Static is exempt from auth (also unauthenticated browser fingerprinting + # noise, but it's the same CSS regardless of viewer). if path == "/static/style.css": self._send(200, _route_static_css(), content_type="text/css; charset=utf-8") return - if path == "/login": - self._send(200, _route_login_get()) - return if not _is_authed(self.headers): - self._redirect("/login") + self._unauthorized() return if path == "/" or path == "": @@ -724,17 +698,8 @@ class Handler(BaseHTTPRequestHandler): def do_POST(self): path = self.path - # Login POST is exempt from auth (obviously). - if path == "/login": - length = int(self.headers.get("Content-Length", "0") or "0") - raw = self.rfile.read(length) if length else b"" - form = urllib.parse.parse_qs(raw.decode("utf-8", errors="replace")) - token = (form.get("token") or [""])[0] - status, extra, body = _route_login_post(token) - self._send(status, body, content_type="text/plain", extra_headers=extra) - return if not _is_authed(self.headers): - self._redirect("/login") + self._unauthorized() return # /r///edit m = _EDIT_RE.match(path) @@ -775,10 +740,7 @@ def main() -> int: if not _feedback_db(): print("pragent-dashboard: WARNING: PRAGENT_FEEDBACK_DB not set; dashboard will be empty", flush=True) - if _dashboard_token(): - print("pragent-dashboard: auth ON (PRAGENT_DASHBOARD_TOKEN set)", flush=True) - else: - print("pragent-dashboard: auth OFF (tailnet-only mode)", flush=True) + print("pragent-dashboard: auth via oauth2-proxy (X-Forwarded-User required)", flush=True) server = ThreadingHTTPServer(("0.0.0.0", PORT), Handler) print(f"pragent-dashboard: listening on :{PORT}", flush=True) try: diff --git a/tests/pilot/test_dashboard.py b/tests/pilot/test_dashboard.py index 3dd892c..81d0d55 100644 --- a/tests/pilot/test_dashboard.py +++ b/tests/pilot/test_dashboard.py @@ -1,8 +1,9 @@ """Tests for pilot/dashboard.py — stdlib HTTP server rendering dashboard HTML. We spin up the server on an ephemeral port in setUp, drive it with -http.client, and tear it down in tearDown. Tests are split between -no-auth (default — env unset) and auth-on (PRAGENT_DASHBOARD_TOKEN set). +http.client, and tear it down in tearDown. Auth is now performed by +oauth2-proxy: the dashboard trusts `X-Forwarded-User` set by the proxy +and returns 401 (with a Basic challenge) when the header is missing. The dashboard reads `PRAGENT_FEEDBACK_DB` and renders views via `dashboard_data`. We seed an in-memory SQLite at `tmp_path` for each @@ -69,15 +70,72 @@ def _post(port: int, path: str, body: bytes, headers: dict | None = None) -> tup return r.status, h, body_b -class TestDashboardNoAuth(unittest.TestCase): - """PRAGENT_DASHBOARD_TOKEN unset → no cookie check.""" +class TestDashboardAuth(unittest.TestCase): + """Auth gate: require X-Forwarded-User (set by oauth2-proxy). + + When the header is missing every non-static route returns 401 with a + Basic challenge, which lets oauth2-proxy redirect the browser to + Logto. Static is exempt so the unauthenticated probe traffic doesn't + loop the proxy through the auth flow. + """ def setUp(self): self.tmp = tempfile.TemporaryDirectory() self.db = f"{self.tmp.name}/f.db" os.environ["PRAGENT_FEEDBACK_DB"] = self.db - os.environ["PRAGENT_DASHBOARD_TOKEN"] = "" os.environ["DASHBOARD_PORT"] = str(0) # we override below + + self.port = _free_port() + from http.server import ThreadingHTTPServer + self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler) + self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True) + self.thread.start() + + def tearDown(self): + self.srv.shutdown() + self.srv.server_close() + self.thread.join(timeout=2) + for k in ("PRAGENT_FEEDBACK_DB", "DASHBOARD_PORT"): + os.environ.pop(k, None) + self.tmp.cleanup() + + def test_anonymous_overview_returns_401_with_basic_challenge(self): + status, h, body = _get(self.port, "/") + self.assertEqual(status, 401) + self.assertEqual(h.get("WWW-Authenticate"), 'Basic realm="pragent-dashboard"') + self.assertEqual(body, b"unauthorized\n") + + def test_anonymous_repo_returns_401(self): + status, _h, _body = _get(self.port, "/r/alpha/one") + self.assertEqual(status, 401) + + def test_anonymous_post_returns_401(self): + status, _h, _body = _post(self.port, "/r/alpha/one/edit", b"x=1") + self.assertEqual(status, 401) + + def test_authenticated_overview_succeeds(self): + status, h, body = _get(self.port, "/", headers={"X-Forwarded-User": "marcos@example.com"}) + self.assertEqual(status, 200) + self.assertIn(b"Overview", body) + + def test_static_does_not_require_auth(self): + status, h, body = _get(self.port, "/static/style.css") + self.assertEqual(status, 200) + self.assertIn("text/css", h.get("Content-Type", "")) + self.assertGreater(len(body), 50) + + def test_empty_x_forwarded_user_treated_as_anonymous(self): + status, _h, _body = _get(self.port, "/", headers={"X-Forwarded-User": " "}) + self.assertEqual(status, 401) + + +class TestDashboardRender(unittest.TestCase): + """Render-only tests — X-Forwarded-User set, real seeded data.""" + + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.db = f"{self.tmp.name}/f.db" + os.environ["PRAGENT_FEEDBACK_DB"] = self.db # Seed: 2 repos, a couple of reviews + findings each. conn = feedback.init(self.db) for repo, n_prs in (("alpha/one", 2), ("beta/two", 1)): @@ -99,22 +157,21 @@ class TestDashboardNoAuth(unittest.TestCase): conn.close() self.port = _free_port() - # Re-bind the handler's server-class addr by constructing a fresh server. from http.server import ThreadingHTTPServer self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler) self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True) self.thread.start() + self.auth_hdr = {"X-Forwarded-User": "marcos@example.com"} def tearDown(self): self.srv.shutdown() self.srv.server_close() self.thread.join(timeout=2) - for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN", "DASHBOARD_PORT"): - os.environ.pop(k, None) + os.environ.pop("PRAGENT_FEEDBACK_DB", None) self.tmp.cleanup() def test_overview_200_contains_repo_names(self): - status, _h, body = _get(self.port, "/") + status, _h, body = _get(self.port, "/", headers=self.auth_hdr) self.assertEqual(status, 200) text = body.decode() self.assertIn("Overview", text) @@ -122,7 +179,7 @@ class TestDashboardNoAuth(unittest.TestCase): self.assertIn("beta/two", text) def test_repo_page_200(self): - status, _h, body = _get(self.port, "/r/alpha/one") + status, _h, body = _get(self.port, "/r/alpha/one", headers=self.auth_hdr) self.assertEqual(status, 200) text = body.decode() self.assertIn("alpha/one", text) @@ -130,102 +187,17 @@ class TestDashboardNoAuth(unittest.TestCase): self.assertIn("src/file_0.py", text) def test_pr_page_200(self): - status, _h, body = _get(self.port, "/r/alpha/one/1") + status, _h, body = _get(self.port, "/r/alpha/one/1", headers=self.auth_hdr) self.assertEqual(status, 200) text = body.decode() self.assertIn("alpha/one", text) self.assertIn("#1", text) self.assertIn("src/file_0.py", text) - def test_static_css_200(self): - status, h, body = _get(self.port, "/static/style.css") - self.assertEqual(status, 200) - self.assertIn("text/css", h.get("Content-Type", "")) - self.assertGreater(len(body), 50) - - def test_login_form(self): - status, _h, body = _get(self.port, "/login") - self.assertEqual(status, 200) - self.assertIn(b'name="token"', body) - - def test_login_post_wrong_token_401(self): - status, _h, body = _post( - self.port, "/login", b"token=wrong", - ) - self.assertEqual(status, 401) - def test_unknown_route_404(self): - status, _h, _body = _get(self.port, "/no/such/route") + status, _h, _body = _get(self.port, "/no/such/route", headers=self.auth_hdr) self.assertEqual(status, 404) -class TestDashboardWithAuth(unittest.TestCase): - """PRAGENT_DASHBOARD_TOKEN set → all routes (except /login) require cookie.""" - - def setUp(self): - self.tmp = tempfile.TemporaryDirectory() - self.db = f"{self.tmp.name}/f.db" - os.environ["PRAGENT_FEEDBACK_DB"] = self.db - os.environ["PRAGENT_DASHBOARD_TOKEN"] = "secret-token-xyz" - - # Seed one repo with a single PR. - conn = feedback.init(self.db) - rid = feedback.record_review( - conn, repo="o/r", pr=1, head_sha="x", - review_id_gitea=1, body_comment_id=2, - ) - feedback.record_inline_finding( - conn, review_id=rid, repo="o/r", pr=1, - path="a.py", line=1, severity="low", problem="x", - ) - conn.close() - - self.port = _free_port() - from http.server import ThreadingHTTPServer - self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler) - self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True) - self.thread.start() - - def tearDown(self): - self.srv.shutdown() - self.srv.server_close() - self.thread.join(timeout=2) - for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN"): - os.environ.pop(k, None) - self.tmp.cleanup() - - def test_overview_without_cookie_redirects_to_login(self): - status, h, _b = _get(self.port, "/") - self.assertEqual(status, 302) - self.assertIn("/login", h.get("Location", "")) - - def test_login_with_correct_token_sets_cookie_and_redirects(self): - status, h, _b = _post( - self.port, "/login", b"token=secret-token-xyz", - ) - self.assertEqual(status, 302) - self.assertIn("/", h.get("Location", "")) - sc = h.get("Set-Cookie", "") - self.assertIn("pragent_dash=secret-token-xyz", sc) - self.assertIn("HttpOnly", sc) - self.assertIn("SameSite=Strict", sc) - - def test_overview_with_correct_cookie_succeeds(self): - status, _h, body = _get( - self.port, "/", - headers={"Cookie": "pragent_dash=secret-token-xyz"}, - ) - self.assertEqual(status, 200) - self.assertIn(b"Overview", body) - - def test_login_form_does_not_require_auth(self): - status, _h, _b = _get(self.port, "/login") - self.assertEqual(status, 200) - - def test_static_does_not_require_auth(self): - status, _h, _b = _get(self.port, "/static/style.css") - self.assertEqual(status, 200) - - if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/pilot/test_dashboard_edit.py b/tests/pilot/test_dashboard_edit.py index 49adf08..492f51b 100644 --- a/tests/pilot/test_dashboard_edit.py +++ b/tests/pilot/test_dashboard_edit.py @@ -2,7 +2,7 @@ We mock the Gitea HTTP layer (urllib.request.urlopen) so the test never touches the network. The dashboard handler is responsible for: - * auth + CSRF + * auth (X-Forwarded-User set by oauth2-proxy) + CSRF * read .pr-review.json via GET (404 → start from {}) * validate model against cost_model.PRICES * PUT the updated file back, with sha + base64 content @@ -35,12 +35,12 @@ def _free_port() -> int: return port -def _post(port: int, path: str, body: bytes, *, cookie: str | None = None) -> tuple[int, dict, bytes]: +def _post(port: int, path: str, body: bytes, *, headers: dict | None = None) -> tuple[int, dict, bytes]: conn = http.client.HTTPConnection("127.0.0.1", port, timeout=5) - headers = {"Content-Type": "application/x-www-form-urlencoded"} - if cookie: - headers["Cookie"] = cookie - conn.request("POST", path, body=body, headers=headers) + hdrs = {"Content-Type": "application/x-www-form-urlencoded"} + if headers: + hdrs.update(headers) + conn.request("POST", path, body=body, headers=hdrs) r = conn.getresponse() body_b = r.read() h = dict(r.getheaders()) @@ -68,7 +68,6 @@ class TestDashboardEdit(unittest.TestCase): self.tmp = tempfile.TemporaryDirectory() self.db = f"{self.tmp.name}/f.db" os.environ["PRAGENT_FEEDBACK_DB"] = self.db - os.environ["PRAGENT_DASHBOARD_TOKEN"] = "secret-edit-token" os.environ["PRAGENT_BOT_TOKEN"] = "bot-token" # Seed a row so the repo page is meaningful. conn = feedback.init(self.db) @@ -82,7 +81,10 @@ class TestDashboardEdit(unittest.TestCase): self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler) self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True) self.thread.start() - self.cookie = "pragent_dash=secret-edit-token" + # Pull the per-process CSRF secret from the rendered repo page — the + # edit form embeds the same token as a hidden input. + self.csrf = dash._CSRF_SECRET + self.auth_hdr = {"X-Forwarded-User": "marcos@example.com"} # Records of HTTP calls made by the handler. self.calls: list[tuple[str, str, dict | None, bytes | None]] = [] @@ -91,7 +93,7 @@ class TestDashboardEdit(unittest.TestCase): self.srv.shutdown() self.srv.server_close() self.thread.join(timeout=2) - for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN", "PRAGENT_BOT_TOKEN"): + for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_BOT_TOKEN"): os.environ.pop(k, None) self.tmp.cleanup() @@ -114,12 +116,12 @@ class TestDashboardEdit(unittest.TestCase): def test_edit_updates_static_message_and_model(self): form = ( - b"_csrf=secret-edit-token" - b"&static_message=Hello%20world" - b"&model=claude-sonnet-5" - ) + f"_csrf={self.csrf}" + f"&static_message=Hello%20world" + f"&model=claude-sonnet-5" + ).encode() with patch.object(dash.urllib.request, "urlopen", side_effect=self._urlopen): - status, h, _b = _post(self.port, "/r/o/r/edit", form, cookie=self.cookie) + status, h, _b = _post(self.port, "/r/o/r/edit", form, headers=self.auth_hdr) self.assertEqual(status, 302) self.assertEqual(h.get("Location"), "/r/o/r") @@ -139,24 +141,24 @@ class TestDashboardEdit(unittest.TestCase): def test_edit_strips_static_message_to_400(self): long_msg = "x" * 600 form = ( - f"_csrf=secret-edit-token" + f"_csrf={self.csrf}" f"&static_message={long_msg}" f"&model=claude-haiku-4-5" ).encode() with patch.object(dash.urllib.request, "urlopen", side_effect=self._urlopen): - _post(self.port, "/r/o/r/edit", form, cookie=self.cookie) + _post(self.port, "/r/o/r/edit", form, headers=self.auth_hdr) put = next(c for c in self.calls if c[0] == "PUT") cfg = json.loads(base64.b64decode(json.loads(put[3])["content"])) self.assertEqual(len(cfg["static_message"]), 400) def test_edit_rejects_unknown_model_with_flash(self): form = ( - b"_csrf=secret-edit-token" - b"&static_message=hi" - b"&model=does-not-exist" - ) + f"_csrf={self.csrf}" + f"&static_message=hi" + f"&model=does-not-exist" + ).encode() with patch.object(dash.urllib.request, "urlopen", side_effect=self._urlopen): - status, h, _b = _post(self.port, "/r/o/r/edit", form, cookie=self.cookie) + status, h, _b = _post(self.port, "/r/o/r/edit", form, headers=self.auth_hdr) self.assertEqual(status, 302) self.assertIn("flash=", h.get("Location", "")) # No PUT should have been issued. @@ -164,16 +166,18 @@ class TestDashboardEdit(unittest.TestCase): self.assertEqual(put_calls, []) def test_edit_requires_auth(self): - form = b"_csrf=secret-edit-token&static_message=x&model=claude-haiku-4-5" + form = f"_csrf={self.csrf}&static_message=x&model=claude-haiku-4-5".encode() with patch.object(dash.urllib.request, "urlopen", side_effect=self._urlopen): status, h, _b = _post(self.port, "/r/o/r/edit", form) - self.assertEqual(status, 302) - self.assertEqual(h.get("Location"), "/login") + self.assertEqual(status, 401) + self.assertEqual(h.get("WWW-Authenticate"), 'Basic realm="pragent-dashboard"') + # No Gitea calls at all — auth gate fires first. + self.assertEqual(self.calls, []) def test_edit_csrf_mismatch_redirects_without_save(self): - form = b"_csrf=wrong&static_message=x&model=claude-haiku-4-5" + form = f"_csrf=wrong&static_message=x&model=claude-haiku-4-5".encode() with patch.object(dash.urllib.request, "urlopen", side_effect=self._urlopen): - status, h, _b = _post(self.port, "/r/o/r/edit", form, cookie=self.cookie) + status, h, _b = _post(self.port, "/r/o/r/edit", form, headers=self.auth_hdr) self.assertEqual(status, 302) self.assertEqual(h.get("Location"), "/r/o/r") put_calls = [c for c in self.calls if c[0] == "PUT"] @@ -192,9 +196,9 @@ class TestDashboardEdit(unittest.TestCase): return _FakeResp(201, b"{}") return _FakeResp(404, b"") - form = b"_csrf=secret-edit-token&static_message=hi&model=claude-haiku-4-5" + form = f"_csrf={self.csrf}&static_message=hi&model=claude-haiku-4-5".encode() with patch.object(dash.urllib.request, "urlopen", side_effect=_route): - status, h, _b = _post(self.port, "/r/o/r/edit", form, cookie=self.cookie) + status, h, _b = _post(self.port, "/r/o/r/edit", form, headers=self.auth_hdr) self.assertEqual(status, 302) put = next(c for c in self.calls if c[0] == "PUT") payload = json.loads(put[3]) @@ -202,4 +206,4 @@ class TestDashboardEdit(unittest.TestCase): if __name__ == "__main__": - unittest.main() \ No newline at end of file + unittest.main() diff --git a/tests/pilot/test_dashboard_select.py b/tests/pilot/test_dashboard_select.py index 652cafb..9715114 100644 --- a/tests/pilot/test_dashboard_select.py +++ b/tests/pilot/test_dashboard_select.py @@ -38,7 +38,6 @@ class TestRepoEditSelect(unittest.TestCase): self.tmp = tempfile.TemporaryDirectory() self.db = f"{self.tmp.name}/f.db" os.environ["PRAGENT_FEEDBACK_DB"] = self.db - os.environ["PRAGENT_DASHBOARD_TOKEN"] = "" os.environ["PRAGENT_BOT_TOKEN"] = "" conn = feedback.init(self.db) feedback.record_review(conn, repo="o/r", pr=1, head_sha="x") @@ -49,17 +48,18 @@ class TestRepoEditSelect(unittest.TestCase): self.srv = ThreadingHTTPServer(("127.0.0.1", self.port), dash.Handler) self.thread = threading.Thread(target=self.srv.serve_forever, daemon=True) self.thread.start() + self.auth_hdr = {"X-Forwarded-User": "marcos@example.com"} def tearDown(self): self.srv.shutdown() self.srv.server_close() self.thread.join(timeout=2) - for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_DASHBOARD_TOKEN", "PRAGENT_BOT_TOKEN"): + for k in ("PRAGENT_FEEDBACK_DB", "PRAGENT_BOT_TOKEN"): os.environ.pop(k, None) self.tmp.cleanup() def test_repo_page_renders_select_with_one_option_per_price(self): - status, _h, body = _get(self.port, "/r/o/r") + status, _h, body = _get(self.port, "/r/o/r", headers=self.auth_hdr) self.assertEqual(status, 200) text = body.decode() self.assertIn('