a833965909
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m58s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 3m37s
Testy / Kontrola składni wszystkich warstw (push) Successful in 9s
build / build (push) Successful in 18s
Ekran „Konta" dla administratora: zakładanie, kasowanie i nadawanie uprawnień. Zestaw funkcji zależy od konta, a konto ograniczone widzi program KOMPLETNY — tylko mniejszy. PODZIAŁ NA GRUPY. Ekrany to zakładki (7), bo zakładka jest naturalną jednostką — to ją widać w nawigacji. Rozszerzenia to POZIOMY ZŁOŻONOŚCI wewnątrz ekranów: porównanie systemów domów, wykresy dodatkowe, obliczenia zaawansowane, generowanie tekstu przez model (kosztuje pieniądze) i eksport plików. Konto bez porównania domów dostaje horoskop w Whole Sign i nie wie, że systemów jest trzynaście. NIC NIE ZDRADZA, ŻE JEST WIĘCEJ: - brak pozycji w menu zamiast pozycji wyszarzonej, - 404 zamiast 403 — odmowa z powodem sama mówi, że coś tam jest, - rysunki bez uprawnienia w OGÓLE NIE POWSTAJĄ, więc nie ma ich nawet w źródle, - automatyczna dokumentacja API wyłączona. /docs, /redoc i /openapi.json wypisują komplet tras, czyli spis wszystkich funkcji programu — ochrona zakładek nic by nie dała, gdyby obok leżał ich katalog. Znalezione TESTEM przechodzącym po trasach aplikacji, nie przeglądem kodu. KONTO ADMINISTRACYJNE zostaje w APP_USER/APP_PASSWORD, jak było. Nie leży w pliku kont, więc nie da się go skasować ani ograniczyć z ekranu. Konto założone w pliku o tym samym loginie NIE przesłoni administracyjnego — kolejność sprawdzania jest odwrotna, inaczej dałoby się odebrać uprawnienia jedynemu, kto może je nadawać. Uprawnienia administracyjnego nie da się też nadać z formularza: odsiewamy je w normalise(), a nie w handlerze, więc żadne spreparowane żądanie tam nie sięgnie. GRANICA JEST W HANDLERZE, NIE W SZABLONIE. Ukrycie pola chroni przed przypadkiem, nie przed kimś, kto zna nazwy pól — _limit_options() ścina opcje po stronie serwera i test wysyła spreparowane żądanie, żeby to potwierdzić. MAPA TRASA→UPRAWNIENIE JEST JEDNA (features.ROUTES). Rozproszenie jej po dekoratorach kończy się trasą, o której ochronie ktoś zapomniał — a taka dziura jest niewidoczna, dopóki ktoś jej nie znajdzie. Trasa bez wpisu wymaga administratora: przeoczenie ma ZAMYKAĆ, nie otwierać. Test idzie po trasach APLIKACJI, nie po wpisach mapy — inaczej potwierdzałby tylko sam siebie. Konta w pliku JSON na własnym podkatalogu NFS (nie tam, gdzie bazy — zamontowanie całego udziału obeszłoby bokiem DAN-25). Hasła wyłącznie jako hash scrypt, tym samym mechanizmem co APP_USERS. Zapis atomowy, bo przerwanie zapisu na NFS obcięłoby plik, czyli skasowało wszystkie konta naraz. Przy okazji przepisane trzy testy, które greppowały nawigację i main.py: menu powstaje teraz z katalogu funkcji, więc szukanie sztywnych linków w base.html niczego już nie sprawdzało. Wymaga wolumenu na konta — osobny PR w repo deploy. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
70 lines
2.3 KiB
Python
70 lines
2.3 KiB
Python
"""Synastria — technika relacyjna (PRE-04)."""
|
|
import pathlib
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
TPL = (APP / "templates" / "synastry.html").read_text(encoding="utf-8")
|
|
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
|
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
|
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
|
|
|
|
|
def _client():
|
|
import os
|
|
|
|
os.environ.pop("APP_PASSWORD", None)
|
|
from starlette.testclient import TestClient
|
|
|
|
from app.main import app
|
|
|
|
return TestClient(app)
|
|
|
|
|
|
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["synastry"]
|
|
assert tab in features.SCREENS
|
|
assert tab.href and tab.label
|
|
|
|
|
|
def test_routes_exist():
|
|
assert '@app.get("/synastry"' in MAIN and '@app.post("/synastry"' in MAIN
|
|
|
|
|
|
def test_get_renders_two_person_form():
|
|
r = _client().get("/synastry")
|
|
assert r.status_code == 200
|
|
# dwie osoby: pola z prefiksami a_ i b_
|
|
for pfx in ("a_", "b_"):
|
|
assert f'name="{pfx}date"' in r.text and f'name="{pfx}time"' in r.text
|
|
|
|
|
|
def test_form_has_both_people_and_shared_aspect_settings():
|
|
"""Pola dwóch osób generuje pętla — sprawdzamy WYRENDEROWANY formularz."""
|
|
html = _client().get("/synastry").text
|
|
for pfx in ("a_", "b_"):
|
|
assert f'name="{pfx}person"' in html
|
|
assert f'name="{pfx}lat"' in html
|
|
assert 'name="aspect_orb"' in html and 'name="aspect_minor"' in html
|
|
|
|
|
|
def test_handler_calls_synastry_with_both_moments():
|
|
assert "logic.synastry(" in MAIN
|
|
assert '"when_utc": iso_a' in MAIN and '"when_utc": iso_b' in MAIN
|
|
|
|
|
|
def test_client_posts_to_synastry_endpoint():
|
|
assert 'self._post("/chart/synastry"' in CLIENT
|
|
assert '"person_a": person_a' in CLIENT and '"person_b": person_b' in CLIENT
|
|
|
|
|
|
def test_result_table_shows_cross_aspects():
|
|
"""Wynik: kolumny osoba A · aspekt · osoba B · orb."""
|
|
assert "Aspekty synastryczne" in TPL
|
|
assert "a.obj1" in TPL and "a.obj2" in TPL and "a.glyph" in TPL
|