diff --git a/services/astroklient/app/clients/logic_client.py b/services/astroklient/app/clients/logic_client.py index e968acb..04f3f68 100644 --- a/services/astroklient/app/clients/logic_client.py +++ b/services/astroklient/app/clients/logic_client.py @@ -74,7 +74,12 @@ class LogicClient: """Strumień NDJSON w dół — przekazywany dalej ramka po ramce. Idzie przez szyfrowane łącze jak reszta ruchu (PRE-16): bez tego przy - włączonym LINK_ENCRYPTION_REQUIRED serwer odrzuciłby żądanie.""" + włączonym LINK_ENCRYPTION_REQUIRED serwer odrzuciłby żądanie. + + Nie przechodzi przez `_post`, więc pulę konta dokłada sam — tak samo, + z kontekstu i z pierwszeństwem przed polem przyniesionym w payloadzie. + Bez tego horoskop strumieniowany szukałby w całym udziale.""" + payload = {**payload, "tenant": KONTO.get("")} with httpx.Client(timeout=httpx.Timeout(None, connect=15.0)) as client: yield from link_crypto.stream_lines( client, f"{self.base_url}{sciezka}", diff --git a/services/astroklient/tests/conftest.py b/services/astroklient/tests/conftest.py index b72c519..fe2f343 100644 --- a/services/astroklient/tests/conftest.py +++ b/services/astroklient/tests/conftest.py @@ -80,15 +80,66 @@ print("WYNIK:" + json.dumps({ ''' -@pytest.fixture(scope="module") -def wynik(drzewo): +# Generowania tekstu w tym obrazie nie ma, ale most modułów zostaje — więc jego +# transport musi nieść pulę z sesji tak samo jak wywołania ekranów. Osobna sonda, +# bo dokłada trasę, której w produkcie nie ma: w sondzie produktu zafałszowałaby +# każde pytanie o to, co produkt zawiera. +SONDA_MODULU = ''' +import json, os, hashlib, secrets +sol = secrets.token_bytes(16) +dk = hashlib.scrypt(b"tajne", salt=sol, n=2**14, r=8, p=1, dklen=32) +os.environ.update(ASTROKLIENT_USERS="klient:scrypt$%s$%s" % (sol.hex(), dk.hex()), + SESSION_SECRET="t"*64, COOKIES_INSECURE="1", RATE_LIMIT_PER_MIN="0") +from fastapi.responses import StreamingResponse +from starlette.testclient import TestClient +from app.main import app, logic +from app import security, session, features +from app.clients import logic_client +wyslane = {} +def call_json(client, method, url, payload=None, **kw): + wyslane[url.split("/chart/", 1)[1]] = (payload or {}).get("tenant") + return {} +def stream_lines(client, url, payload=None, **kw): + wyslane[url.split("/chart/", 1)[1]] = (payload or {}).get("tenant") + yield '{"type": "result", "result": {}}' +logic_client.link_crypto.call_json = call_json +logic_client.link_crypto.stream_lines = stream_lines +# Trasa w miejscu modułu: woła ten sam transport co moduł generowania, a strumień +# konsumuje równie leniwie — dopiero przy wysyłaniu odpowiedzi, nie w obsłudze trasy. +features.ROUTES[("POST", "/sonda/modul")] = None +@app.post("/sonda/modul") +def sonda_modul(): + podrobione = {"profile": "natal", "tenant": "ktos-inny"} + logic.wywolaj("/chart/prompt", podrobione, 5) + logic.wywolaj("/chart/horoscope", podrobione, 5) + def relay(): + yield from logic.strumien("/chart/horoscope/stream", podrobione) + return StreamingResponse(relay(), media_type="application/x-ndjson") +c = TestClient(app, follow_redirects=False) +H = {"Cookie": f"{session.COOKIE}={security.issue_session('klient')}"} +r = c.post("/sonda/modul", headers=H) +print("WYNIK:" + json.dumps({"status": r.status_code, "pule": wyslane})) +''' + + +def _uruchom(drzewo: pathlib.Path, sonda: str) -> dict: """Aplikacja uruchomiona w OSOBNYM PROCESIE. Importy Pythona są zapamiętywane w procesie, więc sprawdzanie „produktu bez modułu" tam, gdzie moduł już się zaimportował, dałoby wynik fałszywie pozytywny.""" - r = subprocess.run([sys.executable, "-c", SONDA], cwd=drzewo, capture_output=True, + r = subprocess.run([sys.executable, "-c", sonda], cwd=drzewo, capture_output=True, text=True, env={"PYTHONPATH": str(drzewo), "PATH": "/usr/bin:/bin"}) linia = next((l for l in r.stdout.splitlines() if l.startswith("WYNIK:")), None) assert linia, f"sonda nie wystartowała:\n{r.stdout[-2000:]}\n{r.stderr[-2000:]}" return json.loads(linia[len("WYNIK:"):]) + + +@pytest.fixture(scope="module") +def wynik(drzewo): + return _uruchom(drzewo, SONDA) + + +@pytest.fixture(scope="module") +def wynik_modulu(drzewo): + return _uruchom(drzewo, SONDA_MODULU) diff --git a/services/astroklient/tests/test_zlozenie.py b/services/astroklient/tests/test_zlozenie.py index eb7cd81..4988a38 100644 --- a/services/astroklient/tests/test_zlozenie.py +++ b/services/astroklient/tests/test_zlozenie.py @@ -44,3 +44,12 @@ def test_the_pool_comes_from_the_session_not_from_the_form(wynik): """Pula jedzie z zalogowanego konta. Gdyby brała się z żądania, wystarczyłoby podstawić cudzy login, żeby czytać cudze pliki — sonda właśnie to próbuje.""" assert wynik["pula"] == "klient" + + +def test_module_transport_carries_the_session_pool_too(wynik_modulu): + """Prompt i horoskop idą mostem modułów, a strumień horoskopu obok `_post` — + i właśnie tam pula ginęła, więc horoskop szukałby w całym udziale. Sonda + podsuwa cudzy login w payloadzie; ma wygrać konto z sesji.""" + assert wynik_modulu["status"] == 200 + assert wynik_modulu["pule"] == {"prompt": "klient", "horoscope": "klient", + "horoscope/stream": "klient"}