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 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>
256 lines
11 KiB
Python
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, '<')" 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
|