PRE-27: generowanie tekstu przez model znika bez uprawnienia
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
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
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>
This commit was merged in pull request #77.
This commit is contained in:
@@ -64,6 +64,35 @@ jest niewidoczna do chwili, gdy ktoś ją znajdzie. Trasa bez wpisu w mapie wyma
|
||||
uprawnień administracyjnych: **przeoczenie ma zamykać, nie otwierać**. Test
|
||||
przechodzi po wszystkich trasach aplikacji i wymaga, by każda była opisana.
|
||||
|
||||
### Gdy jedna trasa robi kilka rzeczy
|
||||
|
||||
Mapa tras nie wystarcza tam, gdzie jedna trasa obsługuje kilka funkcji naraz.
|
||||
`POST /interpret` liczy horoskop, ale to samo pole `action` prosi o wygenerowanie
|
||||
promptu, napisanie horoskopu przez model albo eksport arkusza. Konto, które ma
|
||||
mieć Interpretacje bez generowania, musi dostać tę trasę — więc granica przebiega
|
||||
wewnątrz niej, po akcjach: `_AKCJE_POD_UPRAWNIENIEM` przypisuje akcji uprawnienie,
|
||||
a `_dozwolona_akcja()` sprowadza żądanie bez uprawnienia do akcji domyślnej ekranu.
|
||||
|
||||
Sprowadza — nie odrzuca. Komunikat „brak uprawnień do generowania" sam w sobie
|
||||
mówiłby, że taka funkcja istnieje, czyli łamałby zasadę drugą po to, żeby
|
||||
wyegzekwować pierwszą. Akcja bez uprawnienia ma wyglądać na literówkę w formularzu.
|
||||
|
||||
### Ślad to nie tylko przycisk
|
||||
|
||||
Wymaganie brzmi „nie może być śladu", i to jest mocniejsze niż schowanie kontrolki.
|
||||
Największym wyciekiem po stronie generowania nie był przycisk, tylko **katalog
|
||||
modeli** — nazwy dostawców, nazwy modeli i rozmiary okien kontekstu — wstrzykiwany
|
||||
w stronę blokiem JSON na każdym ekranie z generowaniem, niezależnie od uprawnień.
|
||||
Dlatego `_llm_catalog_for()` oddaje pusty katalog kontu bez uprawnienia, a szablony
|
||||
trzymają pod bramką także znaczniki (`natalNote`, `reportNatal`), pliki skryptów
|
||||
(`models.js`, `progress.js`, `natal.js`, `predictions.js`) i **zdania opisujące
|
||||
funkcję** — podtytuł ekranu Skompiluj wymieniał interpretację od AI z nazwy.
|
||||
|
||||
Testu na to nie da się napisać przez „sprawdź, czy przycisku nie ma": trzeba
|
||||
sprawdzić, że w źródle strony nie ma żadnego z tych śladów, i mieć kontrolę
|
||||
pozytywną, że przy uprawnieniu wszystkie są. Inaczej test przechodzi także wtedy,
|
||||
gdy generowanie jest zepsute dla wszystkich.
|
||||
|
||||
## Gdzie leżą konta
|
||||
|
||||
Plik JSON wskazany przez `ACCOUNTS_FILE` (domyślnie `/app/state/accounts.json`),
|
||||
|
||||
@@ -129,6 +129,35 @@ def _build_utc(date: str, time: str, tz_offset: float) -> tuple[str, str]:
|
||||
return utc.isoformat(), label
|
||||
|
||||
|
||||
# Akcja formularza → uprawnienie, którego wymaga. Akcji spoza tej mapy nie
|
||||
# ograniczamy; akcja Z mapy, na którą konto nie ma prawa, jest traktowana jak
|
||||
# NIEZNANA — czyli wraca do zachowania domyślnego ekranu.
|
||||
_AKCJE_POD_UPRAWNIENIEM = {"prompt": "ai", "horoscope": "ai", "export": "export"}
|
||||
|
||||
|
||||
def _dozwolona_akcja(request: Request, action: str, domyslna: str) -> str:
|
||||
"""Akcja, której konto nie ma prawa użyć, zachowuje się jak literówka.
|
||||
|
||||
NIE zwracamy błędu ani 404: komunikat „brak uprawnień do generowania" sam
|
||||
w sobie mówiłby, że taka funkcja istnieje (PRE-27). Ukrycie przycisku chroni
|
||||
tylko przed przypadkiem — kto zna nazwę pola, wyśle ją wprost, i to TU musi
|
||||
się zatrzymać."""
|
||||
wymagane = _AKCJE_POD_UPRAWNIENIEM.get(action)
|
||||
if wymagane and wymagane not in _perms(request):
|
||||
return domyslna
|
||||
return action
|
||||
|
||||
|
||||
def _llm_catalog_for(request: Request) -> dict:
|
||||
"""Katalog modeli — pusty, gdy konto nie generuje tekstu przez model.
|
||||
|
||||
Katalog trafia do strony jako blok JSON, więc bez tego ograniczenia nazwy
|
||||
dostawców, modeli i rozmiary okien kontekstu byłyby w źródle KAŻDEJ strony
|
||||
Interpretacji i Kalendarza, także dla kont, które o modelach nie mają prawa
|
||||
wiedzieć."""
|
||||
return _llm_catalog() if "ai" in _perms(request) else {}
|
||||
|
||||
|
||||
def _llm_catalog() -> dict:
|
||||
"""Podpowiedzi modeli dla pola wyboru. Awaria logiki nie może wywrócić strony —
|
||||
pole modelu jest tekstowe, więc bez katalogu nadal da się wpisać model ręcznie."""
|
||||
@@ -175,7 +204,7 @@ def chart_form(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request, "chart.html",
|
||||
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL,
|
||||
"llm_catalog": _llm_catalog()},
|
||||
"llm_catalog": _llm_catalog_for(request)},
|
||||
)
|
||||
|
||||
|
||||
@@ -472,7 +501,7 @@ def interpret_form(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request, "interpret.html",
|
||||
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL,
|
||||
"llm_catalog": _llm_catalog()},
|
||||
"llm_catalog": _llm_catalog_for(request)},
|
||||
)
|
||||
|
||||
|
||||
@@ -494,8 +523,9 @@ def interpret_run(
|
||||
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset,
|
||||
"lat": lat, "lon": lon, "group": group, "prompt_budget": prompt_budget,
|
||||
"llm_provider": llm_provider, "llm_model": llm_model}
|
||||
action = _dozwolona_akcja(request, action, "report")
|
||||
ctx: dict = {"form": form, "result": None, "error": None, "moment": None,
|
||||
"llm_catalog": _llm_catalog()}
|
||||
"llm_catalog": _llm_catalog_for(request)}
|
||||
try:
|
||||
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||
ctx["moment"] = label
|
||||
@@ -540,7 +570,7 @@ def timeline_form(request: Request):
|
||||
return templates.TemplateResponse(
|
||||
request, "timeline.html",
|
||||
{"result": None, "form": default_form(), "location_label": DEFAULT_LOCATION_LABEL,
|
||||
"llm_catalog": _llm_catalog()},
|
||||
"llm_catalog": _llm_catalog_for(request)},
|
||||
)
|
||||
|
||||
|
||||
@@ -563,8 +593,9 @@ def timeline_run(
|
||||
form = {"person": person, "date": date, "time": time, "tz_offset": tz_offset, "lat": lat, "lon": lon,
|
||||
"from_date": from_date, "to_date": to_date, "prompt_budget": prompt_budget,
|
||||
"llm_provider": llm_provider, "llm_model": llm_model}
|
||||
action = _dozwolona_akcja(request, action, "timeline")
|
||||
ctx: dict = {"form": form, "result": None, "error": None, "moment": None,
|
||||
"llm_catalog": _llm_catalog()}
|
||||
"llm_catalog": _llm_catalog_for(request)}
|
||||
try:
|
||||
iso_utc, label = _build_utc(date, time, tz_offset)
|
||||
ctx["moment"] = label
|
||||
|
||||
@@ -3,8 +3,16 @@
|
||||
{% set nav_active = "compile" %}
|
||||
|
||||
{% block content %}
|
||||
{# Opis raportu zależy od tego, z czego konto może go złożyć. Wersja z AI
|
||||
wymieniałaby funkcje, których część kont nie ma — a wymienienie ich w zdaniu
|
||||
jest takim samym śladem jak przycisk. #}
|
||||
{% if can(request, 'ai') %}
|
||||
<p class="sub">Składa w jedno: policzony horoskop, interpretację natalną z AI oraz wszystkie
|
||||
zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie trzeba nic przepisywać.</p>
|
||||
{% else %}
|
||||
<p class="sub">Składa policzony horoskop w jeden raport. Dane pobiera z pozostałych
|
||||
zakładek — nie trzeba nic przepisywać.</p>
|
||||
{% endif %}
|
||||
|
||||
{# Gotowość materiału — wypełniana po stronie przeglądarki (compile.js), bo część
|
||||
kawałków (AI) mieszka w magazynie lokalnym, nie na serwerze. #}
|
||||
@@ -120,14 +128,18 @@ zapamiętane predykcje okresowe. Dane pobiera z pozostałych zakładek — nie t
|
||||
{% if result.zodiac %}· zodiak {{ result.zodiac }}{% endif %}</div>
|
||||
{% include "_result_tables.html" %}
|
||||
|
||||
{% if can(request, 'ai') %}
|
||||
{# ── 4. i 5. Części od AI — wstawia przeglądarka z magazynu ───────── #}
|
||||
<div id="reportNatal"></div>
|
||||
<div id="reportPredictions"></div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if can(request, 'ai') %}
|
||||
{# Magazyny wczytujemy dla ich API odczytu. Ich własne UI samo się wyłącza —
|
||||
każdy sprawdza, czy jego kontener jest na stronie (tu go nie ma). #}
|
||||
<script src="{{ static('natal.js') }}"></script>
|
||||
<script src="{{ static('predictions.js') }}"></script>
|
||||
{% endif %}
|
||||
<script src="{{ static('compile.js') }}"></script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -35,14 +35,19 @@
|
||||
<label><input type="checkbox" name="group" value="true" {{ 'checked' if form.group else '' }}> grupuj identyczne opisy</label>
|
||||
</div>
|
||||
{% if location_label %}<p class="muted small">Wstępnie wpisano lokalizację: <strong>{{ location_label }}</strong> ({{ form.lat }}, {{ form.lon }}). Zmień pola lub kliknij „Tu i teraz".</p>{% endif %}
|
||||
{% include "_prompt_block.html" %}
|
||||
<p class="muted small" id="natalNote"></p>
|
||||
{# Cały blok generowania — budżet, dostawca, model i wynik — istnieje TYLKO
|
||||
dla kont z tym uprawnieniem. Nie ukrywamy go stylem: bez uprawnienia nie ma
|
||||
go w źródle strony, razem z katalogiem modeli, który blok wstrzykuje. #}
|
||||
{% if can(request, 'ai') %}{% include "_prompt_block.html" %}{% endif %}
|
||||
{% if can(request, 'ai') %}<p class="muted small" id="natalNote"></p>{% endif %}
|
||||
<div class="actions">
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit" name="action" value="report">Szukaj interpretacji</button>
|
||||
{% if can(request, 'export') %}<button type="submit" name="action" value="export" class="ghost" title="Pobierz dopasowania jako tabelę roboczą .xlsx — do filtrowania, sortowania i zaznaczania w Excelu (PRE-10).">Pobierz Excel</button>{% endif %}
|
||||
{% if can(request, 'ai') %}
|
||||
<button type="submit" name="action" value="prompt" class="ghost">Generuj prompt (AI)</button>
|
||||
<button type="submit" name="action" value="horoscope">Napisz horoskop (AI)</button>
|
||||
{% endif %}
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
@@ -99,8 +104,12 @@
|
||||
|
||||
<script src="{{ static('now.js') }}"></script>
|
||||
<script src="{{ static('copy.js') }}"></script>
|
||||
{# Skrypty obsługujące WYŁĄCZNIE generowanie przez model. Bez uprawnienia nie ma
|
||||
ich nawet w odsyłaczach — sam adres pliku podpowiadałby, co program potrafi. #}
|
||||
{% if can(request, 'ai') %}
|
||||
<script src="{{ static('models.js') }}"></script>
|
||||
<script src="{{ static('progress.js') }}"></script>
|
||||
{# po progress.js — nasłuchuje zdarzenia o gotowej interpretacji (PRE-23) #}
|
||||
<script src="{{ static('natal.js') }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -40,12 +40,14 @@
|
||||
<input type="date" name="to_date" value="{{ form.to_date or '' }}" required>
|
||||
</label>
|
||||
</div>
|
||||
{% include "_prompt_block.html" %}
|
||||
{% if can(request, 'ai') %}{% include "_prompt_block.html" %}{% endif %}
|
||||
<div class="actions">
|
||||
<button type="button" id="nowBtn" class="ghost">Tu i teraz</button>
|
||||
<button type="submit" name="action" value="timeline">Pokaż kalendarz</button>
|
||||
{% if can(request, 'ai') %}
|
||||
<button type="submit" name="action" value="prompt" class="ghost">Generuj prompt (AI)</button>
|
||||
<button type="submit" name="action" value="horoscope">Napisz horoskop (AI)</button>
|
||||
{% endif %}
|
||||
<span id="geoNote" class="muted small"></span>
|
||||
</div>
|
||||
</form>
|
||||
@@ -93,8 +95,10 @@
|
||||
|
||||
<script src="{{ static('now.js') }}"></script>
|
||||
<script src="{{ static('copy.js') }}"></script>
|
||||
{% if can(request, 'ai') %}
|
||||
<script src="{{ static('models.js') }}"></script>
|
||||
<script src="{{ static('progress.js') }}"></script>
|
||||
{# po progress.js — nasłuchuje zdarzenia, które tamten wysyła po gotowym horoskopie #}
|
||||
<script src="{{ static('predictions.js') }}"></script>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"""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}”"
|
||||
Reference in New Issue
Block a user