fix(testy): awaria magazynu kont wymuszana podmianą, nie prawami pliku
build / build (push) Successful in 7s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m29s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m25s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 6s
Testy / Kontrola składni wszystkich warstw (push) Successful in 5s

Dwa testy z PR #71 przechodziły lokalnie i padały w CI. Przyczyna nie miała nic
wspólnego z badaną rzeczą: wymuszały awarię przez chmod 000 i chmod 555, a CI
działa jako ROOT — root omija bity uprawnień w Linuksie, więc odczyt i zapis się
udawały i asercje leciały na komunikat, którego nie było.

Test zależny od tego, KTO go uruchamia, jest gorszy niż jego brak: daje fałszywe
poczucie pokrycia i zapala się w miejscu niezwiązanym z tym, co sprawdza.

Teraz awarię wymuszamy podmianą dokładnie tego punktu, w którym system plików
mówi „nie": odczytu pliku kont (osłona przepuszcza wszystko inne, żeby szablony
nadal się wczytywały) oraz mkstemp przy zapisie. To drugie jest celowe — mkstemp
wywala się PIERWSZY przy katalogu tylko do odczytu, jeszcze zanim dojdzie do
zapisu i podmiany, i właśnie tego dotyczyła naprawiana poprawka.

Sprawdzian na PRAWDZIWYCH prawach zostaje, ale z pominięciem przy uruchomieniu
z roota — jako kontrola, że podmiana odpowiada rzeczywistości, a nie tylko sama
sobie. W CI się nie wykona i to jest w porządku: znaczyłby tam tyle co nic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit was merged in pull request #73.
This commit is contained in:
2026-08-11 14:40:41 +02:00
committed by gitea
parent ac8a0e9fc6
commit ee3c515d59
@@ -12,6 +12,7 @@ DWIE WŁASNOŚCI, KTÓRYCH PILNUJE TEN PLIK
pozycji w menu, brak rysunków w źródle strony. pozycji w menu, brak rysunków w źródle strony.
""" """
import json import json
import os
import pathlib import pathlib
import pytest 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 # 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 # 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. # (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): def _fail_reading_accounts(monkeypatch, exc=PermissionError(13, "Permission denied")):
d = tmp_path / "stan" """Odczyt PLIKU KONT wywala się; wszystko inne (szablony!) czyta normalnie."""
d.mkdir() import builtins
monkeypatch.setenv("ACCOUNTS_FILE", str(d / "accounts.json"))
d.chmod(mode) from app import accounts as store
return d
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): def test_unreadable_store_explains_itself_instead_of_500(env, tmp_path, monkeypatch):
d = _unreadable(tmp_path, monkeypatch, 0o000) monkeypatch.setenv("ACCOUNTS_FILE", str(tmp_path / "stan" / "accounts.json"))
try: _fail_reading_accounts(monkeypatch)
c = _client(monkeypatch)
r = c.get("/accounts", headers=_auth("szef", "tajne-szefa")) c = _client(monkeypatch)
assert r.status_code == 200, "problem z magazynem nie może wywalać strony" r = c.get("/accounts", headers=_auth("szef", "tajne-szefa"))
assert "Nie mogę odczytać pliku kont" in r.text assert r.status_code == 200, "problem z magazynem nie może wywalać strony"
assert str(d / "accounts.json") in r.text, "komunikat ma podać ŚCIEŻKĘ" assert "Nie mogę odczytać pliku kont" in r.text
finally: assert "accounts.json" in r.text, "komunikat ma podać ŚCIEŻKĘ"
d.chmod(0o755)
def test_read_only_store_refuses_to_save_with_a_reason(env, tmp_path, monkeypatch): 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: try:
c = _client(monkeypatch) c = _client(monkeypatch)
r = c.post("/accounts/create", headers=_auth("szef", "tajne-szefa"), r = c.get("/accounts", headers=_auth("szef", "tajne-szefa"))
follow_redirects=False, assert r.status_code == 200
data={"login": "ala", "password": "x", "granted": ["chart"]}) assert "Nie mogę odczytać pliku kont" in r.text
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"])
finally: finally:
d.chmod(0o755) d.chmod(0o755)