From 64d1afc76d39d1a622405fc1dfa4cac6f013752a Mon Sep 17 00:00:00 2001 From: migatu Date: Tue, 21 Jul 2026 20:53:13 +0200 Subject: [PATCH] fix(presentation): brakujacy token przy /chart/prompt i /chart/horoscope MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blad z mergu: _auth_headers() dodano na galezi hardeningu, ktora odbila sie od mastera ZANIM powstaly metody prompt() i horoscope() (LOG-29/30, LOG-31). Git zmergowal obie zmiany czysto — byly w roznych liniach — ale semantycznie nowe metody wyszly bez tokenu i dostawaly 401 przy wlaczonej ochronie. Skutek dla uzytkownika: przyciski „Generuj prompt (AI)" i „Napisz horoskop" nie dzialaly po wdrozeniu INTERNAL_TOKEN, mimo ze reszta aplikacji dzialala. - naprawione oba wywolania, - nowy test strukturalny (AST): KAZDE wyjscie HTTP w dol musi niesc headers=. Test jednej metody by tego nie zlapal — regula musi byc pilnowana calosciowo. Straznik zweryfikowany sabotazem: po usunieciu naglowka test pada ze wskazaniem konkretnej linii; po przywroceniu 15/15 przechodzi. Co-Authored-By: Claude Opus 4.8 --- .../presentation/app/clients/logic_client.py | 4 +- .../presentation/tests/test_client_auth.py | 62 +++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 services/presentation/tests/test_client_auth.py diff --git a/services/presentation/app/clients/logic_client.py b/services/presentation/app/clients/logic_client.py index 0fb96fc..3fc4608 100644 --- a/services/presentation/app/clients/logic_client.py +++ b/services/presentation/app/clients/logic_client.py @@ -78,7 +78,7 @@ class LogicClient: if from_date and to_date: payload["from_date"], payload["to_date"] = from_date, to_date with httpx.Client(timeout=max(settings.http_timeout, 60.0)) as client: - r = client.post(f"{self.base_url}/chart/prompt", json=payload) + r = client.post(f"{self.base_url}/chart/prompt", json=payload, headers=_auth_headers()) r.raise_for_status() return r.json() @@ -100,7 +100,7 @@ class LogicClient: if from_date and to_date: payload["from_date"], payload["to_date"] = from_date, to_date with httpx.Client(timeout=max(settings.http_timeout, 300.0)) as client: - r = client.post(f"{self.base_url}/chart/horoscope", json=payload) + r = client.post(f"{self.base_url}/chart/horoscope", json=payload, headers=_auth_headers()) r.raise_for_status() return r.json() diff --git a/services/presentation/tests/test_client_auth.py b/services/presentation/tests/test_client_auth.py new file mode 100644 index 0000000..2ea038b --- /dev/null +++ b/services/presentation/tests/test_client_auth.py @@ -0,0 +1,62 @@ +"""Niezmiennik: KAŻDE wyjście HTTP w dół niesie token międzywarstwowy (LOG-32). + +Powód istnienia tego testu: token dodano do klienta na gałęzi, która odbiła się od +mastera zanim powstały metody `prompt()` i `horoscope()`. Git zmergował obie zmiany +czysto (różne linie), ale nowe metody wyszły BEZ tokenu — i dostawały 401 dopiero na +produkcji. Zwykły test jednej metody by tego nie złapał, więc sprawdzamy regułę +strukturalnie: nie ma wywołania bez `headers=`. +""" +import ast +import pathlib + +CLIENT = pathlib.Path(__file__).resolve().parents[1] / "app" / "clients" / "logic_client.py" + + +def _http_calls(path: pathlib.Path) -> list[tuple[str, int, bool]]: + """(nazwa_metody_http, linia, czy_ma_headers) dla każdego client.post/get.""" + tree = ast.parse(path.read_text(encoding="utf-8")) + out = [] + for node in ast.walk(tree): + if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute): + continue + if node.func.attr not in ("post", "get", "put", "patch", "delete"): + continue + if not (isinstance(node.func.value, ast.Name) and node.func.value.id == "client"): + continue + has_headers = any(kw.arg == "headers" for kw in node.keywords) + out.append((node.func.attr, node.lineno, has_headers)) + return out + + +def test_client_module_exists(): + assert CLIENT.is_file() + + +def test_every_outbound_call_sends_auth_header(): + calls = _http_calls(CLIENT) + assert calls, "nie znaleziono żadnego wywołania HTTP — test przestał cokolwiek pilnować" + missing = [f"{CLIENT.name}:{line} client.{verb}()" for verb, line, ok in calls if not ok] + assert not missing, ( + "Wywołania w dół bez tokenu międzywarstwowego (dostaną 401 przy włączonej " + "ochronie): " + ", ".join(missing) + ) + + +def test_auth_headers_helper_is_lazy(): + """Token czytany przy wywołaniu, nie przy imporcie — inaczej pod wystartowałby + z pustym tokenem, gdyby zmienna pojawiła się później.""" + import os + + from app.clients.logic_client import _auth_headers + + old = os.environ.get("INTERNAL_TOKEN") + try: + os.environ["INTERNAL_TOKEN"] = "abc" + assert _auth_headers() == {"X-Astrololo-Token": "abc"} + os.environ.pop("INTERNAL_TOKEN") + assert _auth_headers() == {} # ochrona wyłączona = brak nagłówka + finally: + if old is not None: + os.environ["INTERNAL_TOKEN"] = old + else: + os.environ.pop("INTERNAL_TOKEN", None)