c1f5bea9f7
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Trzeci z pięciu kroków. Zmieniłem jego zakres wobec planu i warto wiedzieć dlaczego: pierwotnie miała to być deduplikacja pięciu kopii link_crypto.py, ale po zrobieniu PR 2 widać, że astroklienta blokuje co innego — ekrany, których nie ma mieć, siedzą wewnątrz jednego main.py. Deduplikacja kryptografii jest realnym długiem, ale niczego nie blokuje. PODZIAŁ. main.py (1023 linie) rozpadł się na podstawa.py (wspólne obiekty i pomocnicy), jedenaście modułów w app/ekrany/ i main.py, który jest już samym ZŁOŻENIEM: lista importów JEST definicją produktu. Podział zrobiony mechanicznie, z osobnym sprawdzeniem, że żadna sekcja nie wołała pomocnika z innej (nie wołała). KATALOG Z REJESTRACJI. Dotąd wszystkie funkcje były wypisane w features.py, więc obraz produktu, który części z nich nie ma, i tak niósł ich nazwy — spis funkcji, których nie ma jak włączyć. Teraz ekran zgłasza siebie, swoje trasy i swoje zasoby przy imporcie własnego modułu, a features.py nie wymienia ani jednego ekranu. Kolejność w nawigacji jest jawna (`kolejnosc`), żeby nie rządziła nią kolejność importów. To samo dotyczy nawigacji administratora: odsyłacz do ekranu kont był wpisany na sztywno w base.html, więc w węższym produkcie zostawał martwy link i nazwa ekranu, którego nie ma. EKSPORT JAKO MODUŁ. Zgodnie z ustaleniem eksport jest funkcją administracyjną, więc musi dać się usunąć. app/moduly/eksport/ zabiera arkusz, trasę PDF-a i akcję formularza. „Można, ale nie temu kontu" i „nie ma takiej możliwości" to dwie różne gwarancje, a eksport wynosi najwięcej treści baz naraz. MOST ODKRYWA MODUŁY. Skoro modułów jest więcej niż jeden, most nie może ich znać z nazwy — nazwa nieobecnego modułu jechałaby do obrazu, w którym go nie ma. Przechodzi więc po podkatalogach app/moduly/ i pyta każdy, co wnosi. Katalog generowania przeniesiony z app/dodatki na app/moduly/dodatki. ZNALEZIONE PRZY OKAZJI. Po wydzieleniu eksportu okazało się, że jego ścieżki SZCZĘŚLIWEJ nie sprawdzał żaden test — badano wyłącznie odmowę dla konta bez uprawnienia. Moduł dostaje zależności z wywołania montującego, więc brak jednej z nich wyszedłby dopiero przy pierwszym kliknięciu. Dopisany test funkcjonalny (realny arkusz, sprawdzany aż do nagłówka ZIP-a) i brakujące zależności. Cztery komentarze w plikach współdzielonych wymieniały zakładkę „Skompiluj", w tym wheelzoom.js ze wzmianką o „przyszłej zakładce" — to samo zgłoszenie, które audyt podnosił wcześniej. TEST ZŁOŻENIA. Buduje węższy produkt NAPRAWDĘ: kopiuje drzewo, usuwa cztery ekrany i oba moduły, uruchamia aplikację w OSOBNYM PROCESIE (importy są zapamiętywane, więc sprawdzanie tego w procesie, który moduł już zaimportował, dawałoby wynik fałszywie pozytywny) i sprawdza, że wstaje, że zachowane ekrany oddają 200, że usunięte oddają 404 (nie 403 i nie 500), że katalog opisuje ten obraz, i że w nawigacji nie ma martwych odsyłaczy. Napisałem najpierw ostrzejszy test — „nazwa ekranu nie pada poza jego modułem" — i go wyrzuciłem: zgłaszał wzmianki o Horoskopie w plikach współdzielonych, choć astroklient Horoskop MA. Ślad ma znaczenie wyłącznie wobec konkretnego złożenia, więc sprawdzenie należy do produktu, nie do mechanizmu. Testy: presentation 368, logic 342, data 37, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
157 lines
6.3 KiB
Python
157 lines
6.3 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
|
|
|
|
from tests.skladanie import zloz_kod
|
|
|
|
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")
|
|
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
|
MAIN = zloz_kod()
|
|
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.
|
|
|
|
Sprawdzamy, że trasa PDF-a w ogóle podaje motyw druku — bez wiązania się
|
|
z dokładnym kształtem wywołania (dochodzą do niego kolejne argumenty).
|
|
Pełny sprawdzian, że KAŻDY rysunek dostaje 'print', jest w test_compile.py."""
|
|
assert '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"}],
|
|
}
|