"""Łącze do usługi render i wariant kosmogramu do druku (PRE-24). Przez to łącze leci CAŁY raport: dane urodzeniowe i opisy z baz. Musi być szyfrowane tak samo jak pozostałe, WŁASNYM kluczem — i musi obowiązywać ten sam niezmiennik co dla klienta logiki: żadnego wyjścia w dół bez tokenu i klucza. """ import pathlib import pytest APP = pathlib.Path(__file__).resolve().parents[1] / "app" CLIENT = APP / "clients" / "render_client.py" SRC = CLIENT.read_text(encoding="utf-8") WHEEL = (APP / "chartwheel.py").read_text(encoding="utf-8") MAIN = (APP / "main.py").read_text(encoding="utf-8") COMPILE_JS = (APP / "static" / "compile.js").read_text(encoding="utf-8") # ─────────────────────────── szyfrowanie łącza ─────────────────────────── def test_uses_its_own_third_key(): """Osobny klucz na parę rozmówców. Przejęcie klucza renderu nie może otwierać łącza do logiki ani do danych.""" from app import link_crypto assert link_crypto.ENV_PRESENTATION_RENDER == "LINK_KEY_PRESENTATION_RENDER" assert link_crypto.ENV_PRESENTATION_RENDER != link_crypto.ENV_PRESENTATION_LOGIC assert link_crypto.ENV_PRESENTATION_RENDER != link_crypto.ENV_LOGIC_DATA assert "ENV_PRESENTATION_RENDER" in SRC def test_report_goes_through_the_encrypted_link(): assert "link_crypto.call(" in SRC assert "link=_link()" in SRC def test_every_outbound_call_carries_token_and_key(): """Ten sam niezmiennik co przy kliencie logiki — tam brak nagłówka wyszedł dopiero na produkcji (401).""" import ast tree = ast.parse(SRC) calls = [n for n in ast.walk(tree) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr in ("call", "call_json")] assert calls, "nie znaleziono wywołania przez link_crypto" for c in calls: kwargs = {k.arg for k in c.keywords} assert "headers" in kwargs and "link" in kwargs def test_health_is_the_only_unencrypted_path(): """/health musi zostać otwarty dla sond k8s — ale nic poza nim.""" from app import link_crypto assert "/health" in link_crypto.PUBLIC_PATHS raw = [line for line in SRC.splitlines() if "client.get(" in line or "client.post(" in line] assert all("/health" in line for line in raw), f"surowe wywołania poza /health: {raw}" def test_key_is_read_lazily(): """Konfiguracja może się zmienić bez restartu procesu (podmiana sekretu).""" assert "def _link()" in SRC and "key_from_env" in SRC # ──────────────────────── kosmogram w wariancie do druku ───────────────── def test_pdf_route_uses_print_theme(): """Samodzielny konwerter SVG→PDF nie zna naszego arkusza stylów.""" assert 'chartwheel.render(chart, theme="print")' in MAIN def test_print_theme_has_no_css_variables(): """`var(--…)` w SVG poza stroną nie rozwiąże się — wyszłyby czarne albo żadne kreski.""" from app import chartwheel chart = _sample_chart() printed = chartwheel.render(chart, theme="print") assert printed and "var(--" not in printed def test_screen_theme_still_uses_css_variables(): """Na stronie koło ma się dostrajać do motywu aplikacji.""" from app import chartwheel assert "var(--" in chartwheel.render(_sample_chart(), theme="screen") def test_print_theme_embeds_the_glyph_font(): """Font glifów podaje CSS klasy `.glyph` — poza stroną trzeba go wpisać W SAM rysunek, inaczej kosmogram w PDF wyszedłby BEZ symboli.""" from app import chartwheel printed = chartwheel.render(_sample_chart(), theme="print") assert 'font-family=' in printed assert "DejaVu Sans" in printed # font obecny w obrazie usługi render def test_screen_theme_leaves_font_to_css(): from app import chartwheel assert "font-family=" not in chartwheel.render(_sample_chart(), theme="screen") def test_unknown_theme_falls_back_to_screen(): from app import chartwheel assert chartwheel.render(_sample_chart(), theme="cokolwiek") == \ chartwheel.render(_sample_chart(), theme="screen") # ─────────────────────────── składanie po stronie klienta ──────────────── def test_browser_sends_all_three_parts(): for field in ("person", "natal", "predictions"): assert field in COMPILE_JS, f"raport wysyłany bez części: {field}" def test_pdf_button_exists(): tpl = (APP / "templates" / "compile.html").read_text(encoding="utf-8") assert 'id="pdfBtn"' in tpl def test_wheel_failure_does_not_block_the_report(): """Brak rysunku ma dać raport bez kosmogramu, a nie brak raportu — tekst kosztował wywołanie modelu, rysunek policzymy zawsze.""" assert "wheel_svg = \"\"" in MAIN or 'wheel_svg, = ""' in MAIN assert "Brak rysunku nie może zablokować raportu" in MAIN # ─────────────────────────────────── pomocnicze ────────────────────────── def _sample_chart(): signs = ["Aries", "Taurus", "Gemini", "Cancer", "Leo", "Virgo", "Libra", "Scorpio", "Sagittarius", "Capricorn", "Aquarius", "Pisces"] glyphs = ["♈", "♉", "♊", "♋", "♌", "♍", "♎", "♏", "♐", "♑", "♒", "♓"] asc = 128.9 asc_sign = int(asc // 30) return { "angles": {"Asc": {"name": "Asc", "decimal": asc, "sign": signs[asc_sign]}, "MC": {"name": "MC", "decimal": 5.0}, "Dsc": {"name": "Dsc", "decimal": (asc + 180) % 360}, "IC": {"name": "IC", "decimal": 185.0}}, "cusps": [{"house": i + 1, "sign": signs[(asc_sign + i) % 12], "decimal": (asc_sign * 30 + i * 30) % 360} for i in range(12)], "sign_glyphs": [{"sign": s, "glyph": g} for s, g in zip(signs, glyphs)], "positions": [{"name": "Sun", "decimal": 40.2, "glyph": "☉", "direction": "D"}], }