2e9d3706ec
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m50s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Testy astrodemo (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 8s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s
Testy / Testy warstwy logicznej (silnik) (pull_request) Failing after 4m43s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m25s
Testy / Testy astrodemo (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 4s
Pierwszy z pięciu kroków budowy trzech produktów: astrodemo (dwie funkcje) → astroklient (pełne astro bez AI) → astrololo (wszystko). Nazwa „astroklient" zostaje zwolniona dla warstwy pośredniej, więc dotychczasowe astroklient-demo nazywa się teraz astrodemo. REBASE. Cztery commity demo przeniesione na aktualnego mastera. Konflikt był jeden — rejestr wymagań (xlsx, binarny, git go nie scali). Master dodał LOG-34, gałąź demo PRE-28 i PRE-29; żaden wspólny wiersz się nie różnił, więc scalone ręcznie: 148 pozycji, wszystkie trzy obecne. ZMIANA NAZWY. Katalog, ciasteczko sesji (astrodemo_sesja), zmienne ASTRODEMO_USERS/USER/PASSWORD, CI, README, docstringi. Ponieważ PR z demo nigdy nie został zmergowany, usługa nie jest nigdzie wdrożona — zmiana nazw niczego nie migruje i nikogo nie wylogowuje. WYCIEKI. astrodemo powstało przed audytem z PRE-27, więc miało komplet tych samych dziur: - /static omijało bramkę (PUBLIC_PREFIXES), a pierwszy komentarz w styles.css brzmiał „Nie kopiujemy stylów pełnej aplikacji" — czyli anonimowy curl dowiadywał się, że istnieje pełna aplikacja. Zasoby idą teraz trasą z jawną listą, komentarze są zdejmowane przy serwowaniu. - Komunikat awarii wypisywał na ekran treść wyjątku httpx, z nazwą usługi i portem. Teraz jedno neutralne zdanie, szczegóły do dziennika. - /health oddawał nazwę warstwy. Teraz samo „ok". - Dziesięć komentarzy i docstringów tłumaczyło decyzje przez porównanie z „pełną aplikacją". Obraz tej usługi się KOMUŚ ODDAJE, więc kto go dostanie, przeczyta też komentarze. Przepisane tak, żeby opisywały tę usługę samą w sobie. - Nagłówek main.py twierdził, że demo dzieli pulę plików z produkcją. To nieprawda od PRE-29 (pule per konto) — opis poprawiony. ZAPORA SŁOWNIKOWA, dwupoziomowa. Poziom „wszędzie" (także w kodzie serwera, bo obraz się oddaje) obejmuje wzmianki o większym rodzeństwie, o modelu językowym i o funkcjach, których tu nie ma. Poziom „do przeglądarki" dokłada słownictwo mechanizmów. Test sprawdza odpowiedzi ORAZ drzewo plików. Jeden wyjątek jest jawny i opisany: stałe protokołu łącza (X-Astrololo-Token, X-Astrololo-Enc, typ treści, etykieta HKDF) niosą nazwę rodziny produktów. Są wspólne z warstwą logiczną, więc zmiana wymaga jednoczesnej podmiany we wszystkich usługach i rotacji — osobna decyzja. Osobny test pilnuje warunku, pod jakim to zostaje: że nie docierają do przeglądarki. Wcześniej przechodziły tylko dlatego, że regex nie dopasowywał po myślniku — przypadek, nie decyzja. Przy okazji: komunikat „ramka bez znacznika astrololo" zmieniony na neutralny we WSZYSTKICH PIĘCIU kopiach link_crypto.py (presentation, astrodemo, logic, data, render), żeby nie rozjechały się przed scaleniem w rdzeń. Te kopie to 2625 linii tego samego kodu. Testy: astrodemo 27, presentation 358, logic 342, data 42, render 41. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
178 lines
8.3 KiB
Python
178 lines
8.3 KiB
Python
"""Zapora słownikowa astrodemo: czego użytkownik demo nie może zobaczyć.
|
|
|
|
astrodemo stoi najniżej w drabinie trzech produktów (astrodemo → astroklient →
|
|
astrololo) i jest dawane najszerzej. Ma dwie funkcje i ma wyglądać, jakby to
|
|
było wszystko, co program potrafi.
|
|
|
|
Test nie sprawdza pojedynczych miejsc, tylko przechodzi WSZYSTKO, co da się
|
|
pobrać, i szuka słów, które nie mają prawa paść. Sprawdza też DRZEWO PLIKÓW
|
|
usługi — bo „nie ma śladu w kodzie" znaczy, że `grep` po obrazie ma nic nie
|
|
znaleźć, a nie tylko że odpowiedź jest czysta.
|
|
|
|
Powód, dla którego to powstało: komentarz w pierwszej linii styles.css mówił
|
|
„Nie kopiujemy stylów pełnej aplikacji", a arkusz pobierał anonim.
|
|
"""
|
|
import pathlib
|
|
import re
|
|
|
|
import pytest
|
|
|
|
APP = pathlib.Path(__file__).resolve().parents[1] / "app"
|
|
|
|
# (a) że istnieje wersja obszerniejsza i jak się nazywa
|
|
WIEKSZY_BRAT = ["pełna aplikacja", "pełnej aplikacji", "pełna wersja", "pełnej wersji",
|
|
"astrololo", "astroklient", "wersji podstawowej", "upgrade", "rozszerzon"]
|
|
# (b) że tekst pisze model językowy
|
|
# Bez samego „token": w tej usłudze znaczy token sesji i token międzywarstwowy,
|
|
# a nie jednostkę rozliczeniową modelu. Zamiast tego zwroty jednoznaczne.
|
|
MODEL = ["LLM", "anthropic", "openai", "gpt", "claude", "prompt",
|
|
"generowanie tekstu", "sztuczn", "okno kontekstu", "model językow"]
|
|
# (c) funkcje, których astrodemo nie ma
|
|
INNE_FUNKCJE = ["kosmogram", "aspektarian", "synastri", "kalendarz", "predykcj",
|
|
"skompiluj", "system domów", "systemy domów", "deklinacj", "antyscj"]
|
|
# (d) że jest ktoś, kto widzi więcej, i że produkt jest w budowie
|
|
MECHANIZMY = ["administrator", "uprawnieni", "walidacj", "kwarantann", "roadmap",
|
|
"w budowie", "widok testowy", "warstwa logiczna", "layer"]
|
|
# Dwa poziomy, bo dwie różne drogi wycieku.
|
|
#
|
|
# WSZĘDZIE — także w kodzie serwera, którego przeglądarka nigdy nie zobaczy.
|
|
# Obraz tej usługi się KOMUŚ ODDAJE, więc kto go dostanie, przeczyta też
|
|
# komentarze i docstringi. Zdanie „w pełnej aplikacji to działa inaczej" mówi mu
|
|
# wprost, że istnieje pełna aplikacja — a to jest dokładnie to, czego ma nie
|
|
# wiedzieć. Komentarz ma tłumaczyć TĘ usługę, bez powoływania się na rodzeństwo.
|
|
WSZEDZIE = WIEKSZY_BRAT + MODEL + INNE_FUNKCJE
|
|
# TYLKO DO PRZEGLĄDARKI — słownictwo inżynierskie opisujące własne działanie tej
|
|
# usługi. W komentarzu serwera jest na miejscu; w odpowiedzi HTTP nie ma czego
|
|
# szukać, bo mówi o mechanizmach, o których użytkownik nie ma wiedzieć.
|
|
DO_PRZEGLADARKI = WSZEDZIE + MECHANIZMY
|
|
ZAKAZANE = DO_PRZEGLADARKI
|
|
WZORCE = [(r"(?i)\b(PRE|LOG|DAN)-\d+\b", "identyfikator wymagania"),
|
|
(r"(?i)\bfeat/[\w-]+", "nazwa gałęzi rozwojowej"),
|
|
(r"(?i)\bAI\b", "model językowy")]
|
|
|
|
|
|
def _grzechy(tresc: str, gdzie: str, slownik: list[str] | None = None) -> list[str]:
|
|
out = []
|
|
for igla in (slownik if slownik is not None else ZAKAZANE):
|
|
if re.search(rf"(?i)(?<![\w-]){re.escape(igla)}", tresc):
|
|
out.append(f"{gdzie}: „{igla}”")
|
|
for wzor, powod in (WZORCE if slownik is None else WZORCE[-1:]):
|
|
m = re.search(wzor, tresc)
|
|
if m:
|
|
out.append(f"{gdzie}: „{m.group(0)}” — {powod}")
|
|
return out
|
|
|
|
|
|
@pytest.fixture()
|
|
def klient(monkeypatch):
|
|
monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo")
|
|
monkeypatch.setenv("SESSION_SECRET", "t" * 64)
|
|
monkeypatch.setenv("COOKIES_INSECURE", "1")
|
|
monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0")
|
|
from starlette.testclient import TestClient
|
|
|
|
from app.main import app
|
|
|
|
return TestClient(app, follow_redirects=False)
|
|
|
|
|
|
def test_nothing_forbidden_reaches_an_anonymous_visitor(klient):
|
|
grzechy, obejrzane = [], 0
|
|
for adres in ("/logowanie", "/health", "/static/styles.css"):
|
|
r = klient.get(adres)
|
|
assert r.status_code == 200, f"{adres}: {r.status_code}"
|
|
obejrzane += 1
|
|
grzechy += _grzechy(r.text, adres)
|
|
assert obejrzane == 3
|
|
assert not grzechy, "\n" + "\n".join(grzechy)
|
|
|
|
|
|
def test_nothing_forbidden_reaches_a_logged_in_visitor(klient, monkeypatch):
|
|
from app import security, session
|
|
|
|
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('demo')}"}
|
|
# Warstwa logiczna niedostępna CELOWO: ekran idzie wtedy ścieżką błędu,
|
|
# a to komunikaty o awarii wypisywały nazwy warstw i tras.
|
|
import httpx
|
|
|
|
from app.main import LogicClient
|
|
|
|
monkeypatch.setattr(LogicClient, "report",
|
|
lambda *a, **k: (_ for _ in ()).throw(httpx.ConnectError("nie da rady")))
|
|
grzechy = []
|
|
for adres in ("/", "/static/styles.css"):
|
|
r = klient.get(adres, headers=naglowki)
|
|
assert r.status_code == 200, f"{adres}: {r.status_code}"
|
|
grzechy += _grzechy(r.text, adres)
|
|
r = klient.post("/", headers=naglowki,
|
|
data={"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
|
|
"lat": "50.06", "lon": "19.94"})
|
|
grzechy += _grzechy(r.text, "POST /")
|
|
assert not grzechy, "\n" + "\n".join(grzechy)
|
|
|
|
|
|
# Stałe protokołu łącza (nagłówki, typ treści, etykieta HKDF) niosą nazwę rodziny
|
|
# produktów, ale są WSPÓLNE z warstwą logiczną: zmiana wymaga jednoczesnej zmiany
|
|
# we wszystkich usługach i rotacji, więc to osobna decyzja. Nie idą do przeglądarki.
|
|
BEZ_KONTROLI = {"app/link_crypto.py"}
|
|
|
|
|
|
def test_the_source_tree_itself_carries_no_trace():
|
|
"""„Bez śladu w kodzie" znaczy: grep po obrazie ma nic nie znaleźć.
|
|
|
|
Odpowiedź może być czysta, a plik i tak nieść komentarz — dokładnie tak
|
|
wyciekał arkusz stylów, zanim komentarze zaczęły być zdejmowane."""
|
|
# Identyfikatory wymagań (PRE-28, LOG-34) zostają: to notatki inżynierskie
|
|
# o tej usłudze, nie informacja o istnieniu czegoś większego.
|
|
grzechy = []
|
|
for f in APP.rglob("*"):
|
|
wzgledna = str(f.relative_to(APP.parent))
|
|
if (not f.is_file() or "__pycache__" in str(f) or f.suffix == ".pyc"
|
|
or wzgledna in BEZ_KONTROLI):
|
|
continue
|
|
try:
|
|
grzechy += _grzechy(f.read_text(encoding="utf-8"),
|
|
str(f.relative_to(APP.parent)), WSZEDZIE)
|
|
except UnicodeDecodeError:
|
|
continue
|
|
assert not grzechy, "\n" + "\n".join(grzechy)
|
|
|
|
|
|
def test_an_unknown_asset_is_indistinguishable_from_one_that_never_existed(klient):
|
|
from app import security, session
|
|
|
|
naglowki = {"Cookie": f"{session.COOKIE}={security.issue_session('demo')}"}
|
|
nieistniejacy = klient.get("/static/nie-ma-takiego.js", headers=naglowki)
|
|
for adres in ("/static/main.py", "/static/../app/config.py", "/static/vendor/cokolwiek.js"):
|
|
r = klient.get(adres, headers=naglowki)
|
|
assert r.status_code == nieistniejacy.status_code == 404, f"{adres}: {r.status_code}"
|
|
assert r.text == nieistniejacy.text
|
|
|
|
|
|
def test_the_protocol_constants_are_the_one_known_exception():
|
|
"""Świadomie zostawiona dziura — opisana, żeby nie była przypadkiem.
|
|
|
|
Stałe łącza międzywarstwowego (`X-Astrololo-Token`, `X-Astrololo-Enc`, typ
|
|
treści, etykieta HKDF) niosą nazwę rodziny produktów. Są WSPÓLNE z warstwą
|
|
logiczną, więc zmiana wymaga jednoczesnej podmiany we wszystkich usługach
|
|
i rotacji sekretów — to osobna decyzja, nie efekt uboczny tej zmiany.
|
|
|
|
Warunek, pod jakim to zostaje: te stałe nie mają prawa dotrzeć do
|
|
przeglądarki. Idą wyłącznie w nagłówkach żądań DO warstwy logicznej.
|
|
Ten test pilnuje właśnie tego warunku, a nie samego istnienia stałych.
|
|
"""
|
|
rdzen = "astrololo"
|
|
protokol = [f for f in APP.rglob("*.py")
|
|
if f.is_file() and rdzen in f.read_text(encoding="utf-8").lower()]
|
|
assert protokol, "stałe zniknęły — jeśli je przemianowano, ten wyjątek jest zbędny"
|
|
for f in protokol:
|
|
tresc = f.read_text(encoding="utf-8").lower()
|
|
for linia in tresc.splitlines():
|
|
if rdzen not in linia:
|
|
continue
|
|
# Każde wystąpienie musi być stałą protokołu albo nazwą kanału logów —
|
|
# nigdy tekstem, który mógłby trafić do szablonu.
|
|
assert any(z in linia for z in ("x-astrololo", "vnd.astrololo",
|
|
"astrololo/link/", 'getlogger("astrololo')), \
|
|
f"{f.name}: „{linia.strip()[:70]}” to nie jest stała protokołu"
|