"""Sesje logowania — rdzeń podpisywania (LOG-34). Testy dzielą się na dwie grupy i ta druga jest ważniejsza: „czy działa" oraz „czy da się oszukać". Kod, który tylko przechodzi tę pierwszą, wpuszcza każdego, kto spróbuje czegoś innego niż zamierzony przebieg. """ import json import time import pytest from app import session KLUCZ = "x" * 64 ODCISK = session.fingerprint("scrypt$sól$hash") @pytest.fixture() def klucz(monkeypatch): monkeypatch.setenv("SESSION_SECRET", KLUCZ) monkeypatch.delenv("SESSION_MAX_AGE", raising=False) monkeypatch.delenv("SESSION_IDLE", raising=False) # ── klucz: fail-closed ─────────────────────────────────────────────────── def test_no_key_means_no_sessions_not_a_random_one(monkeypatch): """Losowy klucz przy starcie wylogowywałby wszystkich przy każdym restarcie poda — wygląda jak awaria i uczy ludzi ignorować ekran logowania.""" monkeypatch.delenv("SESSION_SECRET", raising=False) with pytest.raises(session.SessionError): session.secret() assert session.available() is False def test_a_short_key_is_refused_with_a_reason(monkeypatch): monkeypatch.setenv("SESSION_SECRET", "krótki") with pytest.raises(session.SessionError) as e: session.secret() assert "32" in str(e.value) # ── zwykły przebieg ────────────────────────────────────────────────────── def test_issue_and_read_round_trip(klucz): dane = session.read(session.issue("ala", ODCISK, 3)) assert dane["login"] == "ala" and dane["odcisk"] == ODCISK and dane["wersja"] == 3 def test_fingerprint_never_carries_the_password(klucz): """Odcisk jedzie do przeglądarki, więc nie może dać się cofnąć do hasła.""" odcisk = session.fingerprint("tajne-hasło-użytkownika") assert "tajne" not in odcisk and "hasło" not in odcisk assert len(odcisk) == 16 assert session.fingerprint("a") != session.fingerprint("b") # ── próby oszukania ────────────────────────────────────────────────────── def test_a_tampered_payload_is_refused(klucz): token = session.issue("ala", ODCISK) surowy, _, podpis = token.rpartition(".") dane = json.loads(session._unb64(surowy)) dane["u"] = "szef" # podmiana konta podrobiony = session._b64(json.dumps(dane, separators=(",", ":"), sort_keys=True).encode()) + "." + podpis assert session.read(podrobiony) is None def test_a_tampered_signature_is_refused(klucz): token = session.issue("ala", ODCISK) surowy, _, podpis = token.rpartition(".") assert session.read(f"{surowy}.{podpis[:-2]}xx") is None def test_a_token_signed_with_another_key_is_refused(klucz, monkeypatch): token = session.issue("ala", ODCISK) monkeypatch.setenv("SESSION_SECRET", "y" * 64) assert session.read(token) is None @pytest.mark.parametrize("smiec", ["", None, "bez-kropki", ".", "a.b", "!!!.???", "a" * 500]) def test_garbage_never_raises_it_just_refuses(klucz, smiec): """Wejście przychodzi z przeglądarki, więc wyjątek tutaj to 500 na każdym żądaniu z popsutym ciasteczkiem — czyli aplikacja nie do odratowania inaczej niż przez wyczyszczenie przeglądarki.""" assert session.read(smiec) is None def test_a_token_from_the_future_is_refused(klucz): """Przestawiony zegar albo podrobienie — w obu razach nie ma powodu ufać.""" przyszly = session.issue("ala", ODCISK, now=time.time() + 3600) assert session.read(przyszly) is None def test_reading_without_a_key_refuses_instead_of_exploding(klucz, monkeypatch): token = session.issue("ala", ODCISK) monkeypatch.delenv("SESSION_SECRET", raising=False) assert session.read(token) is None # ── wygasanie ──────────────────────────────────────────────────────────── def test_a_session_dies_of_old_age(klucz, monkeypatch): monkeypatch.setenv("SESSION_MAX_AGE", "100") token = session.issue("ala", ODCISK, now=time.time() - 200) assert session.read(token) is None def test_a_session_dies_of_inactivity(klucz, monkeypatch): """Twardy kres jeszcze daleko, ale nikt nie ruszał — koniec.""" monkeypatch.setenv("SESSION_MAX_AGE", "100000") monkeypatch.setenv("SESSION_IDLE", "60") stary = session.issue("ala", ODCISK, now=time.time() - 600) assert session.read(stary) is None def test_activity_extends_the_session_but_not_past_the_hard_limit(klucz, monkeypatch): monkeypatch.setenv("SESSION_MAX_AGE", "1000") monkeypatch.setenv("SESSION_IDLE", "100") teraz = time.time() token = session.issue("ala", ODCISK, now=teraz - 900) odswiezony = session.refresh(session.read(token, now=teraz - 900), now=teraz - 10) assert session.read(odswiezony, now=teraz) is not None, "ruch nie przedłużył sesji" # ...ale twardego kresu nie przeskoczy assert session.read(odswiezony, now=teraz + 200) is None def test_refresh_stays_quiet_until_it_is_worth_it(klucz): """Bez progu każde żądanie odsyłałoby Set-Cookie — także po arkusze stylów.""" dane = session.read(session.issue("ala", ODCISK)) assert session.refresh(dane) is None assert session.refresh(dane, now=time.time() + session.REFRESH_AFTER + 1) is not None # ── ustawienia ciasteczka ──────────────────────────────────────────────── def test_the_cookie_cannot_be_read_by_scripts_or_sent_cross_site(klucz): p = session.cookie_params() assert p["httponly"] is True, "jeden wstrzyknięty skrypt wyniósłby sesję" assert p["samesite"] == "strict", "obca strona mogłaby działać w imieniu zalogowanego" assert p["secure"] is True