diff --git a/services/presentation/tests/test_kontrola_dostepu.py b/services/presentation/tests/test_kontrola_dostepu.py index f9fbb2b..c5c1efe 100644 --- a/services/presentation/tests/test_kontrola_dostepu.py +++ b/services/presentation/tests/test_kontrola_dostepu.py @@ -12,6 +12,7 @@ DWIE WŁASNOŚCI, KTÓRYCH PILNUJE TEN PLIK pozycji w menu, brak rysunków w źródle strony. """ import json +import os import pathlib import pytest @@ -315,40 +316,80 @@ def test_deleting_an_account_cannot_touch_the_administrator(env, monkeypatch): # Ekran kont to JEDYNE miejsce, z którego administrator może naprawić problem # z magazynem — więc musi na nim przeczytać, co i gdzie jest nie tak. Gołe 500 # (tak było na pierwszym wdrożeniu) zostawia go z niczym. +# +# AWARIĘ WYMUSZAMY PODMIANĄ, NIE PRAWAMI PLIKU. Pierwsza wersja robiła chmod 000 +# i chmod 555 — i przechodziła lokalnie, a padała w CI, bo CI działa jako ROOT, +# a root omija bity uprawnień w Linuksie. Test zależny od tego, kto go uruchamia, +# jest gorszy niż jego brak: daje fałszywe poczucie pokrycia i zapala się +# w miejscu, które z badaną rzeczą nie ma nic wspólnego. +# +# Podmiana jest przy okazji CELNIEJSZA: sprawdzamy dokładnie ten punkt, w którym +# system plików mówi „nie", zamiast liczyć, że da się go do tego zmusić. -def _unreadable(tmp_path, monkeypatch, mode): - d = tmp_path / "stan" - d.mkdir() - monkeypatch.setenv("ACCOUNTS_FILE", str(d / "accounts.json")) - d.chmod(mode) - return d +def _fail_reading_accounts(monkeypatch, exc=PermissionError(13, "Permission denied")): + """Odczyt PLIKU KONT wywala się; wszystko inne (szablony!) czyta normalnie.""" + import builtins + + from app import accounts as store + + real_open, target = builtins.open, store.store_path() + + def guarded(file, *a, **kw): + if str(file) == target: + raise exc + return real_open(file, *a, **kw) + + monkeypatch.setattr(builtins, "open", guarded) def test_unreadable_store_explains_itself_instead_of_500(env, tmp_path, monkeypatch): - d = _unreadable(tmp_path, monkeypatch, 0o000) - try: - c = _client(monkeypatch) - r = c.get("/accounts", headers=_auth("szef", "tajne-szefa")) - assert r.status_code == 200, "problem z magazynem nie może wywalać strony" - assert "Nie mogę odczytać pliku kont" in r.text - assert str(d / "accounts.json") in r.text, "komunikat ma podać ŚCIEŻKĘ" - finally: - d.chmod(0o755) + monkeypatch.setenv("ACCOUNTS_FILE", str(tmp_path / "stan" / "accounts.json")) + _fail_reading_accounts(monkeypatch) + + c = _client(monkeypatch) + r = c.get("/accounts", headers=_auth("szef", "tajne-szefa")) + assert r.status_code == 200, "problem z magazynem nie może wywalać strony" + assert "Nie mogę odczytać pliku kont" in r.text + assert "accounts.json" in r.text, "komunikat ma podać ŚCIEŻKĘ" def test_read_only_store_refuses_to_save_with_a_reason(env, tmp_path, monkeypatch): - d = _unreadable(tmp_path, monkeypatch, 0o555) + """Podmieniamy mkstemp, bo to ON wywala się PIERWSZY przy katalogu tylko do + odczytu — jeszcze zanim dojdzie do zapisu i podmiany pliku. Obudowanie samego + os.replace niczego by nie dało i właśnie tego dotyczyła poprawka.""" + import tempfile + + monkeypatch.setenv("ACCOUNTS_FILE", str(tmp_path / "stan" / "accounts.json")) + monkeypatch.setattr(tempfile, "mkstemp", + lambda *a, **kw: (_ for _ in ()).throw( + PermissionError(13, "Read-only file system"))) + + c = _client(monkeypatch) + r = c.post("/accounts/create", headers=_auth("szef", "tajne-szefa"), + follow_redirects=False, + data={"login": "ala", "password": "x", "granted": ["chart"]}) + assert r.status_code == 303 + # Komunikat jedzie w parametrze zapytania, więc jest zakodowany — + # porównanie na surowym nagłówku sprawdzałoby procenty, nie treść. + from urllib.parse import unquote_plus + + assert "Nie mogę zapisać pliku kont" in unquote_plus(r.headers["location"]) + + +@pytest.mark.skipif(os.geteuid() == 0, + reason="root omija bity uprawnień — sprawdzian nic by nie znaczył") +def test_real_file_permissions_produce_the_same_message(env, tmp_path, monkeypatch): + """Kontrola, że podmiana wyżej odpowiada RZECZYWISTOŚCI, a nie tylko sama + sobie. Pomijana przy uruchomieniu z roota — czyli w CI.""" + d = tmp_path / "stan" + d.mkdir() + monkeypatch.setenv("ACCOUNTS_FILE", str(d / "accounts.json")) + d.chmod(0o000) try: c = _client(monkeypatch) - r = c.post("/accounts/create", headers=_auth("szef", "tajne-szefa"), - follow_redirects=False, - data={"login": "ala", "password": "x", "granted": ["chart"]}) - assert r.status_code == 303 - # Komunikat jedzie w parametrze zapytania, więc jest zakodowany — - # porównanie na surowym nagłówku sprawdzałoby procenty, nie treść. - from urllib.parse import unquote_plus - - assert "Nie mogę zapisać pliku kont" in unquote_plus(r.headers["location"]) + r = c.get("/accounts", headers=_auth("szef", "tajne-szefa")) + assert r.status_code == 200 + assert "Nie mogę odczytać pliku kont" in r.text finally: d.chmod(0o755)