""" 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//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//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//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)