Files
astrololo/services/presentation/tests/test_ukrywanie_ai.py
T
gitea 1d0ff2f72e
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 9m25s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 5s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 5s
build / build (push) Successful in 1m39s
Testy / Testy warstwy logicznej (silnik) (push) Failing after 4m45s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m32s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 7s
Testy / Kontrola składni wszystkich warstw (push) Successful in 6s
PRE-27: generowanie tekstu przez model znika bez uprawnienia
Wymaganie było mocniejsze niż schowanie przycisku: po niedostępnej funkcji nie
może zostać śladu w źródle strony. Największy wyciek nie był przyciskiem —
_prompt_block wstrzykiwał w stronę CAŁY katalog modeli jako JSON (dostawcy,
nazwy modeli, rozmiary okien kontekstu), na każdym ekranie z generowaniem,
niezależnie od uprawnień konta.

Druga dziura była głębsza: handlery nie sprawdzały nic. Trasa /interpret musi
być dostępna dla konta z Interpretacjami, więc granica przebiega WEWNĄTRZ niej,
po polu `action` — spreparowany formularz z action=prompt generował tekst, a
action=export pobierał arkusz, mimo że szablon chował oba przyciski.

Akcja bez uprawnienia wraca do akcji domyślnej ekranu zamiast dawać błąd:
komunikat „brak uprawnień do generowania" sam w sobie mówiłby, że taka funkcja
istnieje.

Ślady wycięte także tam, gdzie nie były kontrolką: znaczniki natalNote/
reportNatal, pliki models.js/progress.js/natal.js/predictions.js oraz podtytuł
ekranu Skompiluj, który wymieniał interpretację od AI z nazwy.

12 testów; 9 z nich pada na kodzie sprzed poprawki (sprawdzone przez cofnięcie
zmian w app/). Kontrola pozytywna pilnuje, żeby nie przechodziły dlatego, że
generowanie jest zepsute dla wszystkich.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 13:10:16 +02:00

151 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Generowanie tekstu przez model znika bez uprawnienia (PRE-27 × LOG-29/30/31).
Wymaganie jest MOCNIEJSZE niż „przycisk ma być schowany": po niedostępnej funkcji
nie może zostać ŚLAD w źródle strony. To rozróżnienie ma znaczenie praktyczne —
ukrycie stylem albo atrybutem `hidden` zostawia wszystko w HTML-u, więc podgląd
źródła nadal opowiada, co program potrafi.
Największy wyciek nie był przyciskiem: blok generowania wstrzykiwał w stronę CAŁY
katalog modeli jako JSON — nazwy dostawców, nazwy modeli i rozmiary okien
kontekstu — i robił to na każdej stronie Interpretacji i Kalendarza, niezależnie
od uprawnień konta.
"""
import pytest
from app import accounts as store
# Słowa, które nie mają prawa paść przy koncie bez uprawnienia. Mieszanka
# widocznego tekstu, nazw pól formularza, identyfikatorów dla JS i nazw plików —
# bo ślad zostawia każde z nich z osobna.
SLADY_AI = [
"Generuj prompt", "Napisz horoskop", "llmCatalog", "llmProvider", "llmModel",
"llm_provider", "llm_model", "prompt_budget", "Budżet promptu", "Dostawca",
"anthropic", "openai", "models.js", "progress.js",
'value="prompt"', 'value="horoscope"',
]
RAPORT = {"engine": "test", "objects": []}
KALENDARZ = {"engine": "test", "days": []}
@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 _auth(user):
from app import security, session
return {"Cookie": f"{session.COOKIE}={security.issue_session(user)}"}
def _client(monkeypatch):
"""Klient ze śledzeniem tego, co poszło do warstwy logicznej."""
from starlette.testclient import TestClient
from app.main import app, logic
wolania = []
monkeypatch.setattr(logic, "report", lambda **kw: (wolania.append("report"), RAPORT)[1])
monkeypatch.setattr(logic, "timeline", lambda **kw: (wolania.append("timeline"), KALENDARZ)[1])
monkeypatch.setattr(logic, "prompt",
lambda **kw: (wolania.append("prompt"), {"prompt": "TREŚĆ"})[1])
monkeypatch.setattr(logic, "horoscope",
lambda **kw: (wolania.append("horoscope"), {"text": "TREŚĆ"})[1])
monkeypatch.setattr(logic, "llm_models", lambda: {"providers": {"openai": ["gpt-x"]}})
return TestClient(app, follow_redirects=False), wolania
NATAL = {"date": "1984-04-30", "time": "11:20", "tz_offset": "2",
"lat": "50.06", "lon": "19.94"}
OKRES = {**NATAL, "from_date": "2026-01-01", "to_date": "2026-02-01"}
# ── brak śladu w źródle strony ──────────────────────────────────────────
@pytest.mark.parametrize("ekran", ["/interpret", "/timeline"])
def test_no_trace_of_the_model_features_without_the_permission(env, monkeypatch, ekran):
store.create("ala", "x", ["interpret", "timeline"])
c, _ = _client(monkeypatch)
html = c.get(ekran, headers=_auth("ala")).text
for slad in SLADY_AI:
assert slad not in html, f"{ekran} zdradza „{slad}”"
@pytest.mark.parametrize("ekran", ["/interpret", "/timeline"])
def test_the_same_screens_carry_them_with_the_permission(env, monkeypatch, ekran):
"""Kontrola pozytywna. Bez niej test wyżej przechodziłby także wtedy, gdyby
generowanie było zepsute dla wszystkich."""
store.create("ola", "x", ["interpret", "timeline", "ai"])
c, _ = _client(monkeypatch)
html = c.get(ekran, headers=_auth("ola")).text
for musi_byc in ("Generuj prompt", "llmCatalog", "llmProvider", "models.js"):
assert musi_byc in html, f"{ekran}: brakuje „{musi_byc}” mimo uprawnienia"
def test_the_model_catalogue_never_reaches_a_page_it_should_not(env, monkeypatch):
"""Katalog jedzie do strony blokiem JSON. Bez ograniczenia nazwy dostawców,
modeli i rozmiary okien kontekstu byłyby w źródle KAŻDEJ strony."""
store.create("ala", "x", ["interpret"])
c, _ = _client(monkeypatch)
html = c.get("/interpret", headers=_auth("ala")).text
assert "gpt-x" not in html and "providers" not in html
# ── spreparowane żądanie nie kupuje funkcji ─────────────────────────────
@pytest.mark.parametrize("akcja", ["prompt", "horoscope"])
def test_a_crafted_request_cannot_generate_without_the_permission(env, monkeypatch, akcja):
"""Ukrycie przycisku chroni przed przypadkiem, nie przed kimś, kto zna nazwę
pola. Granicą jest handler."""
store.create("ala", "x", ["interpret"])
c, wolania = _client(monkeypatch)
r = c.post("/interpret", headers=_auth("ala"), data={**NATAL, "action": akcja})
assert r.status_code == 200
assert akcja not in wolania, f"wygenerowano „{akcja}” bez uprawnienia"
assert wolania == ["report"], f"oczekiwano powrotu do domyślnej akcji, było: {wolania}"
@pytest.mark.parametrize("akcja", ["prompt", "horoscope"])
def test_the_same_on_the_calendar_screen(env, monkeypatch, akcja):
store.create("ala", "x", ["timeline"])
c, wolania = _client(monkeypatch)
c.post("/timeline", headers=_auth("ala"), data={**OKRES, "action": akcja})
assert akcja not in wolania and wolania == ["timeline"]
def test_a_crafted_export_needs_the_export_permission_too(env, monkeypatch):
"""Ta sama dziura obejmowała eksport: szablon chował przycisk, handler nie
sprawdzał niczego, więc spreparowane żądanie pobierało arkusz."""
store.create("ala", "x", ["interpret"])
c, _ = _client(monkeypatch)
r = c.post("/interpret", headers=_auth("ala"), data={**NATAL, "action": "export"})
assert r.status_code == 200
assert "spreadsheetml" not in r.headers.get("content-type", ""), "pobrano arkusz bez uprawnienia"
def test_the_streaming_route_stays_shut(env, monkeypatch):
"""Pisanie horoskopu idzie osobną trasą — ona też musi być zamknięta,
i to jako NIEISTNIEJĄCA, nie jako odmowa."""
store.create("ala", "x", ["interpret"])
c, wolania = _client(monkeypatch)
r = c.post("/horoscope/stream", headers=_auth("ala"), json={"profile": "natal"})
assert r.status_code == 404
assert wolania == []
def test_refusal_never_explains_itself(env, monkeypatch):
"""Komunikat „brak uprawnień do generowania" sam w sobie mówiłby, że taka
funkcja istnieje. Akcja bez uprawnienia ma zachowywać się jak literówka."""
store.create("ala", "x", ["interpret"])
c, _ = _client(monkeypatch)
html = c.post("/interpret", headers=_auth("ala"), data={**NATAL, "action": "prompt"}).text
for slowo in ("uprawnie", "brak dostępu", "nie wolno", "zabronione", "AI", "model"):
assert slowo not in html, f"odpowiedź tłumaczy odmowę słowem „{slowo}”"