338ec24e90
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m20s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m26s
Testy / Testy astrodemo (pull_request) Successful in 9m25s
Testy / Testy astroklient (pull_request) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 6s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 12s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m21s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Testy astroklient (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 4s
Astroklient przedstawiał się na stronie głównej jako „astrololo" — w tytule karty i w nagłówku. To najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać, czego mu nie dano. Cała reszta izolacji przestaje mieć znaczenie, gdy produkt sam się przedstawia nazwą rodzica. Wzięło się stąd, że nazwa była wpisana na sztywno w szablony, a szablony są WSPÓLNE — dokładnie ta sama przyczyna, co przy zaszytym w base.html warunku na administratora. Nazwa mieszka teraz w module `produkt.py`, który nakładka produktu nadpisuje; szablony o nią pytają, zamiast ją znać. astrodemo NIE było dotknięte: ma własne kopie szablonów i pokazuje „astrodemo". DRUGIE ŹRÓDŁO WYCIEKU, mniej oczywiste: nazwa siedziała też w identyfikatorach JavaScriptu, które widzi każdy, kto otworzy narzędzia deweloperskie — zdarzenie `astrololo:coords` i klucz `astrololo.form.v1` w localStorage. Przemianowane na neutralne `kosmogram:*`; te identyfikatory nie mają powodu nosić nazwy rodziny produktów. SKUTEK UBOCZNY: zmiana klucza w localStorage kasuje raz zapamiętane wersje robocze formularza. Sprawdzone, że NIE wyciekają: nagłówki X-Astrololo-* jadą wyłącznie między warstwami, a dokumentacja OpenAPI (gdzie zostaje tytuł aplikacji FastAPI) oddaje 404 także zalogowanemu. TEST rozdziela to, co widzi człowiek, od identyfikatorów wewnętrznych. Nazwa rdzenia wolno występować w nazwach loggerów, materiale do wyprowadzania kluczy i nagłówkach międzywarstwowych; nie wolno w szablonach, zasobach ani w treści odpowiedzi. Zakaz na wszystko byłby albo obejściem przez zmianę nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej okazji. Sprawdzony w obie strony: po cofnięciu nakładki test pada. Testy: astroklient 15, presentation 368, astrodemo 28, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
138 lines
6.0 KiB
Python
138 lines
6.0 KiB
Python
"""Po czym astroklienta NIE MA — ani w odpowiedzi, ani w kodzie w obrazie.
|
|
|
|
Ta usługa dzieli kod z pełną aplikacją, więc granica między produktami nie jest
|
|
oczywista z samego patrzenia: przebiega przez listę importów w `main.py` i przez
|
|
`usun.txt`. Ten test sprawdza jej SKUTEK na złożonym drzewie — bo tylko skutek
|
|
trafia do obrazu, który komuś się oddaje.
|
|
|
|
„Bez śladu" znaczy tu również bez zaprzeczeń. Zdanie „nie ma tu generowania
|
|
tekstu" mówi wprost, że coś takiego istnieje, więc jest takim samym śladem jak
|
|
przycisk. Pierwsza wersja tego testu wyłapała trzy moje własne docstringi
|
|
wyliczające nieobecne funkcje.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import pathlib
|
|
import re
|
|
|
|
import pytest
|
|
|
|
# Funkcje, których ten produkt nie ma — ani ich słownictwo.
|
|
GENEROWANIE = ["prompt", "LLM", "model językow", "generowanie tekstu", "sztuczn",
|
|
"anthropic", "openai", "gpt", "claude", "horoscope",
|
|
"x-ai.css", "models.js", "progress.js", "natal.js", "predictions.js"]
|
|
NIEOBECNE_EKRANY = ["Skompiluj", "Ustawienia", "Konta i uprawnien", "Eksport plików"]
|
|
MECHANIZMY = ["administrator", "kwarantann", "walidacj", "reguły przyjmowania"]
|
|
SLOWA = GENEROWANIE + NIEOBECNE_EKRANY + MECHANIZMY
|
|
WZORCE = [(r"(?<![\w-])AI(?![\w-])", "skrót nazwy funkcji")]
|
|
|
|
|
|
def _pliki(drzewo: pathlib.Path):
|
|
for f in sorted((drzewo / "app").rglob("*")):
|
|
if f.is_file() and f.suffix != ".pyc" and "__pycache__" not in str(f):
|
|
yield f
|
|
|
|
|
|
def test_the_shipped_tree_carries_no_trace(drzewo):
|
|
grzechy = []
|
|
for f in _pliki(drzewo):
|
|
try:
|
|
tresc = f.read_text(encoding="utf-8")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
gdzie = f.relative_to(drzewo)
|
|
for slowo in SLOWA:
|
|
if re.search(rf"(?i)(?<![\w-]){re.escape(slowo)}", tresc):
|
|
grzechy.append(f"{gdzie}: „{slowo}”")
|
|
for wzor, powod in WZORCE:
|
|
m = re.search(wzor, tresc)
|
|
if m:
|
|
grzechy.append(f"{gdzie}: „{m.group(0)}” — {powod}")
|
|
assert not grzechy, (
|
|
"\nW drzewie astroklienta padły słowa o funkcjach, których nie ma. "
|
|
"`grep` po kontenerze ma nic nie znaleźć:\n " + "\n ".join(grzechy))
|
|
|
|
|
|
def test_the_check_would_notice_if_something_slipped_in(drzewo, tmp_path):
|
|
"""Kontrola negatywna: zapora ma coś łapać.
|
|
|
|
Bez niej test wyżej przechodziłby także wtedy, gdyby lista słów była pusta
|
|
albo gdyby przeszukiwanie trafiało w niewłaściwy katalog."""
|
|
podrzucony = drzewo / "app" / "podrzucony.py"
|
|
podrzucony.write_text("# generowanie tekstu przez model\n", encoding="utf-8")
|
|
try:
|
|
with pytest.raises(AssertionError):
|
|
test_the_shipped_tree_carries_no_trace(drzewo)
|
|
finally:
|
|
podrzucony.unlink()
|
|
|
|
|
|
def test_the_removal_list_matches_what_is_actually_composed(drzewo):
|
|
"""usun.txt i main.py muszą mówić to samo.
|
|
|
|
Rozjazd między nimi znaczy albo martwy kod w obrazie (moduł usunięty
|
|
z importów, ale nie z listy), albo błąd dopiero przy uruchomieniu (plik
|
|
usunięty, a wciąż importowany). Jedno i drugie wychodzi za późno.
|
|
"""
|
|
main = (drzewo / "app" / "main.py").read_text(encoding="utf-8")
|
|
zaimportowane = set(re.findall(r"^\s{4}(\w+),$", main, re.M))
|
|
obecne = {f.stem for f in (drzewo / "app" / "ekrany").glob("*.py")
|
|
if f.stem != "__init__"}
|
|
assert zaimportowane == obecne, (
|
|
f"ekrany w drzewie: {sorted(obecne)}\n"
|
|
f"ekrany importowane: {sorted(zaimportowane)}")
|
|
|
|
|
|
def test_no_optional_module_survived(drzewo):
|
|
"""Katalogu modułów opcjonalnych nie ma wcale — most odkrywa wtedy zero."""
|
|
assert not (drzewo / "app" / "moduly").exists()
|
|
|
|
|
|
# ── nazwa produktu, z którego ten produkt powstał ─────────────────────────
|
|
# Osobno od listy wyżej, bo granica przebiega gdzie indziej: „astrololo" wolno
|
|
# występować w identyfikatorach WEWNĘTRZNYCH (nazwy loggerów, materiał do
|
|
# wyprowadzania kluczy, nagłówki między warstwami), a nie wolno w niczym, co
|
|
# dociera do przeglądarki. Zakaz na wszystko byłby albo obejściem przez zmianę
|
|
# nazw kluczy kryptograficznych, albo martwą regułą wyłączaną przy pierwszej
|
|
# okazji.
|
|
NAZWA_RDZENIA = "astrololo"
|
|
WIDOCZNE_DLA_KLIENTA = ("templates", "static")
|
|
|
|
|
|
def test_the_parent_product_is_not_named_in_anything_the_browser_receives(drzewo):
|
|
"""Szablony i zasoby są WSPÓLNE z pełną aplikacją, więc jej nazwa jechała
|
|
z nimi do tego obrazu — i astroklient przedstawiał się jako astrololo.
|
|
|
|
To jest najpoważniejszy z możliwych śladów: mówi klientowi wprost, że trzyma
|
|
okrojoną wersję czegoś większego, i podaje frazę, po której da się wyszukać,
|
|
czego mu nie dano."""
|
|
grzechy = []
|
|
for katalog in WIDOCZNE_DLA_KLIENTA:
|
|
for f in sorted((drzewo / "app" / katalog).rglob("*")):
|
|
if not f.is_file() or "__pycache__" in str(f):
|
|
continue
|
|
try:
|
|
tresc = f.read_text(encoding="utf-8")
|
|
except UnicodeDecodeError:
|
|
continue
|
|
if NAZWA_RDZENIA in tresc.lower():
|
|
grzechy.append(str(f.relative_to(drzewo)))
|
|
assert not grzechy, (
|
|
f"\nNazwa „{NAZWA_RDZENIA}” pada w plikach, które dostaje "
|
|
f"przeglądarka:\n " + "\n ".join(grzechy))
|
|
|
|
|
|
def test_the_rendered_pages_carry_this_products_own_name(wynik):
|
|
"""Sprawdzane na ODPOWIEDZI, nie na szablonie: liczy się to, co dociera do
|
|
człowieka, a nie to, co jest w pliku przed złożeniem."""
|
|
assert wynik["nazwa_w_naglowku"] == "astroklient"
|
|
assert wynik["nazwa_w_tytule"] == "astroklient"
|
|
assert not wynik["rdzen_w_tresci"], (
|
|
"nazwa pełnej aplikacji pada w treści strony oddawanej klientowi")
|
|
|
|
|
|
def test_the_api_documentation_is_not_reachable(wynik):
|
|
"""Tytuł aplikacji FastAPI wciąż niesie nazwę rdzenia, więc dokumentacja
|
|
OpenAPI byłaby wyciekiem tylnymi drzwiami — nawet gdy szablony są czyste."""
|
|
assert all(k == 404 for k in wynik["dokumentacja"].values()), wynik["dokumentacja"]
|