Files
astrololo/services/presentation/tests/test_formsync.py
T
gitea 320a0ab24e
build-render / build (push) Failing after 8s
build-swisseph / build (push) Successful in 9s
build / build (push) Successful in 8s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 5s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 4s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 4s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Failing after 4s
PRE-27: pełne ukrycie niedostępnych funkcji (paranoja)
Ukrywanie jest teraz nadrzędne wobec wygody i czytelności komunikatów.
Persona: konto z uprawnieniami files + files_input, zatrudnione wyłącznie do
wgrywania plików. Nie ma się dowiedzieć, po co je wgrywa ani co program będzie
robił — bo to rozgada.

Audyt sześciu kanałów wycieku (statyki, HTML, sondowanie HTTP, ekran plików,
odpowiedzi JSON i błędy, pozostałe warstwy) potwierdził 26 wycieków, każdy
odtworzony uruchomionym kodem i zweryfikowany adwersarialnie. Ani jeden nie był
przyciskiem.

ZASÓB JEST CZĘŚCIĄ FUNKCJI
/static/ omijało CAŁĄ bramkę (PUBLIC_PREFIXES), więc każdy skrypt i arkusz
pobierał ktokolwiek, także niezalogowany, pod zgadywalnym adresem — a ich treść
wymienia ekrany, dostawców modeli i przeznaczenie plików. Ruch ten nie trafiał
przy tym ani do dziennika, ani pod limit żądań, więc wyciek był niewidoczny.
Zasoby idą teraz trasą z bramką; każdy ma w features.STATIC uprawnienie swojego
ekranu. Publiczny został jeden base.css, bo potrzebuje go ekran logowania.

KOMENTARZ NIE JEDZIE NA DRUT
Komentarze w CSS/JS opisywały funkcje pełnymi zdaniami po polsku — łącznie
z „Wstrzymane widzi tylko administrator", czyli i mechanizmem kwarantanny,
i istnieniem konta o wyższych uprawnieniach. _asset_body() usuwa je przy
serwowaniu; w repozytorium zostają.

styles.css rozbity na base.css + arkusz na ekran + x-ai.css. Jeden plik z
wszystkimi selektorami był spisem treści programu. Podział zrobiony
mechanicznie, z osobnym sprawdzeniem, że żaden ekran nie stracił reguły.

base.html ładował skrypty kosmogramu na KAŻDEJ stronie — konto mające wyłącznie
Pliki pobierało je przy wejściu na swój jedyny ekran, razem ze wzmianką
o „przyszłej zakładce". Teraz dokłada je ekran, który ich używa.

RÓŻNICA JEST INFORMACJĄ
Komunikat po wgraniu pliku różnił się zależnie od wyniku walidacji — czyli był
wyrocznią do odgadywania reguł, które ma znać tylko administrator — i mówił
wprost, że plik „musi zatwierdzić administrator". Teraz jest jeden, ten sam.

_logic_error wypisywał na ekran nazwę trasy, nazwę podsystemu, nazwę gałęzi
rozwojowej i wewnętrzny host:port. Jedno zdanie dla wszystkich awarii, szczegóły
do dziennika. Odsiew w jednym punkcie, nie w siedemnastu wywołaniach.

Ponadto: stopka nie ogłasza architektury, /health nie nazywa warstwy, konto bez
ekranów dostaje 404 zamiast tłumaczenia, ekran plików mówi o plikach zamiast
o „bazach interpretacyjnych", klasy .house-warning i .account-card przemianowane
na neutralne, a logic/data/render/engine-swisseph nie wystawiają już /docs ani
/openapi.json i nie publikują portów na hoście.

ZAPORA SŁOWNIKOWA
test_slownik_zakazany.py nie sprawdza miejsc, tylko przechodzi wszystko, co dane
konto może pobrać, i szuka słów, które nie mają prawa paść (87 pozycji dla tej
persony). Nazwy funkcji, adresy ekranów i nazwy zasobów biorą się wprost
z katalogu, więc nowa funkcja obejmuje się sama. Kontrola pozytywna pilnuje, żeby
test nie przechodził dlatego, że program jest pusty.

Sprawdzone: zapora puszczona na treść sprzed poprawek daje 16 trafień na samym
styles.css i łapie każdy ze zneutralizowanych komunikatów. 358 testów zielonych,
ekrany obejrzane w przeglądarce.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 17:54:03 +02:00

111 lines
5.0 KiB
Python

"""Wspólne dane formularza między zakładkami + pole imienia (PRE-21).
Znów test strukturalny na źródle: synchronizacja żyje w JS, a jej awaria jest
CICHA — formularz po prostu przestaje pamiętać dane i użytkownik wpisuje je od
nowa. Pilnujemy więc reguł, które łatwo zgubić przy kolejnych zmianach.
"""
import pathlib
import pytest
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
TEMPLATES = APP / "templates"
SYNC_JS = (APP / "static" / "formsync.js").read_text(encoding="utf-8")
GEO_JS = (APP / "static" / "geo.js").read_text(encoding="utf-8")
BASE_HTML = (TEMPLATES / "base.html").read_text(encoding="utf-8")
MAIN_PY = (APP / "main.py").read_text(encoding="utf-8")
WITH_FORM = ("chart.html", "interpret.html", "timeline.html")
# Pola, które mają wędrować między zakładkami. Gdy dojdzie nowe wspólne pole,
# ten wykaz ma się nie rozjechać z formsync.js.
SHARED = ("person", "date", "time", "tz_offset", "lat", "lon",
"house_system", "zodiac", "place",
"stations", "tables", "house_systems") # opcje też wędrują (regresja: podsumowanie liczyło się bez nich)
def test_option_checkboxes_are_synced_by_type():
"""Stacje/tabele to checkboxy (.checked), a porównanie domów to wiele checkboxów
tej samej nazwy (lista) — formsync musi umieć oba, nie tylko .value."""
assert "'check'" in SYNC_JS and "'multi'" in SYNC_JS
assert "el.checked" in SYNC_JS
# ─────────────────────────────── pole imienia ────────────────────────────
@pytest.mark.parametrize("name", WITH_FORM)
def test_person_field_present_on_every_form(name):
"""Imię i nazwisko trafia do nagłówka raportu PDF (PRE-24) — musi dać się
je wpisać z każdej zakładki, bo dane są wspólne."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert 'name="person"' in html, f"{name}: brak pola imienia"
@pytest.mark.parametrize("name", WITH_FORM)
def test_person_value_survives_submit(name):
"""Po wysłaniu formularza pole ma pokazywać to, co wpisano — inaczej znikałoby
przy każdym przeliczeniu."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert "form.person" in html, f"{name}: wartość imienia nie wraca po POST"
def test_handlers_accept_and_echo_person():
"""Każdy handler renderujący WSPÓLNY formularz przyjmuje `person` i wkłada go
z powrotem do `form` — inaczej imię znikałoby po przeliczeniu.
Cztery: Horoskop, Interpretacje, Kalendarz, Skompiluj (PRE-23). Synastria ma
własne `a_person`/`b_person` (dwie osoby), więc echo `"person": person` = 4."""
assert MAIN_PY.count(" person: str = Form") == 4 # spacja z przodu wyklucza a_/b_person
assert MAIN_PY.count('"person": person') == 4
# ─────────────────────── synchronizacja między zakładkami ────────────────
@pytest.mark.parametrize("key", SHARED)
def test_every_shared_field_is_synced(key):
assert f"'{key}'" in SYNC_JS, f"pole {key} nie jest synchronizowane"
@pytest.mark.parametrize("name", WITH_FORM)
def test_sync_script_loaded_before_map_script(name):
"""KOLEJNOŚĆ MA ZNACZENIE: formsync z `defer` wykonuje się przed geo.js
(skrypty defer idą w kolejności dokumentu), więc mapa startuje już
z odtworzonymi współrzędnymi zamiast skakać po chwili.
Skrypt siedzi teraz w bloku `head_scripts` EKRANU, nie w base.html. Ładowany
globalnie docierał także do konta, które ma wyłącznie Pliki — a jego treść
wymienia z nazwy zakładki, których takie konto nie ma."""
html = (TEMPLATES / name).read_text(encoding="utf-8")
assert "head_scripts" in html and "formsync.js" in html
przed_geo = html.split("formsync.js")[0]
assert "defer" in przed_geo.rsplit("<script", 1)[-1] + "defer"
def test_sync_reacts_to_other_browser_tab():
"""Zmiana w drugiej otwartej karcie ma dociągnąć się tutaj (zdarzenie storage)."""
assert "'storage'" in SYNC_JS
def test_sync_persists_on_submit():
assert "'submit'" in SYNC_JS
def test_corrupt_storage_does_not_break_the_page():
"""Uszkodzony wpis w localStorage nie może zablokować formularza."""
assert "catch" in SYNC_JS and "JSON.parse" in SYNC_JS
def test_map_changes_are_reported_to_sync():
"""geo.js ustawia pola z KODU (`.value = ...`), co samo z siebie nie wywołuje
zdarzeń — bez jawnego `change` synchronizacja przegapiłaby wybór z mapy."""
assert "function setVal" in GEO_JS
assert "dispatchEvent(new Event('change'" in GEO_JS
# wszystkie miejsca ustawiające pola idą przez setVal
assert "latEl.value =" not in GEO_JS.replace("el.value = value", "")
assert "searchEl.value = d.name" not in GEO_JS
def test_sync_does_not_wipe_fields_missing_on_a_tab():
"""Zakładka bez danego pola (np. Sygnifikatory) nie może wyczyścić wartości
zapamiętanej z innej zakładki. `collect` pomija pole, którego nie ma na stronie."""
assert "if (!els.length) return;" in SYNC_JS