f5dec15e4d
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 11m13s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m49s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 29s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 20s
build-render / build (push) Successful in 44s
build / build (push) Successful in 54s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 11m19s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m59s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 34s
Testy / Kontrola składni wszystkich warstw (push) Successful in 25s
Zasada: co składamy, dodajemy i wyświetlamy, MA trafiać do finalnego raportu
(«Skompiluj») i do PDF-a — nie tylko na stronę /chart. Do tej pory do raportu i
druku szło samo koło; aspektarian (etap 5) i deklinacja/antyscja (etap 6) były
tylko w podglądzie horoskopu.
RAPORT «Skompiluj» (podgląd): handler /compile liczy teraz wszystkie cztery
rysunki, a szablon je pokazuje (koło, aspektarian, deklinacja, antyscja).
PDF (usługa render) — uogólnienie z jednego rysunku na LISTĘ:
- Kontrakt raportu: `figures = [{svg, caption}]` (uporządkowana). `wheel_svg`
zostaje dla zgodności wstecznej jako pojedynczy rysunek.
- `compile.py`: każdy SVG osobno przez rsvg-convert → PDF (fig0…figN); braki/
błędy pomijane (rysunek nie może wywalić raportu, tekst ważniejszy).
- `latex.py build()`: rysunki w sekcji 3 (po danych, przed natalną) w PODANEJ
kolejności, każdy z podpisem. Jedna reguła składania: keepaspectratio z limitem
szerokości i wysokości — kwadratowe (koło, aspektarian) ogranicza wysokość,
szerokie (deklinacja, antyscja) szerokość, bez zniekształceń.
- `compile_pdf` (prezentacja): renderuje komplet w motywie DRUKU i wysyła jako
`figures`.
Testy: render +5 (kolejność, podpisy, zgodność wsteczna, pomijanie pustych),
prezentacja +3 (raport pokazuje komplet, PDF składa komplet). Render 32,
prezentacja 173.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
"""Łą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 rysunków ma dać raport bez nich, a nie brak raportu — tekst kosztował
|
|
wywołanie modelu, rysunki policzymy zawsze."""
|
|
assert "figures: list[dict] = []" in MAIN
|
|
assert "Brak rysunków 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"}],
|
|
}
|