fix(auth): SECRET_KEY di default rifiutato, session.permanent, logout solo POST (#33)
config.py: guard_default_secret_key, verificata a ogni richiesta (prima di create_app, TESTING/DEBUG non sono ancora impostati dai fixture di test) — rifiuta con RuntimeError se CLIENT_SECRET_KEY è rimasto al default committato fuori da debug/testing. auth.py: session.permanent=True al login (senza, PERMANENT_SESSION_LIFETIME non aveva mai effetto); /auth/logout solo POST, con CSRF. Aggiornati i tre punti che lo chiamavano: profile.html e navbar.html (link -> form POST), auto-logout JS in base.html (fetch POST + redirect al login). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -54,6 +54,9 @@ class TestLoginPost:
|
||||
with client.session_transaction() as sess:
|
||||
assert sess.get("api_key") == "new-api-key-abc"
|
||||
assert sess["user"]["username"] == "testuser"
|
||||
# #33: without this, PERMANENT_SESSION_LIFETIME (8h) never applied
|
||||
# and Flask used a browser-length session instead.
|
||||
assert sess.permanent is True
|
||||
|
||||
def test_login_post_error_shows_error(self, client, mock_api_client):
|
||||
"""Failed login returns the login page with error flash."""
|
||||
@@ -83,13 +86,13 @@ class TestLoginPost:
|
||||
|
||||
|
||||
class TestLogout:
|
||||
"""GET/POST /auth/logout tests."""
|
||||
"""POST /auth/logout tests (#33: GET is refused - logout must carry CSRF)."""
|
||||
|
||||
def test_logout_clears_session(self, logged_in_client, mock_api_client):
|
||||
"""Logout clears session and redirects to login."""
|
||||
mock_api_client.post.return_value = {}
|
||||
|
||||
resp = logged_in_client.get("/auth/logout", follow_redirects=False)
|
||||
resp = logged_in_client.post("/auth/logout", follow_redirects=False)
|
||||
assert resp.status_code == 302
|
||||
assert "/auth/login" in resp.headers["Location"]
|
||||
|
||||
@@ -98,6 +101,11 @@ class TestLogout:
|
||||
assert "api_key" not in sess
|
||||
assert "user" not in sess
|
||||
|
||||
def test_logout_get_is_rejected(self, logged_in_client):
|
||||
"""#33: a bare GET (link, prefetch, forged <img>) no longer logs anyone out."""
|
||||
resp = logged_in_client.get("/auth/logout", follow_redirects=False)
|
||||
assert resp.status_code == 405
|
||||
|
||||
|
||||
class TestUnauthenticatedRedirect:
|
||||
"""Protected routes redirect unauthenticated users."""
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""Tests for the CLIENT_SECRET_KEY default guard (#33).
|
||||
|
||||
Uses the raw function (config.guard_default_secret_key) for the unit-level
|
||||
cases, and the app's wired before_request hook for one end-to-end check.
|
||||
"""
|
||||
import pytest
|
||||
from flask import Flask
|
||||
|
||||
from config import DEFAULT_SECRET_KEY, guard_default_secret_key
|
||||
|
||||
|
||||
def _app(secret_key, debug=False, testing=False):
|
||||
app = Flask(__name__)
|
||||
app.config["SECRET_KEY"] = secret_key
|
||||
app.config["DEBUG"] = debug
|
||||
app.config["TESTING"] = testing
|
||||
return app
|
||||
|
||||
|
||||
class TestGuardFunction:
|
||||
def test_default_key_in_production_raises(self):
|
||||
app = _app(DEFAULT_SECRET_KEY, debug=False, testing=False)
|
||||
with pytest.raises(RuntimeError, match="CLIENT_SECRET_KEY"):
|
||||
guard_default_secret_key(app)
|
||||
|
||||
def test_default_key_with_debug_is_allowed(self):
|
||||
app = _app(DEFAULT_SECRET_KEY, debug=True, testing=False)
|
||||
guard_default_secret_key(app) # must not raise
|
||||
|
||||
def test_default_key_with_testing_is_allowed(self):
|
||||
app = _app(DEFAULT_SECRET_KEY, debug=False, testing=True)
|
||||
guard_default_secret_key(app) # must not raise
|
||||
|
||||
def test_custom_key_in_production_is_allowed(self):
|
||||
app = _app("a-real-random-secret", debug=False, testing=False)
|
||||
guard_default_secret_key(app) # must not raise
|
||||
|
||||
|
||||
class TestGuardWiredInApp:
|
||||
"""The before_request hook registered in create_app() behaves the same way."""
|
||||
|
||||
def test_default_key_not_debug_not_testing_raises_on_request(self, monkeypatch):
|
||||
monkeypatch.delenv("FLASK_DEBUG", raising=False)
|
||||
monkeypatch.delenv("CLIENT_SECRET_KEY", raising=False)
|
||||
monkeypatch.setenv("STATION_CODE", "ST-TEST")
|
||||
import config
|
||||
import importlib
|
||||
importlib.reload(config)
|
||||
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
# Neither debug nor testing set - the production-like case (#33).
|
||||
assert app.config["SECRET_KEY"] == config.DEFAULT_SECRET_KEY
|
||||
assert app.debug is False
|
||||
assert app.testing is False
|
||||
|
||||
with app.test_request_context("/auth/login"):
|
||||
with pytest.raises(RuntimeError, match="CLIENT_SECRET_KEY"):
|
||||
app.preprocess_request()
|
||||
|
||||
def test_testing_flag_lets_requests_through(self, monkeypatch):
|
||||
"""This is exactly the ordering the whole suite relies on: fixtures
|
||||
call create_app() then set TESTING=True afterwards, and the guard
|
||||
must still see it (checked per-request, not at create_app time)."""
|
||||
monkeypatch.setenv("STATION_CODE", "ST-TEST")
|
||||
import config
|
||||
import importlib
|
||||
importlib.reload(config)
|
||||
|
||||
from app import create_app
|
||||
app = create_app()
|
||||
app.config["TESTING"] = True
|
||||
|
||||
with app.test_client() as c:
|
||||
resp = c.get("/auth/login")
|
||||
assert resp.status_code == 200
|
||||
Reference in New Issue
Block a user