"""Session lifecycle and account state.
These lock in the two fixes from wave 0: sessions now actually expire, and
deactivating an account now closes the sessions it already holds.
"""
import re
import pytest
from app.extensions import db
from app.models import User
#: Matches the flash markup emitted by layouts/base.html.
_FLASH_PATTERN = re.compile(
r'
\s*
(.*?)',
re.S,
)
def _flash_messages(body):
"""Flash texts rendered in a response, independent of the locale."""
return [m.strip() for m in _FLASH_PATTERN.findall(body)]
def _set_active(app, user_id, active):
with app.app_context():
user = db.session.get(User, user_id)
user.is_active_account = active
db.session.commit()
class TestSessionExpiry:
def test_login_issues_an_expiring_session_cookie(self, app, client, as_role):
"""PERMANENT_SESSION_LIFETIME only applies to permanent sessions.
Before the fix, session.permanent was never set anywhere in app/,
so Flask emitted a browser-session cookie with no Expires attribute
and the configured one-hour lifetime was silently ignored.
"""
as_role('player')
cookie = client.get_cookie('session')
assert cookie is not None, 'no session cookie was issued at login'
assert cookie.expires is not None, (
'session cookie has no expiry: session.permanent was not set, '
'so PERMANENT_SESSION_LIFETIME has no effect'
)
def test_session_lifetime_matches_configuration(self, app):
from datetime import timedelta
assert app.permanent_session_lifetime == timedelta(seconds=3600)
class TestAccountDeactivation:
def test_deactivated_account_cannot_log_in(self, app, client, make_user, login):
user_id = make_user('player')
_set_active(app, user_id, False)
with app.app_context():
username = db.session.get(User, user_id).username
login(username)
response = client.get('/users/profile', follow_redirects=False)
assert response.status_code in (301, 302)
assert '/auth/login' in response.headers.get('Location', '')
def test_deactivated_account_loses_its_existing_session(self, app, client, as_role):
"""The fix that matters: revocation has to reach live sessions.
is_active_account used to be consulted only at login. User did not
override UserMixin.is_active, so Flask-Login treated every account
as active, and disabling someone merely stopped them reconnecting —
their open session kept working.
"""
user_id = as_role('player')
assert client.get('/users/profile').status_code == 200
_set_active(app, user_id, False)
response = client.get('/users/profile', follow_redirects=False)
assert response.status_code in (301, 302), (
'a deactivated account kept access with its existing session'
)
assert '/auth/login' in response.headers.get('Location', '')
def test_is_active_property_tracks_the_column(self, app, make_user):
user_id = make_user('coach')
with app.app_context():
user = db.session.get(User, user_id)
assert user.is_active is True
user.is_active_account = False
assert user.is_active is False
class TestLogout:
def test_logout_ends_the_session(self, client, as_role):
as_role('player')
assert client.get('/users/profile').status_code == 200
client.post('/auth/logout')
response = client.get('/users/profile', follow_redirects=False)
assert response.status_code in (301, 302)
assert '/auth/login' in response.headers.get('Location', '')
def test_a_get_no_longer_logs_anyone_out(self, client, as_role):
"""SEC-019 — a GET route carries no CSRF token, so any page could
sign the user out with

."""
as_role('player')
assert client.get('/auth/logout').status_code == 405
assert client.get('/users/profile').status_code == 200
def test_the_logout_control_carries_a_csrf_token(self, app_with_csrf, make_user, login):
"""The nav entry is a form now; without the token it would 400 on
every user, and only in production where CSRF is on."""
client = app_with_csrf.test_client()
with app_with_csrf.app_context():
from app.extensions import hash_password
from app.models import Player
user = Player(username='navtest', password_hash=hash_password('Password123'),
role='player', full_name='Nav Test', email='nav@example.test')
db.session.add(user)
db.session.commit()
page = client.get('/auth/login').get_data(as_text=True)
token = re.search(r'name="csrf_token" value="([^"]+)"', page).group(1)
client.post('/auth/login', data={'username': 'navtest',
'password': 'Password123',
'csrf_token': token})
body = client.get('/users/profile').get_data(as_text=True)
form = re.search(
r'