"""Teste pentru dashboard-ul de control (dashboard/api.py). Zero retea catre exterior si zero systemctl real: `_sysctl` e inlocuit in fiecare test cu un dublu care inregistreaza argumentele. Serverul HTTP porneste pe un port efemer legat de 127.0.0.1, exact cum ruleaza in productie. """ from __future__ import annotations import json import pathlib import subprocess import sys import threading import time import urllib.error import urllib.request from http.server import ThreadingHTTPServer import pytest ROOT = pathlib.Path(__file__).resolve().parent.parent sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "dashboard")) import config # noqa: E402 api = pytest.importorskip("api", reason="dashboard/api.py") # --- ajutoare --------------------------------------------------------------- def _cp(stdout: str = "", rc: int = 0, stderr: str = "") -> subprocess.CompletedProcess: return subprocess.CompletedProcess(args=[], returncode=rc, stdout=stdout, stderr=stderr) @pytest.fixture() def systemctl_fals(monkeypatch): """Inlocuieste systemctl. `apeluri` retine ce s-ar fi executat.""" apeluri: list[tuple[str, ...]] = [] props = { "ActiveState": "active", "SubState": "running", "UnitFileState": "enabled", "MainPID": "4242", "MemoryCurrent": "1048576", "NRestarts": "3", "ActiveEnterTimestampMonotonic": "0", "ActiveEnterTimestamp": "", } def fake(*args, timeout=30.0): apeluri.append(tuple(args)) if args[0] == "show": return _cp(props.get(args[2], "")) return _cp("") monkeypatch.setattr(api, "_sysctl", fake) fake.apeluri = apeluri # type: ignore[attr-defined] fake.props = props # type: ignore[attr-defined] return fake @pytest.fixture() def server(state_dir, systemctl_fals, monkeypatch): """Dashboard-ul pe un port efemer + un client mic cu cookie.""" (state_dir / "env").write_text("DASHBOARD_TOKEN=secret-de-test\n", encoding="utf-8") config.reload(state_dir) api.reset_token_cache() srv = ThreadingHTTPServer(("127.0.0.1", 0), api.Handler) srv.daemon_threads = True threading.Thread(target=srv.serve_forever, daemon=True).start() base = f"http://127.0.0.1:{srv.server_address[1]}" class Client: def __init__(self): self.cookie = "" def call(self, path, data=None, method=None): req = urllib.request.Request( base + path, data=json.dumps(data).encode() if data is not None else None, method=method or ("POST" if data is not None else "GET"), ) req.add_header("Content-Type", "application/json") if self.cookie: req.add_header("Cookie", self.cookie) def _json(body: str): """Rutele /api/ intorc JSON; paginile intorc HTML — nu esuam pe ele.""" try: return json.loads(body) if body else {} except ValueError: return {"_html": body} try: with urllib.request.urlopen(req, timeout=10) as r: sc = r.headers.get("Set-Cookie") if sc: self.cookie = sc.split(";", 1)[0] return r.status, _json(r.read().decode()), r except urllib.error.HTTPError as e: return e.code, _json(e.read().decode()), e def login(self, token="secret-de-test"): return self.call("/api/auth/login", {"token": token}) try: yield Client() finally: srv.shutdown() srv.server_close() api.reset_token_cache() def _scrie_stare(state_dir, threads: dict, cost: float = 1.5): (state_dir / "state.json").write_text(json.dumps({ "version": 1, "threads": threads, "cost": {"day": "2026-08-30", "usd": cost}, }), encoding="utf-8") # --- autentificare ---------------------------------------------------------- def test_api_fara_cookie_da_401(server): assert server.call("/api/status")[0] == 401 def test_index_fara_cookie_redirectioneaza_la_login(server): status, data, resp = server.call("/") # urllib urmareste redirectul singur: ajungem pe pagina de login, nu pe index assert status == 200 assert resp.url.endswith("/login.html") assert "DASHBOARD_TOKEN" in data["_html"] def test_token_gresit_e_refuzat(server): assert server.login("gresit")[0] == 401 assert server.call("/api/status")[0] == 401 def test_login_apoi_status(server): assert server.login()[0] == 200 status, data, _ = server.call("/api/status") assert status == 200 assert data["service"]["unit"] == api.SERVICE assert data["service"]["restarts"] == 3 assert data["service"]["memory_bytes"] == 1048576 def test_logout_invalideaza_cookie(server): server.login() server.call("/api/auth/logout", {}) server.cookie = "dashboard=" assert server.call("/api/status")[0] == 401 def test_token_lipsa_din_env_nu_deschide_dashboardul(state_dir, monkeypatch): """Fara DASHBOARD_TOKEN se genereaza unul aleator, nu se sare peste auth.""" (state_dir / "env").write_text("", encoding="utf-8") config.reload(state_dir) api.reset_token_cache() try: tok = api.dashboard_token() assert len(tok) >= 20 assert api.dashboard_token() == tok # stabil in cadrul procesului finally: api.reset_token_cache() # --- control de serviciu ---------------------------------------------------- def test_actiune_necunoscuta_e_respinsa(server): server.login() status, data, _ = server.call("/api/service", {"action": "mask"}) assert status == 400 assert not data["ok"] def test_nu_se_poate_alege_unitatea_din_request(server, systemctl_fals): """Chiar daca cererea cere alt unit, se actioneaza tot pe puntea Discord.""" server.login() server.call("/api/service", {"action": "restart", "service": "ssh.service", "unit": "ssh.service"}) actiuni = [a for a in systemctl_fals.apeluri if a[0] == "restart"] assert actiuni == [("restart", api.SERVICE)] def test_restart_blocat_de_tur_in_zbor(server, state_dir, systemctl_fals): server.login() _scrie_stare(state_dir, {"111": {"inflight": {"turn_id": "t1", "started_at": 1.0}}}) status, data, _ = server.call("/api/service", {"action": "restart"}) assert status == 409 assert data["inflight"] == ["111"] assert not [a for a in systemctl_fals.apeluri if a[0] == "restart"] def test_restart_cu_force_trece_peste_tur(server, state_dir, systemctl_fals): server.login() _scrie_stare(state_dir, {"111": {"inflight": {"turn_id": "t1", "started_at": 1.0}}}) status, data, _ = server.call("/api/service", {"action": "restart", "force": True}) assert status == 200 and data["ok"] assert ("restart", api.SERVICE) in systemctl_fals.apeluri def test_start_nu_cere_force(server, state_dir, systemctl_fals): """`start` nu poate intrerupe nimic, deci nu are de ce sa fie blocat.""" server.login() _scrie_stare(state_dir, {"111": {"inflight": {"turn_id": "t1", "started_at": 1.0}}}) assert server.call("/api/service", {"action": "start"})[0] == 200 def test_esecul_systemctl_ajunge_la_client(server, monkeypatch): server.login() monkeypatch.setattr(api, "_sysctl", lambda *a, timeout=30.0: _cp(rc=1, stderr="Unit not found")) status, data, _ = server.call("/api/service", {"action": "restart"}) assert status == 500 and "Unit not found" in data["error"] # --- stare, jurnale, diagnostic -------------------------------------------- def test_threads_view_marcheaza_tur_in_zbor(state_dir): _scrie_stare(state_dir, { "a": {"cwd": "/workspace/x", "model": "sonnet", "cost_usd_total": 2.5, "last_active": 10, "inflight": {"turn_id": "t"}}, "b": {"cwd": "/workspace/y", "model": "opus", "last_active": 20}, }) view = api.threads_view(api.read_state()) assert [t["thread_id"] for t in view] == ["b", "a"] # sortare desc dupa activitate assert view[1]["inflight"] and not view[0]["inflight"] assert view[1]["cost_usd"] == 2.5 def test_state_corupt_nu_arunca(state_dir): (state_dir / "state.json").write_text("{ nu e json", encoding="utf-8") assert api.read_state() == {} assert api.threads_view(api.read_state()) == [] def test_logs_taie_si_plafoneaza(server, state_dir): server.login() (config.LOG_DIR).mkdir(parents=True, exist_ok=True) api.bot_log().write_text("\n".join(f"linia {i}" for i in range(500)), encoding="utf-8") _, data, _ = server.call("/api/logs?lines=5") assert data["lines"] == [f"linia {i}" for i in range(495, 500)] def test_logs_fisier_inexistent(server): server.login() _, data, _ = server.call("/api/logs?file=infra") assert "nu exista" in data["lines"][0] def test_doctor_prinde_deny_ul_care_taie_ssh(server, state_dir): """Regresia din 2026-08-30: Bash(ssh:*) in deny a taiat accesul la infra.""" server.login() (state_dir / "bot-settings.json").write_text(json.dumps( {"permissions": {"deny": ["Bash(ssh:*)", "Bash(qm destroy:*)"]}}), encoding="utf-8") _, data, _ = server.call("/api/doctor") check = next(c for c in data["checks"] if "deny" in c["name"]) assert not check["pass"] and "Bash(ssh:*)" in check["detail"] def test_doctor_accepta_deny_ul_curatat(server, state_dir): server.login() (state_dir / "bot-settings.json").write_text(json.dumps( {"permissions": {"deny": ["Bash(qm destroy:*)"]}}), encoding="utf-8") _, data, _ = server.call("/api/doctor") check = next(c for c in data["checks"] if "deny" in c["name"]) assert check["pass"] # --- aprobari --------------------------------------------------------------- def _cerere(state_dir, rid="abc123", status="pending"): config.APPROVALS_DIR.mkdir(parents=True, exist_ok=True) (config.APPROVALS_DIR / f"{rid}.json").write_text(json.dumps({ "request_id": rid, "thread_id": "111", "tool_name": "Bash", "command": "rm -rf /var/tmp/x", "rule": "rm_recursiv", "reason": "stergere recursiva", "created_at": time.time(), "expires_at": time.time() + 300, "status": status, }), encoding="utf-8") def test_approvals_arata_doar_pending(server, state_dir): server.login() _cerere(state_dir, "aaa", "pending") _cerere(state_dir, "bbb", "allow") _, data, _ = server.call("/api/approvals") assert [a["request_id"] for a in data["approvals"]] == ["aaa"] assert data["approvals"][0]["expires_in"] > 0 def test_status_numara_aprobarile(server, state_dir): server.login() _cerere(state_dir, "aaa") _, data, _ = server.call("/api/status") assert data["pending_approvals"] == 1 def test_decizie_invalida_e_respinsa(server): server.login() assert server.call("/api/approvals/decide", {"request_id": "aaa", "decision": "poate"})[0] == 400 def test_request_id_cu_traversare_e_respins(server): server.login() assert server.call("/api/approvals/decide", {"request_id": "../../etc/passwd", "decision": "allow"})[0] == 400 def test_decizia_ajunge_in_fisierul_cererii(server, state_dir): server.login() _cerere(state_dir, "aaa") status, data, _ = server.call("/api/approvals/decide", {"request_id": "aaa", "decision": "allow"}) assert status == 200 and data["ok"] scris = json.loads((config.APPROVALS_DIR / "aaa.json").read_text()) assert scris["status"] == "allow" def test_cerere_inexistenta_da_404(server): server.login() assert server.call("/api/approvals/decide", {"request_id": "nuexista", "decision": "allow"})[0] == 404 # --- rute ------------------------------------------------------------------- def test_ruta_necunoscuta(server): server.login() assert server.call("/api/nope")[0] == 404 assert server.call("/api/nope", {})[0] == 404