PRE-27: pełne ukrycie niedostępnych funkcji (paranoja)
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
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
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>
This commit was merged in pull request #78.
This commit is contained in:
@@ -16,11 +16,11 @@ TEMPLATES = pathlib.Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
|
||||
|
||||
def test_static_url_carries_content_hash():
|
||||
assert re.fullmatch(r"/static/styles\.css\?v=[0-9a-f]{8}", static_url("styles.css"))
|
||||
assert re.fullmatch(r"/static/base\.css\?v=[0-9a-f]{8}", static_url("base.css"))
|
||||
|
||||
|
||||
def test_version_is_content_based_so_differs_between_files():
|
||||
assert _asset_version("styles.css") != _asset_version("formsync.js")
|
||||
assert _asset_version("base.css") != _asset_version("formsync.js")
|
||||
|
||||
|
||||
def test_missing_file_does_not_crash():
|
||||
@@ -35,7 +35,17 @@ def test_no_template_serves_raw_static_paths():
|
||||
assert 'href="/static/' not in txt, f"{f.name}: surowy href bez cache-bustingu"
|
||||
|
||||
|
||||
def test_base_uses_helper_for_stylesheet_and_scripts():
|
||||
def test_base_uses_helper_for_the_shared_stylesheet():
|
||||
base = (TEMPLATES / "base.html").read_text(encoding="utf-8")
|
||||
assert "static('styles.css')" in base
|
||||
assert "static('formsync.js')" in base
|
||||
assert "static('base.css')" in base
|
||||
|
||||
|
||||
def test_version_follows_what_is_sent_not_what_is_on_disk():
|
||||
"""Adres niesie hash treści WYSYŁANEJ, bo komentarze są przy serwowaniu
|
||||
usuwane. Gdyby hash szedł z pliku na dysku, poprawka w samym komentarzu
|
||||
podbijałaby adres i kasowała cache przeglądarki bez żadnej zmiany dla niej."""
|
||||
import hashlib
|
||||
|
||||
from app.main import _asset_body
|
||||
|
||||
assert _asset_version("base.css") == hashlib.md5(_asset_body("base.css")).hexdigest()[:8]
|
||||
|
||||
@@ -65,12 +65,19 @@ def test_every_shared_field_is_synced(key):
|
||||
assert f"'{key}'" in SYNC_JS, f"pole {key} nie jest synchronizowane"
|
||||
|
||||
|
||||
def test_sync_script_loaded_before_map_script():
|
||||
"""KOLEJNOŚĆ MA ZNACZENIE: formsync w <head> 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."""
|
||||
assert "formsync.js" in BASE_HTML, "formsync.js nie jest ładowany globalnie"
|
||||
assert "defer" in BASE_HTML.split("formsync.js")[0].rsplit("<script", 1)[-1] + "defer"
|
||||
@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():
|
||||
|
||||
@@ -113,7 +113,10 @@ def test_fallback_notice_is_shown_prominently():
|
||||
sygnałem, że podział domów jest z innego systemu."""
|
||||
html = _render("_result_tables.html",
|
||||
result={"house_warnings": ["UWAGA: system domów nie ma definicji"]})
|
||||
assert "house-warning" in html
|
||||
# Klasa nazywa się neutralnie: `house-warning` siedziała też na ekranie
|
||||
# logowania i na ekranie plików, czyli nazwa z astrologii lądowała tam,
|
||||
# gdzie astrologii ma nie być widać.
|
||||
assert "notice-warn" in html
|
||||
assert "UWAGA: system domów nie ma definicji" in html
|
||||
|
||||
|
||||
@@ -184,7 +187,12 @@ def test_wheel_orientation_reaches_the_renderer(monkeypatch, orientation):
|
||||
# overflow-x, ROZPYCHAŁA CAŁY DOKUMENT — 1713 px przy oknie 1280 px, czyli
|
||||
# poziomy pasek przewijania na body i rozjechany układ.
|
||||
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
|
||||
|
||||
def test_comparison_table_scrolls_instead_of_widening_the_page():
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
"""Zapora słownikowa: czego konto NIE MOŻE zobaczyć w żadnej swojej odpowiedzi.
|
||||
|
||||
Poprzednie testy pilnowały pojedynczych miejsc — tego przycisku, tamtego bloku.
|
||||
Wyciek prawie nigdy nie siedzi w miejscu, o którym się pamięta: katalog modeli
|
||||
jechał blokiem JSON, nazwy ekranów komentarzem w arkuszu stylów, mechanizm
|
||||
kwarantanny zdaniem po polsku w komentarzu CSS, a nazwa gałęzi rozwojowej
|
||||
w komunikacie o błędzie. Wspólne mają jedno — pewne SŁOWA nie mają prawa paść.
|
||||
|
||||
Więc test nie sprawdza miejsc, tylko przechodzi WSZYSTKO, co dane konto może
|
||||
pobrać (strony i zasoby), i szuka słów zakazanych. Nowy ekran, nowy skrypt,
|
||||
nowy komunikat wchodzą pod tę samą zaporę bez dopisywania czegokolwiek.
|
||||
|
||||
Dwie z trzech list biorą się wprost z katalogu funkcji, więc nie rozjadą się
|
||||
z programem: dopisanie funkcji automatycznie dopisuje jej nazwę i jej adres do
|
||||
tego, czego konto bez niej nie ma prawa zobaczyć.
|
||||
"""
|
||||
import re
|
||||
|
||||
import pytest
|
||||
|
||||
from app import accounts as store
|
||||
from app import features
|
||||
|
||||
# ── persona ──────────────────────────────────────────────────────────────
|
||||
# Osoba zatrudniona wyłącznie do wgrywania plików. Ma widzieć swój ekran i nic
|
||||
# poza nim — ani co program robi, ani co będzie robił, ani że ktoś widzi więcej.
|
||||
WGRYWACZ = ["files", "files_input"]
|
||||
|
||||
# ── słowa zakazane, których NIE DA SIĘ wyprowadzić z katalogu ────────────
|
||||
# Katalog zna nazwy funkcji, ale nie zna dziedziny programu ani słownictwa jego
|
||||
# mechanizmów. Te trzeba wypisać, i każde ma tu swój powód.
|
||||
DZIEDZINA = [
|
||||
# (c) po co są wgrywane pliki
|
||||
"astrolog", "horoskop", "kosmogram", "zodiak", "efemeryd", "sygnifikator",
|
||||
"interpretacyjn", "aspektarian", "antyscj", "deklinacj",
|
||||
# „baza" mówi, że to nie są zwykłe pliki, tylko zbiory czegoś. Na ekranie
|
||||
# wgrywającego wszystko nazywa się plikiem; w częściach administracyjnych
|
||||
# słowo zostaje, bo tam jest właściwe (i tam ten test nie zagląda).
|
||||
"baz",
|
||||
# (b) że tekst pisze model językowy i u kogo
|
||||
"LLM", "anthropic", "openai", "gpt", "claude", "prompt", "token",
|
||||
# (e)/(f) mechanizmy i to, że istnieje ktoś, kto widzi więcej
|
||||
"administrator", "uprawnieni", "walidacj", "kwarantann", "wstrzyman",
|
||||
"zatwierdzi", "przypisan", "zakłada", "zakładał",
|
||||
# „konto" w obu przypadkach: pierwsza wersja listy miała samo „konto",
|
||||
# więc przepuszczała zdanie „Nie masz konta" — literówka, która kosztowałaby
|
||||
# cały sens testu, gdyby nie wyszła przy oglądaniu strony.
|
||||
"konto", "konta", "kontem",
|
||||
# (d) że produkt jest w budowie i ma ponumerowany backlog
|
||||
"roadmap", "przyszł", "planowan", "w budowie", "widok testowy",
|
||||
# topologia: gdzie pukać, żeby ominąć logowanie
|
||||
"warstwa logiczna", "warstwie logicznej", "openapi", "swagger",
|
||||
# architektura: „to jedna z warstw" mówi, że warstw jest więcej
|
||||
"layer", "presentation", "prezentacj",
|
||||
"localhost", "http://logic", "8001", "8002", "8003",
|
||||
]
|
||||
WZORCE = [
|
||||
(r"(?i)\b(PRE|LOG|DAN)-\d+\b", "identyfikator wymagania (ujawnia backlog)"),
|
||||
(r"(?i)\bfeat/[\w-]+", "nazwa gałęzi rozwojowej"),
|
||||
(r"(?i)\bmodel(u|em|e|i|ach|owi)?\b", "generowanie tekstu przez model"),
|
||||
(r"(?i)\bAI\b", "generowanie tekstu przez model"),
|
||||
]
|
||||
|
||||
|
||||
def _zakazane_dla(perms: set[str]) -> list[tuple[str, str]]:
|
||||
"""(igła, powód) — wyprowadzone z katalogu plus lista dziedzinowa."""
|
||||
out = [(s, "słownik dziedziny/mechanizmu") for s in DZIEDZINA]
|
||||
for f in features.ALL:
|
||||
if f.key in perms:
|
||||
continue
|
||||
out.append((f.label, f"nazwa funkcji, której to konto nie ma ({f.key})"))
|
||||
if f.href and f.href != "/":
|
||||
out.append((f.href, f"adres ekranu, którego to konto nie ma ({f.key})"))
|
||||
# Nazwa zasobu też jest wyciekiem: `<script src=".../wheelzoom.js">` w źródle
|
||||
# mówi, że istnieje coś do powiększania, nawet jeśli sam plik zwróci 404.
|
||||
# Wykaz bierze się z mapy zasobów, więc nowy skrypt obejmuje sam z siebie.
|
||||
for nazwa, need in features.STATIC.items():
|
||||
if not features.can(frozenset(perms), need):
|
||||
out.append((nazwa, f"nazwa zasobu, którego to konto nie może pobrać"))
|
||||
return out
|
||||
|
||||
|
||||
# Ekran logowania MUSI mówić o koncie — inaczej nie da się go opisać. To jedyne
|
||||
# odstępstwo i jest wąskie: dotyczy tej jednej strony i tych dwóch słów.
|
||||
WYJATKI = {"/logowanie": {"konto", "konta", "kontem"}}
|
||||
|
||||
|
||||
def _przeskanuj(tresc: str, perms: set[str], gdzie: str) -> list[str]:
|
||||
wolno = WYJATKI.get(gdzie, set())
|
||||
grzechy = []
|
||||
for igla, powod in _zakazane_dla(perms):
|
||||
if igla in wolno:
|
||||
continue
|
||||
if re.search(rf"(?i)(?<![\w-]){re.escape(igla)}", tresc):
|
||||
grzechy.append(f"{gdzie}: „{igla}” — {powod}")
|
||||
for wzor, powod in WZORCE:
|
||||
m = re.search(wzor, tresc)
|
||||
if m:
|
||||
grzechy.append(f"{gdzie}: „{m.group(0)}” — {powod}")
|
||||
return grzechy
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def env(tmp_path, monkeypatch):
|
||||
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")
|
||||
|
||||
|
||||
def _klient(monkeypatch):
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from app.main import app, logic
|
||||
|
||||
# Warstwa logiczna niedostępna CELOWO: ekran plików idzie wtedy ścieżką
|
||||
# błędu, a to właśnie komunikaty o błędach wypisywały nazwy tras, nazwę
|
||||
# gałęzi i wewnętrzny adres z portem.
|
||||
import httpx
|
||||
|
||||
def pada(*a, **k):
|
||||
raise httpx.ConnectError("[Errno 111] connect to http://logic:8001/files failed")
|
||||
|
||||
for nazwa in ("files_list", "report", "timeline", "prompt", "horoscope", "positions"):
|
||||
if hasattr(logic, nazwa):
|
||||
monkeypatch.setattr(logic, nazwa, pada, raising=False)
|
||||
return TestClient(app, follow_redirects=False)
|
||||
|
||||
|
||||
def _co_moze_pobrac(perms: set[str]) -> list[str]:
|
||||
"""Wszystko, co to konto ma prawo dostać: strony i zasoby."""
|
||||
strony = [p for (m, p), need in features.ROUTES.items()
|
||||
if m == "GET" and features.can(frozenset(perms), need)]
|
||||
zasoby = ["/static/base.css"] + [
|
||||
f"/static/{n}" for n, need in features.STATIC.items()
|
||||
if features.can(frozenset(perms), need)]
|
||||
return sorted(set(strony + zasoby))
|
||||
|
||||
|
||||
def test_the_uploader_never_reads_a_forbidden_word(env, monkeypatch):
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('wgrywacz')}"}
|
||||
perms = set(WGRYWACZ)
|
||||
|
||||
grzechy, obejrzane = [], 0
|
||||
for adres in _co_moze_pobrac(perms):
|
||||
r = c.get(adres, headers=naglowki)
|
||||
if r.status_code != 200:
|
||||
continue
|
||||
obejrzane += 1
|
||||
grzechy += _przeskanuj(r.text, perms, adres)
|
||||
assert obejrzane >= 3, f"test niczego nie obejrzał ({obejrzane}) — zapora byłaby pozorna"
|
||||
assert not grzechy, "\n" + "\n".join(grzechy)
|
||||
|
||||
|
||||
def test_an_anonymous_visitor_never_reads_a_forbidden_word(env, monkeypatch):
|
||||
"""Przed zalogowaniem widać ekran logowania i jeden arkusz. Nic więcej,
|
||||
i nic w nich nie może nazywać funkcji."""
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
c = _klient(monkeypatch)
|
||||
grzechy, obejrzane = [], 0
|
||||
for adres in ("/logowanie", "/health", "/static/base.css"):
|
||||
r = c.get(adres)
|
||||
assert r.status_code == 200, f"{adres}: {r.status_code}"
|
||||
obejrzane += 1
|
||||
grzechy += _przeskanuj(r.text, set(), adres)
|
||||
assert obejrzane == 3
|
||||
assert not grzechy, "\n" + "\n".join(grzechy)
|
||||
|
||||
|
||||
def test_every_other_asset_is_denied_to_the_uploader(env, monkeypatch):
|
||||
"""Zasób spoza jego zestawu ma być NIE DO ODRÓŻNIENIA od nieistniejącego."""
|
||||
store.create("wgrywacz", "x", WGRYWACZ)
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('wgrywacz')}"}
|
||||
wolno = set(_co_moze_pobrac(set(WGRYWACZ)))
|
||||
nieistniejacy = c.get("/static/nie-ma-takiego-pliku.js", headers=naglowki)
|
||||
for nazwa in features.STATIC:
|
||||
adres = f"/static/{nazwa}"
|
||||
if adres in wolno:
|
||||
continue
|
||||
r = c.get(adres, headers=naglowki)
|
||||
assert r.status_code == nieistniejacy.status_code == 404, f"{adres}: {r.status_code}"
|
||||
assert r.text == nieistniejacy.text, f"{adres}: odpowiedź różni się od „nie ma takiego pliku”"
|
||||
|
||||
|
||||
def test_the_catalogue_drives_the_list_so_it_cannot_go_stale(env):
|
||||
"""Zapora ma się uczyć z katalogu, nie z pamięci tego, kto ją pisał."""
|
||||
zakazane = {i for i, _ in _zakazane_dla(set(WGRYWACZ))}
|
||||
assert "Sygnifikatory" in zakazane and "/interpret" in zakazane
|
||||
# a to, co konto MA, nie może trafić na listę zakazanych
|
||||
assert "Pliki" not in zakazane
|
||||
|
||||
|
||||
def test_the_owner_sees_everything(env, monkeypatch):
|
||||
"""Kontrola pozytywna: dla administratora te słowa MAJĄ się pojawiać.
|
||||
Bez niej zapora przechodziłaby także wtedy, gdyby program był pusty."""
|
||||
from app import security, session
|
||||
|
||||
c = _klient(monkeypatch)
|
||||
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('szef')}"}
|
||||
html = c.get("/accounts", headers=naglowki).text
|
||||
assert "Sygnifikatory" in html and "Generowanie tekstu przez model" in html
|
||||
@@ -15,13 +15,31 @@ import re
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
JS = (APP / "static" / "wheeltip.js").read_text(encoding="utf-8")
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_script_is_loaded_globally():
|
||||
"""W base.html, bo koło jest też na zakładce „Skompiluj"."""
|
||||
assert "wheeltip.js" in BASE
|
||||
def test_script_is_loaded_only_where_there_is_a_wheel():
|
||||
"""Koło jest na Horoskopie i na Skompiluj — i tylko tam ten skrypt ma prawo
|
||||
się pojawić.
|
||||
|
||||
Wcześniej ładowany był z base.html, czyli na KAŻDEJ stronie. Konto, które ma
|
||||
wyłącznie Pliki, pobierało go przy zwykłym wejściu na swój jedyny ekran,
|
||||
a jego treść wymienia z nazwy zakładki, o których takie konto nie ma prawa
|
||||
wiedzieć — łącznie z tą, której jeszcze nie ma."""
|
||||
from pathlib import Path
|
||||
|
||||
szablony = Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
z_kolem, bez_kola = set(), set()
|
||||
for f in szablony.glob("*.html"):
|
||||
(z_kolem if "wheeltip.js" in f.read_text(encoding="utf-8") else bez_kola).add(f.name)
|
||||
assert z_kolem == {"chart.html", "compile.html"}, f"ładowany też przez: {z_kolem}"
|
||||
assert "files.html" in bez_kola and "logowanie.html" in bez_kola
|
||||
|
||||
|
||||
def test_tooltip_sits_above_the_zoom_overlay_but_below_the_progress_window():
|
||||
|
||||
@@ -9,13 +9,31 @@ import re
|
||||
|
||||
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
||||
JS = (APP / "static" / "wheelzoom.js").read_text(encoding="utf-8")
|
||||
CSS = (APP / "static" / "styles.css").read_text(encoding="utf-8")
|
||||
# Arkusz jest teraz rozbity per uprawnienie (arkusz wspólny + arkusze ekranów),
|
||||
# bo jeden plik ze wszystkimi selektorami był spisem funkcji programu dla
|
||||
# każdego, kto go pobrał. Reguły szukamy w całości — który arkusz ją niesie,
|
||||
# pilnuje osobny test pokrycia.
|
||||
CSS = "\n".join(f.read_text(encoding="utf-8")
|
||||
for f in sorted((APP / "static").glob("*.css")))
|
||||
BASE = (APP / "templates" / "base.html").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_script_is_loaded_globally():
|
||||
"""Ładowane w base.html, bo koło pojawi się też na zakładce Skompiluj."""
|
||||
assert "wheelzoom.js" in BASE
|
||||
def test_script_is_loaded_only_where_there_is_a_wheel():
|
||||
"""Koło jest na Horoskopie i na Skompiluj — i tylko tam ten skrypt ma prawo
|
||||
się pojawić.
|
||||
|
||||
Wcześniej ładowany był z base.html, czyli na KAŻDEJ stronie. Konto, które ma
|
||||
wyłącznie Pliki, pobierało go przy zwykłym wejściu na swój jedyny ekran,
|
||||
a jego treść wymienia z nazwy zakładki, o których takie konto nie ma prawa
|
||||
wiedzieć — łącznie z tą, której jeszcze nie ma."""
|
||||
from pathlib import Path
|
||||
|
||||
szablony = Path(__file__).resolve().parents[1] / "app" / "templates"
|
||||
z_kolem, bez_kola = set(), set()
|
||||
for f in szablony.glob("*.html"):
|
||||
(z_kolem if "wheelzoom.js" in f.read_text(encoding="utf-8") else bez_kola).add(f.name)
|
||||
assert z_kolem == {"chart.html", "compile.html"}, f"ładowany też przez: {z_kolem}"
|
||||
assert "files.html" in bez_kola and "logowanie.html" in bez_kola
|
||||
|
||||
|
||||
def test_toggles_on_click():
|
||||
|
||||
Reference in New Issue
Block a user