"""Tests for session-based WebSocket authentication.""" import time import pytest from src.network.session import Session, SessionManager, DEFAULT_SESSION_TTL class TestSession: """Tests for the Session data object.""" def test_session_creation(self): s = Session(session_id="abc123", client_id="browser-1") assert s.session_id == "abc123" assert s.client_id == "browser-1" assert not s.expired assert s.ttl_remaining > 0 assert s.created_at > 0 def test_session_metadata(self): s = Session("abc", "client", metadata={"user": "admin"}) assert s.metadata["user"] == "admin" def test_session_expiry(self): s = Session("abc", "client", ttl=0.01) time.sleep(0.02) assert s.expired assert s.ttl_remaining == 0.0 def test_session_not_expired(self): s = Session("abc", "client", ttl=3600) assert not s.expired assert s.ttl_remaining > 3500 # generous margin def test_session_custom_ttl(self): s = Session("abc", "client", ttl=60) assert 59 < s.ttl_remaining <= 60 class TestSessionManager: """Tests for SessionManager.""" @pytest.fixture def mgr(self): return SessionManager(ttl=3600, max_sessions=10) def test_initial_state(self, mgr): assert mgr.active_sessions == 0 stats = mgr.stats assert stats["active_sessions"] == 0 assert stats["total_created"] == 0 assert stats["total_destroyed"] == 0 assert stats["max_sessions"] == 10 assert stats["default_ttl"] == 3600 def test_create_session(self, mgr): token = mgr.create_session("browser-chrome") assert token is not None assert len(token) == 64 # HMAC-SHA256 hex digest assert mgr.active_sessions == 1 assert mgr.stats["total_created"] == 1 def test_validate_valid_token(self, mgr): token = mgr.create_session("browser-firefox") session = mgr.validate_token(token) assert session is not None assert session.client_id == "browser-firefox" assert not session.expired def test_validate_invalid_token(self, mgr): session = mgr.validate_token("invalid-token") assert session is None def test_validate_empty_token(self, mgr): assert mgr.validate_token("") is None assert mgr.validate_token(None) is None # type: ignore def test_validate_expired_token(self, mgr): token = mgr.create_session("browser", ttl=0.01) time.sleep(0.02) session = mgr.validate_token(token) assert session is None assert mgr.active_sessions == 0 def test_destroy_session(self, mgr): token = mgr.create_session("browser") assert mgr.active_sessions == 1 destroyed = mgr.destroy_session(token) assert destroyed assert mgr.active_sessions == 0 assert mgr.validate_token(token) is None def test_destroy_invalid_token(self, mgr): assert mgr.destroy_session("nope") is False def test_destroy_by_id(self, mgr): token = mgr.create_session("browser") session = mgr.validate_token(token) assert session is not None sid = session.session_id destroyed = mgr.destroy_by_id(sid) assert destroyed assert mgr.validate_token(token) is None def test_destroy_by_invalid_id(self, mgr): assert mgr.destroy_by_id("nonexistent") is False def test_get_session(self, mgr): token = mgr.create_session("browser") session = mgr.get_session(token) assert session is not None assert session.client_id == "browser" # Even expired sessions are returned by get_session session.expires_at = time.time() - 1 assert mgr.get_session(token) is not None def test_refresh_session(self, mgr): token = mgr.create_session("browser", ttl=3600) session = mgr.get_session(token) original_expiry = session.expires_at # Fast-forward expiry then refresh session.expires_at = time.time() + 1 # 1 second left assert mgr.refresh_session(token) session = mgr.get_session(token) assert session.expires_at > original_expiry def test_refresh_expired_session(self, mgr): token = mgr.create_session("browser", ttl=0.01) time.sleep(0.02) assert not mgr.refresh_session(token) def test_multiple_sessions(self, mgr): tokens = [mgr.create_session(f"client-{i}") for i in range(5)] assert mgr.active_sessions == 5 assert mgr.stats["total_created"] == 5 # Validate all for token in tokens: assert mgr.validate_token(token) is not None def test_max_sessions(self, mgr): """Test that exceeding max_sessions evicts oldest.""" # Fill up to max for i in range(10): mgr.create_session(f"client-{i}") assert mgr.active_sessions == 10 # Create one more — should evict oldest mgr.create_session("overflow") assert mgr.active_sessions == 10 assert mgr.stats["total_created"] == 11 assert mgr.stats["total_destroyed"] == 1 def test_stale_eviction_on_validate(self, mgr): """Test that stale sessions are evicted when validate is called.""" token1 = mgr.create_session("client-a", ttl=0.01) token2 = mgr.create_session("client-b", ttl=3600) time.sleep(0.02) # Token1 expired, token2 still valid assert mgr.validate_token(token1) is None assert mgr.active_sessions == 1 # token1 evicted assert mgr.validate_token(token2) is not None def test_stats_evicts_stale(self, mgr): """Test that stats evicts stale sessions.""" mgr.create_session("client-a", ttl=0.01) mgr.create_session("client-b", ttl=3600) time.sleep(0.02) stats = mgr.stats assert stats["active_sessions"] == 1 # Only client-b remains def test_session_metadata_storage(self, mgr): token = mgr.create_session("browser", metadata={"role": "admin", "permissions": ["read", "write"]}) session = mgr.validate_token(token) assert session.metadata == {"role": "admin", "permissions": ["read", "write"]} class TestSessionTokens: """Tests for token uniqueness and security properties.""" def test_tokens_are_unique(self): mgr = SessionManager() tokens = {mgr.create_session("client") for _ in range(100)} assert len(tokens) == 100 # All unique def test_tokens_are_opaque(self): """Tokens should not contain the session ID in plaintext.""" mgr = SessionManager() token = mgr.create_session("client") session = mgr.get_session(token) assert session is not None assert session.session_id not in token assert "client" not in token