640 lines
23 KiB
Python
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) |