astrololo: ekrany i eksport jako moduły, katalog z rejestracji (3/5)
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Failing after 3s
Testy / Testy astrodemo (pull_request) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (pull_request) Failing after 2s
Testy / Kontrola składni wszystkich warstw (pull_request) Failing after 3s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Failing after 3s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Failing after 3s
Testy / Testy astrodemo (push) Failing after 3s
Testy / Build obrazu silnika B (swisseph) (push) Failing after 2s
Testy / Kontrola składni wszystkich warstw (push) Failing after 3s
build / build (push) Successful in 9s
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>
This commit was merged in pull request #81.
This commit is contained in:
@@ -19,7 +19,27 @@ def zloz(nazwa: str) -> str:
|
||||
tresc = (APP / "templates" / nazwa).read_text(encoding="utf-8")
|
||||
|
||||
def wstaw(m: re.Match) -> str:
|
||||
plik = APP / "dodatki" / "templates" / m.group(1)
|
||||
return plik.read_text(encoding="utf-8") if plik.exists() else ""
|
||||
for katalog in sorted((APP / "moduly").glob("*/templates")):
|
||||
plik = katalog / m.group(1)
|
||||
if plik.exists():
|
||||
return plik.read_text(encoding="utf-8")
|
||||
return ""
|
||||
|
||||
return GNIAZDO.sub(wstaw, tresc)
|
||||
|
||||
|
||||
def zloz_kod() -> str:
|
||||
"""Cały kod tras aplikacji jako jeden tekst — podstawa plus wszystkie ekrany.
|
||||
|
||||
Trasy mieszkają teraz w `app/ekrany/*.py`, a `main.py` jest samym złożeniem.
|
||||
Testy strukturalne pilnują reguł, które mają obowiązywać W CAŁEJ aplikacji
|
||||
(„każde wejście do baz zostawia ślad w dzienniku", „orb jedzie do logiki"),
|
||||
więc muszą widzieć całość — inaczej sprawdzałyby jeden plik i przepuściły
|
||||
regułę złamaną w drugim.
|
||||
"""
|
||||
czesci = [(APP / "podstawa.py").read_text(encoding="utf-8")]
|
||||
czesci += [f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "ekrany").glob("*.py"))]
|
||||
czesci += [f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "moduly").rglob("*.py"))]
|
||||
return "\n".join(czesci)
|
||||
|
||||
@@ -201,5 +201,9 @@ def test_handlers_report_record_counts():
|
||||
wyniósł 5000 rekordów" — czyli nie wykryłby powolnego wypompowywania bazy."""
|
||||
import pathlib
|
||||
|
||||
main = (pathlib.Path(security.__file__).parent / "main.py").read_text(encoding="utf-8")
|
||||
# Reguła obowiązuje w CAŁEJ aplikacji, więc patrzymy na wszystkie trasy,
|
||||
# nie na jeden plik — po podziale main.py jest samym złożeniem.
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
main = zloz_kod()
|
||||
assert main.count("security.audit_records(") >= 3 # sygnifikatory, raport, eksport
|
||||
|
||||
@@ -17,7 +17,7 @@ import re
|
||||
import pytest
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
MODUL = APP / "dodatki"
|
||||
MODUL = APP / "moduly" / "dodatki"
|
||||
|
||||
SLOWA = [
|
||||
# nazwa funkcji i jej słownictwo
|
||||
@@ -79,14 +79,11 @@ def test_removing_the_module_leaves_a_working_application():
|
||||
Most (`app.rozszerzenia`) musi odpowiadać sensownie, gdy modułu nie ma —
|
||||
inaczej astroklient nie wystartuje, a dowiemy się o tym dopiero przy
|
||||
budowaniu jego obrazu."""
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
from app import rozszerzenia
|
||||
|
||||
zapamietane = rozszerzenia._modul
|
||||
zapamietane = rozszerzenia._MODULY
|
||||
try:
|
||||
rozszerzenia._modul = None
|
||||
rozszerzenia._MODULY = []
|
||||
assert rozszerzenia.funkcje() == ()
|
||||
assert rozszerzenia.trasy() == {}
|
||||
assert rozszerzenia.zasoby() == {}
|
||||
@@ -98,8 +95,7 @@ def test_removing_the_module_leaves_a_working_application():
|
||||
assert rozszerzenia.pola_formularza()() == {}
|
||||
rozszerzenia.zamontuj(None) # ma nie wybuchnąć
|
||||
finally:
|
||||
rozszerzenia._modul = zapamietane
|
||||
importlib.reload(sys.modules["app.rozszerzenia"]) if False else None
|
||||
rozszerzenia._MODULY = zapamietane
|
||||
|
||||
|
||||
def test_the_bridge_itself_names_nothing():
|
||||
|
||||
@@ -7,11 +7,14 @@ regresja podsumowania).
|
||||
"""
|
||||
import pathlib
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
|
||||
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
||||
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje obu formularzy
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||
SYNC = (APP / "static" / "formsync.js").read_text(encoding="utf-8")
|
||||
COMPILE_JS = (APP / "static" / "compile.js").read_text(encoding="utf-8")
|
||||
|
||||
@@ -11,7 +11,7 @@ import pathlib
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
from tests.skladanie import zloz
|
||||
from tests.skladanie import zloz, zloz_kod
|
||||
|
||||
# Szablon z rozwiniętymi wstawkami modułów — patrz tests/skladanie.py.
|
||||
TPL = zloz("compile.html")
|
||||
@@ -19,9 +19,10 @@ 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 / "dodatki" / "static" / "natal.js").read_text(encoding="utf-8")
|
||||
NATAL_JS = (APP / "moduly" / "dodatki" / "static" / "natal.js").read_text(encoding="utf-8")
|
||||
INTERPRET = zloz("interpret.html")
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
|
||||
|
||||
def _client():
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Produkt składa się z modułów — i węższe złożenie naprawdę działa.
|
||||
|
||||
Ekrany mieszkają w `app/ekrany/*.py`, moduły opcjonalne w `app/moduly/*`, a
|
||||
`main.py` jest samym złożeniem. Sens tego podziału jest jeden: z tego samego
|
||||
kodu ma dać się zbudować WĘŻSZY produkt, w którym pozostałych ekranów nie ma —
|
||||
nie są wyłączone, tylko ich nie ma.
|
||||
|
||||
Test buduje takie złożenie naprawdę: kopiuje drzewo, usuwa moduły, uruchamia
|
||||
aplikację w osobnym procesie i sprawdza, co się stało. Robi to w podprocesie,
|
||||
bo importy Pythona są zapamiętywane w procesie — sprawdzanie „aplikacji bez
|
||||
modułu" w tym samym procesie, w którym moduł już się zaimportował, dawałoby
|
||||
wynik fałszywie pozytywny.
|
||||
|
||||
Czego ten test NIE sprawdza: czy w drzewie nie ma wzmianek o nieobecnych
|
||||
ekranach. To ma sens dopiero wobec KONKRETNEGO produktu (wiadomo wtedy, których
|
||||
ekranów nie ma) i tam jest sprawdzane — patrz zapory słownikowe.
|
||||
"""
|
||||
import json
|
||||
import pathlib
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
|
||||
# Złożenie węższe: same funkcje astrologiczne, bez raportu zbiorczego,
|
||||
# bez administracji, bez generowania i bez wynoszenia treści na zewnątrz.
|
||||
EKRANY_WASKIE = ["horoskop", "interpretacje", "kalendarz", "logowanie",
|
||||
"lokalizacja", "sygnifikatory", "synastria"]
|
||||
USUWANE = ["ekrany/skompiluj.py", "ekrany/ustawienia.py", "ekrany/konta.py",
|
||||
"ekrany/pliki.py", "moduly/dodatki", "moduly/eksport"]
|
||||
|
||||
SONDA = '''
|
||||
import json, os, tempfile
|
||||
os.environ.update(ACCOUNTS_FILE=tempfile.mkdtemp()+"/a.json", APP_USER="szef",
|
||||
APP_PASSWORD="t", RATE_LIMIT_PER_MIN="0",
|
||||
SESSION_SECRET="t"*64, COOKIES_INSECURE="1")
|
||||
from starlette.testclient import TestClient
|
||||
from app.main import app
|
||||
from app import security, session, features
|
||||
c = TestClient(app, follow_redirects=False)
|
||||
H = {"Cookie": f"{session.COOKIE}={security.issue_session('szef')}"}
|
||||
wynik = {
|
||||
"ekrany": [f.label for f in features.SCREENS],
|
||||
"rozszerzenia": [f.label for f in features.EXTRAS],
|
||||
"dziala": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/", "/interpret", "/timeline", "/synastry", "/significators")},
|
||||
"nie_ma": {a: c.get(a, headers=H).status_code
|
||||
for a in ("/compile", "/settings", "/accounts", "/files")},
|
||||
# Liczymy odsyłacze WEWNĄTRZ paska nawigacji. Zliczanie wszystkich na stronie
|
||||
# łapało też przypis OSM z wyszukiwarki miejsca — czyli test mierzyłby
|
||||
# co innego, niż nazwa mu każe.
|
||||
"nawigacja": (lambda h: h.split("<nav>")[1].split("</nav>")[0].count('<a href="'))(
|
||||
c.get("/", headers=H).text),
|
||||
}
|
||||
print("WYNIK:" + json.dumps(wynik))
|
||||
'''
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def waskie(tmp_path_factory):
|
||||
korzen = tmp_path_factory.mktemp("waskie")
|
||||
shutil.copytree(APP, korzen / "app",
|
||||
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"))
|
||||
for sciezka in USUWANE:
|
||||
cel = korzen / "app" / sciezka
|
||||
shutil.rmtree(cel) if cel.is_dir() else cel.unlink()
|
||||
(korzen / "app" / "main.py").write_text(
|
||||
"from app import rozszerzenia\n"
|
||||
"from app.podstawa import _build_utc, _logic_error, app, logic, templates\n"
|
||||
f"from app.ekrany import {', '.join(EKRANY_WASKIE)} # noqa: F401\n"
|
||||
"rozszerzenia.zamontuj(app, logic=logic, templates=templates,\n"
|
||||
" buduj_utc=_build_utc, blad=_logic_error)\n",
|
||||
encoding="utf-8")
|
||||
wynik = subprocess.run([sys.executable, "-c", SONDA], cwd=korzen,
|
||||
capture_output=True, text=True,
|
||||
env={"PYTHONPATH": str(korzen), "PATH": "/usr/bin:/bin"})
|
||||
linia = next((l for l in wynik.stdout.splitlines() if l.startswith("WYNIK:")), None)
|
||||
assert linia, f"sonda nie wystartowała:\n{wynik.stdout[-2000:]}\n{wynik.stderr[-2000:]}"
|
||||
return json.loads(linia[len("WYNIK:"):])
|
||||
|
||||
|
||||
def test_the_narrow_build_starts_at_all(waskie):
|
||||
"""Najpierw to: aplikacja bez połowy modułów ma w ogóle wstać."""
|
||||
assert waskie["dziala"]["/"] == 200
|
||||
|
||||
|
||||
def test_every_screen_it_kept_works(waskie):
|
||||
assert all(kod == 200 for kod in waskie["dziala"].values()), waskie["dziala"]
|
||||
|
||||
|
||||
def test_every_screen_it_dropped_is_gone_not_merely_closed(waskie):
|
||||
"""404, nie 403 i nie 500: ekran ma NIE ISTNIEĆ, a nie być odmówiony."""
|
||||
assert all(kod == 404 for kod in waskie["nie_ma"].values()), waskie["nie_ma"]
|
||||
|
||||
|
||||
def test_the_catalogue_describes_this_build_not_some_other(waskie):
|
||||
"""Katalog funkcji powstaje ze zgłoszeń, więc opisuje ten obraz.
|
||||
|
||||
Wcześniej lista była wpisana na sztywno i obraz bez części ekranów i tak
|
||||
niósł ich nazwy — czyli spis funkcji, których nie ma jak włączyć."""
|
||||
assert set(waskie["ekrany"]) == {"Horoskop", "Interpretacje", "Kalendarz",
|
||||
"Synastria", "Sygnifikatory"}
|
||||
assert "Eksport plików" not in waskie["rozszerzenia"]
|
||||
assert "Generowanie tekstu przez model" not in waskie["rozszerzenia"]
|
||||
|
||||
|
||||
def test_navigation_has_no_dead_links(waskie):
|
||||
"""Pozycje administracyjne też idą z rejestru — wpisany na sztywno odsyłacz
|
||||
do ekranu kont zostawał w produkcie, który tego ekranu nie ma."""
|
||||
assert waskie["nawigacja"] == len(waskie["ekrany"])
|
||||
@@ -8,12 +8,15 @@ import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
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")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN_PY = zloz_kod()
|
||||
|
||||
WITH_FORM = ("chart.html", "interpret.html", "timeline.html")
|
||||
|
||||
|
||||
@@ -7,12 +7,15 @@ import pathlib
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
CHART = (APP / "templates" / "chart.html").read_text(encoding="utf-8")
|
||||
COMPILE = (APP / "templates" / "compile.html").read_text(encoding="utf-8")
|
||||
OPTS = (APP / "templates" / "_form_options.html").read_text(encoding="utf-8") # wspólne opcje
|
||||
TABLES = (APP / "templates" / "_result_tables.html").read_text(encoding="utf-8") # wspólne tabele
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
|
||||
from app.house_systems import HOUSE_SYSTEMS, LIMITED, label as house_label # noqa: E402
|
||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||
@@ -193,7 +196,7 @@ def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation):
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted([*(APP / "static").glob("*.css"),
|
||||
*(APP / "dodatki" / "static").glob("*.css")]))
|
||||
*(APP / "moduly" / "dodatki" / "static").glob("*.css")]))
|
||||
|
||||
|
||||
def test_comparison_table_scrolls_instead_of_widening_the_page():
|
||||
|
||||
@@ -9,8 +9,8 @@ import pathlib
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
# Skrypty generowania mieszkają w module `app/ai/` — katalogu, którego
|
||||
# w obrazie bez tej funkcji po prostu nie ma.
|
||||
JS = (APP / "dodatki" / "static" / "predictions.js").read_text(encoding="utf-8")
|
||||
PROGRESS = (APP / "dodatki" / "static" / "progress.js").read_text(encoding="utf-8")
|
||||
JS = (APP / "moduly" / "dodatki" / "static" / "predictions.js").read_text(encoding="utf-8")
|
||||
PROGRESS = (APP / "moduly" / "dodatki" / "static" / "progress.js").read_text(encoding="utf-8")
|
||||
from tests.skladanie import zloz
|
||||
|
||||
TIMELINE = zloz("timeline.html")
|
||||
|
||||
@@ -8,11 +8,14 @@ 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")
|
||||
MAIN = (APP / "main.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")
|
||||
|
||||
|
||||
|
||||
@@ -4,10 +4,14 @@ import pathlib
|
||||
|
||||
from openpyxl import load_workbook
|
||||
|
||||
from app.report_export import HEADERS, report_to_xlsx
|
||||
# Eksport jest modułem odłączalnym — mieszka w app/moduly/eksport/.
|
||||
from app.moduly.eksport.arkusz import HEADERS, report_to_xlsx
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
MAIN = (APP / "main.py").read_text(encoding="utf-8")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
INTERPRET = (APP / "templates" / "interpret.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
@@ -70,7 +74,46 @@ def test_interpret_has_export_button():
|
||||
|
||||
|
||||
def test_handler_returns_xlsx_download():
|
||||
assert 'if action == "export":' in MAIN
|
||||
# Eksport jest modułem odłączalnym, więc akcję zgłasza on sam — ekran nie
|
||||
# wymienia jej z nazwy. Pilnujemy zgłoszenia, nie dawnego `if` w handlerze.
|
||||
assert '"export": "export"' in MAIN
|
||||
assert "report_to_xlsx" in MAIN
|
||||
assert "spreadsheetml.sheet" in MAIN # media type .xlsx
|
||||
assert 'filename="interpretacje.xlsx"' in MAIN
|
||||
|
||||
|
||||
def test_the_export_action_really_returns_a_spreadsheet(tmp_path, monkeypatch):
|
||||
"""Ścieżka szczęśliwa, nie tylko odmowa.
|
||||
|
||||
Testy uprawnień sprawdzały dotąd, że konto BEZ uprawnienia arkusza nie
|
||||
dostanie. Po wydzieleniu eksportu do modułu okazało się, że nikt nie
|
||||
sprawdza, czy konto Z uprawnieniem dostaje cokolwiek — a moduł dostawał
|
||||
zależności dopiero z wywołania montującego i brak jednej z nich wyszedłby
|
||||
dopiero przy pierwszym kliknięciu.
|
||||
"""
|
||||
import os
|
||||
|
||||
monkeypatch.setenv("ACCOUNTS_FILE", str(tmp_path / "accounts.json"))
|
||||
monkeypatch.setenv("APP_USER", "szef")
|
||||
monkeypatch.setenv("APP_PASSWORD", "tajne-szefa")
|
||||
monkeypatch.delenv("APP_USERS", raising=False)
|
||||
monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0")
|
||||
monkeypatch.setenv("SESSION_SECRET", "t" * 64)
|
||||
monkeypatch.setenv("COOKIES_INSECURE", "1")
|
||||
os.environ.setdefault("SESSION_SECRET", "t" * 64)
|
||||
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app import security, session
|
||||
from app.main import app, logic
|
||||
|
||||
monkeypatch.setattr(logic, "report", lambda **kw: {"objects": [], "engine": "test"})
|
||||
c = TestClient(app, follow_redirects=False)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('szef')}"}
|
||||
r = c.post("/interpret", headers=naglowki,
|
||||
data={"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
|
||||
"lat": "50.06", "lon": "19.94", "action": "export"})
|
||||
assert r.status_code == 200
|
||||
assert "spreadsheetml" in r.headers.get("content-type", "")
|
||||
assert r.headers.get("content-disposition", "").endswith('"interpretacje.xlsx"')
|
||||
assert r.content[:2] == b"PK", "arkusz .xlsx to archiwum ZIP — to nim nie jest"
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Ekran ustawień: bazy na udziale (PRE-09)."""
|
||||
import pathlib
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
TPL = (APP / "templates" / "settings.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")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
"""Synastria — technika relacyjna (PRE-04)."""
|
||||
import pathlib
|
||||
|
||||
from tests.skladanie import zloz_kod
|
||||
|
||||
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")
|
||||
# Trasy mieszkają w app/ekrany/* — patrz tests/skladanie.py.
|
||||
MAIN = zloz_kod()
|
||||
CLIENT = (APP / "clients" / "logic_client.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ def _client(monkeypatch):
|
||||
monkeypatch.setattr(logic, "timeline", lambda **kw: (wolania.append("timeline"), KALENDARZ)[1])
|
||||
# Generowanie mieszka w module `app/ai/` i ma własnego klienta — wspólny
|
||||
# klient nie zna tych metod, bo jego plik jedzie do każdego obrazu.
|
||||
from app.dodatki import klient
|
||||
from app.moduly.dodatki import klient
|
||||
|
||||
monkeypatch.setattr(klient, "prompt",
|
||||
lambda *a, **kw: (wolania.append("prompt"), {"prompt": "TREŚĆ"})[1])
|
||||
|
||||
@@ -21,7 +21,7 @@ JS = (APP / "static" / "wheeltip.js").read_text(encoding="utf-8")
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted([*(APP / "static").glob("*.css"),
|
||||
*(APP / "dodatki" / "static").glob("*.css")]))
|
||||
*(APP / "moduly" / "dodatki" / "static").glob("*.css")]))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ JS = (APP / "static" / "wheelzoom.js").read_text(encoding="utf-8")
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted([*(APP / "static").glob("*.css"),
|
||||
*(APP / "dodatki" / "static").glob("*.css")]))
|
||||
*(APP / "moduly" / "dodatki" / "static").glob("*.css")]))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user