Files
astrololo/services/presentation/tests/test_compile.py
T
gitea 338ec24e90
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
astroklient: własna nazwa produktu zamiast odziedziczonej po rdzeniu
Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule
karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi
wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da
się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie,
gdy produkt sam się przedstawia nazwą rodzica.

Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są
WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na
administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka
produktu nadpisuje; szablony o nią pytają, zamiast ją znać.

astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo".

DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach
JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie
`astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na
neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny
produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane
wersje robocze formularza.

Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między
warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje
404 także zalogowanemu.

TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa
rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy
i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści
odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy
kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji.

Sprawdzony w obie strony: po cofnięciu nakładki test pada.

Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42,
render 41.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 16:37:03 +02:00

256 lines
11 KiB
Python

"""Zakładka „Skompiluj" — zbiorczy raport (PRE-23).
Raport składa się z trzech kawałków liczonych w różnych miejscach: horoskopu
(serwer), interpretacji natalnej i predykcji okresowych (magazyn przeglądarki).
Pilnujemy, żeby zakładka umiała je zebrać, a przede wszystkim — żeby POWIEDZIAŁA,
czego brakuje. Niekompletny raport, o którym dowiadujesz się dopiero z PDF-a,
byłby najgorszym wynikiem.
"""
import pathlib
from starlette.testclient import TestClient
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
from tests.skladanie import zloz, zloz_kod
# Szablon z rozwiniętymi wstawkami modułów — patrz tests/skladanie.py.
TPL = zloz("compile.html")
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
# Skrypty generowania mieszkają w module `app/ai/` — katalogu, którego
# w obrazie bez tej funkcji po prostu nie ma.
NATAL_JS = (APP / "moduly" / "dodatki" / "static" / "natal.js").read_text(encoding="utf-8")
INTERPRET = zloz("interpret.html")
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
MAIN = zloz_kod()
def _client():
import os
os.environ.pop("APP_PASSWORD", None) # bez bramki logowania w teście
from app.main import app
return TestClient(app)
# ───────────────────────────────── nawigacja i trasa ─────────────────────
def test_tab_is_in_the_menu():
"""Zakładka jest w KATALOGU FUNKCJI — stamtąd buduje się nawigacja.
Menu nie jest już listą wpisaną w base.html: powstaje z uprawnień konta
(PRE-26), więc szukanie w szablonie sztywnego linku nic by nie mówiło.
Że nawigacja faktycznie renderuje katalog, sprawdza test_kontrola_dostepu."""
from app import features
tab = features.BY_KEY["compile"]
assert tab in features.SCREENS
assert tab.href and tab.label
def test_get_renders_empty_form():
r = _client().get("/compile")
assert r.status_code == 200
assert "Złóż podsumowanie" in r.text
def test_routes_exist():
assert '@app.get("/compile"' in MAIN and '@app.post("/compile"' in MAIN
# ─────────────────────────── kolejność w raporcie ────────────────────────
def test_report_order_matches_the_brief():
"""Prośba partnerów: najpierw imię i nazwisko, potem wprowadzone dane, potem
RYSUNEK kosmogramu, a dopiero po nim interpretacja natalna i predykcje."""
person = TPL.index('id="reportPerson"')
wheel = TPL.index("wheel_svg")
natal = TPL.index('id="reportNatal"')
preds = TPL.index('id="reportPredictions"')
assert person < wheel < natal < preds
def test_person_name_heads_the_report():
assert 'id="reportPerson"' in TPL
assert "form.person" in TPL
# ───── wszystkie rysunki w raporcie i w PDF (nie tylko koło) ─────
# Zasada: co pokazujemy na stronie, ma trafić do podsumowania i do PDF-a.
def test_report_shows_every_chart_figure():
"""Raport «Skompiluj» pokazuje wszystkie rysunki, nie tylko koło."""
for cls in ("wheel-fig", "aspectarian-fig", "declination-fig", "antiscia-fig"):
assert cls in TPL, f"brak {cls} w raporcie"
def test_compile_page_renders_all_figures():
"""Handler /compile liczy wszystkie rysunki do podglądu raportu."""
for var in ("aspectarian_svg", "declination_svg", "antiscia_svg"):
assert var in MAIN, f"/compile nie ustawia {var}"
def test_pdf_bundles_all_figures_in_print_theme(monkeypatch):
"""compile_pdf składa KOMPLET rysunków w motywie DRUKU.
Sprawdzamy przez WYWOŁANIE trasy, nie przez szukanie tekstu w main.py:
poprzednia wersja greppowała `render(chart, theme="print")` i pękała przy
każdym dopisaniu argumentu, mimo że zachowanie zostawało poprawne."""
seen: dict[str, str] = {}
from app import chartwheel
from app.clients.render_client import RenderClient
from app.main import logic
for name in ("render", "render_aspectarian", "render_declination", "render_antiscia"):
original = getattr(chartwheel, name)
def spy(chart, theme="screen", _n=name, _o=original, **kw):
seen[_n] = theme
return _o(chart, theme=theme, **kw)
monkeypatch.setattr(chartwheel, name, spy)
monkeypatch.setattr(logic, "positions", lambda **kw: _pdf_sample_chart())
monkeypatch.setattr(RenderClient, "pdf", lambda self, report: b"%PDF-1.4 stub")
r = _client().post("/compile/pdf", json={"person": "Jan", "data": {
"date": "1984-04-30", "time": "11:20", "tz_offset": 2, "lat": 50.06, "lon": 19.94}})
assert r.status_code == 200, r.text[:300]
assert set(seen) == {"render", "render_aspectarian",
"render_declination", "render_antiscia"}, seen
assert set(seen.values()) == {"print"}, seen
def _pdf_sample_chart() -> dict:
cusps = [{"house": i + 1, "sign": "Aries", "in_sign": "0", "decimal": float(i * 30),
"sign_glyph": "♈"} for i in range(12)]
ang = {k: {"name": k, "sign": "Aries", "in_sign": "0", "decimal": 0.0,
"sign_glyph": "♈"} for k in ("Asc", "MC", "Dsc", "IC")}
return {"engine": "test", "positions": [], "cusps": cusps, "angles": ang,
"sign_glyphs": [{"sign": "Aries", "glyph": "♈"}], "house_system": "equal"}
# ──────────────────────── zbieranie materiału z magazynów ────────────────
def test_reads_stores_through_their_api_not_raw_storage():
"""Format danych ma jednego właściciela — moduł, który je zapisuje.
Sięganie tu wprost do localStorage rozjechałoby się przy pierwszej zmianie."""
assert "window.kosmogramNatal" in JS
assert "window.kosmogramPredictions" in JS
# chodzi o brak BEZPOŚREDNIEGO dostępu, nie o samo słowo (pada w komentarzu)
assert "localStorage.getItem" not in JS
assert "localStorage.setItem" not in JS
def test_stores_are_loaded_on_the_tab():
for src in ("natal.js", "predictions.js", "compile.js"):
assert src in TPL, f"brak {src} na zakładce"
def test_stores_load_before_the_assembler():
"""Szukamy TAGÓW skryptów, nie samych nazw — te padają też w komentarzach.
Statyka jedzie przez helper cache-bustingu (PRE-26): src="{{ static('X') }}"."""
def tag(name):
return TPL.index("static('" + name + "')")
assert tag("natal.js") < tag("compile.js")
assert tag("predictions.js") < tag("compile.js")
# ─────────────────────────── mówi, czego brakuje ─────────────────────────
def test_shows_what_is_missing():
assert 'id="readiness"' in TPL
assert "rdy-missing" in JS
for hint in ("Interpretacje", "Kalendarz"):
assert hint in JS, f"brak podpowiedzi, gdzie uzupełnić: {hint}"
def test_all_three_parts_are_checked():
for label in ("Policzony horoskop", "Interpretacja natalna", "Predykcje okresowe"):
assert label in JS
# ───────────────────────────── natalna zapamiętywana ─────────────────────
def test_natal_is_captured_on_interpret_tab():
assert "natal.js" in INTERPRET
assert 'id="natalNote"' in INTERPRET
def test_natal_store_keeps_a_single_entry():
"""Natalna jest JEDNA — dotyczy momentu urodzenia, nie okresu. Ponowne
wygenerowanie ma podmienić, nie dokładać."""
assert "setItem" in NATAL_JS and "push" not in NATAL_JS
def test_natal_only_stores_natal_profile():
assert "'natal'" in NATAL_JS
def test_full_storage_is_reported():
assert "pełny" in NATAL_JS
# ─────────────────────────────── bezpieczeństwo ──────────────────────────
def test_ai_text_is_escaped_before_injection():
"""Treść od modelu wstawiamy do DOM — bez ucieczki byłby to wektor wstrzyknięcia."""
assert "function esc" in JS
assert "replace(/</g, '&lt;')" in JS
def test_does_nothing_on_other_tabs():
assert "if (!document.getElementById('readiness')) return;" in JS
# ─────── podsumowanie NIE gubi opcji policzonych przy horoskopie ──────────
# Regresja 2026-07-28: „Skompiluj" przeliczało horoskop bez stacji/tabel/
# porównania domów, więc podsumowanie miało braki względem tego, co policzono.
def test_compile_form_carries_the_options():
"""Te same opcje co „Horoskop" — z WSPÓLNEGO pliku (formsync je synchronizuje)."""
assert '{% include "_form_options.html" %}' in TPL
opts = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8")
for f in ('name="stations"', 'name="tables"', 'name="house_systems"'):
assert f in opts, f"brak opcji we wspólnym pliku: {f}"
def test_compile_handler_recomputes_with_all_options(monkeypatch):
"""„Skompiluj" przelicza horoskop z KOMPLETEM opcji z formularza.
To jest ta regresja z 2026-07-28: podsumowanie liczyło się z okrojonym
zestawem i miało braki względem tego, co widać na „Horoskopie". Sprawdzamy
przez wywołanie trasy, nie przez szukanie napisów w main.py."""
seen = {}
from app.main import app, logic
monkeypatch.setattr(logic, "positions", lambda **kw: (seen.update(kw), _pdf_sample_chart())[1])
r = _client().post("/compile", data={
"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
"lat": "50.06", "lon": "19.94", "house_system": "koch",
"house_systems": ["koch", "equal"], "stations": "true", "tables": "true",
"zodiac": "sidereal_lahiri", "aspect_minor": "true"})
assert r.status_code == 200, r.text[:300]
assert seen["house_systems"] == ["koch", "equal"]
assert seen["stations"] is True and seen["tables"] is True
assert seen["zodiac"] == "sidereal_lahiri" and seen["aspect_minor"] is True
def test_compile_pdf_recomputes_with_all_options():
assert 'stations=bool(data.get("stations"))' in MAIN
assert 'house_systems=[s for s in (data.get("house_systems")' in MAIN
def test_compile_pdf_payload_includes_options():
assert "house_systems: multi('house_systems')" in JS
assert "stations: checked('stations')" in JS
def test_summary_uses_shared_result_tables():
"""Podsumowanie pokazuje te same tabele co horoskop — jeden wspólny plik,
żeby nie mogły znowu się rozjechać."""
assert '{% include "_result_tables.html" %}' in TPL