champ-chatbot / tests /unit /classes /test_session_tracker.py
qyle's picture
deployment
8b9e569 verified
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"}