import pytest import time from unittest.mock import patch from classes.session_tracker import SessionTracker from constants import FOUR_HOURS class TestSessionTracker: """Unit tests for SessionTracker class""" @pytest.fixture def tracker(self): """Fresh tracker instance for each test""" return SessionTracker() @pytest.fixture def sample_session_id(self): return "test-session-123" # ==================== update_session Tests ==================== def test_update_session_new_session(self, tracker, sample_session_id): """Test updating a new session creates an entry with current timestamp""" before = time.time() tracker.update_session(sample_session_id) after = time.time() assert sample_session_id in tracker.session_timestamp_map timestamp = tracker.session_timestamp_map[sample_session_id] assert before <= timestamp <= after def test_update_session_existing_session(self, tracker, sample_session_id): """Test updating an existing session updates its timestamp""" # First update tracker.update_session(sample_session_id) first_timestamp = tracker.session_timestamp_map[sample_session_id] # Wait a bit time.sleep(0.01) # Second update tracker.update_session(sample_session_id) second_timestamp = tracker.session_timestamp_map[sample_session_id] assert second_timestamp > first_timestamp def test_update_session_multiple_sessions(self, tracker): """Test updating multiple different sessions""" sessions = ["session-1", "session-2", "session-3"] for session_id in sessions: tracker.update_session(session_id) assert len(tracker.session_timestamp_map) == 3 for session_id in sessions: assert session_id in tracker.session_timestamp_map @patch("time.time") def test_update_session_stores_exact_timestamp( self, mock_time, tracker, sample_session_id ): """Test that update_session stores the exact timestamp from time.time()""" mock_time.return_value = 1234567890.123 tracker.update_session(sample_session_id) assert tracker.session_timestamp_map[sample_session_id] == 1234567890.123 # ==================== delete_session Tests ==================== def test_delete_session_existing_session(self, tracker, sample_session_id): """Test deleting an existing session""" tracker.update_session(sample_session_id) tracker.delete_session(sample_session_id) assert sample_session_id not in tracker.session_timestamp_map def test_delete_session_nonexistent_session(self, tracker): """Test deleting a nonexistent session (should not raise error)""" # Should not raise any exception tracker.delete_session("nonexistent-session") assert "nonexistent-session" not in tracker.session_timestamp_map def test_delete_session_does_not_affect_other_sessions(self, tracker): """Test that deleting one session doesn't affect others""" tracker.update_session("session-1") tracker.update_session("session-2") tracker.update_session("session-3") tracker.delete_session("session-2") assert "session-1" in tracker.session_timestamp_map assert "session-2" not in tracker.session_timestamp_map assert "session-3" in tracker.session_timestamp_map def test_delete_session_multiple_times(self, tracker, sample_session_id): """Test deleting the same session multiple times""" tracker.update_session(sample_session_id) tracker.delete_session(sample_session_id) tracker.delete_session(sample_session_id) # Should not raise error assert sample_session_id not in tracker.session_timestamp_map # ==================== delete_inactive_sessions Tests ==================== @patch("time.time") def test_delete_inactive_sessions_no_inactive(self, mock_time, tracker): """Test when all sessions are active (within FOUR_HOURS)""" mock_time.return_value = 1000.0 tracker.update_session("session-1") tracker.update_session("session-2") # Move time forward but less than FOUR_HOURS mock_time.return_value = 1000.0 + FOUR_HOURS - 100 deleted = tracker.delete_inactive_sessions() assert deleted == [] assert len(tracker.session_timestamp_map) == 2 @patch("time.time") def test_delete_inactive_sessions_all_inactive(self, mock_time, tracker): """Test when all sessions are inactive (older than FOUR_HOURS)""" mock_time.return_value = 1000.0 tracker.update_session("session-1") tracker.update_session("session-2") tracker.update_session("session-3") # Move time forward beyond FOUR_HOURS mock_time.return_value = 1000.0 + FOUR_HOURS + 100 deleted = tracker.delete_inactive_sessions() assert len(deleted) == 3 assert set(deleted) == {"session-1", "session-2", "session-3"} assert len(tracker.session_timestamp_map) == 0 @patch("time.time") def test_delete_inactive_sessions_mixed(self, mock_time, tracker): """Test when some sessions are inactive and some are active""" # Create old sessions mock_time.return_value = 1000.0 tracker.update_session("old-session-1") tracker.update_session("old-session-2") # Create recent session mock_time.return_value = 1000.0 + FOUR_HOURS + 100 tracker.update_session("recent-session") # Check for inactive sessions deleted = tracker.delete_inactive_sessions() assert len(deleted) == 2 assert set(deleted) == {"old-session-1", "old-session-2"} assert "recent-session" in tracker.session_timestamp_map assert len(tracker.session_timestamp_map) == 1 @patch("time.time") def test_delete_inactive_sessions_exactly_at_boundary(self, mock_time, tracker): """Test session exactly at FOUR_HOURS boundary""" mock_time.return_value = 1000.0 tracker.update_session("boundary-session") # Move time forward exactly FOUR_HOURS mock_time.return_value = 1000.0 + FOUR_HOURS deleted = tracker.delete_inactive_sessions() # Should NOT be deleted (not GREATER than FOUR_HOURS) assert deleted == [] assert "boundary-session" in tracker.session_timestamp_map @patch("time.time") def test_delete_inactive_sessions_one_second_over(self, mock_time, tracker): """Test session one second over FOUR_HOURS boundary""" mock_time.return_value = 1000.0 tracker.update_session("session") # Move time forward FOUR_HOURS + 1 second mock_time.return_value = 1000.0 + FOUR_HOURS + 1 deleted = tracker.delete_inactive_sessions() # Should be deleted assert deleted == ["session"] assert len(tracker.session_timestamp_map) == 0 @patch("time.time") def test_delete_inactive_sessions_empty_tracker(self, mock_time, tracker): """Test deleting inactive sessions when tracker is empty""" mock_time.return_value = 1000.0 deleted = tracker.delete_inactive_sessions() assert deleted == [] assert len(tracker.session_timestamp_map) == 0 @patch("time.time") def test_delete_inactive_sessions_returns_list(self, mock_time, tracker): """Test that delete_inactive_sessions returns a list""" mock_time.return_value = 1000.0 tracker.update_session("session") mock_time.return_value = 1000.0 + FOUR_HOURS + 100 deleted = tracker.delete_inactive_sessions() assert isinstance(deleted, list) # ==================== delete_oldest_session Tests ==================== @patch("time.time") def test_delete_oldest_session_single_session( self, mock_time, tracker, sample_session_id ): """Test deleting the oldest session when only one exists""" mock_time.return_value = 1000.0 tracker.update_session(sample_session_id) oldest = tracker.delete_oldest_session() assert oldest == sample_session_id assert len(tracker.session_timestamp_map) == 0 @patch("time.time") def test_delete_oldest_session_multiple_sessions(self, mock_time, tracker): """Test deleting the oldest session among multiple""" mock_time.return_value = 1000.0 tracker.update_session("oldest") mock_time.return_value = 2000.0 tracker.update_session("middle") mock_time.return_value = 3000.0 tracker.update_session("newest") oldest = tracker.delete_oldest_session() assert oldest == "oldest" assert "oldest" not in tracker.session_timestamp_map assert "middle" in tracker.session_timestamp_map assert "newest" in tracker.session_timestamp_map def test_delete_oldest_session_empty_tracker(self, tracker): """Test deleting oldest session when tracker is empty""" oldest = tracker.delete_oldest_session() assert oldest is None @patch("time.time") def test_delete_oldest_session_same_timestamps(self, mock_time, tracker): """Test deleting oldest when multiple sessions have same timestamp""" mock_time.return_value = 1000.0 tracker.update_session("session-1") tracker.update_session("session-2") tracker.update_session("session-3") oldest = tracker.delete_oldest_session() # Should delete one of them (deterministic based on dict iteration) assert oldest in ["session-1", "session-2", "session-3"] assert len(tracker.session_timestamp_map) == 2 @patch("time.time") def test_delete_oldest_session_updates_after_initial(self, mock_time, tracker): """Test that updated sessions are not considered oldest""" mock_time.return_value = 1000.0 tracker.update_session("first-created") mock_time.return_value = 2000.0 tracker.update_session("second-created") # Update first-created to be more recent mock_time.return_value = 3000.0 tracker.update_session("first-created") oldest = tracker.delete_oldest_session() # "second-created" should be oldest now assert oldest == "second-created" assert "first-created" in tracker.session_timestamp_map @patch("time.time") def test_delete_oldest_session_successive_calls(self, mock_time, tracker): """Test calling delete_oldest_session multiple times""" mock_time.return_value = 1000.0 tracker.update_session("session-1") mock_time.return_value = 2000.0 tracker.update_session("session-2") mock_time.return_value = 3000.0 tracker.update_session("session-3") # Delete in order from oldest to newest first = tracker.delete_oldest_session() second = tracker.delete_oldest_session() third = tracker.delete_oldest_session() fourth = tracker.delete_oldest_session() # Empty tracker assert first == "session-1" assert second == "session-2" assert third == "session-3" assert fourth is None assert len(tracker.session_timestamp_map) == 0 @patch("time.time") def test_delete_oldest_session_does_not_affect_others(self, mock_time, tracker): """Test that deleting oldest doesn't affect other session timestamps""" mock_time.return_value = 1000.0 tracker.update_session("old") mock_time.return_value = 2000.0 tracker.update_session("new") new_timestamp = tracker.session_timestamp_map["new"] tracker.delete_oldest_session() # "new" should still have the same timestamp assert tracker.session_timestamp_map["new"] == new_timestamp # ==================== Integration Tests ==================== @patch("time.time") def test_full_lifecycle(self, mock_time, tracker): """Test complete session lifecycle: create, update, delete inactive, delete oldest""" # Create sessions at different times mock_time.return_value = 1000.0 tracker.update_session("session-1") mock_time.return_value = 2000.0 tracker.update_session("session-2") mock_time.return_value = 3000.0 tracker.update_session("session-3") # Update session-1 to make it newer mock_time.return_value = 4000.0 tracker.update_session("session-1") # Move past FOUR_HOURS for session-2 mock_time.return_value = 2000.0 + FOUR_HOURS + 100 # Delete inactive deleted_inactive = tracker.delete_inactive_sessions() assert "session-2" in deleted_inactive assert len(tracker.session_timestamp_map) == 2 # Delete oldest oldest = tracker.delete_oldest_session() assert oldest == "session-3" # Oldest remaining assert len(tracker.session_timestamp_map) == 1 assert "session-1" in tracker.session_timestamp_map def test_update_then_delete_same_session(self, tracker, sample_session_id): """Test updating then immediately deleting the same session""" tracker.update_session(sample_session_id) assert sample_session_id in tracker.session_timestamp_map tracker.delete_session(sample_session_id) assert sample_session_id not in tracker.session_timestamp_map @patch("time.time") def test_delete_oldest_after_delete_inactive(self, mock_time, tracker): """Test delete_oldest after delete_inactive has removed some sessions""" mock_time.return_value = 1000.0 tracker.update_session("old-1") tracker.update_session("old-2") mock_time.return_value = 1000.0 + FOUR_HOURS + 100 tracker.update_session("new-1") tracker.update_session("new-2") # Delete inactive (removes old-1 and old-2) tracker.delete_inactive_sessions() # Delete oldest of remaining oldest = tracker.delete_oldest_session() # Both new sessions have same timestamp, one should be deleted assert oldest in ["new-1", "new-2"] assert len(tracker.session_timestamp_map) == 1 @patch("time.time") def test_stress_test_many_sessions(self, mock_time, tracker): """Test handling many sessions""" num_sessions = 1000 for i in range(num_sessions): mock_time.return_value = 1000.0 + i tracker.update_session(f"session-{i}") assert len(tracker.session_timestamp_map) == num_sessions # Delete oldest should remove session-0 oldest = tracker.delete_oldest_session() assert oldest == "session-0" assert len(tracker.session_timestamp_map) == num_sessions - 1 @patch("time.time") def test_timestamp_precision(self, mock_time, tracker): """Test that timestamps maintain precision""" mock_time.return_value = 1234567890.123456789 tracker.update_session("session") stored_timestamp = tracker.session_timestamp_map["session"] assert stored_timestamp == 1234567890.123456789 def test_empty_session_id(self, tracker): """Test handling empty string as session ID""" tracker.update_session("") assert "" in tracker.session_timestamp_map tracker.delete_session("") assert "" not in tracker.session_timestamp_map @patch("time.time") def test_delete_inactive_preserves_order_independence(self, mock_time, tracker): """Test that delete_inactive doesn't depend on insertion order""" # Create sessions in random order mock_time.return_value = 3000.0 tracker.update_session("session-3") mock_time.return_value = 1000.0 tracker.update_session("session-1") mock_time.return_value = 2000.0 tracker.update_session("session-2") # All should be deleted mock_time.return_value = 3000.0 + FOUR_HOURS + 100 deleted = tracker.delete_inactive_sessions() assert len(deleted) == 3 assert set(deleted) == {"session-1", "session-2", "session-3"}