AIstudioProxyAPI / tests /test_soft_context_swapping.py
peijun1's picture
Deploy AI Studio Proxy API to Hugging Face Spaces
a5784e9
Raw
History Blame Contribute Delete
20 kB
#!/usr/bin/env python3
"""
Test Suite for Soft Context Swapping Implementation
This test suite validates the "Soft Context Swapping" feature that replaces
the previous browser restart mechanism with efficient cookie-based authentication
profile rotation.
Key improvements being tested:
1. Uses context.clear_cookies() and context.add_cookies() instead of browser restart
2. Performance improvement from 3-5 seconds (hard restart) to <1 second (soft swap)
3. Maintains browser state while only swapping authentication cookies
4. Proper error handling and fallback mechanisms
Run with: python tests/test_soft_context_swapping.py
"""
import json
import os
import sys
import time
import unittest
from unittest.mock import AsyncMock, Mock, patch
# Add project root to Python path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
from api_utils.server_state import state
from browser_utils.auth_rotation import _get_next_profile, perform_auth_rotation
from config.global_state import GlobalState
class TestSoftContextSwapping(unittest.TestCase):
"""Test suite for Soft Context Swapping implementation"""
def setUp(self):
"""Initialize test environment before each test"""
# Reset global state
GlobalState.reset_quota_status()
GlobalState.init_rotation_lock()
# Create mock page instance with mock context
self.mock_page = Mock()
self.mock_page.is_closed.return_value = False
self.mock_page.context = Mock()
self.mock_context = self.mock_page.context
# Setup async mock methods for context operations
self.mock_context.clear_cookies = AsyncMock()
self.mock_context.add_cookies = AsyncMock()
# Store original state values
self.original_page_instance = state.page_instance
self.original_browser_instance = state.browser_instance
# Set mock state values
state.page_instance = self.mock_page
state.browser_instance = None # Should not be used in soft swap
# Mock profile data
self.test_cookies = [
{
"name": "session_id",
"value": "test_session_123",
"domain": ".aistudio.google.com",
"path": "/",
},
{
"name": "auth_token",
"value": "test_auth_456",
"domain": ".aistudio.google.com",
"path": "/",
},
]
self.test_storage_state = {"cookies": self.test_cookies, "origins": []}
def tearDown(self):
"""Clean up after each test"""
# Restore original state values
state.page_instance = self.original_page_instance
state.browser_instance = self.original_browser_instance
# Clean up any test files
test_profile_paths = [
"auth_profiles/saved/test_profile_1.json",
"auth_profiles/active/test_profile_2.json",
"auth_profiles/emergency/test_emergency.json",
]
for path in test_profile_paths:
if os.path.exists(path):
os.remove(path)
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_soft_context_swap_performed(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test that soft context swap is performed with clear_cookies() and add_cookies()"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True # Successful canary test
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Execute rotation
result = await perform_auth_rotation()
# Verify soft context swap was performed
self.assertTrue(result, "Rotation should complete successfully")
self.mock_context.clear_cookies.assert_called_once()
self.mock_context.add_cookies.assert_called_once_with(self.test_cookies)
# Verify browser restart functions were NOT called
# (These should remain None/unmocked, indicating they weren't used)
self.assertIsNone(state.browser_instance)
# Verify canary test was called
mock_canary.assert_called_once_with(self.mock_page)
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_rotation_performance_benchmark(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test that soft context swapping completes within performance target (<1 second)"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Measure rotation time
start_time = time.time()
result = await perform_auth_rotation()
end_time = time.time()
rotation_time = end_time - start_time
# Verify performance target
self.assertTrue(result, "Rotation should complete successfully")
self.assertLess(
rotation_time,
1.0,
f"Soft context swap took {rotation_time:.3f}s, should be < 1.0s",
)
# Log performance for verification
print(f"Soft context swap completed in {rotation_time:.3f} seconds")
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_browser_restart_not_called(self, mock_canary, mock_get_next_profile):
"""Test that browser restart functions are not called during soft context swap"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True
# Create a separate mock to track browser restart attempts
getattr(
state.browser_instance, "close", None
) if state.browser_instance else None
# Mock the page instance to ensure it's NOT closed
self.mock_page.close = AsyncMock()
# Mock file operations
with patch("builtins.open") as mock_file_open:
mock_file = Mock()
mock_file_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Execute rotation
await perform_auth_rotation()
# Verify browser instance methods were NOT called
self.mock_page.close.assert_not_called()
# Verify browser instance was not used (remains None)
self.assertIsNone(state.browser_instance)
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_context_operations_sequence(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test that context operations are called in the correct sequence"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Execute rotation
await perform_auth_rotation()
# Verify the sequence of operations
clear_call_args = self.mock_context.clear_cookies.call_args_list
add_call_args = self.mock_context.add_cookies.call_args_list
# Should have exactly one call to each
self.assertEqual(
len(clear_call_args), 1, "clear_cookies should be called exactly once"
)
self.assertEqual(
len(add_call_args), 1, "add_cookies should be called exactly once"
)
# Verify clear_cookies was called before add_cookies
clear_time = clear_call_args[0][1].get("timestamp", 0) if clear_call_args else 0
add_time = add_call_args[0][1].get("timestamp", 1) if add_call_args else 1
# In the actual implementation, we verify this by call order
self.assertLess(
clear_time, add_time, "clear_cookies should be called before add_cookies"
)
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_error_during_soft_swap(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test error handling during soft context swap"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Make add_cookies raise an exception
self.mock_context.add_cookies.side_effect = Exception("Cookie injection failed")
# Execute rotation and expect it to handle the error gracefully
result = await perform_auth_rotation()
# Should fail due to the error
self.assertFalse(result, "Rotation should fail when cookie injection fails")
# Verify both operations were attempted
self.mock_context.clear_cookies.assert_called_once()
self.mock_context.add_cookies.assert_called_once()
@patch("browser_utils.auth_rotation._get_next_profile")
async def test_no_available_profiles(self, mock_get_next_profile):
"""Test behavior when no auth profiles are available"""
# Setup mocks
mock_get_next_profile.return_value = None
# Execute rotation
result = await perform_auth_rotation()
# Should fail due to no profiles
self.assertFalse(result, "Rotation should fail when no profiles are available")
# Verify context operations were NOT called
self.mock_context.clear_cookies.assert_not_called()
self.mock_context.add_cookies.assert_not_called()
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
async def test_page_instance_unavailable(self, mock_open, mock_get_next_profile):
"""Test behavior when page instance is unavailable or closed"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Simulate closed page
self.mock_page.is_closed.return_value = True
# Execute rotation
result = await perform_auth_rotation()
# Should fail due to unavailable page
self.assertFalse(result, "Rotation should fail when page is closed")
# Verify context operations were NOT called
self.mock_context.clear_cookies.assert_not_called()
self.mock_context.add_cookies.assert_not_called()
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_canary_test_failure_handling(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test behavior when canary test fails after soft context swap"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = False # Failed canary test
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Execute rotation
result = await perform_auth_rotation()
# Should fail due to canary test failure
self.assertFalse(result, "Rotation should fail when canary test fails")
# Verify context operations WERE called (soft swap was attempted)
self.mock_context.clear_cookies.assert_called_once()
self.mock_context.add_cookies.assert_called_once()
def test_rotation_lock_management(self):
"""Test that rotation lock is properly managed during soft context swap"""
# Verify initial state
self.assertTrue(
GlobalState.AUTH_ROTATION_LOCK.is_set(),
"Rotation lock should be initially set",
)
# Test will be completed by the async tests above which verify lock behavior
# This is a basic test to ensure the lock mechanism is working
@patch("browser_utils.auth_rotation._get_next_profile")
@patch("builtins.open")
@patch("browser_utils.auth_rotation._perform_canary_test")
async def test_profile_state_updated(
self, mock_canary, mock_open, mock_get_next_profile
):
"""Test that global profile state is updated after successful rotation"""
# Setup mocks
test_profile_path = "auth_profiles/saved/test_profile.json"
mock_get_next_profile.return_value = test_profile_path
mock_canary.return_value = True
# Mock file operations
mock_file = Mock()
mock_open.return_value.__enter__.return_value = mock_file
mock_file.read.return_value = json.dumps(self.test_storage_state)
# Execute rotation
result = await perform_auth_rotation()
# Verify success
self.assertTrue(result, "Rotation should complete successfully")
# Verify server state was updated
self.assertEqual(state.current_auth_profile_path, test_profile_path)
self.assertEqual(os.environ.get("ACTIVE_AUTH_JSON_PATH"), test_profile_path)
class TestSoftContextSwappingIntegration(unittest.TestCase):
"""Integration tests for soft context swapping with real components"""
def setUp(self):
"""Set up integration test environment"""
# Create temporary test profile files
self.test_cookies = [
{
"name": "session_id",
"value": "test_session_123",
"domain": ".aistudio.google.com",
"path": "/",
}
]
self.test_storage_state = {"cookies": self.test_cookies, "origins": []}
# Create test profile directories
os.makedirs("auth_profiles/saved", exist_ok=True)
os.makedirs("auth_profiles/active", exist_ok=True)
os.makedirs("auth_profiles/emergency", exist_ok=True)
# Create test profile files
self.test_profile_1 = "auth_profiles/saved/test_profile_1.json"
self.test_profile_2 = "auth_profiles/active/test_profile_2.json"
self.test_emergency = "auth_profiles/emergency/test_emergency.json"
with open(self.test_profile_1, "w") as f:
json.dump(self.test_storage_state, f)
with open(self.test_profile_2, "w") as f:
json.dump(self.test_storage_state, f)
with open(self.test_emergency, "w") as f:
json.dump(self.test_storage_state, f)
def tearDown(self):
"""Clean up integration test files"""
test_files = [self.test_profile_1, self.test_profile_2, self.test_emergency]
for file_path in test_files:
if os.path.exists(file_path):
os.remove(file_path)
def test_profile_selection_integration(self):
"""Test that profile selection works with actual file system"""
# Test profile selection from standard directories
selected_profile = _get_next_profile()
# Should find and return one of our test profiles
self.assertIsNotNone(selected_profile, "Should find available profiles")
assert selected_profile is not None # Type narrowing for type checker
self.assertTrue(selected_profile.endswith(".json"), "Should return JSON file")
# Should be one of our test profiles (use absolute paths for comparison)
# Note: The rotation logic now includes emergency profiles in standard rotation scan
valid_profiles = [
os.path.abspath(self.test_profile_1),
os.path.abspath(self.test_profile_2),
os.path.abspath(self.test_emergency),
]
self.assertIn(
os.path.abspath(selected_profile),
valid_profiles,
"Should select from available profiles",
)
def test_cooldown_profile_exclusion(self):
"""Test that profiles in cooldown are excluded from selection"""
# Import cooldown functionality to simulate cooldown state
from browser_utils.auth_rotation import _COOLDOWN_PROFILES
# Add one profile to cooldown
_COOLDOWN_PROFILES[self.test_profile_1] = time.time() + 3600 # 1 hour cooldown
# Test profile selection
selected_profile = _get_next_profile()
# Should not return the cooled-down profile
self.assertIsNotNone(selected_profile, "Should find available profiles")
self.assertNotEqual(
selected_profile, self.test_profile_1, "Should exclude cooled-down profiles"
)
def run_tests():
"""Run all soft context swapping tests and provide summary"""
print("Soft Context Swapping - Test Suite")
print("=" * 60)
print("Testing the new cookie-based authentication rotation system...")
print("Performance target: <1 second (vs old 3-5 second hard restart)")
print("=" * 60)
# Create test suite
test_classes = [TestSoftContextSwapping, TestSoftContextSwappingIntegration]
total_tests = 0
passed_tests = 0
for test_class in test_classes:
print(f"\nRunning {test_class.__name__}...")
suite = unittest.TestLoader().loadTestsFromTestCase(test_class)
result = unittest.TextTestRunner(verbosity=2).run(suite)
total_tests += result.testsRun
passed_tests += result.testsRun - len(result.failures) - len(result.errors)
if result.failures:
print(f" ❌ Failures: {len(result.failures)}")
for failure in result.failures:
print(f" - {failure[0]}")
if result.errors:
print(f" 🚨 Errors: {len(result.errors)}")
for error in result.errors:
print(f" - {error[0]}")
print("\n" + "=" * 60)
print("Test Summary:")
print(f" Total Tests: {total_tests}")
print(f" Passed: {passed_tests}")
print(
f" Success Rate: {(passed_tests / total_tests) * 100:.1f}%"
if total_tests > 0
else " Success Rate: 0%"
)
if passed_tests == total_tests:
print("\n🎉 All tests passed! Soft Context Swapping is working correctly.")
print("✅ Performance improvements validated")
print("✅ Cookie-based rotation confirmed")
print("✅ Browser restart prevention verified")
else:
print(
f"\n⚠️ {total_tests - passed_tests} test(s) failed. Review the implementation."
)
return passed_tests == total_tests
if __name__ == "__main__":
success = run_tests()
sys.exit(0 if success else 1)