diff --git a/src/jira_hub/api.py b/src/jira_hub/api.py index d8ee44c..ce5c2b2 100644 --- a/src/jira_hub/api.py +++ b/src/jira_hub/api.py @@ -1,5 +1,6 @@ """HTTP API хаба (stdlib http.server, поток на запрос, соединение SQLite на запрос). -Маршруты /agent/* — агентский токен, остальные — клиентский (specs/hub-api/spec.md).""" +Маршруты /agent/* — агентский токен, остальные — клиентский (specs/hub-api/spec.md). +Страница доски (GET /, GET /links) — без токена: статический файл без данных.""" import hmac import json import logging @@ -9,6 +10,7 @@ import time import traceback from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer from pathlib import Path +from typing import NamedTuple from urllib.parse import parse_qs, urlparse from . import backup, db, mirror, queue, relay, settings @@ -20,9 +22,16 @@ from .queue import ClaimedError log = logging.getLogger(__name__) MAX_BODY = 16 * 1024 * 1024 # предел тела запроса, байт +PAGE = Path(__file__).parent / "web" / "links.html" # страница доски связей; читается на каждый запрос SOCKET_TIMEOUT = 60 # чтение запроса; ожидание долгого claim идёт уже после чтения тела и сокет не держит +class Raw(NamedTuple): + """Не-JSON ответ обработчика (страница доски).""" + content_type: str + data: bytes + + class ApiError(Exception): def __init__(self, status, error, detail=None): super().__init__(error) @@ -142,6 +151,10 @@ def h_board(hub, conn, m, q, body): return 200, mirror.board(conn, settings.get(conn, "base_jql")) +def h_page(hub, conn, m, q, body): + return 200, Raw("text/html; charset=utf-8", PAGE.read_bytes()) + + def h_note_put(hub, conn, m, q, body): fields = {k: v for k, v in body.items() if k != "updated_by"} try: @@ -249,6 +262,7 @@ def h_settings_put(hub, conn, m, q, body): ROUTES = [ + ("GET", r"^/(links)?$", h_page, None), ("POST", r"^/agent/claim$", h_claim, "agent"), ("GET", r"^/agent/settings$", h_agent_settings, "agent"), ("GET", r"^/agent/commands/(\d+)$", h_agent_command, "agent"), @@ -318,7 +332,8 @@ class Handler(BaseHTTPRequestHandler): match = rx.match(url.path) if not match: continue - self._auth(scope) + if scope is not None: + self._auth(scope) try: body = json.loads(raw.decode("utf-8")) if raw else {} except ValueError as e: # битый JSON или не UTF-8 @@ -339,10 +354,16 @@ class Handler(BaseHTTPRequestHandler): self._send(500, {"error": "internal", "detail": str(e)}) def _send(self, status, payload): - data = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8") + if isinstance(payload, Raw): + ctype, data, extra = payload.content_type, payload.data, {"Cache-Control": "no-cache"} + else: + ctype, extra = "application/json; charset=utf-8", {} + data = json.dumps(payload, ensure_ascii=False, default=str).encode("utf-8") self.send_response(status) - self.send_header("Content-Type", "application/json; charset=utf-8") + self.send_header("Content-Type", ctype) self.send_header("Content-Length", str(len(data))) + for name, value in extra.items(): + self.send_header(name, value) self.end_headers() self.wfile.write(data) diff --git a/src/jira_hub/web/links.html b/src/jira_hub/web/links.html index aee532b..a9678ae 100644 --- a/src/jira_hub/web/links.html +++ b/src/jira_hub/web/links.html @@ -1,3 +1,5 @@ + + Спринт 2 линк diff --git a/tests/test_api.py b/tests/test_api.py index cd80506..2545102 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,5 +1,7 @@ import json import threading +import urllib.error +import urllib.request import pytest @@ -363,3 +365,23 @@ def test_board_route(hub): with pytest.raises(HubError) as e: client.get("/board") assert e.value.status == 401 + + +def _raw_get(base, path): + with urllib.request.urlopen(base + path, timeout=10) as resp: + return resp.status, resp.headers, resp.read().decode("utf-8") + + +def test_page_served_without_token(hub): + hub["agent"].post("/agent/state", {"issues": [{"key": "ZZTEST-9001", "summary": "тема из зеркала", "labels": []}], + "links": []}) + for path in ("/", "/links"): + status, headers, body = _raw_get(hub["base"], path) + assert status == 200 + assert headers["Content-Type"] == "text/html; charset=utf-8" + assert headers["Cache-Control"] == "no-cache" + assert body.lstrip().lower().startswith("") + assert "ZZTEST-9001" not in body and "тема из зеркала" not in body + with pytest.raises(urllib.error.HTTPError) as e: + urllib.request.urlopen(hub["base"] + "/links/x", timeout=10) + assert e.value.code == 404