astrodemo: zmiana nazwy, rebase na mastera i domknięcie wycieków
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>
This commit is contained in:
2026-08-26 12:05:40 +02:00
parent c73ba6a964
commit 2e9d3706ec
24 changed files with 327 additions and 99 deletions
+301
View File
@@ -0,0 +1,301 @@
"""astrodemo — wersja demonstracyjna (PRE-28).
DWIE WŁASNOŚCI, KTÓRYCH PILNUJE TEN PLIK
1. Demo ma DWIE funkcje i ani jednej więcej. Sprawdzamy to na trasach aplikacji,
nie w szablonie: trasa, o której nikt nie pamiętał, jest tu realnym ryzykiem,
bo ta usługa rozmawia z warstwą danych produkcji.
2. „Dodać plik do bazy" znaczy, że plik ZACZYNA BYĆ UŻYWANY. W pełnej aplikacji
wgranie i włączenie to dwie decyzje (DAN-27); tutaj muszą być jedną, inaczej
po wgraniu nic się nie zmienia i demo wygląda na zepsute.
"""
import base64
import pytest
from app import security
@pytest.fixture()
def env(monkeypatch):
monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientA:hasloA,klientB:hasloB")
monkeypatch.delenv("ASTRODEMO_USER", raising=False)
monkeypatch.delenv("ASTRODEMO_PASSWORD", raising=False)
monkeypatch.setenv("RATE_LIMIT_PER_MIN", "0")
# Bez klucza podpisu usługa celowo nie wstaje (fail-closed, LOG-34).
monkeypatch.setenv("SESSION_SECRET", "t" * 64)
monkeypatch.setenv("COOKIES_INSECURE", "1") # TestClient jedzie po http
def _auth(user="demo", password=""):
"""Nagłówek z WAŻNĄ SESJĄ — odpowiednik bycia zalogowanym.
Po przejściu z Basic na sesje (LOG-34) „zalogowany" znaczy „ma podpisane
ciasteczko", nie „ma nagłówek z hasłem". Hasło przyjmowane tylko po to, żeby
nie przepisywać wszystkich wywołań — sprawdzanie poświadczeń ma własne testy."""
from app import security, session
return {"Cookie": f"{session.COOKIE}={security.issue_session(user)}"}
REPORT = {
"engine": "test",
"objects": [{
"object": "Sun", "sign": "Taurus", "house": 11, "direction": "D",
"planet_token": "Su", "planet_total": 3,
"facets": [{"label": "w znaku Taurus", "token": "Su Tau", "count": 1, "score": 5.0,
"samples": [{"significator": "Su Tau", "expanded": "Sun in Taurus",
"effect": "wytrwały, praktyczny"}]}],
}],
}
def _client(monkeypatch, calls=None, upload=None):
"""Podmieniamy METODY KLASY, nie obiekt modułowy: klient jest budowany per
żądanie, żeby pula wynikała z zalogowanego konta. Dzięki temu podglądamy
też `self.tenant`, czyli to, do której puli faktycznie poszło zapytanie."""
from starlette.testclient import TestClient
from app.clients.logic_client import LogicClient
from app.main import app
calls = calls if calls is not None else []
domyslny = {"path": "nowa.xlsx", "name": "nowa.xlsx", "accepted": True}
monkeypatch.setattr(LogicClient, "report",
lambda self, **kw: (calls.append(("report", self.tenant, kw)), REPORT)[1])
monkeypatch.setattr(LogicClient, "files_upload",
lambda self, *a, **kw: (calls.append(("upload", self.tenant, a)),
upload or domyslny)[1])
monkeypatch.setattr(LogicClient, "files_status",
lambda self, *a, **kw: (calls.append(("status", self.tenant, a)), {})[1])
return TestClient(app), calls
FORM = {"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
"lat": "50.06", "lon": "19.94"}
# ── powierzchnia: dokładnie dwie funkcje ────────────────────────────────
def test_the_service_exposes_nothing_beyond_its_two_functions():
"""Trasa, o której nikt nie pamiętał, jest tu realnym ryzykiem — ta usługa
rozmawia z warstwą danych PRODUKCJI."""
from app.main import app
trasy = {(m, r.path) for r in app.routes for m in (getattr(r, "methods", None) or set())
if not r.path.startswith("/static") and m in {"GET", "POST"}}
assert trasy == {("GET", "/"), ("POST", "/"), ("POST", "/plik"), ("GET", "/health"),
("GET", "/logowanie"), ("POST", "/logowanie"), ("POST", "/wyloguj")}
def test_the_client_can_only_ask_for_what_the_demo_needs():
"""Klient warstwy logicznej jest okrojony CELOWO: metody, której nie ma, nikt
przypadkiem nie zawoła i nie podpowie ona, że po drugiej stronie jest więcej."""
from app.clients.logic_client import LogicClient
publiczne = {n for n in dir(LogicClient) if not n.startswith("_")}
assert publiczne == {"report", "files_upload", "files_status", "health"}
def test_api_docs_are_off():
"""/docs wypisałoby komplet tras — demo ma nie zdradzać własnej powierzchni."""
from app.main import app
assert app.docs_url is None and app.openapi_url is None and app.redoc_url is None
# ── logowanie ────────────────────────────────────────────────────────────
def test_without_credentials_nothing_is_served(env, monkeypatch):
c, _ = _client(monkeypatch)
for method, path in (("get", "/"), ("post", "/"), ("post", "/plik")):
r = getattr(c, method)(path, follow_redirects=False)
assert r.status_code == 303, path
assert "/logowanie" in r.headers["location"], path
assert c.get("/health").status_code == 200, "sonda musi działać bez hasła"
def test_demo_account_is_separate_from_the_main_application(env, monkeypatch):
"""Konto astrodemo MUSI być własne: demo pracuje na tej samej warstwie
danych co produkcja, więc odcięcie go nie może wymagać zmiany haseł
w głównej aplikacji."""
monkeypatch.setenv("APP_USER", "astrololo")
monkeypatch.setenv("APP_PASSWORD", "haslo-glownej-aplikacji")
c, _ = _client(monkeypatch)
assert security.verify("astrololo", "haslo-glownej-aplikacji") is None
assert security.verify("demo", "tajne-demo") == "demo"
def test_a_password_with_polish_letters_is_refused_not_crashed(env, monkeypatch):
monkeypatch.setenv("ASTRODEMO_USERS", "demo:zażółć-gęślą")
c, _ = _client(monkeypatch)
# ── funkcja 1: dodanie pliku ─────────────────────────────────────────────
def test_adding_a_file_uploads_AND_switches_it_on(env, monkeypatch):
"""Sedno wymagania: jedna czynność, nie dwie."""
c, calls = _client(monkeypatch)
r = c.post("/plik", headers=_auth(), follow_redirects=False,
files={"upload": ("baza.xlsx", b"zawartosc", "application/vnd.ms-excel")})
assert r.status_code == 303
rodzaje = [k[0] for k in calls]
assert rodzaje == ["upload", "status"], f"oczekiwano wgrania i włączenia, było: {rodzaje}"
assert calls[1][2][1] == "active", "plik nie został włączony do użytku"
def test_a_rejected_file_is_not_switched_on_and_the_reason_stays_hidden(env, monkeypatch):
"""Walidacja zostaje, ale jej REGUŁY zna wyłącznie administrator (DAN-27) —
demo nie ma prawa ich zdradzić, nawet mimochodem."""
from urllib.parse import unquote_plus
c, calls = _client(monkeypatch, upload={"path": "zla.xlsx", "name": "zla.xlsx",
"accepted": False})
r = c.post("/plik", headers=_auth(), follow_redirects=False,
files={"upload": ("zla.xlsx", b"x", "text/plain")})
assert r.status_code == 303
assert [k[0] for k in calls] == ["upload"], "odrzucony plik został włączony do użytku"
komunikat = unquote_plus(r.headers["location"])
for slowo in ("walidacj", "reguł", "kolumn", "rozszerzeni", "rozmiar"):
assert slowo not in komunikat, f"komunikat zdradza mechanizm: „{slowo}"
def test_an_empty_file_is_refused_before_anything_is_sent(env, monkeypatch):
c, calls = _client(monkeypatch)
r = c.post("/plik", headers=_auth(), follow_redirects=False,
files={"upload": ("pusty.xlsx", b"", "application/vnd.ms-excel")})
assert r.status_code == 303 and calls == []
# ── funkcja 2: interpretacja urodzeniowa ────────────────────────────────
def test_asking_for_an_interpretation_returns_the_matches(env, monkeypatch):
c, calls = _client(monkeypatch)
r = c.post("/", headers=_auth(), data=FORM)
assert r.status_code == 200
assert "Sun in Taurus" in r.text and "wytrwały, praktyczny" in r.text
assert [k[0] for k in calls] == ["report"]
def test_local_time_is_converted_to_utc_before_asking(env, monkeypatch):
"""11:20 przy offsecie +2 to 09:20 UTC. Pomyłka tutaj przesuwa CAŁY horoskop,
a wynik nadal wygląda wiarygodnie — dlatego sprawdzamy wprost."""
c, calls = _client(monkeypatch)
c.post("/", headers=_auth(), data=FORM)
assert calls[0][2]["when_utc_iso"].startswith("1984-04-30T09:20")
def test_a_malformed_date_is_explained_not_thrown(env, monkeypatch):
c, calls = _client(monkeypatch)
r = c.post("/", headers=_auth(), data={**FORM, "date": "30 kwietnia"})
assert r.status_code == 200 and "RRRR-MM-DD" in r.text
assert calls == [], "poszło zapytanie mimo błędnych danych"
def test_a_broken_logic_layer_is_reported_not_crashed(env, monkeypatch):
import httpx
from app.clients.logic_client import LogicClient
c, _ = _client(monkeypatch)
monkeypatch.setattr(LogicClient, "report", lambda self, **kw: (_ for _ in ()).throw(
httpx.ConnectError("brak połączenia")))
r = c.post("/", headers=_auth(), data=FORM)
# Komunikat jest CELOWO nieinformacyjny: treść wyjątku niosła nazwę usługi,
# jej port i nazwy tras, czyli mapę tego, co stoi za tym ekranem.
assert r.status_code == 200
assert "Nie udało się teraz wykonać tej operacji" in r.text
assert "Warstwa logiczna" not in r.text and "8001" not in r.text
def test_the_page_never_offers_anything_it_cannot_do(env, monkeypatch):
"""Demo ma wyglądać na KOMPLETNE w swojej postaci — żadnych śladów funkcji,
których nie ma (ta sama zasada, co przy uprawnieniach w PRE-27)."""
c, _ = _client(monkeypatch)
html = c.get("/", headers=_auth()).text
for slowo in ("Kosmogram", "Synastria", "Kalendarz", "Skompiluj", "Ustawienia",
"Konta", "PDF", "Excel", "prompt", "model"):
assert slowo not in html, f"strona wspomina o „{slowo}”, czego demo nie umie"
# ── pule kont: izolacja (PRE-29) ─────────────────────────────────────────
# Demo jest udostępniane szeroko i różnym osobom, więc pula musi wynikać
# z tego, KTO SIĘ ZALOGOWAŁ — i z niczego innego. Wyciek byłby cichy: klient
# zobaczyłby cudze bazy jako własne.
def test_the_pool_is_taken_from_the_login(env, monkeypatch):
c, calls = _client(monkeypatch)
c.post("/", headers=_auth("klientA", "hasloA"), data=FORM)
c.post("/", headers=_auth("klientB", "hasloB"), data=FORM)
assert [k[1] for k in calls] == ["klientA", "klientB"]
def test_uploads_go_to_the_pool_of_the_account_that_sent_them(env, monkeypatch):
c, calls = _client(monkeypatch)
c.post("/plik", headers=_auth("klientA", "hasloA"), follow_redirects=False,
files={"upload": ("baza.xlsx", b"x", "application/vnd.ms-excel")})
assert {k[1] for k in calls} == {"klientA"}, "wgranie trafiło do cudzej puli"
def test_the_pool_cannot_be_chosen_from_the_form(env, monkeypatch):
"""Gdyby pula przychodziła z żądania, wystarczyłoby podstawić cudzy login."""
c, calls = _client(monkeypatch)
c.post("/", headers=_auth("klientA", "hasloA"),
data={**FORM, "tenant": "klientB", "user": "klientB", "login": "klientB"})
assert [k[1] for k in calls] == ["klientA"], "pula dała się podmienić z formularza"
def test_every_account_has_its_own_pool_by_construction(env, monkeypatch):
"""Każdy login = inna pula. Dwa konta nie mogą dzielić zbioru baz."""
c, calls = _client(monkeypatch)
for login, haslo in (("demo", "tajne-demo"), ("klientA", "hasloA"), ("klientB", "hasloB")):
c.post("/", headers=_auth(login, haslo), data=FORM)
pule = [k[1] for k in calls]
assert len(set(pule)) == len(pule) == 3
def test_an_unknown_account_gets_nothing(env, monkeypatch):
"""Bez ważnej sesji żądanie nie schodzi niżej — nie wystarczy, że skończy
się przekierowaniem: nie może po drodze dotknąć warstwy logicznej."""
c, calls = _client(monkeypatch)
r = c.post("/", data=FORM, follow_redirects=False)
assert r.status_code == 303 and "/logowanie" in r.headers["location"]
assert calls == [], "poszło zapytanie mimo braku sesji"
def test_logging_in_and_out_of_the_demo(env, monkeypatch):
"""Pełny obieg: demo bywa używane na cudzych komputerach, więc wyjście musi
naprawdę kończyć sesję, a nie tylko tak wyglądać."""
from app import session
c, _ = _client(monkeypatch)
r = c.post("/logowanie", follow_redirects=False,
data={"login": "klientA", "haslo": "hasloA", "dokad": "/"})
assert r.status_code == 303 and session.COOKIE in r.cookies
wyjscie = c.post("/wyloguj", headers=_auth("klientA"), follow_redirects=False)
assert wyjscie.status_code == 303
ustawione = wyjscie.headers.get("set-cookie", "")
assert session.COOKIE in ustawione and "Max-Age=0" in ustawione
def test_removing_an_account_kills_its_open_session(env, monkeypatch):
"""Demo nie ma gdzie trwale zapisywać stanu, więc zdalne unieważnienie robi
się przez ASTRODEMO_USERS — usunięcie albo zmiana hasła ubija sesję odciskiem."""
c, _ = _client(monkeypatch)
ciasteczko = _auth("klientA")
assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 200
monkeypatch.setenv("ASTRODEMO_USERS", "demo:tajne-demo,klientB:hasloB") # bez klientA
assert c.get("/", headers=ciasteczko, follow_redirects=False).status_code == 303
def test_the_demo_login_cannot_be_turned_into_a_redirector(env, monkeypatch):
c, _ = _client(monkeypatch)
for zly in ("https://obcy.pl", "//obcy.pl", "http://obcy.pl/x"):
r = c.post("/logowanie", follow_redirects=False,
data={"login": "demo", "haslo": "tajne-demo", "dokad": zly})
assert r.headers["location"] == "/", f"przepuszczono: {zly}"
@@ -0,0 +1,177 @@
"""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"