"""Tests for OpenRouter sync tasks. T56: Task to sync usage stats from OpenRouter. T57: Task to validate API keys. """ import pytest from datetime import datetime, date, timedelta from decimal import Decimal from unittest.mock import Mock, patch, MagicMock, AsyncMock import httpx from apscheduler.triggers.interval import IntervalTrigger from apscheduler.triggers.cron import CronTrigger @pytest.mark.unit class TestSyncUsageStats: """Test suite for sync_usage_stats task.""" def test_sync_usage_stats_has_correct_decorator(self): """Test that sync_usage_stats has correct scheduled_job decorator.""" # Arrange from openrouter_monitor.tasks.sync import sync_usage_stats from openrouter_monitor.tasks.scheduler import get_scheduler # Act scheduler = get_scheduler() job = scheduler.get_job('sync_usage_stats') # Assert assert job is not None assert job.func == sync_usage_stats assert isinstance(job.trigger, IntervalTrigger) assert job.trigger.interval.total_seconds() == 3600 # 1 hour def test_sync_usage_stats_is_async_function(self): """Test that sync_usage_stats is an async function.""" # Arrange from openrouter_monitor.tasks.sync import sync_usage_stats import inspect # Assert assert inspect.iscoroutinefunction(sync_usage_stats) @pytest.mark.asyncio async def test_sync_usage_stats_handles_empty_keys(self): """Test that sync completes gracefully with no active keys.""" # Arrange from openrouter_monitor.tasks.sync import sync_usage_stats # Create mock result with empty keys mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [] async def mock_execute(*args, **kwargs): return mock_result mock_db = MagicMock() mock_db.execute = mock_execute mock_db.commit = AsyncMock() with patch('openrouter_monitor.tasks.sync.SessionLocal') as mock_session: mock_session.return_value.__enter__ = Mock(return_value=mock_db) mock_session.return_value.__exit__ = Mock(return_value=False) # Act & Assert - should complete without error await sync_usage_stats() @pytest.mark.asyncio async def test_sync_usage_stats_handles_decryption_error(self): """Test that sync handles decryption errors gracefully.""" # Arrange from openrouter_monitor.tasks.sync import sync_usage_stats mock_key = MagicMock() mock_key.id = 1 mock_key.key_encrypted = "encrypted" mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [mock_key] async def mock_execute(*args, **kwargs): return mock_result mock_db = MagicMock() mock_db.execute = mock_execute mock_db.commit = AsyncMock() with patch('openrouter_monitor.tasks.sync.SessionLocal') as mock_session, \ patch('openrouter_monitor.tasks.sync.EncryptionService') as mock_encrypt: mock_session.return_value.__enter__ = Mock(return_value=mock_db) mock_session.return_value.__exit__ = Mock(return_value=False) # Simulate decryption error mock_encrypt_instance = MagicMock() mock_encrypt_instance.decrypt.side_effect = Exception("Decryption failed") mock_encrypt.return_value = mock_encrypt_instance # Act & Assert - should not raise await sync_usage_stats() @pytest.mark.unit class TestValidateApiKeys: """Test suite for validate_api_keys task (T57).""" def test_validate_api_keys_has_correct_decorator(self): """Test that validate_api_keys has correct scheduled_job decorator.""" # Arrange from openrouter_monitor.tasks.sync import validate_api_keys from openrouter_monitor.tasks.scheduler import get_scheduler # Act scheduler = get_scheduler() job = scheduler.get_job('validate_api_keys') # Assert assert job is not None assert job.func == validate_api_keys assert isinstance(job.trigger, CronTrigger) # Should be a daily cron trigger at specific hour def test_validate_api_keys_is_async_function(self): """Test that validate_api_keys is an async function.""" # Arrange from openrouter_monitor.tasks.sync import validate_api_keys import inspect # Assert assert inspect.iscoroutinefunction(validate_api_keys) @pytest.mark.asyncio async def test_validate_api_keys_handles_empty_keys(self): """Test that validation completes gracefully with no active keys.""" # Arrange from openrouter_monitor.tasks.sync import validate_api_keys # Create mock result with empty keys mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [] async def mock_execute(*args, **kwargs): return mock_result mock_db = MagicMock() mock_db.execute = mock_execute mock_db.commit = AsyncMock() with patch('openrouter_monitor.tasks.sync.SessionLocal') as mock_session: mock_session.return_value.__enter__ = Mock(return_value=mock_db) mock_session.return_value.__exit__ = Mock(return_value=False) # Act & Assert - should complete without error await validate_api_keys() @pytest.mark.asyncio async def test_validate_api_keys_handles_decryption_error(self): """Test that validation handles decryption errors gracefully.""" # Arrange from openrouter_monitor.tasks.sync import validate_api_keys mock_key = MagicMock() mock_key.id = 1 mock_key.key_encrypted = "encrypted" mock_result = MagicMock() mock_result.scalars.return_value.all.return_value = [mock_key] async def mock_execute(*args, **kwargs): return mock_result mock_db = MagicMock() mock_db.execute = mock_execute mock_db.commit = AsyncMock() with patch('openrouter_monitor.tasks.sync.SessionLocal') as mock_session, \ patch('openrouter_monitor.tasks.sync.EncryptionService') as mock_encrypt: mock_session.return_value.__enter__ = Mock(return_value=mock_db) mock_session.return_value.__exit__ = Mock(return_value=False) # Simulate decryption error mock_encrypt_instance = MagicMock() mock_encrypt_instance.decrypt.side_effect = Exception("Decryption failed") mock_encrypt.return_value = mock_encrypt_instance # Act & Assert - should not raise await validate_api_keys() @pytest.mark.unit class TestSyncConstants: """Test suite for sync module constants.""" def test_openrouter_urls_defined(self): """Test that OpenRouter URLs are defined.""" from openrouter_monitor.tasks.sync import ( OPENROUTER_USAGE_URL, OPENROUTER_AUTH_URL, RATE_LIMIT_DELAY ) assert 'openrouter.ai' in OPENROUTER_USAGE_URL assert 'openrouter.ai' in OPENROUTER_AUTH_URL assert RATE_LIMIT_DELAY == 0.35 def test_rate_limit_delay_respects_openrouter_limits(self): """Test that rate limit delay respects OpenRouter 20 req/min limit.""" from openrouter_monitor.tasks.sync import RATE_LIMIT_DELAY # 20 requests per minute = 3 seconds per request # We use 0.35s to be safe (allows ~171 req/min, well under limit) assert RATE_LIMIT_DELAY >= 0.3 # At least 0.3s assert RATE_LIMIT_DELAY <= 1.0 # But not too slow