Aniworld/tests/integration/test_api_integration.py

640 lines
23 KiB
Python

"""
Integration tests for API endpoints using Flask test client.
This module provides integration tests that actually make HTTP requests
to the Flask application to test the complete request/response cycle.
"""
import unittest
import json
import tempfile
import os
from unittest.mock import patch, MagicMock
import sys
# Add parent directories to path for imports
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src'))
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src', 'server'))
class APIIntegrationTestBase(unittest.TestCase):
"""Base class for API integration tests."""
def setUp(self):
"""Set up test fixtures before each test method."""
# Mock all the complex dependencies to avoid initialization issues
self.patches = {}
# Mock the main series app and related components
self.patches['series_app'] = patch('src.server.app.series_app')
self.patches['config'] = patch('src.server.app.config')
self.patches['session_manager'] = patch('src.server.app.session_manager')
self.patches['socketio'] = patch('src.server.app.socketio')
# Start all patches
self.mock_series_app = self.patches['series_app'].start()
self.mock_config = self.patches['config'].start()
self.mock_session_manager = self.patches['session_manager'].start()
self.mock_socketio = self.patches['socketio'].start()
# Configure mock config
self.mock_config.anime_directory = '/test/anime'
self.mock_config.has_master_password.return_value = True
self.mock_config.save_config = MagicMock()
# Configure mock session manager
self.mock_session_manager.sessions = {}
self.mock_session_manager.get_session_info.return_value = {
'authenticated': False,
'session_id': None
}
try:
# Import and create the Flask app
from src.server.app import app
app.config['TESTING'] = True
app.config['WTF_CSRF_ENABLED'] = False
self.app = app
self.client = app.test_client()
except ImportError as e:
self.skipTest(f"Cannot import Flask app: {e}")
def tearDown(self):
"""Clean up after each test method."""
# Stop all patches
for patch_obj in self.patches.values():
patch_obj.stop()
def authenticate_session(self):
"""Helper method to set up authenticated session."""
session_id = 'test-session-123'
self.mock_session_manager.sessions[session_id] = {
'authenticated': True,
'created_at': 1234567890,
'last_accessed': 1234567890
}
self.mock_session_manager.get_session_info.return_value = {
'authenticated': True,
'session_id': session_id
}
# Mock session validation
def mock_require_auth(func):
return func
def mock_optional_auth(func):
return func
with patch('src.server.app.require_auth', mock_require_auth), \
patch('src.server.app.optional_auth', mock_optional_auth):
return session_id
class TestAuthenticationAPI(APIIntegrationTestBase):
"""Integration tests for authentication API endpoints."""
def test_auth_status_get(self):
"""Test GET /api/auth/status endpoint."""
response = self.client.get('/api/auth/status')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertIn('authenticated', data)
self.assertIn('has_master_password', data)
self.assertIn('setup_required', data)
@patch('src.server.app.require_auth', lambda f: f) # Skip auth decorator
def test_auth_setup_post(self):
"""Test POST /api/auth/setup endpoint."""
test_data = {'password': 'new_master_password'}
self.mock_config.has_master_password.return_value = False
self.mock_session_manager.create_session.return_value = 'new-session'
response = self.client.post(
'/api/auth/setup',
data=json.dumps(test_data),
content_type='application/json'
)
# Should not be 404 (route exists)
self.assertNotEqual(response.status_code, 404)
def test_auth_login_post(self):
"""Test POST /api/auth/login endpoint."""
test_data = {'password': 'test_password'}
self.mock_session_manager.login.return_value = {
'success': True,
'session_id': 'test-session'
}
response = self.client.post(
'/api/auth/login',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertNotEqual(response.status_code, 404)
def test_auth_logout_post(self):
"""Test POST /api/auth/logout endpoint."""
self.authenticate_session()
response = self.client.post('/api/auth/logout')
self.assertNotEqual(response.status_code, 404)
class TestConfigurationAPI(APIIntegrationTestBase):
"""Integration tests for configuration API endpoints."""
@patch('src.server.app.require_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.init_series_app') # Mock series app initialization
def test_config_directory_post(self):
"""Test POST /api/config/directory endpoint."""
test_data = {'directory': '/new/test/directory'}
response = self.client.post(
'/api/config/directory',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertNotEqual(response.status_code, 404)
# Should be successful or have validation error, but route should exist
self.assertIn(response.status_code, [200, 400, 500])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_scheduler_config_get(self):
"""Test GET /api/scheduler/config endpoint."""
response = self.client.get('/api/scheduler/config')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertIn('success', data)
self.assertIn('config', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_scheduler_config_post(self):
"""Test POST /api/scheduler/config endpoint."""
test_data = {
'enabled': True,
'time': '02:30',
'auto_download_after_rescan': True
}
response = self.client.post(
'/api/scheduler/config',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_advanced_config_get(self):
"""Test GET /api/config/section/advanced endpoint."""
response = self.client.get('/api/config/section/advanced')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('config', data)
self.assertIn('max_concurrent_downloads', data['config'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_advanced_config_post(self):
"""Test POST /api/config/section/advanced endpoint."""
test_data = {
'max_concurrent_downloads': 5,
'provider_timeout': 45,
'enable_debug_mode': True
}
response = self.client.post(
'/api/config/section/advanced',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
class TestSeriesAPI(APIIntegrationTestBase):
"""Integration tests for series management API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_series_get_with_data(self):
"""Test GET /api/series endpoint with mock data."""
# Mock series data
mock_serie = MagicMock()
mock_serie.folder = 'test_anime'
mock_serie.name = 'Test Anime'
mock_serie.episodeDict = {'Season 1': [1, 2, 3, 4, 5]}
self.mock_series_app.List.GetList.return_value = [mock_serie]
response = self.client.get('/api/series')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('series', data)
self.assertIn('total_series', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_series_get_no_data(self):
"""Test GET /api/series endpoint with no data."""
self.mock_series_app = None
with patch('src.server.app.series_app', None):
response = self.client.get('/api/series')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertEqual(len(data['series']), 0)
self.assertEqual(data['total_series'], 0)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_search_post(self):
"""Test POST /api/search endpoint."""
test_data = {'query': 'test anime search'}
mock_results = [
{'name': 'Test Anime 1', 'link': 'https://example.com/anime1'},
{'name': 'Test Anime 2', 'link': 'https://example.com/anime2'}
]
self.mock_series_app.search.return_value = mock_results
response = self.client.post(
'/api/search',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('results', data)
self.assertIn('total', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_search_post_empty_query(self):
"""Test POST /api/search endpoint with empty query."""
test_data = {'query': ''}
response = self.client.post(
'/api/search',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
data = json.loads(response.data)
self.assertEqual(data['status'], 'error')
self.assertIn('empty', data['message'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.is_scanning', False)
@patch('src.server.app.is_process_running')
@patch('threading.Thread')
def test_rescan_post(self, mock_thread, mock_is_running):
"""Test POST /api/rescan endpoint."""
mock_is_running.return_value = False
response = self.client.post('/api/rescan')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('started', data['message'])
class TestDownloadAPI(APIIntegrationTestBase):
"""Integration tests for download management API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.is_downloading', False)
@patch('src.server.app.is_process_running')
def test_download_post(self, mock_is_running):
"""Test POST /api/download endpoint."""
mock_is_running.return_value = False
test_data = {'series': 'test_series', 'episodes': [1, 2, 3]}
response = self.client.post(
'/api/download',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
class TestStatusAPI(APIIntegrationTestBase):
"""Integration tests for status and monitoring API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.is_process_running')
def test_process_locks_status_get(self, mock_is_running):
"""Test GET /api/process/locks/status endpoint."""
mock_is_running.return_value = False
response = self.client.get('/api/process/locks/status')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('locks', data)
self.assertIn('rescan', data['locks'])
self.assertIn('download', data['locks'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch.dict('os.environ', {'ANIME_DIRECTORY': '/test/anime'})
def test_status_get(self):
"""Test GET /api/status endpoint."""
response = self.client.get('/api/status')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('directory', data)
self.assertIn('series_count', data)
self.assertIn('timestamp', data)
class TestLoggingAPI(APIIntegrationTestBase):
"""Integration tests for logging management API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_config_get(self):
"""Test GET /api/logging/config endpoint."""
response = self.client.get('/api/logging/config')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('config', data)
self.assertIn('log_level', data['config'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_config_post(self):
"""Test POST /api/logging/config endpoint."""
test_data = {
'log_level': 'DEBUG',
'enable_console_logging': False
}
response = self.client.post(
'/api/logging/config',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_files_get(self):
"""Test GET /api/logging/files endpoint."""
response = self.client.get('/api/logging/files')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('files', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_test_post(self):
"""Test POST /api/logging/test endpoint."""
response = self.client.post('/api/logging/test')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_cleanup_post(self):
"""Test POST /api/logging/cleanup endpoint."""
test_data = {'days': 7}
response = self.client.post(
'/api/logging/cleanup',
data=json.dumps(test_data),
content_type='application/json'
)
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('7 days', data['message'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_logging_tail_get(self):
"""Test GET /api/logging/files/<filename>/tail endpoint."""
response = self.client.get('/api/logging/files/test.log/tail?lines=50')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('content', data)
self.assertEqual(data['filename'], 'test.log')
class TestBackupAPI(APIIntegrationTestBase):
"""Integration tests for configuration backup API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_config_backup_create_post(self):
"""Test POST /api/config/backup endpoint."""
response = self.client.post('/api/config/backup')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('filename', data)
self.assertIn('config_backup_', data['filename'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_config_backups_get(self):
"""Test GET /api/config/backups endpoint."""
response = self.client.get('/api/config/backups')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn('backups', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_config_backup_restore_post(self):
"""Test POST /api/config/backup/<filename>/restore endpoint."""
filename = 'config_backup_20231201_143000.json'
response = self.client.post(f'/api/config/backup/{filename}/restore')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
self.assertIn(filename, data['message'])
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
def test_config_backup_download_get(self):
"""Test GET /api/config/backup/<filename>/download endpoint."""
filename = 'config_backup_20231201_143000.json'
response = self.client.get(f'/api/config/backup/{filename}/download')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertTrue(data['success'])
class TestDiagnosticsAPI(APIIntegrationTestBase):
"""Integration tests for diagnostics and monitoring API endpoints."""
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.network_health_checker')
def test_network_diagnostics_get(self, mock_checker):
"""Test GET /api/diagnostics/network endpoint."""
mock_checker.get_network_status.return_value = {
'internet_connected': True,
'dns_working': True
}
mock_checker.check_url_reachability.return_value = True
response = self.client.get('/api/diagnostics/network')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('data', data)
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.error_recovery_manager')
def test_diagnostics_errors_get(self, mock_manager):
"""Test GET /api/diagnostics/errors endpoint."""
mock_manager.error_history = [
{'timestamp': '2023-12-01T14:30:00', 'error': 'Test error'}
]
mock_manager.blacklisted_urls = {'bad_url.com': True}
response = self.client.get('/api/diagnostics/errors')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('data', data)
@patch('src.server.app.require_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.error_recovery_manager')
def test_recovery_clear_blacklist_post(self, mock_manager):
"""Test POST /api/recovery/clear-blacklist endpoint."""
mock_manager.blacklisted_urls = {'url1': True}
response = self.client.post('/api/recovery/clear-blacklist')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
@patch('src.server.app.optional_auth', lambda f: f) # Skip auth decorator
@patch('src.server.app.error_recovery_manager')
def test_recovery_retry_counts_get(self, mock_manager):
"""Test GET /api/recovery/retry-counts endpoint."""
mock_manager.retry_counts = {'url1': 3, 'url2': 5}
response = self.client.get('/api/recovery/retry-counts')
self.assertEqual(response.status_code, 200)
data = json.loads(response.data)
self.assertEqual(data['status'], 'success')
self.assertIn('data', data)
if __name__ == '__main__':
# Run integration tests
loader = unittest.TestLoader()
# Load all test cases
test_classes = [
TestAuthenticationAPI,
TestConfigurationAPI,
TestSeriesAPI,
TestDownloadAPI,
TestStatusAPI,
TestLoggingAPI,
TestBackupAPI,
TestDiagnosticsAPI
]
# Create test suite
suite = unittest.TestSuite()
for test_class in test_classes:
tests = loader.loadTestsFromTestCase(test_class)
suite.addTests(tests)
# Run tests
runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(suite)
# Print summary
print(f"\n{'='*70}")
print(f"API INTEGRATION TEST SUMMARY")
print(f"{'='*70}")
print(f"Tests run: {result.testsRun}")
print(f"Failures: {len(result.failures)}")
print(f"Errors: {len(result.errors)}")
print(f"Skipped: {len(result.skipped) if hasattr(result, 'skipped') else 0}")
if result.testsRun > 0:
success_rate = ((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100)
print(f"Success rate: {success_rate:.1f}%")
# Print details of any failures or errors
if result.failures:
print(f"\n🔥 FAILURES:")
for test, traceback in result.failures:
print(f"{test}")
print(f" {traceback.split('AssertionError: ')[-1].split(chr(10))[0] if 'AssertionError:' in traceback else 'See traceback above'}")
if result.errors:
print(f"\n💥 ERRORS:")
for test, traceback in result.errors:
print(f" 💣 {test}")
error_line = traceback.split(chr(10))[-2] if len(traceback.split(chr(10))) > 1 else 'See traceback above'
print(f" {error_line}")
# Exit with proper code
exit(0 if result.wasSuccessful() else 1)