7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
582 lines
22 KiB
Python
582 lines
22 KiB
Python
"""
|
|
Tests for web/auth/session_manager.py
|
|
|
|
Tests cover:
|
|
- SessionManager class initialization with mocked security defaults
|
|
- Session creation, validation, and destruction
|
|
- Session cleanup and expiration using datetime mocking
|
|
- User session management and session info formatting
|
|
"""
|
|
|
|
import datetime
|
|
from datetime import UTC
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
MOCK_SESSION_TIMEOUT_HOURS = 2
|
|
MOCK_REMEMBER_ME_DAYS = 30
|
|
|
|
# Patch target for the security helper used inside session_manager.py
|
|
_SECURITY_DEFAULT_PATCH = (
|
|
"local_deep_research.web.auth.session_manager.get_security_default"
|
|
)
|
|
|
|
|
|
def _security_default_side_effect(key, default):
|
|
"""Return controlled timeout values for tests."""
|
|
mapping = {
|
|
"security.session_timeout_hours": MOCK_SESSION_TIMEOUT_HOURS,
|
|
"security.session_remember_me_days": MOCK_REMEMBER_ME_DAYS,
|
|
}
|
|
return mapping.get(key, default)
|
|
|
|
|
|
@pytest.fixture()
|
|
def manager():
|
|
"""Create a SessionManager with mocked security defaults."""
|
|
with patch(
|
|
_SECURITY_DEFAULT_PATCH, side_effect=_security_default_side_effect
|
|
):
|
|
from local_deep_research.web.auth.session_manager import (
|
|
SessionManager,
|
|
)
|
|
|
|
return SessionManager()
|
|
|
|
|
|
# ===========================================================================
|
|
# Initialization
|
|
# ===========================================================================
|
|
|
|
|
|
class TestSessionManagerInit:
|
|
"""Tests for SessionManager initialization."""
|
|
|
|
def test_init_creates_empty_sessions_dict(self, manager):
|
|
"""Should initialize with an empty sessions dict."""
|
|
assert manager.sessions == {}
|
|
|
|
def test_init_sets_session_timeout_from_security_defaults(self, manager):
|
|
"""Should set session_timeout based on mocked security default."""
|
|
assert manager.session_timeout == datetime.timedelta(
|
|
hours=MOCK_SESSION_TIMEOUT_HOURS
|
|
)
|
|
|
|
def test_init_sets_remember_me_timeout_from_security_defaults(
|
|
self, manager
|
|
):
|
|
"""Should set remember_me_timeout based on mocked security default."""
|
|
assert manager.remember_me_timeout == datetime.timedelta(
|
|
days=MOCK_REMEMBER_ME_DAYS
|
|
)
|
|
|
|
def test_init_calls_get_security_default(self):
|
|
"""Should call get_security_default for both timeout settings."""
|
|
with patch(_SECURITY_DEFAULT_PATCH) as mock_get:
|
|
mock_get.return_value = 1
|
|
|
|
from local_deep_research.web.auth.session_manager import (
|
|
SessionManager,
|
|
)
|
|
|
|
SessionManager()
|
|
|
|
assert mock_get.call_count == 2
|
|
call_keys = [c.args[0] for c in mock_get.call_args_list]
|
|
assert "security.session_timeout_hours" in call_keys
|
|
assert "security.session_remember_me_days" in call_keys
|
|
|
|
def test_init_uses_custom_timeout_values(self):
|
|
"""Should honour non-default timeout values from security settings."""
|
|
custom_hours = 8
|
|
custom_days = 7
|
|
|
|
def custom_side_effect(key, default):
|
|
if key == "security.session_timeout_hours":
|
|
return custom_hours
|
|
if key == "security.session_remember_me_days":
|
|
return custom_days
|
|
return default
|
|
|
|
with patch(_SECURITY_DEFAULT_PATCH, side_effect=custom_side_effect):
|
|
from local_deep_research.web.auth.session_manager import (
|
|
SessionManager,
|
|
)
|
|
|
|
mgr = SessionManager()
|
|
|
|
assert mgr.session_timeout == datetime.timedelta(hours=custom_hours)
|
|
assert mgr.remember_me_timeout == datetime.timedelta(days=custom_days)
|
|
|
|
|
|
# ===========================================================================
|
|
# Session creation
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCreateSession:
|
|
"""Tests for create_session method."""
|
|
|
|
def test_returns_string_token(self, manager):
|
|
"""Should return a session ID that is a non-empty string."""
|
|
session_id = manager.create_session("alice")
|
|
assert isinstance(session_id, str)
|
|
assert len(session_id) > 0
|
|
|
|
def test_returns_unique_tokens(self, manager):
|
|
"""Should generate unique session IDs across multiple calls."""
|
|
ids = {manager.create_session(f"user{i}") for i in range(50)}
|
|
assert len(ids) == 50
|
|
|
|
def test_stores_session_data(self, manager):
|
|
"""Should store session entry in the sessions dict."""
|
|
sid = manager.create_session("bob")
|
|
assert sid in manager.sessions
|
|
data = manager.sessions[sid]
|
|
assert data["username"] == "bob"
|
|
assert data["remember_me"] is False
|
|
assert isinstance(data["created_at"], datetime.datetime)
|
|
assert isinstance(data["last_access"], datetime.datetime)
|
|
|
|
def test_default_remember_me_is_false(self, manager):
|
|
"""Should default remember_me to False."""
|
|
sid = manager.create_session("carol")
|
|
assert manager.sessions[sid]["remember_me"] is False
|
|
|
|
def test_remember_me_true(self, manager):
|
|
"""Should store remember_me=True when requested."""
|
|
sid = manager.create_session("dave", remember_me=True)
|
|
assert manager.sessions[sid]["remember_me"] is True
|
|
|
|
def test_timestamps_are_utc_aware(self, manager):
|
|
"""Should set created_at and last_access as UTC-aware datetimes."""
|
|
sid = manager.create_session("eve")
|
|
data = manager.sessions[sid]
|
|
assert data["created_at"].tzinfo is not None
|
|
assert data["last_access"].tzinfo is not None
|
|
|
|
def test_multiple_sessions_same_user(self, manager):
|
|
"""Should allow multiple sessions for the same username."""
|
|
sid1 = manager.create_session("frank")
|
|
sid2 = manager.create_session("frank")
|
|
assert sid1 != sid2
|
|
assert sid1 in manager.sessions
|
|
assert sid2 in manager.sessions
|
|
assert manager.sessions[sid1]["username"] == "frank"
|
|
assert manager.sessions[sid2]["username"] == "frank"
|
|
|
|
|
|
# ===========================================================================
|
|
# Session validation
|
|
# ===========================================================================
|
|
|
|
|
|
class TestValidateSession:
|
|
"""Tests for validate_session method."""
|
|
|
|
def test_returns_username_for_valid_session(self, manager):
|
|
"""Should return the username for a freshly created session."""
|
|
sid = manager.create_session("grace")
|
|
assert manager.validate_session(sid) == "grace"
|
|
|
|
def test_returns_none_for_unknown_session_id(self, manager):
|
|
"""Should return None for a session_id that was never created."""
|
|
assert manager.validate_session("totally-bogus-id") is None
|
|
|
|
def test_returns_none_for_expired_regular_session(self, manager):
|
|
"""Should return None when regular session has exceeded session_timeout."""
|
|
fixed_now = datetime.datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
|
|
expired_time = fixed_now - datetime.timedelta(
|
|
hours=MOCK_SESSION_TIMEOUT_HOURS, seconds=1
|
|
)
|
|
|
|
with patch(
|
|
"local_deep_research.web.auth.session_manager.datetime"
|
|
) as mock_dt:
|
|
mock_dt.datetime.now.return_value = fixed_now
|
|
mock_dt.timedelta = datetime.timedelta
|
|
mock_dt.UTC = UTC
|
|
|
|
sid = manager.create_session("hank")
|
|
# Manually set last_access to an expired time
|
|
manager.sessions[sid]["last_access"] = expired_time
|
|
|
|
result = manager.validate_session(sid)
|
|
|
|
assert result is None
|
|
|
|
def test_destroys_expired_session(self, manager):
|
|
"""Should remove the session from self.sessions when it expires."""
|
|
sid = manager.create_session("ivy")
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
manager.validate_session(sid)
|
|
assert sid not in manager.sessions
|
|
|
|
def test_returns_none_for_expired_remember_me_session(self, manager):
|
|
"""Should return None for remember_me session past remember_me_timeout."""
|
|
sid = manager.create_session("jack", remember_me=True)
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(days=MOCK_REMEMBER_ME_DAYS + 1)
|
|
|
|
assert manager.validate_session(sid) is None
|
|
assert sid not in manager.sessions
|
|
|
|
def test_remember_me_survives_regular_timeout(self, manager):
|
|
"""A remember_me session should still be valid after regular timeout elapses."""
|
|
sid = manager.create_session("kate", remember_me=True)
|
|
# 3 hours ago exceeds the 2-hour regular timeout but not 30-day remember_me
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
result = manager.validate_session(sid)
|
|
assert result == "kate"
|
|
|
|
def test_updates_last_access_on_valid_session(self, manager):
|
|
"""Should update last_access to current time when session is valid."""
|
|
sid = manager.create_session("leo")
|
|
old_access = manager.sessions[sid]["last_access"]
|
|
|
|
# Nudge time forward slightly
|
|
with patch(
|
|
"local_deep_research.web.auth.session_manager.datetime"
|
|
) as mock_dt:
|
|
future = old_access + datetime.timedelta(minutes=10)
|
|
mock_dt.datetime.now.return_value = future
|
|
mock_dt.timedelta = datetime.timedelta
|
|
mock_dt.UTC = UTC
|
|
|
|
result = manager.validate_session(sid)
|
|
|
|
assert result == "leo"
|
|
assert manager.sessions[sid]["last_access"] == future
|
|
|
|
def test_does_not_update_last_access_for_unknown_session(self, manager):
|
|
"""Calling validate on unknown id should not create any session entry."""
|
|
manager.validate_session("nonexistent")
|
|
assert "nonexistent" not in manager.sessions
|
|
|
|
def test_expired_boundary_exact_timeout(self, manager):
|
|
"""Session at exactly the timeout boundary should still be valid (uses >)."""
|
|
fixed_now = datetime.datetime(2026, 6, 15, 12, 0, 0, tzinfo=UTC)
|
|
boundary_time = fixed_now - datetime.timedelta(
|
|
hours=MOCK_SESSION_TIMEOUT_HOURS
|
|
)
|
|
|
|
sid = manager.create_session("mia")
|
|
manager.sessions[sid]["last_access"] = boundary_time
|
|
|
|
with patch(
|
|
"local_deep_research.web.auth.session_manager.datetime"
|
|
) as mock_dt:
|
|
mock_dt.datetime.now.return_value = fixed_now
|
|
mock_dt.timedelta = datetime.timedelta
|
|
mock_dt.UTC = UTC
|
|
|
|
# Source code checks ``now - last_access > timeout``,
|
|
# so exactly equal should still be valid.
|
|
result = manager.validate_session(sid)
|
|
|
|
assert result == "mia"
|
|
|
|
|
|
# ===========================================================================
|
|
# Session destruction
|
|
# ===========================================================================
|
|
|
|
|
|
class TestDestroySession:
|
|
"""Tests for destroy_session method."""
|
|
|
|
def test_removes_session(self, manager):
|
|
"""Should remove the session from self.sessions."""
|
|
sid = manager.create_session("nina")
|
|
assert sid in manager.sessions
|
|
manager.destroy_session(sid)
|
|
assert sid not in manager.sessions
|
|
|
|
def test_noop_for_unknown_session_id(self, manager):
|
|
"""Should not raise when destroying an unknown session_id."""
|
|
manager.destroy_session("does-not-exist") # no exception
|
|
|
|
def test_does_not_affect_other_sessions(self, manager):
|
|
"""Destroying one session should leave others intact."""
|
|
sid1 = manager.create_session("oscar")
|
|
sid2 = manager.create_session("oscar")
|
|
manager.destroy_session(sid1)
|
|
assert sid1 not in manager.sessions
|
|
assert sid2 in manager.sessions
|
|
|
|
|
|
# ===========================================================================
|
|
# Cleanup expired sessions
|
|
# ===========================================================================
|
|
|
|
|
|
class TestCleanupExpiredSessions:
|
|
"""Tests for cleanup_expired_sessions method."""
|
|
|
|
def test_removes_expired_regular_sessions(self, manager):
|
|
"""Should remove regular sessions that have exceeded session_timeout."""
|
|
sid = manager.create_session("quinn")
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
manager.cleanup_expired_sessions()
|
|
assert sid not in manager.sessions
|
|
|
|
def test_keeps_valid_regular_sessions(self, manager):
|
|
"""Should keep regular sessions that have not expired."""
|
|
sid = manager.create_session("rosa")
|
|
manager.cleanup_expired_sessions()
|
|
assert sid in manager.sessions
|
|
|
|
def test_removes_only_expired_sessions(self, manager):
|
|
"""Should remove expired sessions while keeping valid ones."""
|
|
expired_sid = manager.create_session("stale_user")
|
|
manager.sessions[expired_sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
valid_sid = manager.create_session("fresh_user")
|
|
|
|
manager.cleanup_expired_sessions()
|
|
|
|
assert expired_sid not in manager.sessions
|
|
assert valid_sid in manager.sessions
|
|
|
|
def test_removes_multiple_expired_sessions(self, manager):
|
|
"""Should remove all expired sessions in a single cleanup pass."""
|
|
expired_ids = []
|
|
for i in range(5):
|
|
sid = manager.create_session(f"expired_{i}")
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
expired_ids.append(sid)
|
|
|
|
valid_sid = manager.create_session("keeper")
|
|
manager.cleanup_expired_sessions()
|
|
|
|
for eid in expired_ids:
|
|
assert eid not in manager.sessions
|
|
assert valid_sid in manager.sessions
|
|
|
|
def test_respects_remember_me_timeout(self, manager):
|
|
"""Should keep remember_me sessions that exceed regular timeout but not remember_me_timeout."""
|
|
sid = manager.create_session("tina", remember_me=True)
|
|
# 3 hours ago: past regular timeout but within remember_me timeout
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
manager.cleanup_expired_sessions()
|
|
assert sid in manager.sessions
|
|
|
|
def test_removes_expired_remember_me_sessions(self, manager):
|
|
"""Should remove remember_me sessions that exceed remember_me_timeout."""
|
|
sid = manager.create_session("uma", remember_me=True)
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(days=MOCK_REMEMBER_ME_DAYS + 1)
|
|
|
|
manager.cleanup_expired_sessions()
|
|
assert sid not in manager.sessions
|
|
|
|
def test_handles_empty_sessions(self, manager):
|
|
"""Should not raise when there are no sessions."""
|
|
manager.cleanup_expired_sessions() # no exception
|
|
assert manager.sessions == {}
|
|
|
|
def test_mixed_regular_and_remember_me(self, manager):
|
|
"""Should correctly handle a mix of regular and remember_me sessions."""
|
|
# Expired regular
|
|
s1 = manager.create_session("u1")
|
|
manager.sessions[s1]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
# Valid remember_me (same age as s1 but has longer timeout)
|
|
s2 = manager.create_session("u2", remember_me=True)
|
|
manager.sessions[s2]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
# Expired remember_me
|
|
s3 = manager.create_session("u3", remember_me=True)
|
|
manager.sessions[s3]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(days=MOCK_REMEMBER_ME_DAYS + 1)
|
|
|
|
# Valid regular
|
|
s4 = manager.create_session("u4")
|
|
|
|
manager.cleanup_expired_sessions()
|
|
|
|
assert s1 not in manager.sessions # expired regular
|
|
assert s2 in manager.sessions # still valid remember_me
|
|
assert s3 not in manager.sessions # expired remember_me
|
|
assert s4 in manager.sessions # still valid regular
|
|
|
|
|
|
# ===========================================================================
|
|
# Active sessions count
|
|
# ===========================================================================
|
|
|
|
|
|
class TestGetActiveSessionsCount:
|
|
"""Tests for get_active_sessions_count method."""
|
|
|
|
def test_returns_zero_when_empty(self, manager):
|
|
"""Should return 0 when there are no sessions."""
|
|
assert manager.get_active_sessions_count() == 0
|
|
|
|
def test_returns_correct_count(self, manager):
|
|
"""Should return the number of active sessions."""
|
|
for i in range(4):
|
|
manager.create_session(f"user{i}")
|
|
assert manager.get_active_sessions_count() == 4
|
|
|
|
def test_triggers_cleanup_before_counting(self, manager):
|
|
"""Should clean up expired sessions before counting."""
|
|
for i in range(3):
|
|
manager.create_session(f"active_{i}")
|
|
|
|
expired_sid = manager.create_session("stale")
|
|
manager.sessions[expired_sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
count = manager.get_active_sessions_count()
|
|
assert count == 3
|
|
assert expired_sid not in manager.sessions
|
|
|
|
def test_count_after_all_expired(self, manager):
|
|
"""Should return 0 when all sessions have expired."""
|
|
for i in range(3):
|
|
sid = manager.create_session(f"user{i}")
|
|
manager.sessions[sid]["last_access"] = datetime.datetime.now(
|
|
UTC
|
|
) - datetime.timedelta(hours=MOCK_SESSION_TIMEOUT_HOURS + 1)
|
|
|
|
assert manager.get_active_sessions_count() == 0
|
|
|
|
|
|
# ===========================================================================
|
|
# User sessions
|
|
# ===========================================================================
|
|
|
|
|
|
class TestGetUserSessions:
|
|
"""Tests for get_user_sessions method."""
|
|
|
|
def test_returns_empty_list_when_no_sessions(self, manager):
|
|
"""Should return an empty list when the user has no sessions."""
|
|
assert manager.get_user_sessions("nobody") == []
|
|
|
|
def test_returns_only_matching_user_sessions(self, manager):
|
|
"""Should return sessions only for the specified username."""
|
|
manager.create_session("alice")
|
|
manager.create_session("alice")
|
|
manager.create_session("bob")
|
|
|
|
alice_sessions = manager.get_user_sessions("alice")
|
|
assert len(alice_sessions) == 2
|
|
|
|
bob_sessions = manager.get_user_sessions("bob")
|
|
assert len(bob_sessions) == 1
|
|
|
|
def test_does_not_return_other_users_sessions(self, manager):
|
|
"""Should never include sessions belonging to other users."""
|
|
manager.create_session("charlie")
|
|
manager.create_session("diana")
|
|
|
|
result = manager.get_user_sessions("charlie")
|
|
assert len(result) == 1
|
|
|
|
def test_truncates_session_id(self, manager):
|
|
"""Should truncate session_id to first 8 characters followed by '...'."""
|
|
sid = manager.create_session("eve")
|
|
result = manager.get_user_sessions("eve")
|
|
|
|
assert len(result) == 1
|
|
truncated_id = result[0]["session_id"]
|
|
assert truncated_id == sid[:8] + "..."
|
|
assert len(truncated_id) == 11 # 8 chars + 3 dots
|
|
|
|
def test_returns_expected_keys(self, manager):
|
|
"""Should return dicts with session_id, created_at, last_access, remember_me."""
|
|
manager.create_session("frank", remember_me=True)
|
|
result = manager.get_user_sessions("frank")
|
|
|
|
assert len(result) == 1
|
|
info = result[0]
|
|
assert set(info.keys()) == {
|
|
"session_id",
|
|
"created_at",
|
|
"last_access",
|
|
"remember_me",
|
|
}
|
|
assert info["remember_me"] is True
|
|
assert isinstance(info["created_at"], datetime.datetime)
|
|
assert isinstance(info["last_access"], datetime.datetime)
|
|
|
|
def test_multiple_sessions_same_user(self, manager):
|
|
"""Should list all sessions when a user has multiple active sessions."""
|
|
sid1 = manager.create_session("grace")
|
|
sid2 = manager.create_session("grace")
|
|
|
|
result = manager.get_user_sessions("grace")
|
|
assert len(result) == 2
|
|
|
|
returned_ids = {r["session_id"] for r in result}
|
|
assert returned_ids == {sid1[:8] + "...", sid2[:8] + "..."}
|
|
|
|
def test_returns_remember_me_flag_per_session(self, manager):
|
|
"""Each session entry should reflect its own remember_me setting."""
|
|
manager.create_session("hana", remember_me=False)
|
|
manager.create_session("hana", remember_me=True)
|
|
|
|
result = manager.get_user_sessions("hana")
|
|
flags = {r["remember_me"] for r in result}
|
|
assert flags == {True, False}
|
|
|
|
|
|
class TestDestroyAllUserSessions:
|
|
"""Tests for destroy_all_user_sessions."""
|
|
|
|
def test_destroys_all_sessions_for_user(self, manager):
|
|
"""Should destroy every session belonging to the target user."""
|
|
manager.create_session("alice", remember_me=False)
|
|
manager.create_session("alice", remember_me=False)
|
|
manager.create_session("bob", remember_me=False)
|
|
|
|
count = manager.destroy_all_user_sessions("alice")
|
|
|
|
assert count == 2
|
|
assert manager.get_user_sessions("alice") == []
|
|
# Bob's session should be untouched
|
|
assert len(manager.get_user_sessions("bob")) == 1
|
|
|
|
def test_returns_zero_for_unknown_user(self, manager):
|
|
"""Should return 0 and not raise when user has no sessions."""
|
|
count = manager.destroy_all_user_sessions("nobody")
|
|
assert count == 0
|
|
|
|
def test_returns_zero_after_double_call(self, manager):
|
|
"""Calling twice should be idempotent."""
|
|
manager.create_session("alice", remember_me=False)
|
|
manager.destroy_all_user_sessions("alice")
|
|
count = manager.destroy_all_user_sessions("alice")
|
|
assert count == 0
|