cleanup
This commit is contained in:
@@ -1 +0,0 @@
|
||||
# Test package initialization
|
||||
@@ -1,90 +0,0 @@
|
||||
# Test configuration and fixtures
|
||||
import os
|
||||
import tempfile
|
||||
import pytest
|
||||
from flask import Flask
|
||||
from src.server.app import create_app
|
||||
from src.server.infrastructure.database.connection import get_database
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def app():
|
||||
"""Create and configure a new app instance for each test."""
|
||||
# Create a temporary file to isolate the database for each test
|
||||
db_fd, db_path = tempfile.mkstemp()
|
||||
|
||||
app = create_app({
|
||||
'TESTING': True,
|
||||
'DATABASE_URL': f'sqlite:///{db_path}',
|
||||
'SECRET_KEY': 'test-secret-key',
|
||||
'WTF_CSRF_ENABLED': False,
|
||||
'LOGIN_DISABLED': True,
|
||||
})
|
||||
|
||||
with app.app_context():
|
||||
# Initialize database tables
|
||||
from src.server.infrastructure.database import models
|
||||
models.db.create_all()
|
||||
|
||||
yield app
|
||||
|
||||
# Clean up
|
||||
os.close(db_fd)
|
||||
os.unlink(db_path)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client(app):
|
||||
"""A test client for the app."""
|
||||
return app.test_client()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner(app):
|
||||
"""A test runner for the app's Click commands."""
|
||||
return app.test_cli_runner()
|
||||
|
||||
|
||||
class AuthActions:
|
||||
def __init__(self, client):
|
||||
self._client = client
|
||||
|
||||
def login(self, username='test', password='test'):
|
||||
return self._client.post(
|
||||
'/auth/login',
|
||||
data={'username': username, 'password': password}
|
||||
)
|
||||
|
||||
def logout(self):
|
||||
return self._client.get('/auth/logout')
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def auth(client):
|
||||
return AuthActions(client)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_anime_data():
|
||||
"""Sample anime data for testing."""
|
||||
return {
|
||||
'title': 'Test Anime',
|
||||
'description': 'A test anime series',
|
||||
'episodes': 12,
|
||||
'status': 'completed',
|
||||
'year': 2023,
|
||||
'genres': ['Action', 'Adventure'],
|
||||
'cover_url': 'https://example.com/cover.jpg'
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_download_data():
|
||||
"""Sample download data for testing."""
|
||||
return {
|
||||
'anime_id': 1,
|
||||
'episode_number': 1,
|
||||
'quality': '1080p',
|
||||
'status': 'pending',
|
||||
'url': 'https://example.com/download/episode1.mp4'
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
"""
|
||||
Pytest configuration for API tests.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add necessary paths 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'))
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def api_test_config():
|
||||
"""Configuration for API tests."""
|
||||
return {
|
||||
'base_url': 'http://localhost:5000',
|
||||
'test_timeout': 30,
|
||||
'mock_data': True
|
||||
}
|
||||
|
||||
def pytest_configure(config):
|
||||
"""Configure pytest with custom markers."""
|
||||
config.addinivalue_line(
|
||||
"markers", "api: mark test as API endpoint test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "auth: mark test as authentication test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "integration: mark test as integration test"
|
||||
)
|
||||
config.addinivalue_line(
|
||||
"markers", "unit: mark test as unit test"
|
||||
)
|
||||
|
||||
def pytest_collection_modifyitems(config, items):
|
||||
"""Auto-mark tests based on their location."""
|
||||
for item in items:
|
||||
# Mark tests based on file path
|
||||
if "test_api" in str(item.fspath):
|
||||
item.add_marker(pytest.mark.api)
|
||||
|
||||
if "integration" in str(item.fspath):
|
||||
item.add_marker(pytest.mark.integration)
|
||||
elif "unit" in str(item.fspath):
|
||||
item.add_marker(pytest.mark.unit)
|
||||
|
||||
if "auth" in item.name.lower():
|
||||
item.add_marker(pytest.mark.auth)
|
||||
@@ -1 +0,0 @@
|
||||
# End-to-end test package
|
||||
@@ -1,545 +0,0 @@
|
||||
"""
|
||||
Performance Tests for Download Operations
|
||||
|
||||
This module contains performance and load tests for the AniWorld application,
|
||||
focusing on download operations, concurrent access, and system limitations.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import time
|
||||
import threading
|
||||
import concurrent.futures
|
||||
import statistics
|
||||
from unittest.mock import Mock, patch
|
||||
import requests
|
||||
import psutil
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Import performance modules
|
||||
from performance_optimizer import (
|
||||
SpeedLimiter, ParallelDownloadManager, DownloadCache,
|
||||
MemoryMonitor, BandwidthMonitor
|
||||
)
|
||||
from database_manager import DatabaseManager
|
||||
from error_handler import RetryMechanism, NetworkHealthChecker
|
||||
from app import app
|
||||
|
||||
|
||||
class TestDownloadPerformance(unittest.TestCase):
|
||||
"""Performance tests for download operations."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up performance test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.speed_limiter = SpeedLimiter(max_speed_mbps=50) # 50 Mbps limit
|
||||
self.download_manager = ParallelDownloadManager(max_workers=4)
|
||||
self.cache = DownloadCache(max_size_mb=100)
|
||||
|
||||
# Performance tracking
|
||||
self.download_times = []
|
||||
self.memory_usage = []
|
||||
self.cpu_usage = []
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up performance test environment."""
|
||||
self.download_manager.shutdown()
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
def mock_download_operation(self, size_mb, delay_seconds=0):
|
||||
"""Mock download operation with specified size and delay."""
|
||||
start_time = time.time()
|
||||
|
||||
# Simulate download delay
|
||||
if delay_seconds > 0:
|
||||
time.sleep(delay_seconds)
|
||||
|
||||
# Simulate memory usage for large files
|
||||
if size_mb > 10:
|
||||
dummy_data = b'x' * (1024 * 1024) # 1MB of dummy data
|
||||
time.sleep(0.1) # Simulate processing time
|
||||
del dummy_data
|
||||
|
||||
end_time = time.time()
|
||||
download_time = end_time - start_time
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'size_mb': size_mb,
|
||||
'duration': download_time,
|
||||
'speed_mbps': (size_mb * 8) / download_time if download_time > 0 else 0
|
||||
}
|
||||
|
||||
def test_single_download_performance(self):
|
||||
"""Test performance of single download operation."""
|
||||
test_sizes = [1, 5, 10, 50, 100] # MB
|
||||
results = []
|
||||
|
||||
for size_mb in test_sizes:
|
||||
with self.subTest(size_mb=size_mb):
|
||||
# Measure memory before
|
||||
process = psutil.Process()
|
||||
memory_before = process.memory_info().rss / 1024 / 1024 # MB
|
||||
|
||||
# Perform mock download
|
||||
result = self.mock_download_operation(size_mb, delay_seconds=0.1)
|
||||
|
||||
# Measure memory after
|
||||
memory_after = process.memory_info().rss / 1024 / 1024 # MB
|
||||
memory_increase = memory_after - memory_before
|
||||
|
||||
results.append({
|
||||
'size_mb': size_mb,
|
||||
'duration': result['duration'],
|
||||
'speed_mbps': result['speed_mbps'],
|
||||
'memory_increase_mb': memory_increase
|
||||
})
|
||||
|
||||
# Verify reasonable performance
|
||||
self.assertLess(result['duration'], 5.0) # Should complete within 5 seconds
|
||||
self.assertLess(memory_increase, size_mb * 2) # Memory usage shouldn't exceed 2x file size
|
||||
|
||||
# Print performance summary
|
||||
print("\nSingle Download Performance Results:")
|
||||
print("Size(MB) | Duration(s) | Speed(Mbps) | Memory++(MB)")
|
||||
print("-" * 50)
|
||||
for result in results:
|
||||
print(f"{result['size_mb']:8} | {result['duration']:11.2f} | {result['speed_mbps']:11.2f} | {result['memory_increase_mb']:12.2f}")
|
||||
|
||||
def test_concurrent_download_performance(self):
|
||||
"""Test performance with multiple concurrent downloads."""
|
||||
concurrent_levels = [1, 2, 4, 8, 16]
|
||||
download_size = 10 # MB per download
|
||||
|
||||
results = []
|
||||
|
||||
for num_concurrent in concurrent_levels:
|
||||
with self.subTest(num_concurrent=num_concurrent):
|
||||
start_time = time.time()
|
||||
|
||||
# Track system resources
|
||||
process = psutil.Process()
|
||||
cpu_before = process.cpu_percent()
|
||||
memory_before = process.memory_info().rss / 1024 / 1024
|
||||
|
||||
# Perform concurrent downloads
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=num_concurrent) as executor:
|
||||
futures = []
|
||||
for i in range(num_concurrent):
|
||||
future = executor.submit(self.mock_download_operation, download_size, 0.2)
|
||||
futures.append(future)
|
||||
|
||||
# Wait for all downloads to complete
|
||||
download_results = [future.result() for future in futures]
|
||||
|
||||
end_time = time.time()
|
||||
total_duration = end_time - start_time
|
||||
|
||||
# Measure resource usage after
|
||||
time.sleep(0.1) # Allow CPU measurement to stabilize
|
||||
cpu_after = process.cpu_percent()
|
||||
memory_after = process.memory_info().rss / 1024 / 1024
|
||||
|
||||
# Calculate metrics
|
||||
total_data_mb = download_size * num_concurrent
|
||||
overall_throughput = total_data_mb / total_duration
|
||||
average_speed = statistics.mean([r['speed_mbps'] for r in download_results])
|
||||
|
||||
results.append({
|
||||
'concurrent': num_concurrent,
|
||||
'total_duration': total_duration,
|
||||
'throughput_mbps': overall_throughput * 8, # Convert to Mbps
|
||||
'average_speed_mbps': average_speed,
|
||||
'cpu_increase': cpu_after - cpu_before,
|
||||
'memory_increase_mb': memory_after - memory_before
|
||||
})
|
||||
|
||||
# Performance assertions
|
||||
self.assertLess(total_duration, 10.0) # Should complete within 10 seconds
|
||||
self.assertTrue(all(r['success'] for r in download_results))
|
||||
|
||||
# Print concurrent performance summary
|
||||
print("\nConcurrent Download Performance Results:")
|
||||
print("Concurrent | Duration(s) | Throughput(Mbps) | Avg Speed(Mbps) | CPU++(%) | Memory++(MB)")
|
||||
print("-" * 85)
|
||||
for result in results:
|
||||
print(f"{result['concurrent']:10} | {result['total_duration']:11.2f} | {result['throughput_mbps']:15.2f} | {result['average_speed_mbps']:15.2f} | {result['cpu_increase']:8.2f} | {result['memory_increase_mb']:12.2f}")
|
||||
|
||||
def test_speed_limiting_performance(self):
|
||||
"""Test download speed limiting effectiveness."""
|
||||
speed_limits = [1, 5, 10, 25, 50] # Mbps
|
||||
download_size = 20 # MB
|
||||
|
||||
results = []
|
||||
|
||||
for limit_mbps in speed_limits:
|
||||
with self.subTest(limit_mbps=limit_mbps):
|
||||
# Configure speed limiter
|
||||
limiter = SpeedLimiter(max_speed_mbps=limit_mbps)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Simulate download with speed limiting
|
||||
chunks_downloaded = 0
|
||||
total_chunks = download_size # 1MB chunks
|
||||
|
||||
for chunk in range(total_chunks):
|
||||
chunk_start = time.time()
|
||||
|
||||
# Simulate chunk download (1MB)
|
||||
time.sleep(0.05) # Base download time
|
||||
|
||||
chunk_end = time.time()
|
||||
chunk_time = chunk_end - chunk_start
|
||||
|
||||
# Calculate speed and apply limiting
|
||||
chunk_size_mb = 1
|
||||
current_speed_mbps = (chunk_size_mb * 8) / chunk_time
|
||||
|
||||
if limiter.should_limit_speed(current_speed_mbps):
|
||||
# Calculate delay needed to meet speed limit
|
||||
target_time = (chunk_size_mb * 8) / limit_mbps
|
||||
actual_delay = max(0, target_time - chunk_time)
|
||||
time.sleep(actual_delay)
|
||||
|
||||
chunks_downloaded += 1
|
||||
|
||||
end_time = time.time()
|
||||
total_duration = end_time - start_time
|
||||
actual_speed_mbps = (download_size * 8) / total_duration
|
||||
|
||||
results.append({
|
||||
'limit_mbps': limit_mbps,
|
||||
'actual_speed_mbps': actual_speed_mbps,
|
||||
'duration': total_duration,
|
||||
'speed_compliance': actual_speed_mbps <= (limit_mbps * 1.1) # Allow 10% tolerance
|
||||
})
|
||||
|
||||
# Verify speed limiting is working (within 10% tolerance)
|
||||
self.assertLessEqual(actual_speed_mbps, limit_mbps * 1.1)
|
||||
|
||||
# Print speed limiting results
|
||||
print("\nSpeed Limiting Performance Results:")
|
||||
print("Limit(Mbps) | Actual(Mbps) | Duration(s) | Compliant")
|
||||
print("-" * 50)
|
||||
for result in results:
|
||||
compliance = "✓" if result['speed_compliance'] else "✗"
|
||||
print(f"{result['limit_mbps']:11} | {result['actual_speed_mbps']:12.2f} | {result['duration']:11.2f} | {compliance:9}")
|
||||
|
||||
def test_cache_performance(self):
|
||||
"""Test download cache performance impact."""
|
||||
cache_sizes = [0, 10, 50, 100, 200] # MB
|
||||
test_urls = [f"http://example.com/video_{i}.mp4" for i in range(20)]
|
||||
|
||||
results = []
|
||||
|
||||
for cache_size_mb in cache_sizes:
|
||||
with self.subTest(cache_size_mb=cache_size_mb):
|
||||
# Create cache with specific size
|
||||
cache = DownloadCache(max_size_mb=cache_size_mb)
|
||||
|
||||
# First pass: populate cache
|
||||
start_time = time.time()
|
||||
for url in test_urls[:10]: # Cache first 10 items
|
||||
dummy_data = b'x' * (1024 * 1024) # 1MB dummy data
|
||||
cache.set(url, dummy_data)
|
||||
populate_time = time.time() - start_time
|
||||
|
||||
# Second pass: test cache hits
|
||||
start_time = time.time()
|
||||
cache_hits = 0
|
||||
for url in test_urls[:10]:
|
||||
cached_data = cache.get(url)
|
||||
if cached_data is not None:
|
||||
cache_hits += 1
|
||||
lookup_time = time.time() - start_time
|
||||
|
||||
# Third pass: test cache misses
|
||||
start_time = time.time()
|
||||
cache_misses = 0
|
||||
for url in test_urls[10:15]: # URLs not in cache
|
||||
cached_data = cache.get(url)
|
||||
if cached_data is None:
|
||||
cache_misses += 1
|
||||
miss_time = time.time() - start_time
|
||||
|
||||
cache_hit_rate = cache_hits / 10.0 if cache_size_mb > 0 else 0
|
||||
|
||||
results.append({
|
||||
'cache_size_mb': cache_size_mb,
|
||||
'populate_time': populate_time,
|
||||
'lookup_time': lookup_time,
|
||||
'miss_time': miss_time,
|
||||
'hit_rate': cache_hit_rate,
|
||||
'cache_hits': cache_hits,
|
||||
'cache_misses': cache_misses
|
||||
})
|
||||
|
||||
# Print cache performance results
|
||||
print("\nCache Performance Results:")
|
||||
print("Cache(MB) | Populate(s) | Lookup(s) | Miss(s) | Hit Rate | Hits | Misses")
|
||||
print("-" * 75)
|
||||
for result in results:
|
||||
print(f"{result['cache_size_mb']:9} | {result['populate_time']:11.3f} | {result['lookup_time']:9.3f} | {result['miss_time']:7.3f} | {result['hit_rate']:8.2%} | {result['cache_hits']:4} | {result['cache_misses']:6}")
|
||||
|
||||
def test_memory_usage_under_load(self):
|
||||
"""Test memory usage under heavy load conditions."""
|
||||
load_scenarios = [
|
||||
{'downloads': 5, 'size_mb': 10, 'name': 'Light Load'},
|
||||
{'downloads': 10, 'size_mb': 20, 'name': 'Medium Load'},
|
||||
{'downloads': 20, 'size_mb': 30, 'name': 'Heavy Load'},
|
||||
{'downloads': 50, 'size_mb': 50, 'name': 'Extreme Load'}
|
||||
]
|
||||
|
||||
results = []
|
||||
|
||||
for scenario in load_scenarios:
|
||||
with self.subTest(scenario=scenario['name']):
|
||||
memory_monitor = MemoryMonitor(threshold_mb=1000) # 1GB threshold
|
||||
|
||||
# Measure baseline memory
|
||||
process = psutil.Process()
|
||||
baseline_memory_mb = process.memory_info().rss / 1024 / 1024
|
||||
|
||||
memory_samples = []
|
||||
|
||||
def memory_sampler():
|
||||
"""Sample memory usage during test."""
|
||||
for _ in range(30): # Sample for 30 seconds max
|
||||
current_memory = process.memory_info().rss / 1024 / 1024
|
||||
memory_samples.append(current_memory)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Start memory monitoring
|
||||
monitor_thread = threading.Thread(target=memory_sampler)
|
||||
monitor_thread.start()
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
# Execute load scenario
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=scenario['downloads']) as executor:
|
||||
futures = []
|
||||
for i in range(scenario['downloads']):
|
||||
future = executor.submit(
|
||||
self.mock_download_operation,
|
||||
scenario['size_mb'],
|
||||
0.1
|
||||
)
|
||||
futures.append(future)
|
||||
|
||||
# Wait for completion
|
||||
download_results = [future.result() for future in futures]
|
||||
|
||||
end_time = time.time()
|
||||
|
||||
# Stop memory monitoring
|
||||
monitor_thread.join(timeout=1)
|
||||
|
||||
# Calculate memory statistics
|
||||
if memory_samples:
|
||||
peak_memory_mb = max(memory_samples)
|
||||
avg_memory_mb = statistics.mean(memory_samples)
|
||||
memory_increase_mb = peak_memory_mb - baseline_memory_mb
|
||||
else:
|
||||
peak_memory_mb = avg_memory_mb = memory_increase_mb = 0
|
||||
|
||||
# Check if memory usage is reasonable
|
||||
expected_memory_mb = scenario['downloads'] * scenario['size_mb'] * 0.1 # 10% of total data
|
||||
memory_efficiency = memory_increase_mb <= expected_memory_mb * 2 # Allow 2x overhead
|
||||
|
||||
results.append({
|
||||
'scenario': scenario['name'],
|
||||
'downloads': scenario['downloads'],
|
||||
'size_mb': scenario['size_mb'],
|
||||
'duration': end_time - start_time,
|
||||
'baseline_memory_mb': baseline_memory_mb,
|
||||
'peak_memory_mb': peak_memory_mb,
|
||||
'avg_memory_mb': avg_memory_mb,
|
||||
'memory_increase_mb': memory_increase_mb,
|
||||
'memory_efficient': memory_efficiency,
|
||||
'all_success': all(r['success'] for r in download_results)
|
||||
})
|
||||
|
||||
# Performance assertions
|
||||
self.assertTrue(all(r['success'] for r in download_results))
|
||||
# Memory increase should be reasonable (not more than 5x the data size)
|
||||
max_acceptable_memory = scenario['downloads'] * scenario['size_mb'] * 5
|
||||
self.assertLess(memory_increase_mb, max_acceptable_memory)
|
||||
|
||||
# Print memory usage results
|
||||
print("\nMemory Usage Under Load Results:")
|
||||
print("Scenario | Downloads | Size(MB) | Duration(s) | Peak(MB) | Avg(MB) | Increase(MB) | Efficient | Success")
|
||||
print("-" * 110)
|
||||
for result in results:
|
||||
efficient = "✓" if result['memory_efficient'] else "✗"
|
||||
success = "✓" if result['all_success'] else "✗"
|
||||
print(f"{result['scenario']:13} | {result['downloads']:9} | {result['size_mb']:8} | {result['duration']:11.2f} | {result['peak_memory_mb']:8.1f} | {result['avg_memory_mb']:7.1f} | {result['memory_increase_mb']:12.1f} | {efficient:9} | {success:7}")
|
||||
|
||||
def test_database_performance_under_load(self):
|
||||
"""Test database performance under concurrent access load."""
|
||||
# Create temporary database
|
||||
test_db = os.path.join(self.test_dir, 'performance_test.db')
|
||||
db_manager = DatabaseManager(test_db)
|
||||
|
||||
concurrent_operations = [1, 5, 10, 20, 50]
|
||||
operations_per_thread = 100
|
||||
|
||||
results = []
|
||||
|
||||
try:
|
||||
for num_threads in concurrent_operations:
|
||||
with self.subTest(num_threads=num_threads):
|
||||
|
||||
def database_worker(worker_id):
|
||||
"""Worker function for database operations."""
|
||||
worker_results = {
|
||||
'inserts': 0,
|
||||
'selects': 0,
|
||||
'updates': 0,
|
||||
'errors': 0,
|
||||
'total_time': 0
|
||||
}
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
for op in range(operations_per_thread):
|
||||
try:
|
||||
anime_id = f"perf-{worker_id}-{op}"
|
||||
|
||||
# Insert operation
|
||||
insert_query = """
|
||||
INSERT INTO anime_metadata
|
||||
(anime_id, name, folder, created_at, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
success = db_manager.execute_update(
|
||||
insert_query,
|
||||
(anime_id, f"Anime {worker_id}-{op}",
|
||||
f"folder_{worker_id}_{op}",
|
||||
time.time(), time.time())
|
||||
)
|
||||
if success:
|
||||
worker_results['inserts'] += 1
|
||||
|
||||
# Select operation
|
||||
select_query = "SELECT * FROM anime_metadata WHERE anime_id = ?"
|
||||
select_results = db_manager.execute_query(select_query, (anime_id,))
|
||||
if select_results:
|
||||
worker_results['selects'] += 1
|
||||
|
||||
# Update operation (every 10th operation)
|
||||
if op % 10 == 0:
|
||||
update_query = "UPDATE anime_metadata SET name = ? WHERE anime_id = ?"
|
||||
success = db_manager.execute_update(
|
||||
update_query,
|
||||
(f"Updated {worker_id}-{op}", anime_id)
|
||||
)
|
||||
if success:
|
||||
worker_results['updates'] += 1
|
||||
|
||||
except Exception as e:
|
||||
worker_results['errors'] += 1
|
||||
|
||||
worker_results['total_time'] = time.time() - start_time
|
||||
return worker_results
|
||||
|
||||
# Execute concurrent database operations
|
||||
start_time = time.time()
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=num_threads) as executor:
|
||||
futures = []
|
||||
for worker_id in range(num_threads):
|
||||
future = executor.submit(database_worker, worker_id)
|
||||
futures.append(future)
|
||||
|
||||
worker_results = [future.result() for future in futures]
|
||||
|
||||
total_time = time.time() - start_time
|
||||
|
||||
# Aggregate results
|
||||
total_inserts = sum(r['inserts'] for r in worker_results)
|
||||
total_selects = sum(r['selects'] for r in worker_results)
|
||||
total_updates = sum(r['updates'] for r in worker_results)
|
||||
total_errors = sum(r['errors'] for r in worker_results)
|
||||
total_operations = total_inserts + total_selects + total_updates
|
||||
|
||||
avg_ops_per_second = total_operations / total_time if total_time > 0 else 0
|
||||
error_rate = total_errors / (total_operations + total_errors) if (total_operations + total_errors) > 0 else 0
|
||||
|
||||
results.append({
|
||||
'threads': num_threads,
|
||||
'total_time': total_time,
|
||||
'total_operations': total_operations,
|
||||
'ops_per_second': avg_ops_per_second,
|
||||
'inserts': total_inserts,
|
||||
'selects': total_selects,
|
||||
'updates': total_updates,
|
||||
'errors': total_errors,
|
||||
'error_rate': error_rate
|
||||
})
|
||||
|
||||
# Performance assertions
|
||||
self.assertLess(error_rate, 0.05) # Less than 5% error rate
|
||||
self.assertGreater(avg_ops_per_second, 10) # At least 10 ops/second
|
||||
|
||||
finally:
|
||||
db_manager.close()
|
||||
|
||||
# Print database performance results
|
||||
print("\nDatabase Performance Under Load Results:")
|
||||
print("Threads | Duration(s) | Total Ops | Ops/Sec | Inserts | Selects | Updates | Errors | Error Rate")
|
||||
print("-" * 95)
|
||||
for result in results:
|
||||
print(f"{result['threads']:7} | {result['total_time']:11.2f} | {result['total_operations']:9} | {result['ops_per_second']:7.1f} | {result['inserts']:7} | {result['selects']:7} | {result['updates']:7} | {result['errors']:6} | {result['error_rate']:9.2%}")
|
||||
|
||||
|
||||
def run_performance_tests():
|
||||
"""Run the complete performance test suite."""
|
||||
print("Running AniWorld Performance Tests...")
|
||||
print("This may take several minutes to complete.")
|
||||
print("=" * 60)
|
||||
|
||||
# Create test suite
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add performance test cases
|
||||
performance_test_classes = [
|
||||
TestDownloadPerformance
|
||||
]
|
||||
|
||||
for test_class in performance_test_classes:
|
||||
tests = unittest.TestLoader().loadTestsFromTestCase(test_class)
|
||||
suite.addTests(tests)
|
||||
|
||||
# Run tests with minimal verbosity for performance focus
|
||||
runner = unittest.TextTestRunner(verbosity=1)
|
||||
start_time = time.time()
|
||||
result = runner.run(suite)
|
||||
total_time = time.time() - start_time
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print(f"Performance Tests Summary:")
|
||||
print(f"Total execution time: {total_time:.2f} seconds")
|
||||
print(f"Tests run: {result.testsRun}")
|
||||
print(f"Failures: {len(result.failures)}")
|
||||
print(f"Errors: {len(result.errors)}")
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
result = run_performance_tests()
|
||||
|
||||
if result.wasSuccessful():
|
||||
print("\nAll performance tests passed! ✅")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nSome performance tests failed! ❌")
|
||||
print("\nCheck the output above for detailed performance metrics.")
|
||||
sys.exit(1)
|
||||
@@ -1 +0,0 @@
|
||||
# Integration test package
|
||||
@@ -1,640 +0,0 @@
|
||||
"""
|
||||
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)
|
||||
@@ -1,619 +0,0 @@
|
||||
"""
|
||||
Integration Tests for Web Interface
|
||||
|
||||
This module contains integration tests for the Flask web application,
|
||||
testing the complete workflow from HTTP requests to database operations.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
import sqlite3
|
||||
from unittest.mock import Mock, MagicMock, patch
|
||||
import threading
|
||||
import time
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Import Flask app and components
|
||||
from app import app, socketio, init_series_app
|
||||
from database_manager import DatabaseManager, AnimeMetadata
|
||||
from auth import session_manager
|
||||
from config import config
|
||||
|
||||
|
||||
class TestWebInterface(unittest.TestCase):
|
||||
"""Integration tests for the web interface."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
# Create temporary directory for test files
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
|
||||
# Configure Flask app for testing
|
||||
app.config['TESTING'] = True
|
||||
app.config['WTF_CSRF_ENABLED'] = False
|
||||
app.config['SECRET_KEY'] = 'test-secret-key'
|
||||
|
||||
self.app = app
|
||||
self.client = app.test_client()
|
||||
|
||||
# Create test database
|
||||
self.test_db_path = os.path.join(self.test_dir, 'test.db')
|
||||
|
||||
# Mock configuration
|
||||
self.original_config = {}
|
||||
for attr in ['anime_directory', 'master_password', 'database_path']:
|
||||
if hasattr(config, attr):
|
||||
self.original_config[attr] = getattr(config, attr)
|
||||
|
||||
config.anime_directory = self.test_dir
|
||||
config.master_password = 'test123'
|
||||
config.database_path = self.test_db_path
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
# Restore original configuration
|
||||
for attr, value in self.original_config.items():
|
||||
setattr(config, attr, value)
|
||||
|
||||
# Clean up temporary files
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
|
||||
# Clear sessions
|
||||
session_manager.clear_all_sessions()
|
||||
|
||||
def test_index_page_unauthenticated(self):
|
||||
"""Test index page redirects to login when unauthenticated."""
|
||||
response = self.client.get('/')
|
||||
|
||||
# Should redirect to login
|
||||
self.assertEqual(response.status_code, 302)
|
||||
self.assertIn('/login', response.location)
|
||||
|
||||
def test_login_page_loads(self):
|
||||
"""Test login page loads correctly."""
|
||||
response = self.client.get('/login')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertIn(b'login', response.data.lower())
|
||||
|
||||
def test_successful_login(self):
|
||||
"""Test successful login flow."""
|
||||
# Attempt login with correct password
|
||||
response = self.client.post('/login', data={
|
||||
'password': 'test123'
|
||||
}, follow_redirects=True)
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should be redirected to main page after successful login
|
||||
|
||||
def test_failed_login(self):
|
||||
"""Test failed login with wrong password."""
|
||||
response = self.client.post('/login', data={
|
||||
'password': 'wrong_password'
|
||||
})
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
# Should return to login page with error
|
||||
|
||||
def test_authenticated_index_page(self):
|
||||
"""Test index page loads when authenticated."""
|
||||
# Login first
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
response = self.client.get('/')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
def test_api_authentication_required(self):
|
||||
"""Test API endpoints require authentication."""
|
||||
# Test unauthenticated API call
|
||||
response = self.client.get('/api/series/list')
|
||||
self.assertEqual(response.status_code, 401)
|
||||
|
||||
# Test authenticated API call
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
response = self.client.get('/api/series/list')
|
||||
# Should not return 401 (might return other codes based on implementation)
|
||||
self.assertNotEqual(response.status_code, 401)
|
||||
|
||||
def test_config_api_endpoints(self):
|
||||
"""Test configuration API endpoints."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Get current config
|
||||
response = self.client.get('/api/config')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
config_data = json.loads(response.data)
|
||||
self.assertIn('anime_directory', config_data)
|
||||
|
||||
def test_download_queue_operations(self):
|
||||
"""Test download queue management."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Get queue status
|
||||
response = self.client.get('/api/queue/status')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
queue_data = json.loads(response.data)
|
||||
self.assertIn('status', queue_data)
|
||||
|
||||
def test_process_locking_endpoints(self):
|
||||
"""Test process locking API endpoints."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Check process locks
|
||||
response = self.client.get('/api/process/locks')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
locks_data = json.loads(response.data)
|
||||
self.assertIn('locks', locks_data)
|
||||
|
||||
def test_database_api_endpoints(self):
|
||||
"""Test database management API endpoints."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Get database info
|
||||
response = self.client.get('/api/database/info')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
db_data = json.loads(response.data)
|
||||
self.assertIn('status', db_data)
|
||||
|
||||
def test_health_monitoring_endpoints(self):
|
||||
"""Test health monitoring API endpoints."""
|
||||
# Authenticate (health endpoints might be public)
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Get system health
|
||||
response = self.client.get('/api/health/system')
|
||||
# Health endpoints might be accessible without auth
|
||||
self.assertIn(response.status_code, [200, 401])
|
||||
|
||||
def test_error_handling(self):
|
||||
"""Test error handling for invalid requests."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Test invalid endpoint
|
||||
response = self.client.get('/api/nonexistent/endpoint')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
# Test invalid method
|
||||
response = self.client.post('/api/series/list')
|
||||
# Should return method not allowed or other appropriate error
|
||||
self.assertIn(response.status_code, [405, 400, 404])
|
||||
|
||||
def test_json_response_format(self):
|
||||
"""Test API responses return valid JSON."""
|
||||
# Authenticate
|
||||
with self.client.session_transaction() as sess:
|
||||
sess['authenticated'] = True
|
||||
sess['session_id'] = 'test-session'
|
||||
session_manager.sessions['test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
# Test various API endpoints for valid JSON
|
||||
endpoints = [
|
||||
'/api/config',
|
||||
'/api/queue/status',
|
||||
'/api/process/locks',
|
||||
'/api/database/info'
|
||||
]
|
||||
|
||||
for endpoint in endpoints:
|
||||
with self.subTest(endpoint=endpoint):
|
||||
response = self.client.get(endpoint)
|
||||
if response.status_code == 200:
|
||||
# Should be valid JSON
|
||||
try:
|
||||
json.loads(response.data)
|
||||
except json.JSONDecodeError:
|
||||
self.fail(f"Invalid JSON response from {endpoint}")
|
||||
|
||||
|
||||
class TestSocketIOEvents(unittest.TestCase):
|
||||
"""Integration tests for SocketIO events."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment for SocketIO."""
|
||||
app.config['TESTING'] = True
|
||||
self.socketio_client = socketio.test_client(app)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up SocketIO test environment."""
|
||||
if self.socketio_client:
|
||||
self.socketio_client.disconnect()
|
||||
|
||||
def test_socketio_connection(self):
|
||||
"""Test SocketIO connection establishment."""
|
||||
self.assertTrue(self.socketio_client.is_connected())
|
||||
|
||||
def test_download_progress_events(self):
|
||||
"""Test download progress event handling."""
|
||||
# Mock download progress update
|
||||
test_progress = {
|
||||
'episode': 'Test Episode 1',
|
||||
'progress': 50,
|
||||
'speed': '1.5 MB/s',
|
||||
'eta': '2 minutes'
|
||||
}
|
||||
|
||||
# Emit progress update
|
||||
socketio.emit('download_progress', test_progress)
|
||||
|
||||
# Check if client receives the event
|
||||
received = self.socketio_client.get_received()
|
||||
# Note: In real tests, you'd check if the client received the event
|
||||
|
||||
def test_scan_progress_events(self):
|
||||
"""Test scan progress event handling."""
|
||||
test_scan_data = {
|
||||
'status': 'scanning',
|
||||
'current_folder': 'Test Anime',
|
||||
'progress': 25,
|
||||
'total_series': 100,
|
||||
'scanned_series': 25
|
||||
}
|
||||
|
||||
# Emit scan progress
|
||||
socketio.emit('scan_progress', test_scan_data)
|
||||
|
||||
# Verify event handling
|
||||
received = self.socketio_client.get_received()
|
||||
# In real implementation, verify the event was received and processed
|
||||
|
||||
|
||||
class TestDatabaseIntegration(unittest.TestCase):
|
||||
"""Integration tests for database operations."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up database integration test environment."""
|
||||
self.test_dir = tempfile.mkdtemp()
|
||||
self.test_db = os.path.join(self.test_dir, 'integration_test.db')
|
||||
self.db_manager = DatabaseManager(self.test_db)
|
||||
|
||||
# Configure Flask app for testing
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
# Authenticate for API calls
|
||||
self.auth_session = {
|
||||
'authenticated': True,
|
||||
'session_id': 'integration-test-session'
|
||||
}
|
||||
session_manager.sessions['integration-test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up database integration test environment."""
|
||||
self.db_manager.close()
|
||||
shutil.rmtree(self.test_dir, ignore_errors=True)
|
||||
session_manager.clear_all_sessions()
|
||||
|
||||
def test_anime_crud_via_api(self):
|
||||
"""Test anime CRUD operations via API endpoints."""
|
||||
# Authenticate session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess.update(self.auth_session)
|
||||
|
||||
# Create anime via API
|
||||
anime_data = {
|
||||
'name': 'Integration Test Anime',
|
||||
'folder': 'integration_test_folder',
|
||||
'key': 'integration-test-key',
|
||||
'description': 'Test anime for integration testing',
|
||||
'genres': ['Action', 'Adventure'],
|
||||
'release_year': 2023,
|
||||
'status': 'ongoing'
|
||||
}
|
||||
|
||||
response = self.client.post('/api/database/anime',
|
||||
data=json.dumps(anime_data),
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
response_data = json.loads(response.data)
|
||||
self.assertEqual(response_data['status'], 'success')
|
||||
|
||||
anime_id = response_data['data']['anime_id']
|
||||
|
||||
# Read anime via API
|
||||
response = self.client.get(f'/api/database/anime/{anime_id}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
self.assertEqual(response_data['status'], 'success')
|
||||
self.assertEqual(response_data['data']['name'], anime_data['name'])
|
||||
|
||||
# Update anime via API
|
||||
update_data = {
|
||||
'description': 'Updated description for integration testing'
|
||||
}
|
||||
|
||||
response = self.client.put(f'/api/database/anime/{anime_id}',
|
||||
data=json.dumps(update_data),
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify update
|
||||
response = self.client.get(f'/api/database/anime/{anime_id}')
|
||||
response_data = json.loads(response.data)
|
||||
self.assertEqual(
|
||||
response_data['data']['description'],
|
||||
update_data['description']
|
||||
)
|
||||
|
||||
# Delete anime via API
|
||||
response = self.client.delete(f'/api/database/anime/{anime_id}')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Verify deletion
|
||||
response = self.client.get(f'/api/database/anime/{anime_id}')
|
||||
self.assertEqual(response.status_code, 404)
|
||||
|
||||
def test_backup_operations_via_api(self):
|
||||
"""Test backup operations via API."""
|
||||
# Authenticate session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess.update(self.auth_session)
|
||||
|
||||
# Create test data
|
||||
anime_data = {
|
||||
'name': 'Backup Test Anime',
|
||||
'folder': 'backup_test_folder',
|
||||
'key': 'backup-test-key'
|
||||
}
|
||||
|
||||
response = self.client.post('/api/database/anime',
|
||||
data=json.dumps(anime_data),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# Create backup via API
|
||||
backup_data = {
|
||||
'backup_type': 'full',
|
||||
'description': 'Integration test backup'
|
||||
}
|
||||
|
||||
response = self.client.post('/api/database/backups/create',
|
||||
data=json.dumps(backup_data),
|
||||
content_type='application/json')
|
||||
|
||||
self.assertEqual(response.status_code, 201)
|
||||
response_data = json.loads(response.data)
|
||||
self.assertEqual(response_data['status'], 'success')
|
||||
|
||||
backup_id = response_data['data']['backup_id']
|
||||
|
||||
# List backups
|
||||
response = self.client.get('/api/database/backups')
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
self.assertGreater(response_data['data']['count'], 0)
|
||||
|
||||
# Verify backup exists in list
|
||||
backup_found = False
|
||||
for backup in response_data['data']['backups']:
|
||||
if backup['backup_id'] == backup_id:
|
||||
backup_found = True
|
||||
break
|
||||
self.assertTrue(backup_found)
|
||||
|
||||
def test_search_functionality(self):
|
||||
"""Test search functionality via API."""
|
||||
# Authenticate session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess.update(self.auth_session)
|
||||
|
||||
# Create test anime for searching
|
||||
test_anime = [
|
||||
{'name': 'Attack on Titan', 'folder': 'attack_titan', 'key': 'attack-titan'},
|
||||
{'name': 'Death Note', 'folder': 'death_note', 'key': 'death-note'},
|
||||
{'name': 'Naruto', 'folder': 'naruto', 'key': 'naruto'}
|
||||
]
|
||||
|
||||
for anime_data in test_anime:
|
||||
response = self.client.post('/api/database/anime',
|
||||
data=json.dumps(anime_data),
|
||||
content_type='application/json')
|
||||
self.assertEqual(response.status_code, 201)
|
||||
|
||||
# Test search
|
||||
search_queries = [
|
||||
('Attack', 1), # Should find "Attack on Titan"
|
||||
('Note', 1), # Should find "Death Note"
|
||||
('Naruto', 1), # Should find "Naruto"
|
||||
('Anime', 0), # Should find nothing
|
||||
('', 0) # Empty search should return error
|
||||
]
|
||||
|
||||
for search_term, expected_count in search_queries:
|
||||
with self.subTest(search_term=search_term):
|
||||
response = self.client.get(f'/api/database/anime/search?q={search_term}')
|
||||
|
||||
if search_term == '':
|
||||
self.assertEqual(response.status_code, 400)
|
||||
else:
|
||||
self.assertEqual(response.status_code, 200)
|
||||
response_data = json.loads(response.data)
|
||||
self.assertEqual(response_data['data']['count'], expected_count)
|
||||
|
||||
|
||||
class TestPerformanceIntegration(unittest.TestCase):
|
||||
"""Integration tests for performance features."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up performance integration test environment."""
|
||||
app.config['TESTING'] = True
|
||||
self.client = app.test_client()
|
||||
|
||||
# Authenticate
|
||||
self.auth_session = {
|
||||
'authenticated': True,
|
||||
'session_id': 'performance-test-session'
|
||||
}
|
||||
session_manager.sessions['performance-test-session'] = {
|
||||
'authenticated': True,
|
||||
'created_at': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up performance test environment."""
|
||||
session_manager.clear_all_sessions()
|
||||
|
||||
def test_performance_monitoring_api(self):
|
||||
"""Test performance monitoring API endpoints."""
|
||||
# Authenticate session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess.update(self.auth_session)
|
||||
|
||||
# Test system metrics
|
||||
response = self.client.get('/api/performance/system-metrics')
|
||||
if response.status_code == 200: # Endpoint might not exist yet
|
||||
metrics_data = json.loads(response.data)
|
||||
self.assertIn('status', metrics_data)
|
||||
|
||||
def test_download_speed_limiting(self):
|
||||
"""Test download speed limiting configuration."""
|
||||
# Authenticate session
|
||||
with self.client.session_transaction() as sess:
|
||||
sess.update(self.auth_session)
|
||||
|
||||
# Test speed limit configuration
|
||||
speed_config = {'max_speed_mbps': 10}
|
||||
|
||||
response = self.client.post('/api/performance/speed-limit',
|
||||
data=json.dumps(speed_config),
|
||||
content_type='application/json')
|
||||
|
||||
# Endpoint might not exist yet, so check for appropriate response
|
||||
self.assertIn(response.status_code, [200, 404, 405])
|
||||
|
||||
|
||||
def run_integration_tests():
|
||||
"""Run the integration test suite."""
|
||||
# Create test suite
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add integration test cases
|
||||
integration_test_classes = [
|
||||
TestWebInterface,
|
||||
TestSocketIOEvents,
|
||||
TestDatabaseIntegration,
|
||||
TestPerformanceIntegration
|
||||
]
|
||||
|
||||
for test_class in integration_test_classes:
|
||||
tests = unittest.TestLoader().loadTestsFromTestCase(test_class)
|
||||
suite.addTests(tests)
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running AniWorld Integration Tests...")
|
||||
print("=" * 50)
|
||||
|
||||
result = run_integration_tests()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"Tests run: {result.testsRun}")
|
||||
print(f"Failures: {len(result.failures)}")
|
||||
print(f"Errors: {len(result.errors)}")
|
||||
|
||||
if result.failures:
|
||||
print("\nFailures:")
|
||||
for test, traceback in result.failures:
|
||||
print(f"- {test}")
|
||||
|
||||
if result.errors:
|
||||
print("\nErrors:")
|
||||
for test, traceback in result.errors:
|
||||
print(f"- {test}")
|
||||
|
||||
if result.wasSuccessful():
|
||||
print("\nAll integration tests passed! ✅")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nSome integration tests failed! ❌")
|
||||
sys.exit(1)
|
||||
@@ -1,498 +0,0 @@
|
||||
"""
|
||||
Automated Testing Pipeline
|
||||
|
||||
This module provides a comprehensive test runner and pipeline for the AniWorld application,
|
||||
including unit tests, integration tests, performance tests, and code coverage reporting.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Import test modules
|
||||
import test_core
|
||||
import test_integration
|
||||
import test_performance
|
||||
|
||||
|
||||
class TestResult:
|
||||
"""Container for test execution results."""
|
||||
|
||||
def __init__(self, test_type, result, execution_time, details=None):
|
||||
self.test_type = test_type
|
||||
self.result = result
|
||||
self.execution_time = execution_time
|
||||
self.details = details or {}
|
||||
self.timestamp = datetime.utcnow()
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert result to dictionary format."""
|
||||
return {
|
||||
'test_type': self.test_type,
|
||||
'success': self.result.wasSuccessful() if hasattr(self.result, 'wasSuccessful') else self.result,
|
||||
'tests_run': self.result.testsRun if hasattr(self.result, 'testsRun') else 0,
|
||||
'failures': len(self.result.failures) if hasattr(self.result, 'failures') else 0,
|
||||
'errors': len(self.result.errors) if hasattr(self.result, 'errors') else 0,
|
||||
'execution_time': self.execution_time,
|
||||
'timestamp': self.timestamp.isoformat(),
|
||||
'details': self.details
|
||||
}
|
||||
|
||||
|
||||
class TestPipeline:
|
||||
"""Automated testing pipeline for AniWorld application."""
|
||||
|
||||
def __init__(self, output_dir=None):
|
||||
self.output_dir = output_dir or os.path.join(os.path.dirname(__file__), 'test_results')
|
||||
self.results = []
|
||||
|
||||
# Create output directory
|
||||
Path(self.output_dir).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def run_unit_tests(self, verbose=True):
|
||||
"""Run unit tests and return results."""
|
||||
print("=" * 60)
|
||||
print("RUNNING UNIT TESTS")
|
||||
print("=" * 60)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run unit tests
|
||||
result = test_core.run_test_suite()
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
test_result = TestResult('unit', result, execution_time)
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
self._print_test_summary('Unit Tests', result, execution_time)
|
||||
|
||||
return test_result
|
||||
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
test_result = TestResult('unit', False, execution_time, {'error': str(e)})
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
print(f"Unit tests failed with error: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def run_integration_tests(self, verbose=True):
|
||||
"""Run integration tests and return results."""
|
||||
print("\n" + "=" * 60)
|
||||
print("RUNNING INTEGRATION TESTS")
|
||||
print("=" * 60)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run integration tests
|
||||
result = test_integration.run_integration_tests()
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
test_result = TestResult('integration', result, execution_time)
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
self._print_test_summary('Integration Tests', result, execution_time)
|
||||
|
||||
return test_result
|
||||
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
test_result = TestResult('integration', False, execution_time, {'error': str(e)})
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
print(f"Integration tests failed with error: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def run_performance_tests(self, verbose=True):
|
||||
"""Run performance tests and return results."""
|
||||
print("\n" + "=" * 60)
|
||||
print("RUNNING PERFORMANCE TESTS")
|
||||
print("=" * 60)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Run performance tests
|
||||
result = test_performance.run_performance_tests()
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
test_result = TestResult('performance', result, execution_time)
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
self._print_test_summary('Performance Tests', result, execution_time)
|
||||
|
||||
return test_result
|
||||
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
test_result = TestResult('performance', False, execution_time, {'error': str(e)})
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
print(f"Performance tests failed with error: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def run_code_coverage(self, test_modules=None, verbose=True):
|
||||
"""Run code coverage analysis."""
|
||||
if verbose:
|
||||
print("\n" + "=" * 60)
|
||||
print("RUNNING CODE COVERAGE ANALYSIS")
|
||||
print("=" * 60)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Check if coverage is available
|
||||
coverage_available = self._check_coverage_available()
|
||||
|
||||
if not coverage_available:
|
||||
if verbose:
|
||||
print("Coverage package not available. Install with: pip install coverage")
|
||||
return TestResult('coverage', False, 0, {'error': 'Coverage package not available'})
|
||||
|
||||
# Determine test modules to include
|
||||
if test_modules is None:
|
||||
test_modules = ['test_core', 'test_integration']
|
||||
|
||||
# Run coverage
|
||||
coverage_data = self._run_coverage_analysis(test_modules)
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
test_result = TestResult('coverage', True, execution_time, coverage_data)
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
self._print_coverage_summary(coverage_data)
|
||||
|
||||
return test_result
|
||||
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
test_result = TestResult('coverage', False, execution_time, {'error': str(e)})
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
print(f"Coverage analysis failed: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def run_load_tests(self, concurrent_users=10, duration_seconds=60, verbose=True):
|
||||
"""Run load tests against the web application."""
|
||||
if verbose:
|
||||
print("\n" + "=" * 60)
|
||||
print(f"RUNNING LOAD TESTS ({concurrent_users} users, {duration_seconds}s)")
|
||||
print("=" * 60)
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
try:
|
||||
# Mock load test implementation
|
||||
load_result = self._run_mock_load_test(concurrent_users, duration_seconds)
|
||||
execution_time = time.time() - start_time
|
||||
|
||||
test_result = TestResult('load', True, execution_time, load_result)
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
self._print_load_test_summary(load_result)
|
||||
|
||||
return test_result
|
||||
|
||||
except Exception as e:
|
||||
execution_time = time.time() - start_time
|
||||
test_result = TestResult('load', False, execution_time, {'error': str(e)})
|
||||
self.results.append(test_result)
|
||||
|
||||
if verbose:
|
||||
print(f"Load tests failed: {e}")
|
||||
|
||||
return test_result
|
||||
|
||||
def run_full_pipeline(self, include_performance=True, include_coverage=True, include_load=False):
|
||||
"""Run the complete testing pipeline."""
|
||||
print("ANIWORLD AUTOMATED TESTING PIPELINE")
|
||||
print("=" * 80)
|
||||
print(f"Started at: {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} UTC")
|
||||
print("=" * 80)
|
||||
|
||||
pipeline_start = time.time()
|
||||
|
||||
# Run unit tests
|
||||
unit_result = self.run_unit_tests()
|
||||
|
||||
# Run integration tests
|
||||
integration_result = self.run_integration_tests()
|
||||
|
||||
# Run performance tests if requested
|
||||
performance_result = None
|
||||
if include_performance:
|
||||
performance_result = self.run_performance_tests()
|
||||
|
||||
# Run code coverage if requested
|
||||
coverage_result = None
|
||||
if include_coverage:
|
||||
coverage_result = self.run_code_coverage()
|
||||
|
||||
# Run load tests if requested
|
||||
load_result = None
|
||||
if include_load:
|
||||
load_result = self.run_load_tests()
|
||||
|
||||
pipeline_time = time.time() - pipeline_start
|
||||
|
||||
# Generate summary report
|
||||
self._generate_pipeline_report(pipeline_time)
|
||||
|
||||
# Return overall success
|
||||
all_successful = all(
|
||||
result.result.wasSuccessful() if hasattr(result.result, 'wasSuccessful') else result.result
|
||||
for result in self.results
|
||||
)
|
||||
|
||||
return all_successful
|
||||
|
||||
def _print_test_summary(self, test_name, result, execution_time):
|
||||
"""Print summary of test execution."""
|
||||
print(f"\n{test_name} Summary:")
|
||||
print(f"Tests run: {result.testsRun}")
|
||||
print(f"Failures: {len(result.failures)}")
|
||||
print(f"Errors: {len(result.errors)}")
|
||||
print(f"Execution time: {execution_time:.2f} seconds")
|
||||
|
||||
if result.failures:
|
||||
print(f"\nFailures ({len(result.failures)}):")
|
||||
for i, (test, error) in enumerate(result.failures[:3]): # Show first 3
|
||||
print(f" {i+1}. {test}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\nErrors ({len(result.errors)}):")
|
||||
for i, (test, error) in enumerate(result.errors[:3]): # Show first 3
|
||||
print(f" {i+1}. {test}")
|
||||
|
||||
status = "PASSED ✅" if result.wasSuccessful() else "FAILED ❌"
|
||||
print(f"\nStatus: {status}")
|
||||
|
||||
def _print_coverage_summary(self, coverage_data):
|
||||
"""Print code coverage summary."""
|
||||
print(f"\nCode Coverage Summary:")
|
||||
print(f"Overall coverage: {coverage_data.get('overall_percentage', 0):.1f}%")
|
||||
print(f"Lines covered: {coverage_data.get('lines_covered', 0)}")
|
||||
print(f"Lines missing: {coverage_data.get('lines_missing', 0)}")
|
||||
print(f"Total lines: {coverage_data.get('total_lines', 0)}")
|
||||
|
||||
if 'file_coverage' in coverage_data:
|
||||
print(f"\nFile Coverage (top 5):")
|
||||
for file_info in coverage_data['file_coverage'][:5]:
|
||||
print(f" {file_info['file']}: {file_info['percentage']:.1f}%")
|
||||
|
||||
def _print_load_test_summary(self, load_result):
|
||||
"""Print load test summary."""
|
||||
print(f"\nLoad Test Summary:")
|
||||
print(f"Concurrent users: {load_result.get('concurrent_users', 0)}")
|
||||
print(f"Duration: {load_result.get('duration_seconds', 0)} seconds")
|
||||
print(f"Total requests: {load_result.get('total_requests', 0)}")
|
||||
print(f"Successful requests: {load_result.get('successful_requests', 0)}")
|
||||
print(f"Failed requests: {load_result.get('failed_requests', 0)}")
|
||||
print(f"Average response time: {load_result.get('avg_response_time', 0):.2f} ms")
|
||||
print(f"Requests per second: {load_result.get('requests_per_second', 0):.1f}")
|
||||
|
||||
def _generate_pipeline_report(self, pipeline_time):
|
||||
"""Generate comprehensive pipeline report."""
|
||||
print("\n" + "=" * 80)
|
||||
print("PIPELINE EXECUTION SUMMARY")
|
||||
print("=" * 80)
|
||||
|
||||
total_tests = sum(
|
||||
result.result.testsRun if hasattr(result.result, 'testsRun') else 0
|
||||
for result in self.results
|
||||
)
|
||||
|
||||
total_failures = sum(
|
||||
len(result.result.failures) if hasattr(result.result, 'failures') else 0
|
||||
for result in self.results
|
||||
)
|
||||
|
||||
total_errors = sum(
|
||||
len(result.result.errors) if hasattr(result.result, 'errors') else 0
|
||||
for result in self.results
|
||||
)
|
||||
|
||||
successful_suites = sum(
|
||||
1 for result in self.results
|
||||
if (hasattr(result.result, 'wasSuccessful') and result.result.wasSuccessful()) or result.result is True
|
||||
)
|
||||
|
||||
print(f"Total execution time: {pipeline_time:.2f} seconds")
|
||||
print(f"Test suites run: {len(self.results)}")
|
||||
print(f"Successful suites: {successful_suites}/{len(self.results)}")
|
||||
print(f"Total tests executed: {total_tests}")
|
||||
print(f"Total failures: {total_failures}")
|
||||
print(f"Total errors: {total_errors}")
|
||||
|
||||
print(f"\nSuite Breakdown:")
|
||||
for result in self.results:
|
||||
status = "PASS" if (hasattr(result.result, 'wasSuccessful') and result.result.wasSuccessful()) or result.result is True else "FAIL"
|
||||
print(f" {result.test_type.ljust(15)}: {status.ljust(6)} ({result.execution_time:.2f}s)")
|
||||
|
||||
# Save detailed report to file
|
||||
self._save_detailed_report(pipeline_time)
|
||||
|
||||
overall_success = successful_suites == len(self.results) and total_failures == 0 and total_errors == 0
|
||||
final_status = "PIPELINE PASSED ✅" if overall_success else "PIPELINE FAILED ❌"
|
||||
print(f"\n{final_status}")
|
||||
|
||||
return overall_success
|
||||
|
||||
def _save_detailed_report(self, pipeline_time):
|
||||
"""Save detailed test report to JSON file."""
|
||||
report_data = {
|
||||
'pipeline_execution': {
|
||||
'start_time': datetime.utcnow().isoformat(),
|
||||
'total_time': pipeline_time,
|
||||
'total_suites': len(self.results),
|
||||
'successful_suites': sum(
|
||||
1 for r in self.results
|
||||
if (hasattr(r.result, 'wasSuccessful') and r.result.wasSuccessful()) or r.result is True
|
||||
)
|
||||
},
|
||||
'test_results': [result.to_dict() for result in self.results]
|
||||
}
|
||||
|
||||
report_file = os.path.join(self.output_dir, f'test_report_{int(time.time())}.json')
|
||||
with open(report_file, 'w') as f:
|
||||
json.dump(report_data, f, indent=2)
|
||||
|
||||
print(f"\nDetailed report saved to: {report_file}")
|
||||
|
||||
def _check_coverage_available(self):
|
||||
"""Check if coverage package is available."""
|
||||
try:
|
||||
import coverage
|
||||
return True
|
||||
except ImportError:
|
||||
return False
|
||||
|
||||
def _run_coverage_analysis(self, test_modules):
|
||||
"""Run code coverage analysis."""
|
||||
# Mock coverage analysis since we don't want to require coverage package
|
||||
# In a real implementation, this would use the coverage package
|
||||
|
||||
return {
|
||||
'overall_percentage': 75.5,
|
||||
'lines_covered': 1245,
|
||||
'lines_missing': 405,
|
||||
'total_lines': 1650,
|
||||
'file_coverage': [
|
||||
{'file': 'Serie.py', 'percentage': 85.2, 'lines_covered': 89, 'lines_missing': 15},
|
||||
{'file': 'SerieList.py', 'percentage': 78.9, 'lines_covered': 123, 'lines_missing': 33},
|
||||
{'file': 'SerieScanner.py', 'percentage': 72.3, 'lines_covered': 156, 'lines_missing': 60},
|
||||
{'file': 'database_manager.py', 'percentage': 82.1, 'lines_covered': 234, 'lines_missing': 51},
|
||||
{'file': 'performance_optimizer.py', 'percentage': 68.7, 'lines_covered': 198, 'lines_missing': 90}
|
||||
]
|
||||
}
|
||||
|
||||
def _run_mock_load_test(self, concurrent_users, duration_seconds):
|
||||
"""Run mock load test (placeholder for real load testing)."""
|
||||
# This would integrate with tools like locust, artillery, or custom load testing
|
||||
import time
|
||||
import random
|
||||
|
||||
print(f"Simulating load test with {concurrent_users} concurrent users for {duration_seconds} seconds...")
|
||||
|
||||
# Simulate load test execution
|
||||
time.sleep(min(duration_seconds / 10, 5)) # Simulate some time for demo
|
||||
|
||||
# Mock results
|
||||
total_requests = concurrent_users * duration_seconds * random.randint(2, 8)
|
||||
failed_requests = int(total_requests * random.uniform(0.01, 0.05)) # 1-5% failure rate
|
||||
successful_requests = total_requests - failed_requests
|
||||
|
||||
return {
|
||||
'concurrent_users': concurrent_users,
|
||||
'duration_seconds': duration_seconds,
|
||||
'total_requests': total_requests,
|
||||
'successful_requests': successful_requests,
|
||||
'failed_requests': failed_requests,
|
||||
'avg_response_time': random.uniform(50, 200), # 50-200ms
|
||||
'requests_per_second': total_requests / duration_seconds,
|
||||
'success_rate': (successful_requests / total_requests) * 100
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
"""Main function to run the testing pipeline."""
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='AniWorld Testing Pipeline')
|
||||
parser.add_argument('--unit', action='store_true', help='Run unit tests only')
|
||||
parser.add_argument('--integration', action='store_true', help='Run integration tests only')
|
||||
parser.add_argument('--performance', action='store_true', help='Run performance tests only')
|
||||
parser.add_argument('--coverage', action='store_true', help='Run code coverage analysis')
|
||||
parser.add_argument('--load', action='store_true', help='Run load tests')
|
||||
parser.add_argument('--all', action='store_true', help='Run complete pipeline')
|
||||
parser.add_argument('--output-dir', help='Output directory for test results')
|
||||
parser.add_argument('--concurrent-users', type=int, default=10, help='Number of concurrent users for load tests')
|
||||
parser.add_argument('--load-duration', type=int, default=60, help='Duration for load tests in seconds')
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Create pipeline
|
||||
pipeline = TestPipeline(args.output_dir)
|
||||
|
||||
success = True
|
||||
|
||||
if args.all or (not any([args.unit, args.integration, args.performance, args.coverage, args.load])):
|
||||
# Run full pipeline
|
||||
success = pipeline.run_full_pipeline(
|
||||
include_performance=True,
|
||||
include_coverage=True,
|
||||
include_load=args.load
|
||||
)
|
||||
else:
|
||||
# Run specific test suites
|
||||
if args.unit:
|
||||
result = pipeline.run_unit_tests()
|
||||
success &= result.result.wasSuccessful() if hasattr(result.result, 'wasSuccessful') else result.result
|
||||
|
||||
if args.integration:
|
||||
result = pipeline.run_integration_tests()
|
||||
success &= result.result.wasSuccessful() if hasattr(result.result, 'wasSuccessful') else result.result
|
||||
|
||||
if args.performance:
|
||||
result = pipeline.run_performance_tests()
|
||||
success &= result.result.wasSuccessful() if hasattr(result.result, 'wasSuccessful') else result.result
|
||||
|
||||
if args.coverage:
|
||||
result = pipeline.run_code_coverage()
|
||||
success &= result.result if isinstance(result.result, bool) else result.result.wasSuccessful()
|
||||
|
||||
if args.load:
|
||||
result = pipeline.run_load_tests(args.concurrent_users, args.load_duration)
|
||||
success &= result.result if isinstance(result.result, bool) else result.result.wasSuccessful()
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -1,281 +0,0 @@
|
||||
"""
|
||||
Integration tests to verify no route conflicts exist.
|
||||
|
||||
This module ensures that all routes are unique and properly configured
|
||||
after consolidation efforts.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
from typing import Dict, List, Tuple, Set
|
||||
from collections import defaultdict
|
||||
|
||||
# Add src to path for imports
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src'))
|
||||
|
||||
|
||||
class TestRouteConflicts:
|
||||
"""Test suite to detect and prevent route conflicts."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures."""
|
||||
self.route_registry = defaultdict(list)
|
||||
self.blueprint_routes = {}
|
||||
|
||||
def test_no_duplicate_routes(self):
|
||||
"""
|
||||
Ensure no route conflicts exist across all controllers.
|
||||
|
||||
This test scans all controller files for route definitions
|
||||
and verifies that no two routes have the same path and method.
|
||||
"""
|
||||
routes = self._extract_all_routes()
|
||||
conflicts = self._find_route_conflicts(routes)
|
||||
|
||||
assert len(conflicts) == 0, f"Route conflicts found: {conflicts}"
|
||||
|
||||
def test_url_prefix_consistency(self):
|
||||
"""
|
||||
Test that URL prefixes follow consistent patterns.
|
||||
|
||||
Verifies that all API routes follow the /api/v1/ prefix pattern
|
||||
where appropriate.
|
||||
"""
|
||||
routes = self._extract_all_routes()
|
||||
inconsistent_routes = []
|
||||
|
||||
for route_info in routes:
|
||||
path = route_info['path']
|
||||
controller = route_info['controller']
|
||||
|
||||
# Skip non-API routes
|
||||
if not path.startswith('/api/'):
|
||||
continue
|
||||
|
||||
# Check for version consistency
|
||||
if path.startswith('/api/') and not path.startswith('/api/v1/'):
|
||||
# Some exceptions are allowed (like /api/health)
|
||||
allowed_exceptions = ['/api/health', '/api/config', '/api/scheduler', '/api/logging']
|
||||
if not any(path.startswith(exc) for exc in allowed_exceptions):
|
||||
inconsistent_routes.append({
|
||||
'path': path,
|
||||
'controller': controller,
|
||||
'issue': 'Missing version prefix'
|
||||
})
|
||||
|
||||
# This is a warning test - inconsistencies should be noted but not fail
|
||||
if inconsistent_routes:
|
||||
print(f"URL prefix inconsistencies found (consider standardizing): {inconsistent_routes}")
|
||||
|
||||
def test_blueprint_name_uniqueness(self):
|
||||
"""
|
||||
Test that all Blueprint names are unique.
|
||||
|
||||
Ensures no Blueprint naming conflicts exist.
|
||||
"""
|
||||
blueprint_names = self._extract_blueprint_names()
|
||||
duplicates = self._find_duplicates(blueprint_names)
|
||||
|
||||
assert len(duplicates) == 0, f"Duplicate blueprint names found: {duplicates}"
|
||||
|
||||
def test_route_parameter_consistency(self):
|
||||
"""
|
||||
Test that route parameters follow consistent naming patterns.
|
||||
|
||||
Ensures parameters like {id} vs {episode_id} are used consistently.
|
||||
"""
|
||||
routes = self._extract_all_routes()
|
||||
parameter_patterns = defaultdict(set)
|
||||
|
||||
for route_info in routes:
|
||||
path = route_info['path']
|
||||
# Extract parameter patterns
|
||||
if '<' in path:
|
||||
# Extract parameter names like <int:episode_id>
|
||||
import re
|
||||
params = re.findall(r'<[^>]+>', path)
|
||||
for param in params:
|
||||
# Normalize parameter (remove type hints)
|
||||
clean_param = param.replace('<int:', '<').replace('<string:', '<').replace('<', '').replace('>', '')
|
||||
parameter_patterns[clean_param].add(route_info['controller'])
|
||||
|
||||
# Check for inconsistent ID naming
|
||||
id_patterns = {k: v for k, v in parameter_patterns.items() if 'id' in k}
|
||||
if len(id_patterns) > 3: # Allow some variation
|
||||
print(f"Consider standardizing ID parameter naming: {dict(id_patterns)}")
|
||||
|
||||
def test_http_method_coverage(self):
|
||||
"""
|
||||
Test that CRUD operations are consistently implemented.
|
||||
|
||||
Ensures that resources supporting CRUD have all necessary methods.
|
||||
"""
|
||||
routes = self._extract_all_routes()
|
||||
resource_methods = defaultdict(set)
|
||||
|
||||
for route_info in routes:
|
||||
path = route_info['path']
|
||||
method = route_info['method']
|
||||
|
||||
# Group by resource (extract base path)
|
||||
if '/api/v1/' in path:
|
||||
resource = path.split('/api/v1/')[1].split('/')[0]
|
||||
resource_methods[resource].add(method)
|
||||
|
||||
# Check for incomplete CRUD implementations
|
||||
incomplete_crud = {}
|
||||
for resource, methods in resource_methods.items():
|
||||
if 'GET' in methods or 'POST' in methods: # If it has read/write operations
|
||||
missing_methods = {'GET', 'POST', 'PUT', 'DELETE'} - methods
|
||||
if missing_methods:
|
||||
incomplete_crud[resource] = missing_methods
|
||||
|
||||
# This is informational - not all resources need full CRUD
|
||||
if incomplete_crud:
|
||||
print(f"Resources with incomplete CRUD operations: {incomplete_crud}")
|
||||
|
||||
def _extract_all_routes(self) -> List[Dict]:
|
||||
"""
|
||||
Extract all route definitions from controller files.
|
||||
|
||||
Returns:
|
||||
List of route information dictionaries
|
||||
"""
|
||||
routes = []
|
||||
controller_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src', 'server', 'web', 'controllers')
|
||||
|
||||
# This would normally scan actual controller files
|
||||
# For now, return mock data based on our analysis
|
||||
mock_routes = [
|
||||
{'path': '/api/v1/anime', 'method': 'GET', 'controller': 'anime.py', 'function': 'list_anime'},
|
||||
{'path': '/api/v1/anime', 'method': 'POST', 'controller': 'anime.py', 'function': 'create_anime'},
|
||||
{'path': '/api/v1/anime/<int:id>', 'method': 'GET', 'controller': 'anime.py', 'function': 'get_anime'},
|
||||
{'path': '/api/v1/episodes', 'method': 'GET', 'controller': 'episodes.py', 'function': 'list_episodes'},
|
||||
{'path': '/api/v1/episodes', 'method': 'POST', 'controller': 'episodes.py', 'function': 'create_episode'},
|
||||
{'path': '/api/health', 'method': 'GET', 'controller': 'health.py', 'function': 'health_check'},
|
||||
{'path': '/api/health/system', 'method': 'GET', 'controller': 'health.py', 'function': 'system_health'},
|
||||
{'path': '/status', 'method': 'GET', 'controller': 'health.py', 'function': 'basic_status'},
|
||||
{'path': '/ping', 'method': 'GET', 'controller': 'health.py', 'function': 'ping'},
|
||||
]
|
||||
|
||||
return mock_routes
|
||||
|
||||
def _find_route_conflicts(self, routes: List[Dict]) -> List[Dict]:
|
||||
"""
|
||||
Find conflicting routes (same path and method).
|
||||
|
||||
Args:
|
||||
routes: List of route information
|
||||
|
||||
Returns:
|
||||
List of conflicts found
|
||||
"""
|
||||
route_map = {}
|
||||
conflicts = []
|
||||
|
||||
for route in routes:
|
||||
key = (route['path'], route['method'])
|
||||
if key in route_map:
|
||||
conflicts.append({
|
||||
'path': route['path'],
|
||||
'method': route['method'],
|
||||
'controllers': [route_map[key]['controller'], route['controller']]
|
||||
})
|
||||
else:
|
||||
route_map[key] = route
|
||||
|
||||
return conflicts
|
||||
|
||||
def _extract_blueprint_names(self) -> List[Tuple[str, str]]:
|
||||
"""
|
||||
Extract all Blueprint names from controller files.
|
||||
|
||||
Returns:
|
||||
List of (blueprint_name, controller_file) tuples
|
||||
"""
|
||||
# Mock blueprint names based on our analysis
|
||||
blueprint_names = [
|
||||
('anime', 'anime.py'),
|
||||
('episodes', 'episodes.py'),
|
||||
('health_check', 'health.py'),
|
||||
('auth', 'auth.py'),
|
||||
('config', 'config.py'),
|
||||
('scheduler', 'scheduler.py'),
|
||||
('logging', 'logging.py'),
|
||||
('storage', 'storage.py'),
|
||||
('search', 'search.py'),
|
||||
('downloads', 'downloads.py'),
|
||||
('maintenance', 'maintenance.py'),
|
||||
('performance', 'performance.py'),
|
||||
('process', 'process.py'),
|
||||
('integrations', 'integrations.py'),
|
||||
('diagnostics', 'diagnostics.py'),
|
||||
('database', 'database.py'),
|
||||
('bulk_api', 'bulk.py'),
|
||||
('backups', 'backups.py'),
|
||||
]
|
||||
|
||||
return blueprint_names
|
||||
|
||||
def _find_duplicates(self, items: List[Tuple[str, str]]) -> List[str]:
|
||||
"""
|
||||
Find duplicate items in a list.
|
||||
|
||||
Args:
|
||||
items: List of (name, source) tuples
|
||||
|
||||
Returns:
|
||||
List of duplicate names
|
||||
"""
|
||||
seen = set()
|
||||
duplicates = []
|
||||
|
||||
for name, source in items:
|
||||
if name in seen:
|
||||
duplicates.append(name)
|
||||
seen.add(name)
|
||||
|
||||
return duplicates
|
||||
|
||||
|
||||
class TestControllerStandardization:
|
||||
"""Test suite for controller standardization compliance."""
|
||||
|
||||
def test_base_controller_usage(self):
|
||||
"""
|
||||
Test that controllers properly inherit from BaseController.
|
||||
|
||||
This would check that new controllers use the base controller
|
||||
instead of implementing duplicate functionality.
|
||||
"""
|
||||
# This would scan controller files to ensure they inherit BaseController
|
||||
# For now, this is a placeholder test
|
||||
assert True # Placeholder
|
||||
|
||||
def test_shared_decorators_usage(self):
|
||||
"""
|
||||
Test that controllers use shared decorators instead of local implementations.
|
||||
|
||||
Ensures @handle_api_errors, @require_auth, etc. are imported
|
||||
from shared modules rather than locally implemented.
|
||||
"""
|
||||
# This would scan for decorator usage patterns
|
||||
# For now, this is a placeholder test
|
||||
assert True # Placeholder
|
||||
|
||||
def test_response_format_consistency(self):
|
||||
"""
|
||||
Test that all endpoints return consistent response formats.
|
||||
|
||||
Ensures all responses follow the standardized format:
|
||||
{"status": "success/error", "message": "...", "data": ...}
|
||||
"""
|
||||
# This would test actual endpoint responses
|
||||
# For now, this is a placeholder test
|
||||
assert True # Placeholder
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run the tests
|
||||
pytest.main([__file__, "-v"])
|
||||
@@ -1 +0,0 @@
|
||||
# Unit test package
|
||||
@@ -1,390 +0,0 @@
|
||||
"""
|
||||
Unit tests for the BaseController class.
|
||||
|
||||
This module tests the common functionality provided by the BaseController
|
||||
to ensure consistent behavior across all controllers.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import logging
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import HTTPException
|
||||
from pydantic import BaseModel, ValidationError
|
||||
|
||||
# Import the BaseController and decorators
|
||||
import sys
|
||||
import os
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), '..', '..', '..', 'src'))
|
||||
|
||||
from server.web.controllers.base_controller import (
|
||||
BaseController,
|
||||
handle_api_errors,
|
||||
require_auth,
|
||||
optional_auth,
|
||||
validate_json_input
|
||||
)
|
||||
|
||||
|
||||
class MockPydanticModel(BaseModel):
|
||||
"""Mock Pydantic model for testing validation."""
|
||||
name: str
|
||||
age: int
|
||||
|
||||
|
||||
class TestBaseController:
|
||||
"""Test cases for BaseController class."""
|
||||
|
||||
def setup_method(self):
|
||||
"""Setup test fixtures."""
|
||||
self.controller = BaseController()
|
||||
|
||||
def test_initialization(self):
|
||||
"""Test BaseController initialization."""
|
||||
assert self.controller is not None
|
||||
assert hasattr(self.controller, 'logger')
|
||||
assert isinstance(self.controller.logger, logging.Logger)
|
||||
|
||||
def test_handle_error(self):
|
||||
"""Test error handling functionality."""
|
||||
test_error = ValueError("Test error")
|
||||
result = self.controller.handle_error(test_error, 400)
|
||||
|
||||
assert isinstance(result, HTTPException)
|
||||
assert result.status_code == 400
|
||||
assert str(test_error) in str(result.detail)
|
||||
|
||||
def test_handle_error_default_status_code(self):
|
||||
"""Test error handling with default status code."""
|
||||
test_error = RuntimeError("Runtime error")
|
||||
result = self.controller.handle_error(test_error)
|
||||
|
||||
assert isinstance(result, HTTPException)
|
||||
assert result.status_code == 500
|
||||
|
||||
def test_validate_request_success(self):
|
||||
"""Test successful request validation."""
|
||||
mock_model = MockPydanticModel(name="John", age=25)
|
||||
result = self.controller.validate_request(mock_model)
|
||||
|
||||
assert result is True
|
||||
|
||||
def test_validate_request_failure(self):
|
||||
"""Test failed request validation."""
|
||||
# Create a mock that raises validation error
|
||||
mock_model = Mock()
|
||||
mock_model.side_effect = ValidationError("Validation failed", MockPydanticModel)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
self.controller.validate_request(mock_model)
|
||||
|
||||
def test_format_response_basic(self):
|
||||
"""Test basic response formatting."""
|
||||
data = {"test": "value"}
|
||||
result = self.controller.format_response(data)
|
||||
|
||||
expected = {
|
||||
"status": "success",
|
||||
"message": "Success",
|
||||
"data": data
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_format_response_custom_message(self):
|
||||
"""Test response formatting with custom message."""
|
||||
data = {"user_id": 123}
|
||||
message = "User created successfully"
|
||||
result = self.controller.format_response(data, message)
|
||||
|
||||
expected = {
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
assert result == expected
|
||||
|
||||
def test_format_error_response_basic(self):
|
||||
"""Test basic error response formatting."""
|
||||
message = "Error occurred"
|
||||
result, status_code = self.controller.format_error_response(message)
|
||||
|
||||
expected = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"error_code": 400
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 400
|
||||
|
||||
def test_format_error_response_with_details(self):
|
||||
"""Test error response formatting with details."""
|
||||
message = "Validation failed"
|
||||
details = {"field": "name", "error": "required"}
|
||||
result, status_code = self.controller.format_error_response(message, 422, details)
|
||||
|
||||
expected = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"error_code": 422,
|
||||
"details": details
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 422
|
||||
|
||||
def test_create_success_response_minimal(self):
|
||||
"""Test minimal success response creation."""
|
||||
result, status_code = self.controller.create_success_response()
|
||||
|
||||
expected = {
|
||||
"status": "success",
|
||||
"message": "Operation successful"
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 200
|
||||
|
||||
def test_create_success_response_full(self):
|
||||
"""Test full success response creation with all parameters."""
|
||||
data = {"items": [1, 2, 3]}
|
||||
message = "Data retrieved"
|
||||
pagination = {"page": 1, "total": 100}
|
||||
meta = {"version": "1.0"}
|
||||
|
||||
result, status_code = self.controller.create_success_response(
|
||||
data=data,
|
||||
message=message,
|
||||
status_code=201,
|
||||
pagination=pagination,
|
||||
meta=meta
|
||||
)
|
||||
|
||||
expected = {
|
||||
"status": "success",
|
||||
"message": message,
|
||||
"data": data,
|
||||
"pagination": pagination,
|
||||
"meta": meta
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 201
|
||||
|
||||
def test_create_error_response_minimal(self):
|
||||
"""Test minimal error response creation."""
|
||||
message = "Something went wrong"
|
||||
result, status_code = self.controller.create_error_response(message)
|
||||
|
||||
expected = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"error_code": 400
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 400
|
||||
|
||||
def test_create_error_response_full(self):
|
||||
"""Test full error response creation with all parameters."""
|
||||
message = "Custom error"
|
||||
details = {"trace": "error trace"}
|
||||
error_code = "CUSTOM_ERROR"
|
||||
|
||||
result, status_code = self.controller.create_error_response(
|
||||
message=message,
|
||||
status_code=422,
|
||||
details=details,
|
||||
error_code=error_code
|
||||
)
|
||||
|
||||
expected = {
|
||||
"status": "error",
|
||||
"message": message,
|
||||
"error_code": error_code,
|
||||
"details": details
|
||||
}
|
||||
assert result == expected
|
||||
assert status_code == 422
|
||||
|
||||
|
||||
class TestHandleApiErrors:
|
||||
"""Test cases for handle_api_errors decorator."""
|
||||
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_handle_value_error(self, mock_jsonify):
|
||||
"""Test handling of ValueError."""
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
raise ValueError("Invalid input")
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Invalid input data'
|
||||
assert call_args['error_code'] == 400
|
||||
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_handle_permission_error(self, mock_jsonify):
|
||||
"""Test handling of PermissionError."""
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
raise PermissionError("Access denied")
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Access denied'
|
||||
assert call_args['error_code'] == 403
|
||||
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_handle_file_not_found_error(self, mock_jsonify):
|
||||
"""Test handling of FileNotFoundError."""
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
raise FileNotFoundError("File not found")
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Resource not found'
|
||||
assert call_args['error_code'] == 404
|
||||
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
@patch('server.web.controllers.base_controller.logging')
|
||||
def test_handle_generic_exception(self, mock_logging, mock_jsonify):
|
||||
"""Test handling of generic exceptions."""
|
||||
mock_jsonify.return_value = Mock()
|
||||
mock_logger = Mock()
|
||||
mock_logging.getLogger.return_value = mock_logger
|
||||
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
raise RuntimeError("Unexpected error")
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Internal server error'
|
||||
assert call_args['error_code'] == 500
|
||||
|
||||
def test_handle_http_exception_reraise(self):
|
||||
"""Test that HTTPExceptions are re-raised."""
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
raise HTTPException(status_code=404, detail="Not found")
|
||||
|
||||
with pytest.raises(HTTPException):
|
||||
test_function()
|
||||
|
||||
def test_successful_execution(self):
|
||||
"""Test that successful functions execute normally."""
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
assert result == "success"
|
||||
|
||||
|
||||
class TestValidateJsonInput:
|
||||
"""Test cases for validate_json_input decorator."""
|
||||
|
||||
@patch('server.web.controllers.base_controller.request')
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_non_json_request(self, mock_jsonify, mock_request):
|
||||
"""Test handling of non-JSON requests."""
|
||||
mock_request.is_json = False
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@validate_json_input(required_fields=['name'])
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Request must contain JSON data'
|
||||
|
||||
@patch('server.web.controllers.base_controller.request')
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_invalid_json(self, mock_jsonify, mock_request):
|
||||
"""Test handling of invalid JSON."""
|
||||
mock_request.is_json = True
|
||||
mock_request.get_json.return_value = None
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@validate_json_input(required_fields=['name'])
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert call_args['message'] == 'Invalid JSON data'
|
||||
|
||||
@patch('server.web.controllers.base_controller.request')
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_missing_required_fields(self, mock_jsonify, mock_request):
|
||||
"""Test handling of missing required fields."""
|
||||
mock_request.is_json = True
|
||||
mock_request.get_json.return_value = {'age': 25}
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
@validate_json_input(required_fields=['name', 'email'])
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert 'Missing required fields' in call_args['message']
|
||||
|
||||
@patch('server.web.controllers.base_controller.request')
|
||||
def test_successful_validation(self, mock_request):
|
||||
"""Test successful validation with all required fields."""
|
||||
mock_request.is_json = True
|
||||
mock_request.get_json.return_value = {'name': 'John', 'email': 'john@example.com'}
|
||||
|
||||
@validate_json_input(required_fields=['name', 'email'])
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
assert result == "success"
|
||||
|
||||
@patch('server.web.controllers.base_controller.request')
|
||||
@patch('server.web.controllers.base_controller.jsonify')
|
||||
def test_field_validator_failure(self, mock_jsonify, mock_request):
|
||||
"""Test field validator failure."""
|
||||
mock_request.is_json = True
|
||||
mock_request.get_json.return_value = {'age': -5}
|
||||
mock_jsonify.return_value = Mock()
|
||||
|
||||
def validate_age(value):
|
||||
return value > 0
|
||||
|
||||
@validate_json_input(age=validate_age)
|
||||
def test_function():
|
||||
return "success"
|
||||
|
||||
result = test_function()
|
||||
|
||||
mock_jsonify.assert_called_once()
|
||||
call_args = mock_jsonify.call_args[0][0]
|
||||
assert call_args['status'] == 'error'
|
||||
assert 'Invalid value for field: age' in call_args['message']
|
||||
@@ -1,593 +0,0 @@
|
||||
"""
|
||||
Unit Tests for Core Functionality
|
||||
|
||||
This module contains unit tests for the core components of the AniWorld application,
|
||||
including series management, download operations, and API functionality.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import shutil
|
||||
import sqlite3
|
||||
import json
|
||||
from unittest.mock import Mock, MagicMock, patch, call
|
||||
from datetime import datetime, timedelta
|
||||
import threading
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
|
||||
# Import core modules
|
||||
from src.server.core.entities.series import Serie
|
||||
from src.server.core.entities.SerieList import SerieList
|
||||
from src.server.core.SerieScanner import SerieScanner
|
||||
# TODO: Fix imports - these modules may not exist or may be in different locations
|
||||
# from database_manager import DatabaseManager, AnimeMetadata, EpisodeMetadata, BackupManager
|
||||
# from error_handler import ErrorRecoveryManager, RetryMechanism, NetworkHealthChecker
|
||||
# from performance_optimizer import SpeedLimiter, DownloadCache, MemoryMonitor
|
||||
# from api_integration import WebhookManager, ExportManager
|
||||
|
||||
|
||||
class TestSerie(unittest.TestCase):
|
||||
"""Test cases for Serie class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.test_key = "test-key"
|
||||
self.test_name = "Test Anime"
|
||||
self.test_site = "test-site"
|
||||
self.test_folder = "test_folder"
|
||||
self.test_episodes = {1: [1], 2: [2]}
|
||||
|
||||
def test_serie_initialization(self):
|
||||
"""Test Serie object initialization."""
|
||||
serie = Serie(self.test_key, self.test_name, self.test_site, self.test_folder, self.test_episodes)
|
||||
|
||||
self.assertEqual(serie.key, self.test_key)
|
||||
self.assertEqual(serie.name, self.test_name)
|
||||
self.assertEqual(serie.site, self.test_site)
|
||||
self.assertEqual(serie.folder, self.test_folder)
|
||||
self.assertEqual(serie.episodeDict, self.test_episodes)
|
||||
|
||||
def test_serie_str_representation(self):
|
||||
"""Test string representation of Serie."""
|
||||
serie = Serie(self.test_key, self.test_name, self.test_site, self.test_folder, self.test_episodes)
|
||||
str_repr = str(serie)
|
||||
|
||||
self.assertIn(self.test_name, str_repr)
|
||||
self.assertIn(self.test_folder, str_repr)
|
||||
self.assertIn(self.test_key, str_repr)
|
||||
|
||||
def test_serie_episode_management(self):
|
||||
"""Test episode dictionary management."""
|
||||
serie = Serie(self.test_key, self.test_name, self.test_site, self.test_folder, self.test_episodes)
|
||||
|
||||
# Test episode dict
|
||||
self.assertEqual(len(serie.episodeDict), 2)
|
||||
self.assertIn(1, serie.episodeDict)
|
||||
self.assertIn(2, serie.episodeDict)
|
||||
|
||||
def test_serie_equality(self):
|
||||
"""Test Serie equality comparison."""
|
||||
serie1 = Serie(self.test_key, self.test_name, self.test_site, self.test_folder, self.test_episodes)
|
||||
serie2 = Serie(self.test_key, self.test_name, self.test_site, self.test_folder, self.test_episodes)
|
||||
serie3 = Serie("different-key", "Different", self.test_site, self.test_folder, self.test_episodes)
|
||||
|
||||
# Should be equal based on key attributes
|
||||
self.assertEqual(serie1.key, serie2.key)
|
||||
self.assertEqual(serie1.folder, serie2.folder)
|
||||
self.assertNotEqual(serie1.key, serie3.key)
|
||||
|
||||
|
||||
class TestSeriesList(unittest.TestCase):
|
||||
"""Test cases for SeriesList class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
self.series_list = SerieList(self.temp_dir)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test fixtures."""
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_series_list_initialization(self):
|
||||
"""Test SerieList initialization."""
|
||||
self.assertIsInstance(self.series_list.folderDict, dict)
|
||||
self.assertEqual(len(self.series_list.folderDict), 0)
|
||||
|
||||
def test_add_serie_to_list(self):
|
||||
"""Test adding serie to list."""
|
||||
serie = Serie("test-key", "Test", "test-site", "test_folder", {})
|
||||
self.series_list.add(serie)
|
||||
|
||||
self.assertEqual(len(self.series_list.folderDict), 1)
|
||||
self.assertIn("test_folder", self.series_list.folderDict)
|
||||
|
||||
def test_contains_serie(self):
|
||||
"""Test checking if serie exists."""
|
||||
serie = Serie("test-key", "Test", "test-site", "test_folder", {})
|
||||
self.series_list.add(serie)
|
||||
|
||||
self.assertTrue(self.series_list.contains("test-key"))
|
||||
self.assertFalse(self.series_list.contains("nonexistent"))
|
||||
|
||||
def test_get_series_with_missing_episodes(self):
|
||||
"""Test filtering series with missing episodes."""
|
||||
serie1 = Serie("key1", "Anime 1", "test-site", "folder1", {1: [1], 2: [2]}) # Has missing episodes
|
||||
serie2 = Serie("key2", "Anime 2", "test-site", "folder2", {}) # No missing episodes
|
||||
|
||||
self.series_list.add(serie1)
|
||||
self.series_list.add(serie2)
|
||||
|
||||
missing = self.series_list.GetMissingEpisode()
|
||||
self.assertEqual(len(missing), 1)
|
||||
self.assertEqual(missing[0].name, "Anime 1")
|
||||
|
||||
|
||||
class TestDatabaseManager(unittest.TestCase):
|
||||
"""Test cases for DatabaseManager class."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test database."""
|
||||
self.test_db = tempfile.NamedTemporaryFile(delete=False)
|
||||
self.test_db.close()
|
||||
self.db_manager = DatabaseManager(self.test_db.name)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test database."""
|
||||
self.db_manager.close()
|
||||
os.unlink(self.test_db.name)
|
||||
|
||||
def test_database_initialization(self):
|
||||
"""Test database initialization."""
|
||||
# Check if tables exist
|
||||
with self.db_manager.get_connection() as conn:
|
||||
cursor = conn.execute("""
|
||||
SELECT name FROM sqlite_master
|
||||
WHERE type='table' AND name='anime_metadata'
|
||||
""")
|
||||
result = cursor.fetchone()
|
||||
self.assertIsNotNone(result)
|
||||
|
||||
def test_schema_versioning(self):
|
||||
"""Test schema version management."""
|
||||
version = self.db_manager.get_current_version()
|
||||
self.assertIsInstance(version, int)
|
||||
self.assertGreater(version, 0)
|
||||
|
||||
def test_anime_crud_operations(self):
|
||||
"""Test anime CRUD operations."""
|
||||
# Create anime
|
||||
anime = AnimeMetadata(
|
||||
anime_id="test-123",
|
||||
name="Test Anime",
|
||||
folder="test_folder",
|
||||
key="test-key"
|
||||
)
|
||||
|
||||
# Insert
|
||||
query = """
|
||||
INSERT INTO anime_metadata
|
||||
(anime_id, name, folder, key, created_at, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?, ?)
|
||||
"""
|
||||
params = (
|
||||
anime.anime_id, anime.name, anime.folder, anime.key,
|
||||
anime.created_at, anime.last_updated
|
||||
)
|
||||
|
||||
success = self.db_manager.execute_update(query, params)
|
||||
self.assertTrue(success)
|
||||
|
||||
# Read
|
||||
select_query = "SELECT * FROM anime_metadata WHERE anime_id = ?"
|
||||
results = self.db_manager.execute_query(select_query, (anime.anime_id,))
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['name'], anime.name)
|
||||
|
||||
# Update
|
||||
update_query = """
|
||||
UPDATE anime_metadata SET description = ? WHERE anime_id = ?
|
||||
"""
|
||||
success = self.db_manager.execute_update(
|
||||
update_query, ("Updated description", anime.anime_id)
|
||||
)
|
||||
self.assertTrue(success)
|
||||
|
||||
# Verify update
|
||||
results = self.db_manager.execute_query(select_query, (anime.anime_id,))
|
||||
self.assertEqual(results[0]['description'], "Updated description")
|
||||
|
||||
# Delete
|
||||
delete_query = "DELETE FROM anime_metadata WHERE anime_id = ?"
|
||||
success = self.db_manager.execute_update(delete_query, (anime.anime_id,))
|
||||
self.assertTrue(success)
|
||||
|
||||
# Verify deletion
|
||||
results = self.db_manager.execute_query(select_query, (anime.anime_id,))
|
||||
self.assertEqual(len(results), 0)
|
||||
|
||||
|
||||
class TestErrorRecoveryManager(unittest.TestCase):
|
||||
"""Test cases for ErrorRecoveryManager."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up error recovery manager."""
|
||||
self.recovery_manager = ErrorRecoveryManager()
|
||||
|
||||
def test_retry_mechanism(self):
|
||||
"""Test retry mechanism for failed operations."""
|
||||
retry_mechanism = RetryMechanism(max_retries=3, base_delay=0.1)
|
||||
|
||||
# Test successful operation
|
||||
def success_operation():
|
||||
return "success"
|
||||
|
||||
result = retry_mechanism.execute_with_retry(success_operation)
|
||||
self.assertEqual(result, "success")
|
||||
|
||||
# Test failing operation
|
||||
call_count = [0]
|
||||
def failing_operation():
|
||||
call_count[0] += 1
|
||||
if call_count[0] < 3:
|
||||
raise Exception("Temporary failure")
|
||||
return "success"
|
||||
|
||||
result = retry_mechanism.execute_with_retry(failing_operation)
|
||||
self.assertEqual(result, "success")
|
||||
self.assertEqual(call_count[0], 3)
|
||||
|
||||
def test_network_health_checker(self):
|
||||
"""Test network health checking."""
|
||||
checker = NetworkHealthChecker()
|
||||
|
||||
# Mock requests for controlled testing
|
||||
with patch('requests.get') as mock_get:
|
||||
# Test successful check
|
||||
mock_response = Mock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.raise_for_status.return_value = None
|
||||
mock_get.return_value = mock_response
|
||||
|
||||
is_healthy = checker.check_network_health()
|
||||
self.assertTrue(is_healthy)
|
||||
|
||||
# Test failed check
|
||||
mock_get.side_effect = Exception("Network error")
|
||||
is_healthy = checker.check_network_health()
|
||||
self.assertFalse(is_healthy)
|
||||
|
||||
|
||||
class TestPerformanceOptimizer(unittest.TestCase):
|
||||
"""Test cases for performance optimization components."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up performance components."""
|
||||
self.speed_limiter = SpeedLimiter(max_speed_mbps=10)
|
||||
self.download_cache = DownloadCache()
|
||||
|
||||
def test_speed_limiter(self):
|
||||
"""Test download speed limiting."""
|
||||
# Test speed calculation
|
||||
speed_mbps = self.speed_limiter.calculate_current_speed(1024*1024, 1.0) # 1MB in 1 second
|
||||
self.assertEqual(speed_mbps, 8.0) # 1MB/s = 8 Mbps
|
||||
|
||||
# Test should limit
|
||||
should_limit = self.speed_limiter.should_limit_speed(15.0) # Above limit
|
||||
self.assertTrue(should_limit)
|
||||
|
||||
should_not_limit = self.speed_limiter.should_limit_speed(5.0) # Below limit
|
||||
self.assertFalse(should_not_limit)
|
||||
|
||||
def test_download_cache(self):
|
||||
"""Test download caching mechanism."""
|
||||
test_url = "http://example.com/video.mp4"
|
||||
test_data = b"test video data"
|
||||
|
||||
# Test cache miss
|
||||
cached_data = self.download_cache.get(test_url)
|
||||
self.assertIsNone(cached_data)
|
||||
|
||||
# Test cache set and hit
|
||||
self.download_cache.set(test_url, test_data)
|
||||
cached_data = self.download_cache.get(test_url)
|
||||
self.assertEqual(cached_data, test_data)
|
||||
|
||||
# Test cache invalidation
|
||||
self.download_cache.invalidate(test_url)
|
||||
cached_data = self.download_cache.get(test_url)
|
||||
self.assertIsNone(cached_data)
|
||||
|
||||
def test_memory_monitor(self):
|
||||
"""Test memory monitoring."""
|
||||
monitor = MemoryMonitor(threshold_mb=100)
|
||||
|
||||
# Test memory usage calculation
|
||||
usage_mb = monitor.get_current_memory_usage()
|
||||
self.assertIsInstance(usage_mb, (int, float))
|
||||
self.assertGreater(usage_mb, 0)
|
||||
|
||||
# Test threshold checking
|
||||
is_high = monitor.is_memory_usage_high()
|
||||
self.assertIsInstance(is_high, bool)
|
||||
|
||||
|
||||
class TestAPIIntegration(unittest.TestCase):
|
||||
"""Test cases for API integration components."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up API components."""
|
||||
self.webhook_manager = WebhookManager()
|
||||
self.export_manager = ExportManager()
|
||||
|
||||
def test_webhook_manager(self):
|
||||
"""Test webhook functionality."""
|
||||
test_url = "https://example.com/webhook"
|
||||
self.webhook_manager.add_webhook(test_url)
|
||||
|
||||
# Test webhook is registered
|
||||
self.assertIn(test_url, self.webhook_manager.webhooks)
|
||||
|
||||
# Test webhook removal
|
||||
self.webhook_manager.remove_webhook(test_url)
|
||||
self.assertNotIn(test_url, self.webhook_manager.webhooks)
|
||||
|
||||
def test_export_manager(self):
|
||||
"""Test data export functionality."""
|
||||
# Mock series app
|
||||
mock_series_app = Mock()
|
||||
mock_series = Mock()
|
||||
mock_series.name = "Test Anime"
|
||||
mock_series.folder = "test_folder"
|
||||
mock_series.missing = [1, 2, 3]
|
||||
mock_series_app.series_list.series = [mock_series]
|
||||
|
||||
self.export_manager.series_app = mock_series_app
|
||||
|
||||
# Test JSON export
|
||||
json_data = self.export_manager.export_to_json()
|
||||
self.assertIsInstance(json_data, str)
|
||||
|
||||
# Parse and validate JSON
|
||||
parsed_data = json.loads(json_data)
|
||||
self.assertIn('anime_list', parsed_data)
|
||||
self.assertEqual(len(parsed_data['anime_list']), 1)
|
||||
self.assertEqual(parsed_data['anime_list'][0]['name'], "Test Anime")
|
||||
|
||||
# Test CSV export
|
||||
csv_data = self.export_manager.export_to_csv()
|
||||
self.assertIsInstance(csv_data, str)
|
||||
self.assertIn("Test Anime", csv_data)
|
||||
self.assertIn("test_folder", csv_data)
|
||||
|
||||
|
||||
class TestBackupManager(unittest.TestCase):
|
||||
"""Test cases for backup management."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test environment."""
|
||||
self.temp_dir = tempfile.mkdtemp()
|
||||
|
||||
# Create test database
|
||||
self.test_db = os.path.join(self.temp_dir, "test.db")
|
||||
self.db_manager = DatabaseManager(self.test_db)
|
||||
|
||||
# Create backup manager
|
||||
self.backup_manager = BackupManager(
|
||||
self.db_manager,
|
||||
os.path.join(self.temp_dir, "backups")
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up test environment."""
|
||||
self.db_manager.close()
|
||||
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
||||
|
||||
def test_create_backup(self):
|
||||
"""Test backup creation."""
|
||||
# Add some test data
|
||||
anime = AnimeMetadata(
|
||||
anime_id="backup-test",
|
||||
name="Backup Test Anime",
|
||||
folder="backup_test"
|
||||
)
|
||||
|
||||
with self.db_manager.get_connection() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO anime_metadata
|
||||
(anime_id, name, folder, created_at, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (anime.anime_id, anime.name, anime.folder,
|
||||
anime.created_at, anime.last_updated))
|
||||
|
||||
# Create backup
|
||||
backup_info = self.backup_manager.create_full_backup("Test backup")
|
||||
|
||||
self.assertIsNotNone(backup_info)
|
||||
self.assertTrue(os.path.exists(backup_info.backup_path))
|
||||
self.assertGreater(backup_info.size_bytes, 0)
|
||||
|
||||
def test_restore_backup(self):
|
||||
"""Test backup restoration."""
|
||||
# Create initial data
|
||||
anime_id = "restore-test"
|
||||
with self.db_manager.get_connection() as conn:
|
||||
conn.execute("""
|
||||
INSERT INTO anime_metadata
|
||||
(anime_id, name, folder, created_at, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
""", (anime_id, "Original", "original_folder",
|
||||
datetime.utcnow(), datetime.utcnow()))
|
||||
|
||||
# Create backup
|
||||
backup_info = self.backup_manager.create_full_backup("Pre-modification backup")
|
||||
|
||||
# Modify data
|
||||
with self.db_manager.get_connection() as conn:
|
||||
conn.execute("""
|
||||
UPDATE anime_metadata SET name = ? WHERE anime_id = ?
|
||||
""", ("Modified", anime_id))
|
||||
|
||||
# Restore backup
|
||||
success = self.backup_manager.restore_backup(backup_info.backup_id)
|
||||
self.assertTrue(success)
|
||||
|
||||
# Verify restoration
|
||||
results = self.db_manager.execute_query(
|
||||
"SELECT name FROM anime_metadata WHERE anime_id = ?",
|
||||
(anime_id,)
|
||||
)
|
||||
self.assertEqual(len(results), 1)
|
||||
self.assertEqual(results[0]['name'], "Original")
|
||||
|
||||
|
||||
class TestConcurrency(unittest.TestCase):
|
||||
"""Test cases for concurrent operations."""
|
||||
|
||||
def test_concurrent_downloads(self):
|
||||
"""Test concurrent download handling."""
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def mock_download(episode_id):
|
||||
"""Mock download function."""
|
||||
try:
|
||||
# Simulate download work
|
||||
threading.Event().wait(0.1)
|
||||
results.append(f"Downloaded {episode_id}")
|
||||
return True
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
return False
|
||||
|
||||
# Create multiple download threads
|
||||
threads = []
|
||||
for i in range(5):
|
||||
thread = threading.Thread(target=mock_download, args=(f"episode_{i}",))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
# Wait for all threads to complete
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
# Verify results
|
||||
self.assertEqual(len(results), 5)
|
||||
self.assertEqual(len(errors), 0)
|
||||
|
||||
def test_database_concurrent_access(self):
|
||||
"""Test concurrent database access."""
|
||||
# Create temporary database
|
||||
temp_db = tempfile.NamedTemporaryFile(delete=False)
|
||||
temp_db.close()
|
||||
|
||||
try:
|
||||
db_manager = DatabaseManager(temp_db.name)
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
def concurrent_insert(thread_id):
|
||||
"""Concurrent database insert operation."""
|
||||
try:
|
||||
anime_id = f"concurrent-{thread_id}"
|
||||
query = """
|
||||
INSERT INTO anime_metadata
|
||||
(anime_id, name, folder, created_at, last_updated)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
"""
|
||||
success = db_manager.execute_update(
|
||||
query,
|
||||
(anime_id, f"Anime {thread_id}", f"folder_{thread_id}",
|
||||
datetime.utcnow(), datetime.utcnow())
|
||||
)
|
||||
if success:
|
||||
results.append(thread_id)
|
||||
except Exception as e:
|
||||
errors.append(str(e))
|
||||
|
||||
# Create concurrent threads
|
||||
threads = []
|
||||
for i in range(10):
|
||||
thread = threading.Thread(target=concurrent_insert, args=(i,))
|
||||
threads.append(thread)
|
||||
thread.start()
|
||||
|
||||
# Wait for completion
|
||||
for thread in threads:
|
||||
thread.join()
|
||||
|
||||
# Verify results
|
||||
self.assertEqual(len(results), 10)
|
||||
self.assertEqual(len(errors), 0)
|
||||
|
||||
# Verify database state
|
||||
count_results = db_manager.execute_query(
|
||||
"SELECT COUNT(*) as count FROM anime_metadata"
|
||||
)
|
||||
self.assertEqual(count_results[0]['count'], 10)
|
||||
|
||||
db_manager.close()
|
||||
finally:
|
||||
os.unlink(temp_db.name)
|
||||
|
||||
|
||||
def run_test_suite():
|
||||
"""Run the complete test suite."""
|
||||
# Create test suite
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add all test cases
|
||||
test_classes = [
|
||||
TestSerie,
|
||||
TestSeriesList,
|
||||
TestDatabaseManager,
|
||||
TestErrorRecoveryManager,
|
||||
TestPerformanceOptimizer,
|
||||
TestAPIIntegration,
|
||||
TestBackupManager,
|
||||
TestConcurrency
|
||||
]
|
||||
|
||||
for test_class in test_classes:
|
||||
tests = unittest.TestLoader().loadTestsFromTestCase(test_class)
|
||||
suite.addTests(tests)
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("Running AniWorld Unit Tests...")
|
||||
print("=" * 50)
|
||||
|
||||
result = run_test_suite()
|
||||
|
||||
print("\n" + "=" * 50)
|
||||
print(f"Tests run: {result.testsRun}")
|
||||
print(f"Failures: {len(result.failures)}")
|
||||
print(f"Errors: {len(result.errors)}")
|
||||
|
||||
if result.failures:
|
||||
print("\nFailures:")
|
||||
for test, traceback in result.failures:
|
||||
print(f"- {test}: {traceback}")
|
||||
|
||||
if result.errors:
|
||||
print("\nErrors:")
|
||||
for test, traceback in result.errors:
|
||||
print(f"- {test}: {traceback}")
|
||||
|
||||
if result.wasSuccessful():
|
||||
print("\nAll tests passed! ✅")
|
||||
sys.exit(0)
|
||||
else:
|
||||
print("\nSome tests failed! ❌")
|
||||
sys.exit(1)
|
||||
@@ -1 +0,0 @@
|
||||
# Test package initialization
|
||||
@@ -1,557 +0,0 @@
|
||||
"""
|
||||
Test cases for anime API endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask
|
||||
import json
|
||||
|
||||
# Mock the database managers first
|
||||
mock_anime_manager = Mock()
|
||||
mock_download_manager = Mock()
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
with patch.dict('sys.modules', {
|
||||
'src.server.data.anime_manager': Mock(AnimeManager=Mock(return_value=mock_anime_manager)),
|
||||
'src.server.data.download_manager': Mock(DownloadManager=Mock(return_value=mock_download_manager))
|
||||
}):
|
||||
from src.server.web.controllers.api.v1.anime import anime_bp
|
||||
except ImportError:
|
||||
anime_bp = None
|
||||
|
||||
|
||||
class TestAnimeEndpoints:
|
||||
"""Test cases for anime API endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(anime_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def setup_method(self):
|
||||
"""Reset mocks before each test."""
|
||||
mock_anime_manager.reset_mock()
|
||||
mock_download_manager.reset_mock()
|
||||
|
||||
def test_list_anime_success(self, client, mock_session):
|
||||
"""Test GET /anime - list anime with pagination."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Mock anime data
|
||||
mock_anime_list = [
|
||||
{'id': 1, 'name': 'Anime 1', 'url': 'https://example.com/1'},
|
||||
{'id': 2, 'name': 'Anime 2', 'url': 'https://example.com/2'}
|
||||
]
|
||||
|
||||
mock_anime_manager.get_all_anime.return_value = mock_anime_list
|
||||
mock_anime_manager.get_anime_count.return_value = 2
|
||||
|
||||
response = client.get('/api/v1/anime?page=1&per_page=10')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert 'data' in data
|
||||
assert 'pagination' in data
|
||||
assert len(data['data']) == 2
|
||||
|
||||
# Verify manager was called with correct parameters
|
||||
mock_anime_manager.get_all_anime.assert_called_once_with(
|
||||
offset=0, limit=10, search=None, status=None, sort_by='name', sort_order='asc'
|
||||
)
|
||||
|
||||
def test_list_anime_with_search(self, client, mock_session):
|
||||
"""Test GET /anime with search parameter."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_all_anime.return_value = []
|
||||
mock_anime_manager.get_anime_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/anime?search=naruto&status=completed')
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_anime_manager.get_all_anime.assert_called_once_with(
|
||||
offset=0, limit=20, search='naruto', status='completed', sort_by='name', sort_order='asc'
|
||||
)
|
||||
|
||||
def test_get_anime_by_id_success(self, client, mock_session):
|
||||
"""Test GET /anime/<id> - get specific anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime',
|
||||
'url': 'https://example.com/1',
|
||||
'description': 'A test anime'
|
||||
}
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = mock_anime
|
||||
|
||||
response = client.get('/api/v1/anime/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['name'] == 'Test Anime'
|
||||
|
||||
mock_anime_manager.get_anime_by_id.assert_called_once_with(1)
|
||||
|
||||
def test_get_anime_by_id_not_found(self, client, mock_session):
|
||||
"""Test GET /anime/<id> - anime not found."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = None
|
||||
|
||||
response = client.get('/api/v1/anime/999')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'not found' in data['error'].lower()
|
||||
|
||||
def test_create_anime_success(self, client, mock_session):
|
||||
"""Test POST /anime - create new anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
anime_data = {
|
||||
'name': 'New Anime',
|
||||
'url': 'https://example.com/new-anime',
|
||||
'description': 'A new anime',
|
||||
'episodes': 12,
|
||||
'status': 'ongoing'
|
||||
}
|
||||
|
||||
mock_anime_manager.create_anime.return_value = 1
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
**anime_data
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/anime',
|
||||
json=anime_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['name'] == 'New Anime'
|
||||
|
||||
mock_anime_manager.create_anime.assert_called_once()
|
||||
|
||||
def test_create_anime_validation_error(self, client, mock_session):
|
||||
"""Test POST /anime - validation error."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Missing required fields
|
||||
anime_data = {
|
||||
'description': 'A new anime'
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/anime',
|
||||
json=anime_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_create_anime_duplicate(self, client, mock_session):
|
||||
"""Test POST /anime - duplicate anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
anime_data = {
|
||||
'name': 'Existing Anime',
|
||||
'url': 'https://example.com/existing'
|
||||
}
|
||||
|
||||
# Simulate duplicate error
|
||||
mock_anime_manager.create_anime.side_effect = Exception("Duplicate entry")
|
||||
|
||||
response = client.post('/api/v1/anime',
|
||||
json=anime_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_update_anime_success(self, client, mock_session):
|
||||
"""Test PUT /anime/<id> - update anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
update_data = {
|
||||
'name': 'Updated Anime',
|
||||
'description': 'Updated description',
|
||||
'status': 'completed'
|
||||
}
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
'name': 'Original Anime'
|
||||
}
|
||||
mock_anime_manager.update_anime.return_value = True
|
||||
|
||||
response = client.put('/api/v1/anime/1',
|
||||
json=update_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_anime_manager.update_anime.assert_called_once_with(1, update_data)
|
||||
|
||||
def test_update_anime_not_found(self, client, mock_session):
|
||||
"""Test PUT /anime/<id> - anime not found."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = None
|
||||
|
||||
response = client.put('/api/v1/anime/999',
|
||||
json={'name': 'Updated'},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_delete_anime_success(self, client, mock_session):
|
||||
"""Test DELETE /anime/<id> - delete anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime'
|
||||
}
|
||||
mock_anime_manager.delete_anime.return_value = True
|
||||
|
||||
response = client.delete('/api/v1/anime/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_anime_manager.delete_anime.assert_called_once_with(1)
|
||||
|
||||
def test_delete_anime_not_found(self, client, mock_session):
|
||||
"""Test DELETE /anime/<id> - anime not found."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = None
|
||||
|
||||
response = client.delete('/api/v1/anime/999')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_bulk_create_anime_success(self, client, mock_session):
|
||||
"""Test POST /anime/bulk - bulk create anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'anime_list': [
|
||||
{'name': 'Anime 1', 'url': 'https://example.com/1'},
|
||||
{'name': 'Anime 2', 'url': 'https://example.com/2'}
|
||||
]
|
||||
}
|
||||
|
||||
mock_anime_manager.bulk_create_anime.return_value = {
|
||||
'created': 2,
|
||||
'failed': 0,
|
||||
'created_ids': [1, 2]
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/anime/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['created'] == 2
|
||||
assert data['data']['failed'] == 0
|
||||
|
||||
def test_bulk_update_anime_success(self, client, mock_session):
|
||||
"""Test PUT /anime/bulk - bulk update anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'updates': [
|
||||
{'id': 1, 'status': 'completed'},
|
||||
{'id': 2, 'status': 'completed'}
|
||||
]
|
||||
}
|
||||
|
||||
mock_anime_manager.bulk_update_anime.return_value = {
|
||||
'updated': 2,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.put('/api/v1/anime/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['updated'] == 2
|
||||
|
||||
def test_bulk_delete_anime_success(self, client, mock_session):
|
||||
"""Test DELETE /anime/bulk - bulk delete anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'anime_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
mock_anime_manager.bulk_delete_anime.return_value = {
|
||||
'deleted': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.delete('/api/v1/anime/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['deleted'] == 3
|
||||
|
||||
def test_get_anime_episodes_success(self, client, mock_session):
|
||||
"""Test GET /anime/<id>/episodes - get anime episodes."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime'
|
||||
}
|
||||
|
||||
mock_episodes = [
|
||||
{'id': 1, 'number': 1, 'title': 'Episode 1'},
|
||||
{'id': 2, 'number': 2, 'title': 'Episode 2'}
|
||||
]
|
||||
|
||||
mock_anime_manager.get_anime_episodes.return_value = mock_episodes
|
||||
|
||||
response = client.get('/api/v1/anime/1/episodes')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['number'] == 1
|
||||
|
||||
def test_get_anime_stats_success(self, client, mock_session):
|
||||
"""Test GET /anime/<id>/stats - get anime statistics."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime'
|
||||
}
|
||||
|
||||
mock_stats = {
|
||||
'total_episodes': 12,
|
||||
'downloaded_episodes': 8,
|
||||
'download_progress': 66.7,
|
||||
'total_size': 1073741824, # 1GB
|
||||
'downloaded_size': 715827882 # ~680MB
|
||||
}
|
||||
|
||||
mock_anime_manager.get_anime_stats.return_value = mock_stats
|
||||
|
||||
response = client.get('/api/v1/anime/1/stats')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['total_episodes'] == 12
|
||||
assert data['data']['downloaded_episodes'] == 8
|
||||
|
||||
def test_search_anime_success(self, client, mock_session):
|
||||
"""Test GET /anime/search - search anime."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_results = [
|
||||
{'id': 1, 'name': 'Naruto', 'url': 'https://example.com/naruto'},
|
||||
{'id': 2, 'name': 'Naruto Shippuden', 'url': 'https://example.com/naruto-shippuden'}
|
||||
]
|
||||
|
||||
mock_anime_manager.search_anime.return_value = mock_results
|
||||
|
||||
response = client.get('/api/v1/anime/search?q=naruto')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert len(data['data']) == 2
|
||||
assert 'naruto' in data['data'][0]['name'].lower()
|
||||
|
||||
mock_anime_manager.search_anime.assert_called_once_with('naruto', limit=20)
|
||||
|
||||
def test_search_anime_no_query(self, client, mock_session):
|
||||
"""Test GET /anime/search - missing search query."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = client.get('/api/v1/anime/search')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'query parameter' in data['error'].lower()
|
||||
|
||||
|
||||
class TestAnimeAuthentication:
|
||||
"""Test cases for anime endpoints authentication."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(anime_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_unauthenticated_read_access(self, client):
|
||||
"""Test that read operations work without authentication."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
mock_anime_manager.get_all_anime.return_value = []
|
||||
mock_anime_manager.get_anime_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/anime')
|
||||
# Should still work for read operations
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_authenticated_write_access(self, client):
|
||||
"""Test that write operations require authentication."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
response = client.post('/api/v1/anime',
|
||||
json={'name': 'Test'},
|
||||
content_type='application/json')
|
||||
# Should require authentication for write operations
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestAnimeErrorHandling:
|
||||
"""Test cases for anime endpoints error handling."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(anime_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def test_database_error_handling(self, client, mock_session):
|
||||
"""Test handling of database errors."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Simulate database error
|
||||
mock_anime_manager.get_all_anime.side_effect = Exception("Database connection failed")
|
||||
|
||||
response = client.get('/api/v1/anime')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_invalid_json_handling(self, client, mock_session):
|
||||
"""Test handling of invalid JSON."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = client.post('/api/v1/anime',
|
||||
data='invalid json',
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_method_not_allowed(self, client):
|
||||
"""Test method not allowed responses."""
|
||||
if not anime_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = client.patch('/api/v1/anime/1')
|
||||
|
||||
assert response.status_code == 405
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,717 +0,0 @@
|
||||
"""
|
||||
Test cases for downloads API endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask
|
||||
import json
|
||||
|
||||
# Mock the database managers first
|
||||
mock_download_manager = Mock()
|
||||
mock_episode_manager = Mock()
|
||||
mock_anime_manager = Mock()
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
with patch.dict('sys.modules', {
|
||||
'src.server.data.download_manager': Mock(DownloadManager=Mock(return_value=mock_download_manager)),
|
||||
'src.server.data.episode_manager': Mock(EpisodeManager=Mock(return_value=mock_episode_manager)),
|
||||
'src.server.data.anime_manager': Mock(AnimeManager=Mock(return_value=mock_anime_manager))
|
||||
}):
|
||||
from src.server.web.controllers.api.v1.downloads import downloads_bp
|
||||
except ImportError:
|
||||
downloads_bp = None
|
||||
|
||||
|
||||
class TestDownloadEndpoints:
|
||||
"""Test cases for download API endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def setup_method(self):
|
||||
"""Reset mocks before each test."""
|
||||
mock_download_manager.reset_mock()
|
||||
mock_episode_manager.reset_mock()
|
||||
mock_anime_manager.reset_mock()
|
||||
|
||||
def test_list_downloads_success(self, client, mock_session):
|
||||
"""Test GET /downloads - list downloads with pagination."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_downloads = [
|
||||
{
|
||||
'id': 1,
|
||||
'anime_id': 1,
|
||||
'episode_id': 1,
|
||||
'status': 'downloading',
|
||||
'progress': 45.5,
|
||||
'size': 1073741824, # 1GB
|
||||
'downloaded_size': 488447385, # ~465MB
|
||||
'speed': 1048576, # 1MB/s
|
||||
'eta': 600, # 10 minutes
|
||||
'created_at': '2023-01-01 12:00:00'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'anime_id': 1,
|
||||
'episode_id': 2,
|
||||
'status': 'completed',
|
||||
'progress': 100.0,
|
||||
'size': 1073741824,
|
||||
'downloaded_size': 1073741824,
|
||||
'created_at': '2023-01-01 11:00:00',
|
||||
'completed_at': '2023-01-01 11:30:00'
|
||||
}
|
||||
]
|
||||
|
||||
mock_download_manager.get_all_downloads.return_value = mock_downloads
|
||||
mock_download_manager.get_downloads_count.return_value = 2
|
||||
|
||||
response = client.get('/api/v1/downloads?page=1&per_page=10')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert 'data' in data
|
||||
assert 'pagination' in data
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['status'] == 'downloading'
|
||||
|
||||
mock_download_manager.get_all_downloads.assert_called_once_with(
|
||||
offset=0, limit=10, status=None, anime_id=None, sort_by='created_at', sort_order='desc'
|
||||
)
|
||||
|
||||
def test_list_downloads_with_filters(self, client, mock_session):
|
||||
"""Test GET /downloads with filters."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_all_downloads.return_value = []
|
||||
mock_download_manager.get_downloads_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/downloads?status=completed&anime_id=5&sort_by=progress')
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_download_manager.get_all_downloads.assert_called_once_with(
|
||||
offset=0, limit=20, status='completed', anime_id=5, sort_by='progress', sort_order='desc'
|
||||
)
|
||||
|
||||
def test_get_download_by_id_success(self, client, mock_session):
|
||||
"""Test GET /downloads/<id> - get specific download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download = {
|
||||
'id': 1,
|
||||
'anime_id': 1,
|
||||
'episode_id': 1,
|
||||
'status': 'downloading',
|
||||
'progress': 75.0,
|
||||
'size': 1073741824,
|
||||
'downloaded_size': 805306368,
|
||||
'speed': 2097152, # 2MB/s
|
||||
'eta': 150, # 2.5 minutes
|
||||
'file_path': '/downloads/anime1/episode1.mp4',
|
||||
'created_at': '2023-01-01 12:00:00',
|
||||
'started_at': '2023-01-01 12:05:00'
|
||||
}
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = mock_download
|
||||
|
||||
response = client.get('/api/v1/downloads/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['progress'] == 75.0
|
||||
assert data['data']['status'] == 'downloading'
|
||||
|
||||
mock_download_manager.get_download_by_id.assert_called_once_with(1)
|
||||
|
||||
def test_get_download_by_id_not_found(self, client, mock_session):
|
||||
"""Test GET /downloads/<id> - download not found."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = None
|
||||
|
||||
response = client.get('/api/v1/downloads/999')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'not found' in data['error'].lower()
|
||||
|
||||
def test_create_download_success(self, client, mock_session):
|
||||
"""Test POST /downloads - create new download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
download_data = {
|
||||
'episode_id': 1,
|
||||
'quality': '1080p',
|
||||
'priority': 'normal'
|
||||
}
|
||||
|
||||
# Mock episode exists
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'anime_id': 1,
|
||||
'title': 'Episode 1',
|
||||
'url': 'https://example.com/episode/1'
|
||||
}
|
||||
|
||||
mock_download_manager.create_download.return_value = 1
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'episode_id': 1,
|
||||
'status': 'queued',
|
||||
'progress': 0.0
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/downloads',
|
||||
json=download_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['status'] == 'queued'
|
||||
|
||||
mock_download_manager.create_download.assert_called_once()
|
||||
|
||||
def test_create_download_invalid_episode(self, client, mock_session):
|
||||
"""Test POST /downloads - invalid episode_id."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
download_data = {
|
||||
'episode_id': 999,
|
||||
'quality': '1080p'
|
||||
}
|
||||
|
||||
# Mock episode doesn't exist
|
||||
mock_episode_manager.get_episode_by_id.return_value = None
|
||||
|
||||
response = client.post('/api/v1/downloads',
|
||||
json=download_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'episode' in data['error'].lower()
|
||||
|
||||
def test_create_download_validation_error(self, client, mock_session):
|
||||
"""Test POST /downloads - validation error."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Missing required fields
|
||||
download_data = {
|
||||
'quality': '1080p'
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/downloads',
|
||||
json=download_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_pause_download_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/<id>/pause - pause download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'downloading'
|
||||
}
|
||||
mock_download_manager.pause_download.return_value = True
|
||||
|
||||
response = client.put('/api/v1/downloads/1/pause')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert 'paused' in data['message'].lower()
|
||||
|
||||
mock_download_manager.pause_download.assert_called_once_with(1)
|
||||
|
||||
def test_pause_download_not_found(self, client, mock_session):
|
||||
"""Test PUT /downloads/<id>/pause - download not found."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = None
|
||||
|
||||
response = client.put('/api/v1/downloads/999/pause')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_resume_download_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/<id>/resume - resume download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'paused'
|
||||
}
|
||||
mock_download_manager.resume_download.return_value = True
|
||||
|
||||
response = client.put('/api/v1/downloads/1/resume')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert 'resumed' in data['message'].lower()
|
||||
|
||||
mock_download_manager.resume_download.assert_called_once_with(1)
|
||||
|
||||
def test_cancel_download_success(self, client, mock_session):
|
||||
"""Test DELETE /downloads/<id> - cancel download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'downloading'
|
||||
}
|
||||
mock_download_manager.cancel_download.return_value = True
|
||||
|
||||
response = client.delete('/api/v1/downloads/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert 'cancelled' in data['message'].lower()
|
||||
|
||||
mock_download_manager.cancel_download.assert_called_once_with(1)
|
||||
|
||||
def test_get_download_queue_success(self, client, mock_session):
|
||||
"""Test GET /downloads/queue - get download queue."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_queue = [
|
||||
{
|
||||
'id': 1,
|
||||
'episode_id': 1,
|
||||
'status': 'downloading',
|
||||
'progress': 25.0,
|
||||
'position': 1
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'episode_id': 2,
|
||||
'status': 'queued',
|
||||
'progress': 0.0,
|
||||
'position': 2
|
||||
}
|
||||
]
|
||||
|
||||
mock_download_manager.get_download_queue.return_value = mock_queue
|
||||
|
||||
response = client.get('/api/v1/downloads/queue')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['status'] == 'downloading'
|
||||
assert data['data'][1]['status'] == 'queued'
|
||||
|
||||
def test_reorder_download_queue_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/queue/reorder - reorder download queue."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
reorder_data = {
|
||||
'download_ids': [3, 1, 2] # New order
|
||||
}
|
||||
|
||||
mock_download_manager.reorder_download_queue.return_value = True
|
||||
|
||||
response = client.put('/api/v1/downloads/queue/reorder',
|
||||
json=reorder_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_download_manager.reorder_download_queue.assert_called_once_with([3, 1, 2])
|
||||
|
||||
def test_clear_download_queue_success(self, client, mock_session):
|
||||
"""Test DELETE /downloads/queue - clear download queue."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.clear_download_queue.return_value = {
|
||||
'cleared': 5,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.delete('/api/v1/downloads/queue')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['cleared'] == 5
|
||||
|
||||
mock_download_manager.clear_download_queue.assert_called_once()
|
||||
|
||||
def test_get_download_history_success(self, client, mock_session):
|
||||
"""Test GET /downloads/history - get download history."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_history = [
|
||||
{
|
||||
'id': 1,
|
||||
'episode_id': 1,
|
||||
'status': 'completed',
|
||||
'completed_at': '2023-01-01 12:30:00',
|
||||
'file_size': 1073741824
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'episode_id': 2,
|
||||
'status': 'failed',
|
||||
'failed_at': '2023-01-01 11:45:00',
|
||||
'error_message': 'Network timeout'
|
||||
}
|
||||
]
|
||||
|
||||
mock_download_manager.get_download_history.return_value = mock_history
|
||||
mock_download_manager.get_history_count.return_value = 2
|
||||
|
||||
response = client.get('/api/v1/downloads/history?page=1&per_page=10')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert 'data' in data
|
||||
assert 'pagination' in data
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['status'] == 'completed'
|
||||
|
||||
def test_bulk_create_downloads_success(self, client, mock_session):
|
||||
"""Test POST /downloads/bulk - bulk create downloads."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'downloads': [
|
||||
{'episode_id': 1, 'quality': '1080p'},
|
||||
{'episode_id': 2, 'quality': '720p'},
|
||||
{'episode_id': 3, 'quality': '1080p'}
|
||||
]
|
||||
}
|
||||
|
||||
mock_download_manager.bulk_create_downloads.return_value = {
|
||||
'created': 3,
|
||||
'failed': 0,
|
||||
'created_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/downloads/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['created'] == 3
|
||||
assert data['data']['failed'] == 0
|
||||
|
||||
def test_bulk_pause_downloads_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/bulk/pause - bulk pause downloads."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'download_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
mock_download_manager.bulk_pause_downloads.return_value = {
|
||||
'paused': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.put('/api/v1/downloads/bulk/pause',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['paused'] == 3
|
||||
|
||||
def test_bulk_resume_downloads_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/bulk/resume - bulk resume downloads."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'download_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
mock_download_manager.bulk_resume_downloads.return_value = {
|
||||
'resumed': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.put('/api/v1/downloads/bulk/resume',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['resumed'] == 3
|
||||
|
||||
def test_bulk_cancel_downloads_success(self, client, mock_session):
|
||||
"""Test DELETE /downloads/bulk - bulk cancel downloads."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'download_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
mock_download_manager.bulk_cancel_downloads.return_value = {
|
||||
'cancelled': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.delete('/api/v1/downloads/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['cancelled'] == 3
|
||||
|
||||
def test_get_download_stats_success(self, client, mock_session):
|
||||
"""Test GET /downloads/stats - get download statistics."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_stats = {
|
||||
'total_downloads': 150,
|
||||
'completed_downloads': 125,
|
||||
'active_downloads': 3,
|
||||
'failed_downloads': 22,
|
||||
'total_size_downloaded': 107374182400, # 100GB
|
||||
'average_speed': 2097152, # 2MB/s
|
||||
'queue_size': 5
|
||||
}
|
||||
|
||||
mock_download_manager.get_download_stats.return_value = mock_stats
|
||||
|
||||
response = client.get('/api/v1/downloads/stats')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['total_downloads'] == 150
|
||||
assert data['data']['completed_downloads'] == 125
|
||||
assert data['data']['active_downloads'] == 3
|
||||
|
||||
def test_retry_failed_download_success(self, client, mock_session):
|
||||
"""Test PUT /downloads/<id>/retry - retry failed download."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'failed'
|
||||
}
|
||||
mock_download_manager.retry_download.return_value = True
|
||||
|
||||
response = client.put('/api/v1/downloads/1/retry')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert 'retrying' in data['message'].lower()
|
||||
|
||||
mock_download_manager.retry_download.assert_called_once_with(1)
|
||||
|
||||
def test_retry_download_invalid_status(self, client, mock_session):
|
||||
"""Test PUT /downloads/<id>/retry - retry download with invalid status."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'completed' # Can't retry completed downloads
|
||||
}
|
||||
|
||||
response = client.put('/api/v1/downloads/1/retry')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'cannot be retried' in data['error'].lower()
|
||||
|
||||
|
||||
class TestDownloadAuthentication:
|
||||
"""Test cases for download endpoints authentication."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_unauthenticated_read_access(self, client):
|
||||
"""Test that read operations work without authentication."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
mock_download_manager.get_all_downloads.return_value = []
|
||||
mock_download_manager.get_downloads_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/downloads')
|
||||
# Should work for read operations
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_authenticated_write_access(self, client):
|
||||
"""Test that write operations require authentication."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
response = client.post('/api/v1/downloads',
|
||||
json={'episode_id': 1},
|
||||
content_type='application/json')
|
||||
# Should require authentication for write operations
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestDownloadErrorHandling:
|
||||
"""Test cases for download endpoints error handling."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(downloads_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def test_database_error_handling(self, client, mock_session):
|
||||
"""Test handling of database errors."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Simulate database error
|
||||
mock_download_manager.get_all_downloads.side_effect = Exception("Database connection failed")
|
||||
|
||||
response = client.get('/api/v1/downloads')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_download_system_error(self, client, mock_session):
|
||||
"""Test handling of download system errors."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'downloading'
|
||||
}
|
||||
|
||||
# Simulate download system error
|
||||
mock_download_manager.pause_download.side_effect = Exception("Download system unavailable")
|
||||
|
||||
response = client.put('/api/v1/downloads/1/pause')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_invalid_download_status_transition(self, client, mock_session):
|
||||
"""Test handling of invalid status transitions."""
|
||||
if not downloads_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_download_manager.get_download_by_id.return_value = {
|
||||
'id': 1,
|
||||
'status': 'completed'
|
||||
}
|
||||
|
||||
# Try to pause a completed download
|
||||
response = client.put('/api/v1/downloads/1/pause')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'cannot be paused' in data['error'].lower()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,679 +0,0 @@
|
||||
"""
|
||||
Test cases for episodes API endpoints.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask
|
||||
import json
|
||||
|
||||
# Mock the database managers first
|
||||
mock_episode_manager = Mock()
|
||||
mock_anime_manager = Mock()
|
||||
mock_download_manager = Mock()
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
with patch.dict('sys.modules', {
|
||||
'src.server.data.episode_manager': Mock(EpisodeManager=Mock(return_value=mock_episode_manager)),
|
||||
'src.server.data.anime_manager': Mock(AnimeManager=Mock(return_value=mock_anime_manager)),
|
||||
'src.server.data.download_manager': Mock(DownloadManager=Mock(return_value=mock_download_manager))
|
||||
}):
|
||||
from src.server.web.controllers.api.v1.episodes import episodes_bp
|
||||
except ImportError:
|
||||
episodes_bp = None
|
||||
|
||||
|
||||
class TestEpisodeEndpoints:
|
||||
"""Test cases for episode API endpoints."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(episodes_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def setup_method(self):
|
||||
"""Reset mocks before each test."""
|
||||
mock_episode_manager.reset_mock()
|
||||
mock_anime_manager.reset_mock()
|
||||
mock_download_manager.reset_mock()
|
||||
|
||||
def test_list_episodes_success(self, client, mock_session):
|
||||
"""Test GET /episodes - list episodes with pagination."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episodes = [
|
||||
{
|
||||
'id': 1,
|
||||
'anime_id': 1,
|
||||
'number': 1,
|
||||
'title': 'Episode 1',
|
||||
'url': 'https://example.com/episode/1',
|
||||
'status': 'available'
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'anime_id': 1,
|
||||
'number': 2,
|
||||
'title': 'Episode 2',
|
||||
'url': 'https://example.com/episode/2',
|
||||
'status': 'available'
|
||||
}
|
||||
]
|
||||
|
||||
mock_episode_manager.get_all_episodes.return_value = mock_episodes
|
||||
mock_episode_manager.get_episodes_count.return_value = 2
|
||||
|
||||
response = client.get('/api/v1/episodes?page=1&per_page=10')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert 'data' in data
|
||||
assert 'pagination' in data
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['number'] == 1
|
||||
|
||||
mock_episode_manager.get_all_episodes.assert_called_once_with(
|
||||
offset=0, limit=10, anime_id=None, status=None, sort_by='number', sort_order='asc'
|
||||
)
|
||||
|
||||
def test_list_episodes_with_filters(self, client, mock_session):
|
||||
"""Test GET /episodes with filters."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_all_episodes.return_value = []
|
||||
mock_episode_manager.get_episodes_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/episodes?anime_id=1&status=downloaded&sort_by=title&sort_order=desc')
|
||||
|
||||
assert response.status_code == 200
|
||||
mock_episode_manager.get_all_episodes.assert_called_once_with(
|
||||
offset=0, limit=20, anime_id=1, status='downloaded', sort_by='title', sort_order='desc'
|
||||
)
|
||||
|
||||
def test_get_episode_by_id_success(self, client, mock_session):
|
||||
"""Test GET /episodes/<id> - get specific episode."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode = {
|
||||
'id': 1,
|
||||
'anime_id': 1,
|
||||
'number': 1,
|
||||
'title': 'First Episode',
|
||||
'url': 'https://example.com/episode/1',
|
||||
'status': 'available',
|
||||
'duration': 1440,
|
||||
'description': 'The first episode'
|
||||
}
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = mock_episode
|
||||
|
||||
response = client.get('/api/v1/episodes/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['title'] == 'First Episode'
|
||||
|
||||
mock_episode_manager.get_episode_by_id.assert_called_once_with(1)
|
||||
|
||||
def test_get_episode_by_id_not_found(self, client, mock_session):
|
||||
"""Test GET /episodes/<id> - episode not found."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = None
|
||||
|
||||
response = client.get('/api/v1/episodes/999')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'not found' in data['error'].lower()
|
||||
|
||||
def test_create_episode_success(self, client, mock_session):
|
||||
"""Test POST /episodes - create new episode."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
episode_data = {
|
||||
'anime_id': 1,
|
||||
'number': 1,
|
||||
'title': 'New Episode',
|
||||
'url': 'https://example.com/new-episode',
|
||||
'duration': 1440,
|
||||
'description': 'A new episode'
|
||||
}
|
||||
|
||||
# Mock anime exists
|
||||
mock_anime_manager.get_anime_by_id.return_value = {'id': 1, 'name': 'Test Anime'}
|
||||
|
||||
mock_episode_manager.create_episode.return_value = 1
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
**episode_data
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/episodes',
|
||||
json=episode_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['id'] == 1
|
||||
assert data['data']['title'] == 'New Episode'
|
||||
|
||||
mock_episode_manager.create_episode.assert_called_once()
|
||||
|
||||
def test_create_episode_invalid_anime(self, client, mock_session):
|
||||
"""Test POST /episodes - invalid anime_id."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
episode_data = {
|
||||
'anime_id': 999,
|
||||
'number': 1,
|
||||
'title': 'New Episode',
|
||||
'url': 'https://example.com/new-episode'
|
||||
}
|
||||
|
||||
# Mock anime doesn't exist
|
||||
mock_anime_manager.get_anime_by_id.return_value = None
|
||||
|
||||
response = client.post('/api/v1/episodes',
|
||||
json=episode_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'anime' in data['error'].lower()
|
||||
|
||||
def test_create_episode_validation_error(self, client, mock_session):
|
||||
"""Test POST /episodes - validation error."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Missing required fields
|
||||
episode_data = {
|
||||
'title': 'New Episode'
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/episodes',
|
||||
json=episode_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_update_episode_success(self, client, mock_session):
|
||||
"""Test PUT /episodes/<id> - update episode."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
update_data = {
|
||||
'title': 'Updated Episode',
|
||||
'description': 'Updated description',
|
||||
'status': 'downloaded'
|
||||
}
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Original Episode'
|
||||
}
|
||||
mock_episode_manager.update_episode.return_value = True
|
||||
|
||||
response = client.put('/api/v1/episodes/1',
|
||||
json=update_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_episode_manager.update_episode.assert_called_once_with(1, update_data)
|
||||
|
||||
def test_update_episode_not_found(self, client, mock_session):
|
||||
"""Test PUT /episodes/<id> - episode not found."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = None
|
||||
|
||||
response = client.put('/api/v1/episodes/999',
|
||||
json={'title': 'Updated'},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_delete_episode_success(self, client, mock_session):
|
||||
"""Test DELETE /episodes/<id> - delete episode."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode'
|
||||
}
|
||||
mock_episode_manager.delete_episode.return_value = True
|
||||
|
||||
response = client.delete('/api/v1/episodes/1')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_episode_manager.delete_episode.assert_called_once_with(1)
|
||||
|
||||
def test_delete_episode_not_found(self, client, mock_session):
|
||||
"""Test DELETE /episodes/<id> - episode not found."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = None
|
||||
|
||||
response = client.delete('/api/v1/episodes/999')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_bulk_create_episodes_success(self, client, mock_session):
|
||||
"""Test POST /episodes/bulk - bulk create episodes."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'episodes': [
|
||||
{'anime_id': 1, 'number': 1, 'title': 'Episode 1', 'url': 'https://example.com/1'},
|
||||
{'anime_id': 1, 'number': 2, 'title': 'Episode 2', 'url': 'https://example.com/2'}
|
||||
]
|
||||
}
|
||||
|
||||
mock_episode_manager.bulk_create_episodes.return_value = {
|
||||
'created': 2,
|
||||
'failed': 0,
|
||||
'created_ids': [1, 2]
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/episodes/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['created'] == 2
|
||||
assert data['data']['failed'] == 0
|
||||
|
||||
def test_bulk_update_status_success(self, client, mock_session):
|
||||
"""Test PUT /episodes/bulk/status - bulk update episode status."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'episode_ids': [1, 2, 3],
|
||||
'status': 'downloaded'
|
||||
}
|
||||
|
||||
mock_episode_manager.bulk_update_status.return_value = {
|
||||
'updated': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.put('/api/v1/episodes/bulk/status',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['updated'] == 3
|
||||
|
||||
def test_bulk_delete_episodes_success(self, client, mock_session):
|
||||
"""Test DELETE /episodes/bulk - bulk delete episodes."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
bulk_data = {
|
||||
'episode_ids': [1, 2, 3]
|
||||
}
|
||||
|
||||
mock_episode_manager.bulk_delete_episodes.return_value = {
|
||||
'deleted': 3,
|
||||
'failed': 0
|
||||
}
|
||||
|
||||
response = client.delete('/api/v1/episodes/bulk',
|
||||
json=bulk_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['deleted'] == 3
|
||||
|
||||
def test_sync_episodes_success(self, client, mock_session):
|
||||
"""Test POST /episodes/sync - sync episodes for anime."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
sync_data = {
|
||||
'anime_id': 1
|
||||
}
|
||||
|
||||
# Mock anime exists
|
||||
mock_anime_manager.get_anime_by_id.return_value = {'id': 1, 'name': 'Test Anime'}
|
||||
|
||||
mock_episode_manager.sync_episodes.return_value = {
|
||||
'anime_id': 1,
|
||||
'episodes_found': 12,
|
||||
'episodes_added': 5,
|
||||
'episodes_updated': 2
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/episodes/sync',
|
||||
json=sync_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['episodes_found'] == 12
|
||||
assert data['data']['episodes_added'] == 5
|
||||
|
||||
mock_episode_manager.sync_episodes.assert_called_once_with(1)
|
||||
|
||||
def test_sync_episodes_invalid_anime(self, client, mock_session):
|
||||
"""Test POST /episodes/sync - invalid anime_id."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
sync_data = {
|
||||
'anime_id': 999
|
||||
}
|
||||
|
||||
# Mock anime doesn't exist
|
||||
mock_anime_manager.get_anime_by_id.return_value = None
|
||||
|
||||
response = client.post('/api/v1/episodes/sync',
|
||||
json=sync_data,
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'anime' in data['error'].lower()
|
||||
|
||||
def test_get_episode_download_info_success(self, client, mock_session):
|
||||
"""Test GET /episodes/<id>/download - get episode download info."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode'
|
||||
}
|
||||
|
||||
mock_download_info = {
|
||||
'episode_id': 1,
|
||||
'download_id': 5,
|
||||
'status': 'downloading',
|
||||
'progress': 45.5,
|
||||
'speed': 1048576, # 1MB/s
|
||||
'eta': 300, # 5 minutes
|
||||
'file_path': '/downloads/episode1.mp4'
|
||||
}
|
||||
|
||||
mock_download_manager.get_episode_download_info.return_value = mock_download_info
|
||||
|
||||
response = client.get('/api/v1/episodes/1/download')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['status'] == 'downloading'
|
||||
assert data['data']['progress'] == 45.5
|
||||
|
||||
def test_get_episode_download_info_not_downloading(self, client, mock_session):
|
||||
"""Test GET /episodes/<id>/download - episode not downloading."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode'
|
||||
}
|
||||
|
||||
mock_download_manager.get_episode_download_info.return_value = None
|
||||
|
||||
response = client.get('/api/v1/episodes/1/download')
|
||||
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
assert 'download' in data['error'].lower()
|
||||
|
||||
def test_start_episode_download_success(self, client, mock_session):
|
||||
"""Test POST /episodes/<id>/download - start episode download."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode',
|
||||
'url': 'https://example.com/episode/1'
|
||||
}
|
||||
|
||||
mock_download_manager.start_episode_download.return_value = {
|
||||
'download_id': 5,
|
||||
'status': 'queued',
|
||||
'message': 'Download queued successfully'
|
||||
}
|
||||
|
||||
response = client.post('/api/v1/episodes/1/download')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert data['data']['download_id'] == 5
|
||||
assert data['data']['status'] == 'queued'
|
||||
|
||||
mock_download_manager.start_episode_download.assert_called_once_with(1)
|
||||
|
||||
def test_cancel_episode_download_success(self, client, mock_session):
|
||||
"""Test DELETE /episodes/<id>/download - cancel episode download."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode'
|
||||
}
|
||||
|
||||
mock_download_manager.cancel_episode_download.return_value = True
|
||||
|
||||
response = client.delete('/api/v1/episodes/1/download')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
|
||||
mock_download_manager.cancel_episode_download.assert_called_once_with(1)
|
||||
|
||||
def test_get_episodes_by_anime_success(self, client, mock_session):
|
||||
"""Test GET /anime/<anime_id>/episodes - get episodes for anime."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_anime_manager.get_anime_by_id.return_value = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime'
|
||||
}
|
||||
|
||||
mock_episodes = [
|
||||
{'id': 1, 'number': 1, 'title': 'Episode 1'},
|
||||
{'id': 2, 'number': 2, 'title': 'Episode 2'}
|
||||
]
|
||||
|
||||
mock_episode_manager.get_episodes_by_anime.return_value = mock_episodes
|
||||
|
||||
response = client.get('/api/v1/anime/1/episodes')
|
||||
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['success'] is True
|
||||
assert len(data['data']) == 2
|
||||
assert data['data'][0]['number'] == 1
|
||||
|
||||
mock_episode_manager.get_episodes_by_anime.assert_called_once_with(1)
|
||||
|
||||
|
||||
class TestEpisodeAuthentication:
|
||||
"""Test cases for episode endpoints authentication."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(episodes_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_unauthenticated_read_access(self, client):
|
||||
"""Test that read operations work without authentication."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
mock_episode_manager.get_all_episodes.return_value = []
|
||||
mock_episode_manager.get_episodes_count.return_value = 0
|
||||
|
||||
response = client.get('/api/v1/episodes')
|
||||
# Should work for read operations
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_authenticated_write_access(self, client):
|
||||
"""Test that write operations require authentication."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = None # No authentication
|
||||
|
||||
response = client.post('/api/v1/episodes',
|
||||
json={'title': 'Test'},
|
||||
content_type='application/json')
|
||||
# Should require authentication for write operations
|
||||
assert response.status_code == 401
|
||||
|
||||
|
||||
class TestEpisodeErrorHandling:
|
||||
"""Test cases for episode endpoints error handling."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.register_blueprint(episodes_bp, url_prefix='/api/v1')
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session(self):
|
||||
"""Mock session for authentication."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session') as mock_session:
|
||||
mock_session.get.return_value = {'user_id': 1, 'username': 'testuser'}
|
||||
yield mock_session
|
||||
|
||||
def test_database_error_handling(self, client, mock_session):
|
||||
"""Test handling of database errors."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Simulate database error
|
||||
mock_episode_manager.get_all_episodes.side_effect = Exception("Database connection failed")
|
||||
|
||||
response = client.get('/api/v1/episodes')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
def test_invalid_episode_id_parameter(self, client, mock_session):
|
||||
"""Test handling of invalid episode ID parameter."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = client.get('/api/v1/episodes/invalid-id')
|
||||
|
||||
assert response.status_code == 404 # Flask will handle this as route not found
|
||||
|
||||
def test_concurrent_modification_error(self, client, mock_session):
|
||||
"""Test handling of concurrent modification errors."""
|
||||
if not episodes_bp:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_episode_manager.get_episode_by_id.return_value = {
|
||||
'id': 1,
|
||||
'title': 'Test Episode'
|
||||
}
|
||||
|
||||
# Simulate concurrent modification
|
||||
mock_episode_manager.update_episode.side_effect = Exception("Episode was modified by another process")
|
||||
|
||||
response = client.put('/api/v1/episodes/1',
|
||||
json={'title': 'Updated'},
|
||||
content_type='application/json')
|
||||
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert 'error' in data
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,330 +0,0 @@
|
||||
"""
|
||||
Test cases for authentication decorators and utilities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask, request, session, jsonify
|
||||
import json
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
from src.server.web.controllers.shared.auth_decorators import (
|
||||
require_auth, optional_auth, get_current_user, get_client_ip,
|
||||
is_authenticated, logout_current_user
|
||||
)
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
require_auth = None
|
||||
optional_auth = None
|
||||
|
||||
|
||||
class TestAuthDecorators:
|
||||
"""Test cases for authentication decorators."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.secret_key = 'test-secret-key'
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_manager(self):
|
||||
"""Create a mock session manager."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session_manager') as mock:
|
||||
yield mock
|
||||
|
||||
def test_require_auth_authenticated_user(self, app, client, mock_session_manager):
|
||||
"""Test require_auth decorator with authenticated user."""
|
||||
if not require_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = True
|
||||
|
||||
@app.route('/test')
|
||||
@require_auth
|
||||
def test_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['message'] == 'success'
|
||||
|
||||
def test_require_auth_unauthenticated_api_request(self, app, client, mock_session_manager):
|
||||
"""Test require_auth decorator with unauthenticated API request."""
|
||||
if not require_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
@app.route('/api/test')
|
||||
@require_auth
|
||||
def test_api_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/api/test')
|
||||
assert response.status_code == 401
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['code'] == 'AUTH_REQUIRED'
|
||||
|
||||
def test_require_auth_unauthenticated_json_request(self, app, client, mock_session_manager):
|
||||
"""Test require_auth decorator with unauthenticated JSON request."""
|
||||
if not require_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
@app.route('/test')
|
||||
@require_auth
|
||||
def test_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/test', headers={'Accept': 'application/json'})
|
||||
assert response.status_code == 401
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['code'] == 'AUTH_REQUIRED'
|
||||
|
||||
def test_optional_auth_no_master_password(self, app, client, mock_session_manager):
|
||||
"""Test optional_auth decorator when no master password is configured."""
|
||||
if not optional_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
with patch('config.config') as mock_config:
|
||||
mock_config.has_master_password.return_value = False
|
||||
|
||||
@app.route('/test')
|
||||
@optional_auth
|
||||
def test_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['message'] == 'success'
|
||||
|
||||
def test_optional_auth_with_master_password_authenticated(self, app, client, mock_session_manager):
|
||||
"""Test optional_auth decorator with master password and authenticated user."""
|
||||
if not optional_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = True
|
||||
|
||||
with patch('config.config') as mock_config:
|
||||
mock_config.has_master_password.return_value = True
|
||||
|
||||
@app.route('/test')
|
||||
@optional_auth
|
||||
def test_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['message'] == 'success'
|
||||
|
||||
def test_optional_auth_with_master_password_unauthenticated(self, app, client, mock_session_manager):
|
||||
"""Test optional_auth decorator with master password and unauthenticated user."""
|
||||
if not optional_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
with patch('config.config') as mock_config:
|
||||
mock_config.has_master_password.return_value = True
|
||||
|
||||
@app.route('/api/test')
|
||||
@optional_auth
|
||||
def test_endpoint():
|
||||
return jsonify({'message': 'success'})
|
||||
|
||||
response = client.get('/api/test')
|
||||
assert response.status_code == 401
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['code'] == 'AUTH_REQUIRED'
|
||||
|
||||
|
||||
class TestAuthUtilities:
|
||||
"""Test cases for authentication utility functions."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def mock_session_manager(self):
|
||||
"""Create a mock session manager."""
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session_manager') as mock:
|
||||
yield mock
|
||||
|
||||
def test_get_client_ip_direct(self, app):
|
||||
"""Test get_client_ip with direct IP."""
|
||||
if not get_client_ip:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with app.test_request_context('/', environ_base={'REMOTE_ADDR': '192.168.1.100'}):
|
||||
ip = get_client_ip()
|
||||
assert ip == '192.168.1.100'
|
||||
|
||||
def test_get_client_ip_forwarded(self, app):
|
||||
"""Test get_client_ip with X-Forwarded-For header."""
|
||||
if not get_client_ip:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with app.test_request_context('/', headers={'X-Forwarded-For': '203.0.113.1, 192.168.1.100'}):
|
||||
ip = get_client_ip()
|
||||
assert ip == '203.0.113.1'
|
||||
|
||||
def test_get_client_ip_real_ip(self, app):
|
||||
"""Test get_client_ip with X-Real-IP header."""
|
||||
if not get_client_ip:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with app.test_request_context('/', headers={'X-Real-IP': '203.0.113.2'}):
|
||||
ip = get_client_ip()
|
||||
assert ip == '203.0.113.2'
|
||||
|
||||
def test_get_client_ip_unknown(self, app):
|
||||
"""Test get_client_ip with no IP information."""
|
||||
if not get_client_ip:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with app.test_request_context('/'):
|
||||
ip = get_client_ip()
|
||||
assert ip == 'unknown'
|
||||
|
||||
def test_get_current_user_authenticated(self, app, mock_session_manager):
|
||||
"""Test get_current_user with authenticated user."""
|
||||
if not get_current_user:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_info = {
|
||||
'user': 'admin',
|
||||
'login_time': '2023-01-01T00:00:00',
|
||||
'ip_address': '192.168.1.100'
|
||||
}
|
||||
mock_session_manager.is_authenticated.return_value = True
|
||||
mock_session_manager.get_session_info.return_value = mock_session_info
|
||||
|
||||
with app.test_request_context('/'):
|
||||
user = get_current_user()
|
||||
assert user == mock_session_info
|
||||
|
||||
def test_get_current_user_unauthenticated(self, app, mock_session_manager):
|
||||
"""Test get_current_user with unauthenticated user."""
|
||||
if not get_current_user:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
with app.test_request_context('/'):
|
||||
user = get_current_user()
|
||||
assert user is None
|
||||
|
||||
def test_is_authenticated_true(self, app, mock_session_manager):
|
||||
"""Test is_authenticated returns True."""
|
||||
if not is_authenticated:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = True
|
||||
|
||||
with app.test_request_context('/'):
|
||||
result = is_authenticated()
|
||||
assert result is True
|
||||
|
||||
def test_is_authenticated_false(self, app, mock_session_manager):
|
||||
"""Test is_authenticated returns False."""
|
||||
if not is_authenticated:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.is_authenticated.return_value = False
|
||||
|
||||
with app.test_request_context('/'):
|
||||
result = is_authenticated()
|
||||
assert result is False
|
||||
|
||||
def test_logout_current_user(self, app, mock_session_manager):
|
||||
"""Test logout_current_user function."""
|
||||
if not logout_current_user:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_session_manager.logout.return_value = True
|
||||
|
||||
with app.test_request_context('/'):
|
||||
result = logout_current_user()
|
||||
assert result is True
|
||||
mock_session_manager.logout.assert_called_once_with(None)
|
||||
|
||||
|
||||
class TestAuthDecoratorIntegration:
|
||||
"""Integration tests for authentication decorators."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
app.secret_key = 'test-secret-key'
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_decorator_preserves_function_metadata(self, app):
|
||||
"""Test that decorators preserve function metadata."""
|
||||
if not require_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@require_auth
|
||||
def test_function():
|
||||
"""Test function docstring."""
|
||||
return 'test'
|
||||
|
||||
assert test_function.__name__ == 'test_function'
|
||||
assert test_function.__doc__ == 'Test function docstring.'
|
||||
|
||||
def test_multiple_decorators(self, app, client):
|
||||
"""Test using multiple decorators together."""
|
||||
if not require_auth or not optional_auth:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
with patch('src.server.web.controllers.shared.auth_decorators.session_manager') as mock_sm:
|
||||
mock_sm.is_authenticated.return_value = True
|
||||
|
||||
@app.route('/test1')
|
||||
@require_auth
|
||||
def test_endpoint1():
|
||||
return jsonify({'endpoint': 'test1'})
|
||||
|
||||
@app.route('/test2')
|
||||
@optional_auth
|
||||
def test_endpoint2():
|
||||
return jsonify({'endpoint': 'test2'})
|
||||
|
||||
# Test both endpoints
|
||||
response1 = client.get('/test1')
|
||||
assert response1.status_code == 200
|
||||
|
||||
response2 = client.get('/test2')
|
||||
assert response2.status_code == 200
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,455 +0,0 @@
|
||||
"""
|
||||
Test cases for error handling decorators and utilities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask, request, jsonify
|
||||
import json
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
from src.server.web.controllers.shared.error_handlers import (
|
||||
handle_api_errors, handle_database_errors, handle_file_operations,
|
||||
create_error_response, create_success_response, APIException,
|
||||
ValidationError, NotFoundError, PermissionError
|
||||
)
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
handle_api_errors = None
|
||||
handle_database_errors = None
|
||||
handle_file_operations = None
|
||||
create_error_response = None
|
||||
create_success_response = None
|
||||
APIException = None
|
||||
ValidationError = None
|
||||
NotFoundError = None
|
||||
PermissionError = None
|
||||
|
||||
|
||||
class TestErrorHandlingDecorators:
|
||||
"""Test cases for error handling decorators."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_handle_api_errors_success(self, app, client):
|
||||
"""Test handle_api_errors decorator with successful function."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
return {'message': 'success'}
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'success'
|
||||
|
||||
def test_handle_api_errors_with_status_code(self, app, client):
|
||||
"""Test handle_api_errors decorator with tuple return."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
return {'message': 'created'}, 201
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 201
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'success'
|
||||
assert data['message'] == 'created'
|
||||
|
||||
def test_handle_api_errors_value_error(self, app, client):
|
||||
"""Test handle_api_errors decorator with ValueError."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
raise ValueError("Invalid input")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'VALIDATION_ERROR'
|
||||
assert 'Invalid input' in data['message']
|
||||
|
||||
def test_handle_api_errors_permission_error(self, app, client):
|
||||
"""Test handle_api_errors decorator with PermissionError."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
raise PermissionError("Access denied")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 403
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'ACCESS_DENIED'
|
||||
assert data['message'] == 'Access denied'
|
||||
|
||||
def test_handle_api_errors_file_not_found(self, app, client):
|
||||
"""Test handle_api_errors decorator with FileNotFoundError."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
raise FileNotFoundError("File not found")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'NOT_FOUND'
|
||||
assert data['message'] == 'Resource not found'
|
||||
|
||||
def test_handle_api_errors_generic_exception(self, app, client):
|
||||
"""Test handle_api_errors decorator with generic Exception."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
raise Exception("Something went wrong")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'INTERNAL_ERROR'
|
||||
assert data['message'] == 'Internal server error'
|
||||
|
||||
def test_handle_database_errors(self, app, client):
|
||||
"""Test handle_database_errors decorator."""
|
||||
if not handle_database_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_database_errors
|
||||
def test_endpoint():
|
||||
raise Exception("Database connection failed")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'DATABASE_ERROR'
|
||||
|
||||
def test_handle_file_operations_file_not_found(self, app, client):
|
||||
"""Test handle_file_operations decorator with FileNotFoundError."""
|
||||
if not handle_file_operations:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_file_operations
|
||||
def test_endpoint():
|
||||
raise FileNotFoundError("File not found")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 404
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'FILE_NOT_FOUND'
|
||||
|
||||
def test_handle_file_operations_permission_error(self, app, client):
|
||||
"""Test handle_file_operations decorator with PermissionError."""
|
||||
if not handle_file_operations:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_file_operations
|
||||
def test_endpoint():
|
||||
raise PermissionError("Permission denied")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 403
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'PERMISSION_DENIED'
|
||||
|
||||
def test_handle_file_operations_os_error(self, app, client):
|
||||
"""Test handle_file_operations decorator with OSError."""
|
||||
if not handle_file_operations:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_file_operations
|
||||
def test_endpoint():
|
||||
raise OSError("File system error")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 500
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
assert data['error_code'] == 'FILE_SYSTEM_ERROR'
|
||||
|
||||
|
||||
class TestResponseHelpers:
|
||||
"""Test cases for response helper functions."""
|
||||
|
||||
def test_create_error_response_basic(self):
|
||||
"""Test create_error_response with basic parameters."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_error_response("Test error")
|
||||
|
||||
assert status_code == 400
|
||||
assert response['status'] == 'error'
|
||||
assert response['message'] == 'Test error'
|
||||
|
||||
def test_create_error_response_with_code(self):
|
||||
"""Test create_error_response with error code."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_error_response(
|
||||
"Test error",
|
||||
status_code=422,
|
||||
error_code="VALIDATION_ERROR"
|
||||
)
|
||||
|
||||
assert status_code == 422
|
||||
assert response['status'] == 'error'
|
||||
assert response['message'] == 'Test error'
|
||||
assert response['error_code'] == 'VALIDATION_ERROR'
|
||||
|
||||
def test_create_error_response_with_errors_list(self):
|
||||
"""Test create_error_response with errors list."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
errors = ["Field 1 is required", "Field 2 is invalid"]
|
||||
response, status_code = create_error_response(
|
||||
"Validation failed",
|
||||
errors=errors
|
||||
)
|
||||
|
||||
assert response['errors'] == errors
|
||||
|
||||
def test_create_error_response_with_data(self):
|
||||
"""Test create_error_response with additional data."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {"field": "value"}
|
||||
response, status_code = create_error_response(
|
||||
"Test error",
|
||||
data=data
|
||||
)
|
||||
|
||||
assert response['data'] == data
|
||||
|
||||
def test_create_success_response_basic(self):
|
||||
"""Test create_success_response with basic parameters."""
|
||||
if not create_success_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_success_response()
|
||||
|
||||
assert status_code == 200
|
||||
assert response['status'] == 'success'
|
||||
assert response['message'] == 'Operation successful'
|
||||
|
||||
def test_create_success_response_with_data(self):
|
||||
"""Test create_success_response with data."""
|
||||
if not create_success_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {"id": 1, "name": "Test"}
|
||||
response, status_code = create_success_response(
|
||||
data=data,
|
||||
message="Created successfully",
|
||||
status_code=201
|
||||
)
|
||||
|
||||
assert status_code == 201
|
||||
assert response['status'] == 'success'
|
||||
assert response['message'] == 'Created successfully'
|
||||
assert response['data'] == data
|
||||
|
||||
|
||||
class TestCustomExceptions:
|
||||
"""Test cases for custom exception classes."""
|
||||
|
||||
def test_api_exception_basic(self):
|
||||
"""Test APIException with basic parameters."""
|
||||
if not APIException:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = APIException("Test error")
|
||||
|
||||
assert str(exception) == "Test error"
|
||||
assert exception.message == "Test error"
|
||||
assert exception.status_code == 400
|
||||
assert exception.error_code is None
|
||||
assert exception.errors is None
|
||||
|
||||
def test_api_exception_full(self):
|
||||
"""Test APIException with all parameters."""
|
||||
if not APIException:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
errors = ["Error 1", "Error 2"]
|
||||
exception = APIException(
|
||||
"Test error",
|
||||
status_code=422,
|
||||
error_code="CUSTOM_ERROR",
|
||||
errors=errors
|
||||
)
|
||||
|
||||
assert exception.message == "Test error"
|
||||
assert exception.status_code == 422
|
||||
assert exception.error_code == "CUSTOM_ERROR"
|
||||
assert exception.errors == errors
|
||||
|
||||
def test_validation_error(self):
|
||||
"""Test ValidationError exception."""
|
||||
if not ValidationError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = ValidationError("Invalid input")
|
||||
|
||||
assert exception.message == "Invalid input"
|
||||
assert exception.status_code == 400
|
||||
assert exception.error_code == "VALIDATION_ERROR"
|
||||
|
||||
def test_validation_error_with_errors(self):
|
||||
"""Test ValidationError with errors list."""
|
||||
if not ValidationError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
errors = ["Field 1 is required", "Field 2 is invalid"]
|
||||
exception = ValidationError("Validation failed", errors=errors)
|
||||
|
||||
assert exception.message == "Validation failed"
|
||||
assert exception.errors == errors
|
||||
|
||||
def test_not_found_error(self):
|
||||
"""Test NotFoundError exception."""
|
||||
if not NotFoundError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = NotFoundError("Resource not found")
|
||||
|
||||
assert exception.message == "Resource not found"
|
||||
assert exception.status_code == 404
|
||||
assert exception.error_code == "NOT_FOUND"
|
||||
|
||||
def test_not_found_error_default(self):
|
||||
"""Test NotFoundError with default message."""
|
||||
if not NotFoundError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = NotFoundError()
|
||||
|
||||
assert exception.message == "Resource not found"
|
||||
assert exception.status_code == 404
|
||||
|
||||
def test_permission_error_custom(self):
|
||||
"""Test custom PermissionError exception."""
|
||||
if not PermissionError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = PermissionError("Custom access denied")
|
||||
|
||||
assert exception.message == "Custom access denied"
|
||||
assert exception.status_code == 403
|
||||
assert exception.error_code == "ACCESS_DENIED"
|
||||
|
||||
def test_permission_error_default(self):
|
||||
"""Test PermissionError with default message."""
|
||||
if not PermissionError:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
exception = PermissionError()
|
||||
|
||||
assert exception.message == "Access denied"
|
||||
assert exception.status_code == 403
|
||||
|
||||
|
||||
class TestErrorHandlerIntegration:
|
||||
"""Integration tests for error handling."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_nested_decorators(self, app, client):
|
||||
"""Test nested error handling decorators."""
|
||||
if not handle_api_errors or not handle_database_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
@handle_database_errors
|
||||
def test_endpoint():
|
||||
raise ValueError("Test error")
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert data['status'] == 'error'
|
||||
|
||||
def test_decorator_preserves_metadata(self):
|
||||
"""Test that decorators preserve function metadata."""
|
||||
if not handle_api_errors:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@handle_api_errors
|
||||
def test_function():
|
||||
"""Test function docstring."""
|
||||
return "test"
|
||||
|
||||
assert test_function.__name__ == "test_function"
|
||||
assert test_function.__doc__ == "Test function docstring."
|
||||
|
||||
def test_custom_exception_handling(self, app, client):
|
||||
"""Test handling of custom exceptions."""
|
||||
if not handle_api_errors or not APIException:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@handle_api_errors
|
||||
def test_endpoint():
|
||||
raise APIException("Custom error", status_code=422, error_code="CUSTOM")
|
||||
|
||||
# Note: The current implementation doesn't handle APIException specifically
|
||||
# This test documents the current behavior
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 500 # Falls through to generic Exception handling
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,560 +0,0 @@
|
||||
"""
|
||||
Test cases for response helper utilities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
from src.server.web.controllers.shared.response_helpers import (
|
||||
create_response, create_error_response, create_success_response,
|
||||
create_paginated_response, format_anime_data, format_episode_data,
|
||||
format_download_data, format_user_data, format_datetime, format_file_size,
|
||||
add_cors_headers, create_api_response
|
||||
)
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
create_response = None
|
||||
create_error_response = None
|
||||
create_success_response = None
|
||||
create_paginated_response = None
|
||||
format_anime_data = None
|
||||
format_episode_data = None
|
||||
format_download_data = None
|
||||
format_user_data = None
|
||||
format_datetime = None
|
||||
format_file_size = None
|
||||
add_cors_headers = None
|
||||
create_api_response = None
|
||||
|
||||
|
||||
class TestResponseCreation:
|
||||
"""Test cases for response creation functions."""
|
||||
|
||||
def test_create_response_success(self):
|
||||
"""Test create_response with success data."""
|
||||
if not create_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {'test': 'data'}
|
||||
response, status_code = create_response(data, 200)
|
||||
|
||||
assert status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['test'] == 'data'
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_create_response_with_headers(self):
|
||||
"""Test create_response with custom headers."""
|
||||
if not create_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {'test': 'data'}
|
||||
headers = {'X-Custom-Header': 'test-value'}
|
||||
response, status_code = create_response(data, 200, headers)
|
||||
|
||||
assert response.headers.get('X-Custom-Header') == 'test-value'
|
||||
|
||||
def test_create_error_response_basic(self):
|
||||
"""Test create_error_response with basic error."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_error_response("Test error", 400)
|
||||
|
||||
assert status_code == 400
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['error'] == 'Test error'
|
||||
assert response_data['status'] == 'error'
|
||||
|
||||
def test_create_error_response_with_details(self):
|
||||
"""Test create_error_response with error details."""
|
||||
if not create_error_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
details = {'field': 'name', 'issue': 'required'}
|
||||
response, status_code = create_error_response("Validation error", 422, details)
|
||||
|
||||
assert status_code == 422
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['error'] == 'Validation error'
|
||||
assert response_data['details'] == details
|
||||
|
||||
def test_create_success_response_basic(self):
|
||||
"""Test create_success_response with basic success."""
|
||||
if not create_success_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_success_response("Operation successful")
|
||||
|
||||
assert status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['message'] == 'Operation successful'
|
||||
assert response_data['status'] == 'success'
|
||||
|
||||
def test_create_success_response_with_data(self):
|
||||
"""Test create_success_response with data."""
|
||||
if not create_success_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {'created_id': 123}
|
||||
response, status_code = create_success_response("Created successfully", 201, data)
|
||||
|
||||
assert status_code == 201
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['message'] == 'Created successfully'
|
||||
assert response_data['data'] == data
|
||||
|
||||
def test_create_api_response_success(self):
|
||||
"""Test create_api_response for success case."""
|
||||
if not create_api_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {'test': 'data'}
|
||||
response, status_code = create_api_response(data, success=True)
|
||||
|
||||
assert status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['success'] is True
|
||||
assert response_data['data'] == data
|
||||
|
||||
def test_create_api_response_error(self):
|
||||
"""Test create_api_response for error case."""
|
||||
if not create_api_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
error_msg = "Something went wrong"
|
||||
response, status_code = create_api_response(error_msg, success=False, status_code=500)
|
||||
|
||||
assert status_code == 500
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['success'] is False
|
||||
assert response_data['error'] == error_msg
|
||||
|
||||
|
||||
class TestPaginatedResponse:
|
||||
"""Test cases for paginated response creation."""
|
||||
|
||||
def test_create_paginated_response_basic(self):
|
||||
"""Test create_paginated_response with basic pagination."""
|
||||
if not create_paginated_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
items = [{'id': 1}, {'id': 2}, {'id': 3}]
|
||||
page = 1
|
||||
per_page = 10
|
||||
total = 25
|
||||
|
||||
response, status_code = create_paginated_response(items, page, per_page, total)
|
||||
|
||||
assert status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['data'] == items
|
||||
assert response_data['pagination']['page'] == 1
|
||||
assert response_data['pagination']['per_page'] == 10
|
||||
assert response_data['pagination']['total'] == 25
|
||||
assert response_data['pagination']['pages'] == 3 # ceil(25/10)
|
||||
|
||||
def test_create_paginated_response_with_endpoint(self):
|
||||
"""Test create_paginated_response with endpoint for links."""
|
||||
if not create_paginated_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
items = [{'id': 1}]
|
||||
page = 2
|
||||
per_page = 5
|
||||
total = 20
|
||||
endpoint = '/api/items'
|
||||
|
||||
response, status_code = create_paginated_response(
|
||||
items, page, per_page, total, endpoint=endpoint
|
||||
)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
links = response_data['pagination']['links']
|
||||
assert '/api/items?page=1' in links['first']
|
||||
assert '/api/items?page=3' in links['next']
|
||||
assert '/api/items?page=1' in links['prev']
|
||||
assert '/api/items?page=4' in links['last']
|
||||
|
||||
def test_create_paginated_response_first_page(self):
|
||||
"""Test create_paginated_response on first page."""
|
||||
if not create_paginated_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
items = [{'id': 1}]
|
||||
response, status_code = create_paginated_response(items, 1, 10, 20)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
pagination = response_data['pagination']
|
||||
assert pagination['has_prev'] is False
|
||||
assert pagination['has_next'] is True
|
||||
|
||||
def test_create_paginated_response_last_page(self):
|
||||
"""Test create_paginated_response on last page."""
|
||||
if not create_paginated_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
items = [{'id': 1}]
|
||||
response, status_code = create_paginated_response(items, 3, 10, 25)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
pagination = response_data['pagination']
|
||||
assert pagination['has_prev'] is True
|
||||
assert pagination['has_next'] is False
|
||||
|
||||
def test_create_paginated_response_empty(self):
|
||||
"""Test create_paginated_response with empty results."""
|
||||
if not create_paginated_response:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_paginated_response([], 1, 10, 0)
|
||||
|
||||
response_data = json.loads(response.data)
|
||||
assert response_data['data'] == []
|
||||
assert response_data['pagination']['total'] == 0
|
||||
assert response_data['pagination']['pages'] == 0
|
||||
|
||||
|
||||
class TestDataFormatting:
|
||||
"""Test cases for data formatting functions."""
|
||||
|
||||
def test_format_anime_data(self):
|
||||
"""Test format_anime_data function."""
|
||||
if not format_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
anime = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime',
|
||||
'url': 'https://example.com/anime/1',
|
||||
'description': 'A test anime',
|
||||
'episodes': 12,
|
||||
'status': 'completed',
|
||||
'created_at': '2023-01-01 12:00:00',
|
||||
'updated_at': '2023-01-02 12:00:00'
|
||||
}
|
||||
|
||||
formatted = format_anime_data(anime)
|
||||
|
||||
assert formatted['id'] == 1
|
||||
assert formatted['name'] == 'Test Anime'
|
||||
assert formatted['url'] == 'https://example.com/anime/1'
|
||||
assert formatted['description'] == 'A test anime'
|
||||
assert formatted['episodes'] == 12
|
||||
assert formatted['status'] == 'completed'
|
||||
assert 'created_at' in formatted
|
||||
assert 'updated_at' in formatted
|
||||
|
||||
def test_format_anime_data_with_episodes(self):
|
||||
"""Test format_anime_data with episode information."""
|
||||
if not format_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
anime = {
|
||||
'id': 1,
|
||||
'name': 'Test Anime',
|
||||
'url': 'https://example.com/anime/1'
|
||||
}
|
||||
|
||||
episodes = [
|
||||
{'id': 1, 'number': 1, 'title': 'Episode 1'},
|
||||
{'id': 2, 'number': 2, 'title': 'Episode 2'}
|
||||
]
|
||||
|
||||
formatted = format_anime_data(anime, include_episodes=True, episodes=episodes)
|
||||
|
||||
assert 'episodes' in formatted
|
||||
assert len(formatted['episodes']) == 2
|
||||
assert formatted['episodes'][0]['number'] == 1
|
||||
|
||||
def test_format_episode_data(self):
|
||||
"""Test format_episode_data function."""
|
||||
if not format_episode_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
episode = {
|
||||
'id': 1,
|
||||
'anime_id': 5,
|
||||
'number': 1,
|
||||
'title': 'First Episode',
|
||||
'url': 'https://example.com/episode/1',
|
||||
'duration': 1440, # 24 minutes in seconds
|
||||
'status': 'available',
|
||||
'created_at': '2023-01-01 12:00:00'
|
||||
}
|
||||
|
||||
formatted = format_episode_data(episode)
|
||||
|
||||
assert formatted['id'] == 1
|
||||
assert formatted['anime_id'] == 5
|
||||
assert formatted['number'] == 1
|
||||
assert formatted['title'] == 'First Episode'
|
||||
assert formatted['url'] == 'https://example.com/episode/1'
|
||||
assert formatted['duration'] == 1440
|
||||
assert formatted['status'] == 'available'
|
||||
assert 'created_at' in formatted
|
||||
|
||||
def test_format_download_data(self):
|
||||
"""Test format_download_data function."""
|
||||
if not format_download_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
download = {
|
||||
'id': 1,
|
||||
'anime_id': 5,
|
||||
'episode_id': 10,
|
||||
'status': 'downloading',
|
||||
'progress': 75.5,
|
||||
'size': 1073741824, # 1GB in bytes
|
||||
'downloaded_size': 805306368, # 768MB
|
||||
'speed': 1048576, # 1MB/s
|
||||
'eta': 300, # 5 minutes
|
||||
'created_at': '2023-01-01 12:00:00',
|
||||
'started_at': '2023-01-01 12:05:00'
|
||||
}
|
||||
|
||||
formatted = format_download_data(download)
|
||||
|
||||
assert formatted['id'] == 1
|
||||
assert formatted['anime_id'] == 5
|
||||
assert formatted['episode_id'] == 10
|
||||
assert formatted['status'] == 'downloading'
|
||||
assert formatted['progress'] == 75.5
|
||||
assert formatted['size'] == 1073741824
|
||||
assert formatted['downloaded_size'] == 805306368
|
||||
assert formatted['speed'] == 1048576
|
||||
assert formatted['eta'] == 300
|
||||
assert 'created_at' in formatted
|
||||
assert 'started_at' in formatted
|
||||
|
||||
def test_format_user_data(self):
|
||||
"""Test format_user_data function."""
|
||||
if not format_user_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
user = {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'email': 'test@example.com',
|
||||
'password_hash': 'secret_hash',
|
||||
'role': 'user',
|
||||
'last_login': '2023-01-01 12:00:00',
|
||||
'created_at': '2023-01-01 10:00:00'
|
||||
}
|
||||
|
||||
formatted = format_user_data(user)
|
||||
|
||||
assert formatted['id'] == 1
|
||||
assert formatted['username'] == 'testuser'
|
||||
assert formatted['email'] == 'test@example.com'
|
||||
assert formatted['role'] == 'user'
|
||||
assert 'last_login' in formatted
|
||||
assert 'created_at' in formatted
|
||||
# Should not include sensitive data
|
||||
assert 'password_hash' not in formatted
|
||||
|
||||
def test_format_user_data_include_sensitive(self):
|
||||
"""Test format_user_data with sensitive data included."""
|
||||
if not format_user_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
user = {
|
||||
'id': 1,
|
||||
'username': 'testuser',
|
||||
'password_hash': 'secret_hash'
|
||||
}
|
||||
|
||||
formatted = format_user_data(user, include_sensitive=True)
|
||||
|
||||
assert 'password_hash' in formatted
|
||||
assert formatted['password_hash'] == 'secret_hash'
|
||||
|
||||
|
||||
class TestUtilityFormatting:
|
||||
"""Test cases for utility formatting functions."""
|
||||
|
||||
def test_format_datetime_string(self):
|
||||
"""Test format_datetime with string input."""
|
||||
if not format_datetime:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
dt_string = "2023-01-01 12:30:45"
|
||||
formatted = format_datetime(dt_string)
|
||||
|
||||
assert isinstance(formatted, str)
|
||||
assert "2023" in formatted
|
||||
assert "01" in formatted
|
||||
|
||||
def test_format_datetime_object(self):
|
||||
"""Test format_datetime with datetime object."""
|
||||
if not format_datetime:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
dt_object = datetime(2023, 1, 1, 12, 30, 45)
|
||||
formatted = format_datetime(dt_object)
|
||||
|
||||
assert isinstance(formatted, str)
|
||||
assert "2023" in formatted
|
||||
|
||||
def test_format_datetime_none(self):
|
||||
"""Test format_datetime with None input."""
|
||||
if not format_datetime:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
formatted = format_datetime(None)
|
||||
assert formatted is None
|
||||
|
||||
def test_format_datetime_custom_format(self):
|
||||
"""Test format_datetime with custom format."""
|
||||
if not format_datetime:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
dt_string = "2023-01-01 12:30:45"
|
||||
formatted = format_datetime(dt_string, fmt="%Y/%m/%d")
|
||||
|
||||
assert formatted == "2023/01/01"
|
||||
|
||||
def test_format_file_size_bytes(self):
|
||||
"""Test format_file_size with bytes."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
assert format_file_size(512) == "512 B"
|
||||
assert format_file_size(0) == "0 B"
|
||||
|
||||
def test_format_file_size_kilobytes(self):
|
||||
"""Test format_file_size with kilobytes."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
assert format_file_size(1024) == "1.0 KB"
|
||||
assert format_file_size(1536) == "1.5 KB"
|
||||
|
||||
def test_format_file_size_megabytes(self):
|
||||
"""Test format_file_size with megabytes."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
assert format_file_size(1048576) == "1.0 MB"
|
||||
assert format_file_size(1572864) == "1.5 MB"
|
||||
|
||||
def test_format_file_size_gigabytes(self):
|
||||
"""Test format_file_size with gigabytes."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
assert format_file_size(1073741824) == "1.0 GB"
|
||||
assert format_file_size(2147483648) == "2.0 GB"
|
||||
|
||||
def test_format_file_size_terabytes(self):
|
||||
"""Test format_file_size with terabytes."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
assert format_file_size(1099511627776) == "1.0 TB"
|
||||
|
||||
def test_format_file_size_precision(self):
|
||||
"""Test format_file_size with custom precision."""
|
||||
if not format_file_size:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
size = 1536 # 1.5 KB
|
||||
assert format_file_size(size, precision=2) == "1.50 KB"
|
||||
assert format_file_size(size, precision=0) == "2 KB" # Rounded up
|
||||
|
||||
|
||||
class TestCORSHeaders:
|
||||
"""Test cases for CORS header utilities."""
|
||||
|
||||
def test_add_cors_headers_basic(self):
|
||||
"""Test add_cors_headers with basic response."""
|
||||
if not add_cors_headers:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Mock response object
|
||||
response = Mock()
|
||||
response.headers = {}
|
||||
|
||||
result = add_cors_headers(response)
|
||||
|
||||
assert result.headers['Access-Control-Allow-Origin'] == '*'
|
||||
assert 'GET, POST, PUT, DELETE, OPTIONS' in result.headers['Access-Control-Allow-Methods']
|
||||
assert 'Content-Type, Authorization' in result.headers['Access-Control-Allow-Headers']
|
||||
|
||||
def test_add_cors_headers_custom_origin(self):
|
||||
"""Test add_cors_headers with custom origin."""
|
||||
if not add_cors_headers:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = Mock()
|
||||
response.headers = {}
|
||||
|
||||
result = add_cors_headers(response, origin='https://example.com')
|
||||
|
||||
assert result.headers['Access-Control-Allow-Origin'] == 'https://example.com'
|
||||
|
||||
def test_add_cors_headers_custom_methods(self):
|
||||
"""Test add_cors_headers with custom methods."""
|
||||
if not add_cors_headers:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = Mock()
|
||||
response.headers = {}
|
||||
|
||||
result = add_cors_headers(response, methods=['GET', 'POST'])
|
||||
|
||||
assert result.headers['Access-Control-Allow-Methods'] == 'GET, POST'
|
||||
|
||||
def test_add_cors_headers_existing_headers(self):
|
||||
"""Test add_cors_headers preserves existing headers."""
|
||||
if not add_cors_headers:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response = Mock()
|
||||
response.headers = {'X-Custom-Header': 'custom-value'}
|
||||
|
||||
result = add_cors_headers(response)
|
||||
|
||||
assert result.headers['X-Custom-Header'] == 'custom-value'
|
||||
assert 'Access-Control-Allow-Origin' in result.headers
|
||||
|
||||
|
||||
class TestResponseIntegration:
|
||||
"""Integration tests for response helpers."""
|
||||
|
||||
def test_formatted_paginated_response(self):
|
||||
"""Test creating paginated response with formatted data."""
|
||||
if not create_paginated_response or not format_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
anime_list = [
|
||||
{'id': 1, 'name': 'Anime 1', 'url': 'https://example.com/1'},
|
||||
{'id': 2, 'name': 'Anime 2', 'url': 'https://example.com/2'}
|
||||
]
|
||||
|
||||
formatted_items = [format_anime_data(anime) for anime in anime_list]
|
||||
response, status_code = create_paginated_response(formatted_items, 1, 10, 2)
|
||||
|
||||
assert status_code == 200
|
||||
response_data = json.loads(response.data)
|
||||
assert len(response_data['data']) == 2
|
||||
assert response_data['data'][0]['name'] == 'Anime 1'
|
||||
|
||||
def test_error_response_with_cors(self):
|
||||
"""Test error response with CORS headers."""
|
||||
if not create_error_response or not add_cors_headers:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
response, status_code = create_error_response("Test error", 400)
|
||||
response_with_cors = add_cors_headers(response)
|
||||
|
||||
assert 'Access-Control-Allow-Origin' in response_with_cors.headers
|
||||
assert status_code == 400
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,546 +0,0 @@
|
||||
"""
|
||||
Test cases for input validation utilities.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import Mock, patch, MagicMock
|
||||
from flask import Flask, request
|
||||
import json
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
# Import the modules to test
|
||||
try:
|
||||
from src.server.web.controllers.shared.validators import (
|
||||
validate_json_input, validate_query_params, validate_pagination_params,
|
||||
validate_anime_data, validate_file_upload, is_valid_url, is_valid_email,
|
||||
sanitize_string, validate_id_parameter
|
||||
)
|
||||
except ImportError:
|
||||
# Fallback for testing
|
||||
validate_json_input = None
|
||||
validate_query_params = None
|
||||
validate_pagination_params = None
|
||||
validate_anime_data = None
|
||||
validate_file_upload = None
|
||||
is_valid_url = None
|
||||
is_valid_email = None
|
||||
sanitize_string = None
|
||||
validate_id_parameter = None
|
||||
|
||||
|
||||
class TestValidationDecorators:
|
||||
"""Test cases for validation decorators."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_validate_json_input_success(self, app, client):
|
||||
"""Test validate_json_input decorator with valid JSON."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(
|
||||
required_fields=['name'],
|
||||
optional_fields=['description'],
|
||||
field_types={'name': str, 'description': str}
|
||||
)
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test',
|
||||
json={'name': 'Test Name', 'description': 'Test Description'},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_validate_json_input_missing_required(self, app, client):
|
||||
"""Test validate_json_input with missing required field."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(required_fields=['name'])
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test',
|
||||
json={'description': 'Test Description'},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Missing required fields' in data[0]['message']
|
||||
|
||||
def test_validate_json_input_wrong_type(self, app, client):
|
||||
"""Test validate_json_input with wrong field type."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(
|
||||
required_fields=['age'],
|
||||
field_types={'age': int}
|
||||
)
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test',
|
||||
json={'age': 'twenty'},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Type validation failed' in data[0]['message']
|
||||
|
||||
def test_validate_json_input_unexpected_fields(self, app, client):
|
||||
"""Test validate_json_input with unexpected fields."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(
|
||||
required_fields=['name'],
|
||||
optional_fields=['description']
|
||||
)
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test',
|
||||
json={'name': 'Test', 'description': 'Test', 'extra_field': 'unexpected'},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Unexpected fields' in data[0]['message']
|
||||
|
||||
def test_validate_json_input_not_json(self, app, client):
|
||||
"""Test validate_json_input with non-JSON content."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(required_fields=['name'])
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test', data='not json')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Request must be JSON' in data[0]['message']
|
||||
|
||||
def test_validate_query_params_success(self, app, client):
|
||||
"""Test validate_query_params decorator with valid parameters."""
|
||||
if not validate_query_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_query_params(
|
||||
allowed_params=['page', 'limit'],
|
||||
required_params=['page'],
|
||||
param_types={'page': int, 'limit': int}
|
||||
)
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?page=1&limit=10')
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_validate_query_params_missing_required(self, app, client):
|
||||
"""Test validate_query_params with missing required parameter."""
|
||||
if not validate_query_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_query_params(required_params=['page'])
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Missing required parameters' in data[0]['message']
|
||||
|
||||
def test_validate_query_params_unexpected(self, app, client):
|
||||
"""Test validate_query_params with unexpected parameters."""
|
||||
if not validate_query_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_query_params(allowed_params=['page'])
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?page=1&unexpected=value')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Unexpected parameters' in data[0]['message']
|
||||
|
||||
def test_validate_query_params_wrong_type(self, app, client):
|
||||
"""Test validate_query_params with wrong parameter type."""
|
||||
if not validate_query_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_query_params(
|
||||
allowed_params=['page'],
|
||||
param_types={'page': int}
|
||||
)
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?page=abc')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'Parameter type validation failed' in data[0]['message']
|
||||
|
||||
def test_validate_pagination_params_success(self, app, client):
|
||||
"""Test validate_pagination_params decorator with valid parameters."""
|
||||
if not validate_pagination_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_pagination_params
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?page=1&per_page=10&limit=20&offset=5')
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_validate_pagination_params_invalid_page(self, app, client):
|
||||
"""Test validate_pagination_params with invalid page."""
|
||||
if not validate_pagination_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_pagination_params
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?page=0')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'page must be greater than 0' in data[0]['errors']
|
||||
|
||||
def test_validate_pagination_params_invalid_per_page(self, app, client):
|
||||
"""Test validate_pagination_params with invalid per_page."""
|
||||
if not validate_pagination_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test')
|
||||
@validate_pagination_params
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.get('/test?per_page=2000')
|
||||
assert response.status_code == 400
|
||||
data = json.loads(response.data)
|
||||
assert 'per_page cannot exceed 1000' in data[0]['errors']
|
||||
|
||||
def test_validate_id_parameter_success(self, app, client):
|
||||
"""Test validate_id_parameter decorator with valid ID."""
|
||||
if not validate_id_parameter:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test/<int:id>')
|
||||
@validate_id_parameter('id')
|
||||
def test_endpoint(id):
|
||||
return {'status': 'success', 'id': id}
|
||||
|
||||
response = client.get('/test/123')
|
||||
assert response.status_code == 200
|
||||
data = json.loads(response.data)
|
||||
assert data['id'] == 123
|
||||
|
||||
def test_validate_id_parameter_invalid(self, app):
|
||||
"""Test validate_id_parameter decorator with invalid ID."""
|
||||
if not validate_id_parameter:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@validate_id_parameter('id')
|
||||
def test_function(id='abc'):
|
||||
return {'status': 'success'}
|
||||
|
||||
# Since this is a decorator that modifies kwargs,
|
||||
# we test it directly
|
||||
result = test_function(id='abc')
|
||||
# Should return error response
|
||||
assert result[1] == 400
|
||||
|
||||
|
||||
class TestValidationUtilities:
|
||||
"""Test cases for validation utility functions."""
|
||||
|
||||
def test_validate_anime_data_valid(self):
|
||||
"""Test validate_anime_data with valid data."""
|
||||
if not validate_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {
|
||||
'name': 'Test Anime',
|
||||
'url': 'https://example.com/anime/test',
|
||||
'description': 'A test anime',
|
||||
'episodes': 12,
|
||||
'status': 'completed'
|
||||
}
|
||||
|
||||
errors = validate_anime_data(data)
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_validate_anime_data_missing_required(self):
|
||||
"""Test validate_anime_data with missing required fields."""
|
||||
if not validate_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {
|
||||
'description': 'A test anime'
|
||||
}
|
||||
|
||||
errors = validate_anime_data(data)
|
||||
assert len(errors) > 0
|
||||
assert any('Missing required field: name' in error for error in errors)
|
||||
assert any('Missing required field: url' in error for error in errors)
|
||||
|
||||
def test_validate_anime_data_invalid_types(self):
|
||||
"""Test validate_anime_data with invalid field types."""
|
||||
if not validate_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {
|
||||
'name': 123, # Should be string
|
||||
'url': 'invalid-url', # Should be valid URL
|
||||
'episodes': 'twelve' # Should be integer
|
||||
}
|
||||
|
||||
errors = validate_anime_data(data)
|
||||
assert len(errors) > 0
|
||||
assert any('name must be a string' in error for error in errors)
|
||||
assert any('url must be a valid URL' in error for error in errors)
|
||||
assert any('episodes must be an integer' in error for error in errors)
|
||||
|
||||
def test_validate_anime_data_invalid_status(self):
|
||||
"""Test validate_anime_data with invalid status."""
|
||||
if not validate_anime_data:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
data = {
|
||||
'name': 'Test Anime',
|
||||
'url': 'https://example.com/anime/test',
|
||||
'status': 'invalid_status'
|
||||
}
|
||||
|
||||
errors = validate_anime_data(data)
|
||||
assert len(errors) > 0
|
||||
assert any('status must be one of' in error for error in errors)
|
||||
|
||||
def test_validate_file_upload_valid(self):
|
||||
"""Test validate_file_upload with valid file."""
|
||||
if not validate_file_upload:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
# Create a mock file object
|
||||
mock_file = Mock()
|
||||
mock_file.filename = 'test.txt'
|
||||
mock_file.content_length = 1024 # 1KB
|
||||
|
||||
errors = validate_file_upload(mock_file, allowed_extensions=['txt'], max_size_mb=1)
|
||||
assert len(errors) == 0
|
||||
|
||||
def test_validate_file_upload_no_file(self):
|
||||
"""Test validate_file_upload with no file."""
|
||||
if not validate_file_upload:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
errors = validate_file_upload(None)
|
||||
assert len(errors) > 0
|
||||
assert 'No file provided' in errors
|
||||
|
||||
def test_validate_file_upload_empty_filename(self):
|
||||
"""Test validate_file_upload with empty filename."""
|
||||
if not validate_file_upload:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_file = Mock()
|
||||
mock_file.filename = ''
|
||||
|
||||
errors = validate_file_upload(mock_file)
|
||||
assert len(errors) > 0
|
||||
assert 'No file selected' in errors
|
||||
|
||||
def test_validate_file_upload_invalid_extension(self):
|
||||
"""Test validate_file_upload with invalid extension."""
|
||||
if not validate_file_upload:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_file = Mock()
|
||||
mock_file.filename = 'test.exe'
|
||||
|
||||
errors = validate_file_upload(mock_file, allowed_extensions=['txt', 'pdf'])
|
||||
assert len(errors) > 0
|
||||
assert 'File type not allowed' in errors[0]
|
||||
|
||||
def test_validate_file_upload_too_large(self):
|
||||
"""Test validate_file_upload with file too large."""
|
||||
if not validate_file_upload:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
mock_file = Mock()
|
||||
mock_file.filename = 'test.txt'
|
||||
mock_file.content_length = 5 * 1024 * 1024 # 5MB
|
||||
|
||||
errors = validate_file_upload(mock_file, max_size_mb=1)
|
||||
assert len(errors) > 0
|
||||
assert 'File size exceeds maximum' in errors[0]
|
||||
|
||||
def test_is_valid_url_valid(self):
|
||||
"""Test is_valid_url with valid URLs."""
|
||||
if not is_valid_url:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
valid_urls = [
|
||||
'https://example.com',
|
||||
'http://test.co.uk',
|
||||
'https://subdomain.example.com/path',
|
||||
'http://localhost:8080',
|
||||
'https://192.168.1.1:3000/api'
|
||||
]
|
||||
|
||||
for url in valid_urls:
|
||||
assert is_valid_url(url), f"URL should be valid: {url}"
|
||||
|
||||
def test_is_valid_url_invalid(self):
|
||||
"""Test is_valid_url with invalid URLs."""
|
||||
if not is_valid_url:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
invalid_urls = [
|
||||
'not-a-url',
|
||||
'ftp://example.com', # Only http/https supported
|
||||
'https://',
|
||||
'http://.',
|
||||
'just-text'
|
||||
]
|
||||
|
||||
for url in invalid_urls:
|
||||
assert not is_valid_url(url), f"URL should be invalid: {url}"
|
||||
|
||||
def test_is_valid_email_valid(self):
|
||||
"""Test is_valid_email with valid emails."""
|
||||
if not is_valid_email:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
valid_emails = [
|
||||
'test@example.com',
|
||||
'user.name@domain.co.uk',
|
||||
'admin+tag@site.org',
|
||||
'user123@test-domain.com'
|
||||
]
|
||||
|
||||
for email in valid_emails:
|
||||
assert is_valid_email(email), f"Email should be valid: {email}"
|
||||
|
||||
def test_is_valid_email_invalid(self):
|
||||
"""Test is_valid_email with invalid emails."""
|
||||
if not is_valid_email:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
invalid_emails = [
|
||||
'not-an-email',
|
||||
'@domain.com',
|
||||
'user@',
|
||||
'user@domain',
|
||||
'user space@domain.com'
|
||||
]
|
||||
|
||||
for email in invalid_emails:
|
||||
assert not is_valid_email(email), f"Email should be invalid: {email}"
|
||||
|
||||
def test_sanitize_string_basic(self):
|
||||
"""Test sanitize_string with basic input."""
|
||||
if not sanitize_string:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
result = sanitize_string(" Hello World ")
|
||||
assert result == "Hello World"
|
||||
|
||||
def test_sanitize_string_max_length(self):
|
||||
"""Test sanitize_string with max length."""
|
||||
if not sanitize_string:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
long_string = "A" * 100
|
||||
result = sanitize_string(long_string, max_length=50)
|
||||
assert len(result) == 50
|
||||
assert result == "A" * 50
|
||||
|
||||
def test_sanitize_string_control_characters(self):
|
||||
"""Test sanitize_string removes control characters."""
|
||||
if not sanitize_string:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
input_string = "Hello\x00World\x01Test"
|
||||
result = sanitize_string(input_string)
|
||||
assert result == "HelloWorldTest"
|
||||
|
||||
def test_sanitize_string_non_string(self):
|
||||
"""Test sanitize_string with non-string input."""
|
||||
if not sanitize_string:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
result = sanitize_string(123)
|
||||
assert result == "123"
|
||||
|
||||
|
||||
class TestValidatorIntegration:
|
||||
"""Integration tests for validators."""
|
||||
|
||||
@pytest.fixture
|
||||
def app(self):
|
||||
"""Create a test Flask application."""
|
||||
app = Flask(__name__)
|
||||
app.config['TESTING'] = True
|
||||
return app
|
||||
|
||||
@pytest.fixture
|
||||
def client(self, app):
|
||||
"""Create a test client."""
|
||||
return app.test_client()
|
||||
|
||||
def test_multiple_validators(self, app, client):
|
||||
"""Test using multiple validators on the same endpoint."""
|
||||
if not validate_json_input or not validate_pagination_params:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@app.route('/test', methods=['POST'])
|
||||
@validate_json_input(required_fields=['name'])
|
||||
@validate_pagination_params
|
||||
def test_endpoint():
|
||||
return {'status': 'success'}
|
||||
|
||||
response = client.post('/test?page=1&per_page=10',
|
||||
json={'name': 'Test'},
|
||||
content_type='application/json')
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_validator_preserves_metadata(self):
|
||||
"""Test that validators preserve function metadata."""
|
||||
if not validate_json_input:
|
||||
pytest.skip("Module not available")
|
||||
|
||||
@validate_json_input(required_fields=['name'])
|
||||
def test_function():
|
||||
"""Test function docstring."""
|
||||
return "test"
|
||||
|
||||
assert test_function.__name__ == "test_function"
|
||||
assert test_function.__doc__ == "Test function docstring."
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
pytest.main([__file__])
|
||||
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test runner for comprehensive API testing.
|
||||
|
||||
This script runs all API-related tests and provides detailed reporting
|
||||
on test coverage and results.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from io import StringIO
|
||||
import json
|
||||
from datetime import datetime
|
||||
|
||||
# Add paths 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'))
|
||||
|
||||
def run_api_tests():
|
||||
"""Run all API tests and generate comprehensive report."""
|
||||
|
||||
print("🚀 Starting Aniworld API Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
# Test discovery
|
||||
loader = unittest.TestLoader()
|
||||
start_dir = os.path.dirname(__file__)
|
||||
|
||||
# Discover tests from different modules
|
||||
test_suites = []
|
||||
|
||||
# Unit tests
|
||||
try:
|
||||
from test_api_endpoints import (
|
||||
TestAuthenticationEndpoints,
|
||||
TestConfigurationEndpoints,
|
||||
TestSeriesEndpoints,
|
||||
TestDownloadEndpoints,
|
||||
TestProcessManagementEndpoints,
|
||||
TestLoggingEndpoints,
|
||||
TestBackupEndpoints,
|
||||
TestDiagnosticsEndpoints,
|
||||
TestErrorHandling
|
||||
)
|
||||
|
||||
unit_test_classes = [
|
||||
TestAuthenticationEndpoints,
|
||||
TestConfigurationEndpoints,
|
||||
TestSeriesEndpoints,
|
||||
TestDownloadEndpoints,
|
||||
TestProcessManagementEndpoints,
|
||||
TestLoggingEndpoints,
|
||||
TestBackupEndpoints,
|
||||
TestDiagnosticsEndpoints,
|
||||
TestErrorHandling
|
||||
]
|
||||
|
||||
print("✅ Loaded unit test classes")
|
||||
|
||||
for test_class in unit_test_classes:
|
||||
suite = loader.loadTestsFromTestCase(test_class)
|
||||
test_suites.append(('Unit Tests', test_class.__name__, suite))
|
||||
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Could not load unit test classes: {e}")
|
||||
|
||||
# Integration tests
|
||||
try:
|
||||
integration_path = os.path.join(os.path.dirname(__file__), '..', '..', 'integration')
|
||||
integration_file = os.path.join(integration_path, 'test_api_integration.py')
|
||||
|
||||
if os.path.exists(integration_file):
|
||||
sys.path.insert(0, integration_path)
|
||||
|
||||
# Import dynamically to handle potential import errors gracefully
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("test_api_integration", integration_file)
|
||||
if spec and spec.loader:
|
||||
test_api_integration = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(test_api_integration)
|
||||
|
||||
# Get test classes dynamically
|
||||
integration_test_classes = []
|
||||
for name in dir(test_api_integration):
|
||||
obj = getattr(test_api_integration, name)
|
||||
if (isinstance(obj, type) and
|
||||
issubclass(obj, unittest.TestCase) and
|
||||
name.startswith('Test') and
|
||||
name != 'APIIntegrationTestBase'):
|
||||
integration_test_classes.append(obj)
|
||||
|
||||
print(f"✅ Loaded {len(integration_test_classes)} integration test classes")
|
||||
|
||||
for test_class in integration_test_classes:
|
||||
suite = loader.loadTestsFromTestCase(test_class)
|
||||
test_suites.append(('Integration Tests', test_class.__name__, suite))
|
||||
else:
|
||||
print("⚠️ Could not create module spec for integration tests")
|
||||
else:
|
||||
print(f"⚠️ Integration test file not found: {integration_file}")
|
||||
|
||||
except ImportError as e:
|
||||
print(f"⚠️ Could not load integration test classes: {e}")
|
||||
|
||||
# Run tests and collect results
|
||||
total_results = {
|
||||
'total_tests': 0,
|
||||
'total_failures': 0,
|
||||
'total_errors': 0,
|
||||
'total_skipped': 0,
|
||||
'suite_results': []
|
||||
}
|
||||
|
||||
print(f"\n🧪 Running {len(test_suites)} test suites...")
|
||||
print("-" * 60)
|
||||
|
||||
for suite_type, suite_name, suite in test_suites:
|
||||
print(f"\n📋 {suite_type}: {suite_name}")
|
||||
|
||||
# Capture output
|
||||
test_output = StringIO()
|
||||
runner = unittest.TextTestRunner(
|
||||
stream=test_output,
|
||||
verbosity=1,
|
||||
buffer=True
|
||||
)
|
||||
|
||||
# Run the test suite
|
||||
result = runner.run(suite)
|
||||
|
||||
# Update totals
|
||||
total_results['total_tests'] += result.testsRun
|
||||
total_results['total_failures'] += len(result.failures)
|
||||
total_results['total_errors'] += len(result.errors)
|
||||
total_results['total_skipped'] += len(result.skipped) if hasattr(result, 'skipped') else 0
|
||||
|
||||
# Store suite result
|
||||
suite_result = {
|
||||
'suite_type': suite_type,
|
||||
'suite_name': suite_name,
|
||||
'tests_run': result.testsRun,
|
||||
'failures': len(result.failures),
|
||||
'errors': len(result.errors),
|
||||
'skipped': len(result.skipped) if hasattr(result, 'skipped') else 0,
|
||||
'success_rate': ((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100) if result.testsRun > 0 else 0,
|
||||
'failure_details': [f"{test}: {traceback.split('AssertionError: ')[-1].split(chr(10))[0] if 'AssertionError:' in traceback else 'See details'}" for test, traceback in result.failures],
|
||||
'error_details': [f"{test}: {traceback.split(chr(10))[-2] if len(traceback.split(chr(10))) > 1 else 'Unknown error'}" for test, traceback in result.errors]
|
||||
}
|
||||
|
||||
total_results['suite_results'].append(suite_result)
|
||||
|
||||
# Print immediate results
|
||||
status = "✅" if result.wasSuccessful() else "❌"
|
||||
print(f" {status} Tests: {result.testsRun}, Failures: {len(result.failures)}, Errors: {len(result.errors)}")
|
||||
|
||||
if result.failures:
|
||||
print(" 🔥 Failures:")
|
||||
for test, _ in result.failures[:3]: # Show first 3 failures
|
||||
print(f" - {test}")
|
||||
|
||||
if result.errors:
|
||||
print(" 💥 Errors:")
|
||||
for test, _ in result.errors[:3]: # Show first 3 errors
|
||||
print(f" - {test}")
|
||||
|
||||
# Generate comprehensive report
|
||||
print("\n" + "=" * 60)
|
||||
print("📊 COMPREHENSIVE TEST REPORT")
|
||||
print("=" * 60)
|
||||
|
||||
# Overall statistics
|
||||
print(f"📈 OVERALL STATISTICS:")
|
||||
print(f" Total Tests Run: {total_results['total_tests']}")
|
||||
print(f" Total Failures: {total_results['total_failures']}")
|
||||
print(f" Total Errors: {total_results['total_errors']}")
|
||||
print(f" Total Skipped: {total_results['total_skipped']}")
|
||||
|
||||
if total_results['total_tests'] > 0:
|
||||
overall_success_rate = ((total_results['total_tests'] - total_results['total_failures'] - total_results['total_errors']) / total_results['total_tests'] * 100)
|
||||
print(f" Overall Success Rate: {overall_success_rate:.1f}%")
|
||||
|
||||
# Per-suite breakdown
|
||||
print(f"\n📊 PER-SUITE BREAKDOWN:")
|
||||
for suite_result in total_results['suite_results']:
|
||||
status_icon = "✅" if suite_result['failures'] == 0 and suite_result['errors'] == 0 else "❌"
|
||||
print(f" {status_icon} {suite_result['suite_name']}")
|
||||
print(f" Tests: {suite_result['tests_run']}, Success Rate: {suite_result['success_rate']:.1f}%")
|
||||
|
||||
if suite_result['failures'] > 0:
|
||||
print(f" Failures ({suite_result['failures']}):")
|
||||
for failure in suite_result['failure_details'][:2]:
|
||||
print(f" - {failure}")
|
||||
|
||||
if suite_result['errors'] > 0:
|
||||
print(f" Errors ({suite_result['errors']}):")
|
||||
for error in suite_result['error_details'][:2]:
|
||||
print(f" - {error}")
|
||||
|
||||
# API Coverage Report
|
||||
print(f"\n🎯 API ENDPOINT COVERAGE:")
|
||||
|
||||
tested_endpoints = {
|
||||
'Authentication': [
|
||||
'POST /api/auth/setup',
|
||||
'POST /api/auth/login',
|
||||
'POST /api/auth/logout',
|
||||
'GET /api/auth/status'
|
||||
],
|
||||
'Configuration': [
|
||||
'POST /api/config/directory',
|
||||
'GET /api/scheduler/config',
|
||||
'POST /api/scheduler/config',
|
||||
'GET /api/config/section/advanced',
|
||||
'POST /api/config/section/advanced'
|
||||
],
|
||||
'Series Management': [
|
||||
'GET /api/series',
|
||||
'POST /api/search',
|
||||
'POST /api/rescan'
|
||||
],
|
||||
'Download Management': [
|
||||
'POST /api/download'
|
||||
],
|
||||
'System Status': [
|
||||
'GET /api/process/locks/status',
|
||||
'GET /api/status'
|
||||
],
|
||||
'Logging': [
|
||||
'GET /api/logging/config',
|
||||
'POST /api/logging/config',
|
||||
'GET /api/logging/files',
|
||||
'POST /api/logging/test',
|
||||
'POST /api/logging/cleanup',
|
||||
'GET /api/logging/files/<filename>/tail'
|
||||
],
|
||||
'Backup Management': [
|
||||
'POST /api/config/backup',
|
||||
'GET /api/config/backups',
|
||||
'POST /api/config/backup/<filename>/restore',
|
||||
'GET /api/config/backup/<filename>/download'
|
||||
],
|
||||
'Diagnostics': [
|
||||
'GET /api/diagnostics/network',
|
||||
'GET /api/diagnostics/errors',
|
||||
'POST /api/recovery/clear-blacklist',
|
||||
'GET /api/recovery/retry-counts',
|
||||
'GET /api/diagnostics/system-status'
|
||||
]
|
||||
}
|
||||
|
||||
total_endpoints = sum(len(endpoints) for endpoints in tested_endpoints.values())
|
||||
|
||||
for category, endpoints in tested_endpoints.items():
|
||||
print(f" 📂 {category}: {len(endpoints)} endpoints")
|
||||
for endpoint in endpoints:
|
||||
print(f" ✓ {endpoint}")
|
||||
|
||||
print(f"\n 🎯 Total API Endpoints Covered: {total_endpoints}")
|
||||
|
||||
# Recommendations
|
||||
print(f"\n💡 RECOMMENDATIONS:")
|
||||
|
||||
if total_results['total_failures'] > 0:
|
||||
print(" 🔧 Address test failures to improve code reliability")
|
||||
|
||||
if total_results['total_errors'] > 0:
|
||||
print(" 🛠️ Fix test errors - these often indicate setup/import issues")
|
||||
|
||||
if overall_success_rate < 80:
|
||||
print(" ⚠️ Success rate below 80% - consider improving test coverage")
|
||||
elif overall_success_rate >= 95:
|
||||
print(" 🎉 Excellent test success rate! Consider adding more edge cases")
|
||||
|
||||
print(" 📋 Consider adding performance tests for API endpoints")
|
||||
print(" 🔒 Add security testing for authentication endpoints")
|
||||
print(" 📝 Add API documentation tests (OpenAPI/Swagger validation)")
|
||||
|
||||
# Save detailed report to file
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
report_file = f"api_test_report_{timestamp}.json"
|
||||
|
||||
try:
|
||||
report_data = {
|
||||
'timestamp': datetime.now().isoformat(),
|
||||
'summary': {
|
||||
'total_tests': total_results['total_tests'],
|
||||
'total_failures': total_results['total_failures'],
|
||||
'total_errors': total_results['total_errors'],
|
||||
'total_skipped': total_results['total_skipped'],
|
||||
'overall_success_rate': overall_success_rate if total_results['total_tests'] > 0 else 0
|
||||
},
|
||||
'suite_results': total_results['suite_results'],
|
||||
'endpoint_coverage': tested_endpoints
|
||||
}
|
||||
|
||||
with open(report_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
||||
|
||||
print(f"\n💾 Detailed report saved to: {report_file}")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n⚠️ Could not save detailed report: {e}")
|
||||
|
||||
# Final summary
|
||||
print("\n" + "=" * 60)
|
||||
|
||||
if total_results['total_failures'] == 0 and total_results['total_errors'] == 0:
|
||||
print("🎉 ALL TESTS PASSED! API is working correctly.")
|
||||
exit_code = 0
|
||||
else:
|
||||
print("❌ Some tests failed. Please review the issues above.")
|
||||
exit_code = 1
|
||||
|
||||
print(f"🏁 Test run completed at {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit_code = run_api_tests()
|
||||
sys.exit(exit_code)
|
||||
@@ -1,323 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Comprehensive API Test Summary and Runner
|
||||
|
||||
This script provides a complete overview of all the API tests created for the Aniworld Flask application.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime
|
||||
|
||||
# Add paths
|
||||
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'))
|
||||
|
||||
def run_comprehensive_api_tests():
|
||||
"""Run all API tests and provide comprehensive summary."""
|
||||
|
||||
print("🚀 ANIWORLD API TEST SUITE")
|
||||
print("=" * 60)
|
||||
print(f"Execution Time: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
print("=" * 60)
|
||||
|
||||
# Test Results Storage
|
||||
results = {
|
||||
'total_tests': 0,
|
||||
'total_passed': 0,
|
||||
'total_failed': 0,
|
||||
'test_suites': []
|
||||
}
|
||||
|
||||
# 1. Run Simple API Tests (always work)
|
||||
print("\n📋 RUNNING SIMPLE API TESTS")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from test_api_simple import SimpleAPIEndpointTests, APIEndpointCoverageTest
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(loader.loadTestsFromTestCase(SimpleAPIEndpointTests))
|
||||
suite.addTests(loader.loadTestsFromTestCase(APIEndpointCoverageTest))
|
||||
|
||||
runner = unittest.TextTestRunner(verbosity=1, stream=open(os.devnull, 'w'))
|
||||
result = runner.run(suite)
|
||||
|
||||
suite_result = {
|
||||
'name': 'Simple API Tests',
|
||||
'tests_run': result.testsRun,
|
||||
'failures': len(result.failures),
|
||||
'errors': len(result.errors),
|
||||
'success': result.wasSuccessful()
|
||||
}
|
||||
|
||||
results['test_suites'].append(suite_result)
|
||||
results['total_tests'] += result.testsRun
|
||||
if result.wasSuccessful():
|
||||
results['total_passed'] += result.testsRun
|
||||
else:
|
||||
results['total_failed'] += len(result.failures) + len(result.errors)
|
||||
|
||||
print(f"✅ Simple API Tests: {result.testsRun} tests, {len(result.failures)} failures, {len(result.errors)} errors")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Could not run simple API tests: {e}")
|
||||
results['test_suites'].append({
|
||||
'name': 'Simple API Tests',
|
||||
'tests_run': 0,
|
||||
'failures': 0,
|
||||
'errors': 1,
|
||||
'success': False
|
||||
})
|
||||
|
||||
# 2. Try to run Complex API Tests
|
||||
print("\n📋 RUNNING COMPLEX API TESTS")
|
||||
print("-" * 40)
|
||||
|
||||
try:
|
||||
from test_api_endpoints import (
|
||||
TestAuthenticationEndpoints, TestConfigurationEndpoints,
|
||||
TestSeriesEndpoints, TestDownloadEndpoints,
|
||||
TestProcessManagementEndpoints, TestLoggingEndpoints,
|
||||
TestBackupEndpoints, TestDiagnosticsEndpoints, TestErrorHandling
|
||||
)
|
||||
|
||||
# Count tests that don't require complex mocking
|
||||
simple_test_classes = [
|
||||
TestConfigurationEndpoints, # These work
|
||||
TestLoggingEndpoints,
|
||||
TestBackupEndpoints,
|
||||
TestErrorHandling
|
||||
]
|
||||
|
||||
passed_tests = 0
|
||||
failed_tests = 0
|
||||
|
||||
for test_class in simple_test_classes:
|
||||
try:
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.loadTestsFromTestCase(test_class)
|
||||
runner = unittest.TextTestRunner(verbosity=0, stream=open(os.devnull, 'w'))
|
||||
result = runner.run(suite)
|
||||
|
||||
if result.wasSuccessful():
|
||||
passed_tests += result.testsRun
|
||||
else:
|
||||
failed_tests += len(result.failures) + len(result.errors)
|
||||
|
||||
except Exception:
|
||||
failed_tests += 1
|
||||
|
||||
suite_result = {
|
||||
'name': 'Complex API Tests (Partial)',
|
||||
'tests_run': passed_tests + failed_tests,
|
||||
'failures': failed_tests,
|
||||
'errors': 0,
|
||||
'success': failed_tests == 0
|
||||
}
|
||||
|
||||
results['test_suites'].append(suite_result)
|
||||
results['total_tests'] += passed_tests + failed_tests
|
||||
results['total_passed'] += passed_tests
|
||||
results['total_failed'] += failed_tests
|
||||
|
||||
print(f"✅ Complex API Tests: {passed_tests} passed, {failed_tests} failed (import issues)")
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Could not run complex API tests: {e}")
|
||||
results['test_suites'].append({
|
||||
'name': 'Complex API Tests',
|
||||
'tests_run': 0,
|
||||
'failures': 0,
|
||||
'errors': 1,
|
||||
'success': False
|
||||
})
|
||||
|
||||
# 3. Print API Endpoint Coverage
|
||||
print("\n📊 API ENDPOINT COVERAGE")
|
||||
print("-" * 40)
|
||||
|
||||
covered_endpoints = {
|
||||
'Authentication': [
|
||||
'POST /api/auth/setup - Initial password setup',
|
||||
'POST /api/auth/login - User authentication',
|
||||
'POST /api/auth/logout - Session termination',
|
||||
'GET /api/auth/status - Authentication status check'
|
||||
],
|
||||
'Configuration': [
|
||||
'POST /api/config/directory - Update anime directory',
|
||||
'GET /api/scheduler/config - Get scheduler settings',
|
||||
'POST /api/scheduler/config - Update scheduler settings',
|
||||
'GET /api/config/section/advanced - Get advanced settings',
|
||||
'POST /api/config/section/advanced - Update advanced settings'
|
||||
],
|
||||
'Series Management': [
|
||||
'GET /api/series - List all series',
|
||||
'POST /api/search - Search for series online',
|
||||
'POST /api/rescan - Rescan series directory'
|
||||
],
|
||||
'Download Management': [
|
||||
'POST /api/download - Start download process'
|
||||
],
|
||||
'System Status': [
|
||||
'GET /api/process/locks/status - Get process lock status',
|
||||
'GET /api/status - Get system status'
|
||||
],
|
||||
'Logging': [
|
||||
'GET /api/logging/config - Get logging configuration',
|
||||
'POST /api/logging/config - Update logging configuration',
|
||||
'GET /api/logging/files - List log files',
|
||||
'POST /api/logging/test - Test logging functionality',
|
||||
'POST /api/logging/cleanup - Clean up old logs',
|
||||
'GET /api/logging/files/<filename>/tail - Get log file tail'
|
||||
],
|
||||
'Backup Management': [
|
||||
'POST /api/config/backup - Create configuration backup',
|
||||
'GET /api/config/backups - List available backups',
|
||||
'POST /api/config/backup/<filename>/restore - Restore backup',
|
||||
'GET /api/config/backup/<filename>/download - Download backup'
|
||||
],
|
||||
'Diagnostics': [
|
||||
'GET /api/diagnostics/network - Network connectivity diagnostics',
|
||||
'GET /api/diagnostics/errors - Get error history',
|
||||
'POST /api/recovery/clear-blacklist - Clear URL blacklist',
|
||||
'GET /api/recovery/retry-counts - Get retry statistics',
|
||||
'GET /api/diagnostics/system-status - Comprehensive system status'
|
||||
]
|
||||
}
|
||||
|
||||
total_endpoints = 0
|
||||
for category, endpoints in covered_endpoints.items():
|
||||
print(f"\n📂 {category}:")
|
||||
for endpoint in endpoints:
|
||||
print(f" ✓ {endpoint}")
|
||||
total_endpoints += len(endpoints)
|
||||
|
||||
print(f"\n🎯 TOTAL ENDPOINTS COVERED: {total_endpoints}")
|
||||
|
||||
# 4. Print Test Quality Assessment
|
||||
print(f"\n📈 TEST QUALITY ASSESSMENT")
|
||||
print("-" * 40)
|
||||
|
||||
# Calculate overall success rate
|
||||
overall_success = (results['total_passed'] / results['total_tests'] * 100) if results['total_tests'] > 0 else 0
|
||||
|
||||
print(f"Total Tests Created: {results['total_tests']}")
|
||||
print(f"Tests Passing: {results['total_passed']}")
|
||||
print(f"Tests Failing: {results['total_failed']}")
|
||||
print(f"Overall Success Rate: {overall_success:.1f}%")
|
||||
|
||||
# Quality indicators
|
||||
quality_indicators = []
|
||||
|
||||
if results['total_tests'] >= 30:
|
||||
quality_indicators.append("✅ Comprehensive test coverage (30+ tests)")
|
||||
elif results['total_tests'] >= 20:
|
||||
quality_indicators.append("✅ Good test coverage (20+ tests)")
|
||||
else:
|
||||
quality_indicators.append("⚠️ Limited test coverage (<20 tests)")
|
||||
|
||||
if overall_success >= 80:
|
||||
quality_indicators.append("✅ High test success rate (80%+)")
|
||||
elif overall_success >= 60:
|
||||
quality_indicators.append("⚠️ Moderate test success rate (60-80%)")
|
||||
else:
|
||||
quality_indicators.append("❌ Low test success rate (<60%)")
|
||||
|
||||
if total_endpoints >= 25:
|
||||
quality_indicators.append("✅ Excellent API coverage (25+ endpoints)")
|
||||
elif total_endpoints >= 15:
|
||||
quality_indicators.append("✅ Good API coverage (15+ endpoints)")
|
||||
else:
|
||||
quality_indicators.append("⚠️ Limited API coverage (<15 endpoints)")
|
||||
|
||||
print(f"\n🏆 QUALITY INDICATORS:")
|
||||
for indicator in quality_indicators:
|
||||
print(f" {indicator}")
|
||||
|
||||
# 5. Provide Recommendations
|
||||
print(f"\n💡 RECOMMENDATIONS")
|
||||
print("-" * 40)
|
||||
|
||||
recommendations = [
|
||||
"✅ Created comprehensive test suite covering all major API endpoints",
|
||||
"✅ Implemented multiple testing approaches (simple, complex, live)",
|
||||
"✅ Added proper response structure validation",
|
||||
"✅ Included authentication flow testing",
|
||||
"✅ Added input validation testing",
|
||||
"✅ Created error handling pattern tests"
|
||||
]
|
||||
|
||||
if results['total_failed'] > 0:
|
||||
recommendations.append("🔧 Fix import issues in complex tests by improving mock setup")
|
||||
|
||||
if overall_success < 100:
|
||||
recommendations.append("🔧 Address test failures to improve reliability")
|
||||
|
||||
recommendations.extend([
|
||||
"📋 Run tests regularly as part of CI/CD pipeline",
|
||||
"🔒 Add security testing for authentication bypass attempts",
|
||||
"⚡ Add performance testing for API response times",
|
||||
"📝 Consider adding OpenAPI/Swagger documentation validation"
|
||||
])
|
||||
|
||||
for rec in recommendations:
|
||||
print(f" {rec}")
|
||||
|
||||
# 6. Print Usage Instructions
|
||||
print(f"\n🔧 USAGE INSTRUCTIONS")
|
||||
print("-" * 40)
|
||||
|
||||
print("To run the tests:")
|
||||
print("")
|
||||
print("1. Simple Tests (always work):")
|
||||
print(" cd tests/unit/web")
|
||||
print(" python test_api_simple.py")
|
||||
print("")
|
||||
print("2. All Available Tests:")
|
||||
print(" python run_comprehensive_tests.py")
|
||||
print("")
|
||||
print("3. Individual Test Files:")
|
||||
print(" python test_api_endpoints.py # Complex unit tests")
|
||||
print(" python test_api_live.py # Live Flask tests")
|
||||
print("")
|
||||
print("4. Using pytest (if available):")
|
||||
print(" pytest tests/ -k 'test_api' -v")
|
||||
|
||||
# 7. Final Summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🎉 API TEST SUITE SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
|
||||
print(f"✅ Created comprehensive test suite for Aniworld API")
|
||||
print(f"✅ Covered {total_endpoints} API endpoints across 8 categories")
|
||||
print(f"✅ Implemented {results['total_tests']} individual tests")
|
||||
print(f"✅ Achieved {overall_success:.1f}% test success rate")
|
||||
print(f"✅ Added multiple testing approaches and patterns")
|
||||
print(f"✅ Provided detailed documentation and usage instructions")
|
||||
|
||||
print(f"\n📁 Test Files Created:")
|
||||
test_files = [
|
||||
"tests/unit/web/test_api_endpoints.py - Comprehensive unit tests",
|
||||
"tests/unit/web/test_api_simple.py - Simple pattern tests",
|
||||
"tests/unit/web/test_api_live.py - Live Flask app tests",
|
||||
"tests/unit/web/run_api_tests.py - Advanced test runner",
|
||||
"tests/integration/test_api_integration.py - Integration tests",
|
||||
"tests/API_TEST_DOCUMENTATION.md - Complete documentation",
|
||||
"tests/conftest_api.py - Pytest configuration",
|
||||
"run_api_tests.py - Simple command-line runner"
|
||||
]
|
||||
|
||||
for file_info in test_files:
|
||||
print(f" 📄 {file_info}")
|
||||
|
||||
print(f"\nThe API test suite is ready for use! 🚀")
|
||||
|
||||
return 0 if overall_success >= 60 else 1
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit_code = run_comprehensive_api_tests()
|
||||
sys.exit(exit_code)
|
||||
@@ -1,20 +0,0 @@
|
||||
@echo off
|
||||
echo.
|
||||
echo 🚀 AniWorld Core Functionality Tests
|
||||
echo =====================================
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
python run_core_tests.py
|
||||
|
||||
if %ERRORLEVEL% EQU 0 (
|
||||
echo.
|
||||
echo ✅ All tests completed successfully!
|
||||
) else (
|
||||
echo.
|
||||
echo ❌ Some tests failed. Check output above.
|
||||
)
|
||||
|
||||
echo.
|
||||
echo Press any key to continue...
|
||||
pause > nul
|
||||
@@ -1,57 +0,0 @@
|
||||
"""
|
||||
Simple test runner for core AniWorld server functionality.
|
||||
|
||||
This script runs the essential tests to validate JavaScript/CSS generation.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
if __name__ == '__main__':
|
||||
print("🚀 Running AniWorld Core Functionality Tests")
|
||||
print("=" * 50)
|
||||
|
||||
# Import and run the core tests
|
||||
from test_core_functionality import TestManagerGenerationCore, TestComprehensiveSuite
|
||||
|
||||
# Create test suite
|
||||
suite = unittest.TestSuite()
|
||||
|
||||
# Add core manager tests
|
||||
suite.addTest(TestManagerGenerationCore('test_keyboard_shortcut_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_drag_drop_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_accessibility_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_user_preferences_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_advanced_search_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_undo_redo_manager_generation'))
|
||||
suite.addTest(TestManagerGenerationCore('test_multi_screen_manager_generation'))
|
||||
|
||||
# Add comprehensive test
|
||||
suite.addTest(TestComprehensiveSuite('test_all_manager_fixes_comprehensive'))
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=1, buffer=True)
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print summary
|
||||
print("\n" + "=" * 50)
|
||||
if result.wasSuccessful():
|
||||
print("🎉 ALL CORE TESTS PASSED!")
|
||||
print("✅ JavaScript/CSS generation working correctly")
|
||||
print("✅ All manager classes validated")
|
||||
print("✅ No syntax or runtime errors found")
|
||||
else:
|
||||
print("❌ Some core tests failed")
|
||||
if result.failures:
|
||||
for test, error in result.failures:
|
||||
print(f" FAIL: {test}")
|
||||
if result.errors:
|
||||
for test, error in result.errors:
|
||||
print(f" ERROR: {test}")
|
||||
|
||||
print("=" * 50)
|
||||
sys.exit(0 if result.wasSuccessful() else 1)
|
||||
@@ -1,10 +0,0 @@
|
||||
@echo off
|
||||
echo Running AniWorld Server Test Suite...
|
||||
echo.
|
||||
|
||||
cd /d "%~dp0"
|
||||
python run_tests.py
|
||||
|
||||
echo.
|
||||
echo Test run completed.
|
||||
pause
|
||||
@@ -1,108 +0,0 @@
|
||||
"""
|
||||
Test runner for the AniWorld server test suite.
|
||||
|
||||
This script runs all test modules and provides a comprehensive report.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from io import StringIO
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
def run_all_tests():
|
||||
"""Run all test modules and provide a summary report."""
|
||||
|
||||
print("=" * 60)
|
||||
print("AniWorld Server Test Suite")
|
||||
print("=" * 60)
|
||||
|
||||
# Discover and run all tests
|
||||
loader = unittest.TestLoader()
|
||||
test_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
|
||||
# Load all test modules
|
||||
suite = loader.discover(test_dir, pattern='test_*.py')
|
||||
|
||||
# Run tests with detailed output
|
||||
stream = StringIO()
|
||||
runner = unittest.TextTestRunner(
|
||||
stream=stream,
|
||||
verbosity=2,
|
||||
buffer=True
|
||||
)
|
||||
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print results
|
||||
output = stream.getvalue()
|
||||
print(output)
|
||||
|
||||
# Summary
|
||||
print("\n" + "=" * 60)
|
||||
print("TEST SUMMARY")
|
||||
print("=" * 60)
|
||||
|
||||
total_tests = result.testsRun
|
||||
failures = len(result.failures)
|
||||
errors = len(result.errors)
|
||||
skipped = len(result.skipped) if hasattr(result, 'skipped') else 0
|
||||
passed = total_tests - failures - errors - skipped
|
||||
|
||||
print(f"Total Tests Run: {total_tests}")
|
||||
print(f"Passed: {passed}")
|
||||
print(f"Failed: {failures}")
|
||||
print(f"Errors: {errors}")
|
||||
print(f"Skipped: {skipped}")
|
||||
|
||||
if result.wasSuccessful():
|
||||
print("\n🎉 ALL TESTS PASSED! 🎉")
|
||||
print("✅ No JavaScript or CSS generation issues found!")
|
||||
print("✅ All manager classes working correctly!")
|
||||
print("✅ Authentication system validated!")
|
||||
return True
|
||||
else:
|
||||
print("\n❌ Some tests failed. Please check the output above.")
|
||||
|
||||
if result.failures:
|
||||
print(f"\nFailures ({len(result.failures)}):")
|
||||
for test, traceback in result.failures:
|
||||
print(f" - {test}: {traceback.split(chr(10))[-2]}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\nErrors ({len(result.errors)}):")
|
||||
for test, traceback in result.errors:
|
||||
print(f" - {test}: {traceback.split(chr(10))[-2]}")
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def run_specific_test_module(module_name):
|
||||
"""Run a specific test module."""
|
||||
|
||||
print(f"Running tests from module: {module_name}")
|
||||
print("-" * 40)
|
||||
|
||||
loader = unittest.TestLoader()
|
||||
suite = loader.loadTestsFromName(module_name)
|
||||
|
||||
runner = unittest.TextTestRunner(verbosity=2, buffer=True)
|
||||
result = runner.run(suite)
|
||||
|
||||
return result.wasSuccessful()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if len(sys.argv) > 1:
|
||||
# Run specific test module
|
||||
module_name = sys.argv[1]
|
||||
success = run_specific_test_module(module_name)
|
||||
else:
|
||||
# Run all tests
|
||||
success = run_all_tests()
|
||||
|
||||
# Exit with appropriate code
|
||||
sys.exit(0 if success else 1)
|
||||
@@ -1,708 +0,0 @@
|
||||
"""
|
||||
Comprehensive test suite for all API endpoints in the Aniworld Flask application.
|
||||
|
||||
This module provides complete test coverage for:
|
||||
- Authentication endpoints
|
||||
- Configuration endpoints
|
||||
- Series management endpoints
|
||||
- Download and process management
|
||||
- Logging and diagnostics
|
||||
- System status and health monitoring
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import time
|
||||
from unittest.mock import patch, MagicMock, mock_open
|
||||
from datetime import datetime
|
||||
import pytest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 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 BaseAPITest(unittest.TestCase):
|
||||
"""Base test class with common setup and utilities."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
# Mock Flask app and test client
|
||||
self.app = MagicMock()
|
||||
self.client = MagicMock()
|
||||
|
||||
# Mock session manager
|
||||
self.mock_session_manager = MagicMock()
|
||||
self.mock_session_manager.sessions = {}
|
||||
|
||||
# Mock config
|
||||
self.mock_config = MagicMock()
|
||||
self.mock_config.anime_directory = '/test/anime'
|
||||
self.mock_config.has_master_password.return_value = True
|
||||
|
||||
# Mock series app
|
||||
self.mock_series_app = MagicMock()
|
||||
|
||||
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': time.time(),
|
||||
'last_accessed': time.time()
|
||||
}
|
||||
return session_id
|
||||
|
||||
def create_mock_response(self, status_code=200, json_data=None):
|
||||
"""Helper method to create mock HTTP responses."""
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = status_code
|
||||
if json_data:
|
||||
mock_response.get_json.return_value = json_data
|
||||
mock_response.data = json.dumps(json_data).encode()
|
||||
return mock_response
|
||||
|
||||
|
||||
class TestAuthenticationEndpoints(BaseAPITest):
|
||||
"""Test suite for authentication-related API endpoints."""
|
||||
|
||||
def test_auth_setup_endpoint(self):
|
||||
"""Test POST /api/auth/setup endpoint."""
|
||||
test_data = {'password': 'new_master_password'}
|
||||
|
||||
with patch('src.server.app.request') as mock_request, \
|
||||
patch('src.server.app.config') as mock_config, \
|
||||
patch('src.server.app.session_manager') as mock_session:
|
||||
|
||||
mock_request.get_json.return_value = test_data
|
||||
mock_config.has_master_password.return_value = False
|
||||
mock_session.create_session.return_value = 'session-123'
|
||||
|
||||
# This would test the actual endpoint
|
||||
# Since we can't easily import the app here, we test the logic
|
||||
self.assertIsNotNone(test_data['password'])
|
||||
self.assertTrue(len(test_data['password']) > 0)
|
||||
|
||||
def test_auth_login_endpoint(self):
|
||||
"""Test POST /api/auth/login endpoint."""
|
||||
test_data = {'password': 'correct_password'}
|
||||
|
||||
with patch('src.server.app.request') as mock_request, \
|
||||
patch('src.server.app.session_manager') as mock_session:
|
||||
|
||||
mock_request.get_json.return_value = test_data
|
||||
mock_session.login.return_value = {
|
||||
'success': True,
|
||||
'session_id': 'session-123'
|
||||
}
|
||||
|
||||
result = mock_session.login(test_data['password'])
|
||||
self.assertTrue(result['success'])
|
||||
self.assertIn('session_id', result)
|
||||
|
||||
def test_auth_logout_endpoint(self):
|
||||
"""Test POST /api/auth/logout endpoint."""
|
||||
session_id = self.authenticate_session()
|
||||
|
||||
with patch('src.server.app.session_manager') as mock_session:
|
||||
mock_session.logout.return_value = {'success': True}
|
||||
|
||||
result = mock_session.logout(session_id)
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
def test_auth_status_endpoint(self):
|
||||
"""Test GET /api/auth/status endpoint."""
|
||||
with patch('src.server.app.config') as mock_config, \
|
||||
patch('src.server.app.session_manager') as mock_session:
|
||||
|
||||
mock_config.has_master_password.return_value = True
|
||||
mock_session.get_session_info.return_value = {
|
||||
'authenticated': True,
|
||||
'session_id': 'test-session'
|
||||
}
|
||||
|
||||
# Test the expected response structure
|
||||
expected_response = {
|
||||
'authenticated': True,
|
||||
'has_master_password': True,
|
||||
'setup_required': False,
|
||||
'session_info': {'authenticated': True, 'session_id': 'test-session'}
|
||||
}
|
||||
|
||||
self.assertIn('authenticated', expected_response)
|
||||
self.assertIn('has_master_password', expected_response)
|
||||
self.assertIn('setup_required', expected_response)
|
||||
|
||||
|
||||
class TestConfigurationEndpoints(BaseAPITest):
|
||||
"""Test suite for configuration-related API endpoints."""
|
||||
|
||||
def test_config_directory_endpoint(self):
|
||||
"""Test POST /api/config/directory endpoint."""
|
||||
test_data = {'directory': '/new/anime/directory'}
|
||||
|
||||
with patch('src.server.app.config') as mock_config:
|
||||
mock_config.save_config = MagicMock()
|
||||
|
||||
# Test directory update logic
|
||||
mock_config.anime_directory = test_data['directory']
|
||||
mock_config.save_config()
|
||||
|
||||
self.assertEqual(mock_config.anime_directory, test_data['directory'])
|
||||
mock_config.save_config.assert_called_once()
|
||||
|
||||
def test_scheduler_config_get_endpoint(self):
|
||||
"""Test GET /api/scheduler/config endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'enabled': False,
|
||||
'time': '03:00',
|
||||
'auto_download_after_rescan': False,
|
||||
'next_run': None,
|
||||
'last_run': None,
|
||||
'is_running': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertIn('config', expected_response)
|
||||
self.assertIn('enabled', expected_response['config'])
|
||||
|
||||
def test_scheduler_config_post_endpoint(self):
|
||||
"""Test POST /api/scheduler/config endpoint."""
|
||||
test_data = {
|
||||
'enabled': True,
|
||||
'time': '02:30',
|
||||
'auto_download_after_rescan': True
|
||||
}
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Scheduler configuration saved (placeholder)'
|
||||
}
|
||||
|
||||
self.assertIn('success', expected_response)
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
def test_advanced_config_get_endpoint(self):
|
||||
"""Test GET /api/config/section/advanced endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'max_concurrent_downloads': 3,
|
||||
'provider_timeout': 30,
|
||||
'enable_debug_mode': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertIn('config', expected_response)
|
||||
self.assertIn('max_concurrent_downloads', expected_response['config'])
|
||||
|
||||
def test_advanced_config_post_endpoint(self):
|
||||
"""Test POST /api/config/section/advanced endpoint."""
|
||||
test_data = {
|
||||
'max_concurrent_downloads': 5,
|
||||
'provider_timeout': 45,
|
||||
'enable_debug_mode': True
|
||||
}
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Advanced configuration saved successfully'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
|
||||
class TestSeriesEndpoints(BaseAPITest):
|
||||
"""Test suite for series management API endpoints."""
|
||||
|
||||
def test_series_get_endpoint_with_data(self):
|
||||
"""Test GET /api/series endpoint with series data."""
|
||||
mock_series = MagicMock()
|
||||
mock_series.folder = 'test_series'
|
||||
mock_series.name = 'Test Series'
|
||||
mock_series.episodeDict = {'Season 1': [1, 2, 3]}
|
||||
|
||||
with patch('src.server.app.series_app') as mock_app:
|
||||
mock_app.List.GetList.return_value = [mock_series]
|
||||
|
||||
series_list = mock_app.List.GetList()
|
||||
self.assertEqual(len(series_list), 1)
|
||||
self.assertEqual(series_list[0].folder, 'test_series')
|
||||
|
||||
def test_series_get_endpoint_empty(self):
|
||||
"""Test GET /api/series endpoint with no data."""
|
||||
with patch('src.server.app.series_app', None):
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'series': [],
|
||||
'total_series': 0,
|
||||
'message': 'No series data available. Please perform a scan to load series.'
|
||||
}
|
||||
|
||||
self.assertEqual(len(expected_response['series']), 0)
|
||||
self.assertEqual(expected_response['total_series'], 0)
|
||||
|
||||
def test_search_endpoint(self):
|
||||
"""Test POST /api/search endpoint."""
|
||||
test_data = {'query': 'anime search term'}
|
||||
|
||||
mock_results = [
|
||||
{'name': 'Anime 1', 'link': 'https://example.com/anime1'},
|
||||
{'name': 'Anime 2', 'link': 'https://example.com/anime2'}
|
||||
]
|
||||
|
||||
with patch('src.server.app.series_app') as mock_app:
|
||||
mock_app.search.return_value = mock_results
|
||||
|
||||
results = mock_app.search(test_data['query'])
|
||||
self.assertEqual(len(results), 2)
|
||||
self.assertEqual(results[0]['name'], 'Anime 1')
|
||||
|
||||
def test_search_endpoint_empty_query(self):
|
||||
"""Test POST /api/search endpoint with empty query."""
|
||||
test_data = {'query': ''}
|
||||
|
||||
expected_error = {
|
||||
'status': 'error',
|
||||
'message': 'Search query cannot be empty'
|
||||
}
|
||||
|
||||
self.assertEqual(expected_error['status'], 'error')
|
||||
self.assertIn('empty', expected_error['message'])
|
||||
|
||||
def test_rescan_endpoint(self):
|
||||
"""Test POST /api/rescan endpoint."""
|
||||
with patch('src.server.app.is_scanning', False), \
|
||||
patch('src.server.app.is_process_running') as mock_running:
|
||||
|
||||
mock_running.return_value = False
|
||||
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'message': 'Rescan started'
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'success')
|
||||
|
||||
def test_rescan_endpoint_already_running(self):
|
||||
"""Test POST /api/rescan endpoint when already running."""
|
||||
with patch('src.server.app.is_scanning', True):
|
||||
expected_response = {
|
||||
'status': 'error',
|
||||
'message': 'Rescan is already running. Please wait for it to complete.',
|
||||
'is_running': True
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'error')
|
||||
self.assertTrue(expected_response['is_running'])
|
||||
|
||||
|
||||
class TestDownloadEndpoints(BaseAPITest):
|
||||
"""Test suite for download management API endpoints."""
|
||||
|
||||
def test_download_endpoint(self):
|
||||
"""Test POST /api/download endpoint."""
|
||||
test_data = {'series_id': 'test_series', 'episodes': [1, 2, 3]}
|
||||
|
||||
with patch('src.server.app.is_downloading', False), \
|
||||
patch('src.server.app.is_process_running') as mock_running:
|
||||
|
||||
mock_running.return_value = False
|
||||
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'message': 'Download functionality will be implemented with queue system'
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'success')
|
||||
|
||||
def test_download_endpoint_already_running(self):
|
||||
"""Test POST /api/download endpoint when already running."""
|
||||
with patch('src.server.app.is_downloading', True):
|
||||
expected_response = {
|
||||
'status': 'error',
|
||||
'message': 'Download is already running. Please wait for it to complete.',
|
||||
'is_running': True
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'error')
|
||||
self.assertTrue(expected_response['is_running'])
|
||||
|
||||
|
||||
class TestProcessManagementEndpoints(BaseAPITest):
|
||||
"""Test suite for process management API endpoints."""
|
||||
|
||||
def test_process_locks_status_endpoint(self):
|
||||
"""Test GET /api/process/locks/status endpoint."""
|
||||
with patch('src.server.app.is_process_running') as mock_running:
|
||||
mock_running.side_effect = lambda lock: lock == 'rescan'
|
||||
|
||||
expected_locks = {
|
||||
'rescan': {
|
||||
'is_locked': True,
|
||||
'locked_by': 'system',
|
||||
'lock_time': None
|
||||
},
|
||||
'download': {
|
||||
'is_locked': False,
|
||||
'locked_by': None,
|
||||
'lock_time': None
|
||||
}
|
||||
}
|
||||
|
||||
# Test rescan lock
|
||||
self.assertTrue(expected_locks['rescan']['is_locked'])
|
||||
self.assertFalse(expected_locks['download']['is_locked'])
|
||||
|
||||
def test_status_endpoint(self):
|
||||
"""Test GET /api/status endpoint."""
|
||||
with patch.dict('os.environ', {'ANIME_DIRECTORY': '/test/anime'}):
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'directory': '/test/anime',
|
||||
'series_count': 0,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertEqual(expected_response['directory'], '/test/anime')
|
||||
|
||||
|
||||
class TestLoggingEndpoints(BaseAPITest):
|
||||
"""Test suite for logging management API endpoints."""
|
||||
|
||||
def test_logging_config_get_endpoint(self):
|
||||
"""Test GET /api/logging/config endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'log_level': 'INFO',
|
||||
'enable_console_logging': True,
|
||||
'enable_console_progress': True,
|
||||
'enable_fail2ban_logging': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertEqual(expected_response['config']['log_level'], 'INFO')
|
||||
|
||||
def test_logging_config_post_endpoint(self):
|
||||
"""Test POST /api/logging/config endpoint."""
|
||||
test_data = {
|
||||
'log_level': 'DEBUG',
|
||||
'enable_console_logging': False
|
||||
}
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Logging configuration saved (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
def test_logging_files_endpoint(self):
|
||||
"""Test GET /api/logging/files endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'files': []
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertIsInstance(expected_response['files'], list)
|
||||
|
||||
def test_logging_test_endpoint(self):
|
||||
"""Test POST /api/logging/test endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Test logging completed (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
def test_logging_cleanup_endpoint(self):
|
||||
"""Test POST /api/logging/cleanup endpoint."""
|
||||
test_data = {'days': 7}
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Log files older than 7 days have been cleaned up (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertIn('7 days', expected_response['message'])
|
||||
|
||||
def test_logging_tail_endpoint(self):
|
||||
"""Test GET /api/logging/files/<filename>/tail endpoint."""
|
||||
filename = 'test.log'
|
||||
lines = 50
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'content': f'Last {lines} lines of {filename} (placeholder)',
|
||||
'filename': filename
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertEqual(expected_response['filename'], filename)
|
||||
|
||||
|
||||
class TestBackupEndpoints(BaseAPITest):
|
||||
"""Test suite for configuration backup API endpoints."""
|
||||
|
||||
def test_config_backup_create_endpoint(self):
|
||||
"""Test POST /api/config/backup endpoint."""
|
||||
with patch('src.server.app.datetime') as mock_datetime:
|
||||
mock_datetime.now.return_value.strftime.return_value = '20231201_143000'
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Configuration backup created successfully',
|
||||
'filename': 'config_backup_20231201_143000.json'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertIn('config_backup_', expected_response['filename'])
|
||||
|
||||
def test_config_backups_list_endpoint(self):
|
||||
"""Test GET /api/config/backups endpoint."""
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'backups': []
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertIsInstance(expected_response['backups'], list)
|
||||
|
||||
def test_config_backup_restore_endpoint(self):
|
||||
"""Test POST /api/config/backup/<filename>/restore endpoint."""
|
||||
filename = 'config_backup_20231201_143000.json'
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': f'Configuration restored from {filename}'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
self.assertIn(filename, expected_response['message'])
|
||||
|
||||
def test_config_backup_download_endpoint(self):
|
||||
"""Test GET /api/config/backup/<filename>/download endpoint."""
|
||||
filename = 'config_backup_20231201_143000.json'
|
||||
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Backup download endpoint (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
|
||||
class TestDiagnosticsEndpoints(BaseAPITest):
|
||||
"""Test suite for diagnostics and monitoring API endpoints."""
|
||||
|
||||
def test_network_diagnostics_endpoint(self):
|
||||
"""Test GET /api/diagnostics/network endpoint."""
|
||||
mock_network_status = {
|
||||
'internet_connected': True,
|
||||
'dns_working': True,
|
||||
'aniworld_reachable': True
|
||||
}
|
||||
|
||||
with patch('src.server.app.network_health_checker') as mock_checker:
|
||||
mock_checker.get_network_status.return_value = mock_network_status
|
||||
mock_checker.check_url_reachability.return_value = True
|
||||
|
||||
network_status = mock_checker.get_network_status()
|
||||
self.assertTrue(network_status['internet_connected'])
|
||||
|
||||
def test_error_history_endpoint(self):
|
||||
"""Test GET /api/diagnostics/errors endpoint."""
|
||||
mock_errors = [
|
||||
{'timestamp': '2023-12-01T14:30:00', 'error': 'Test error 1'},
|
||||
{'timestamp': '2023-12-01T14:31:00', 'error': 'Test error 2'}
|
||||
]
|
||||
|
||||
with patch('src.server.app.error_recovery_manager') as mock_manager:
|
||||
mock_manager.error_history = mock_errors
|
||||
mock_manager.blacklisted_urls = {'bad_url.com': True}
|
||||
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'recent_errors': mock_errors[-50:],
|
||||
'total_errors': len(mock_errors),
|
||||
'blacklisted_urls': list(mock_manager.blacklisted_urls.keys())
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'success')
|
||||
self.assertEqual(len(expected_response['data']['recent_errors']), 2)
|
||||
|
||||
def test_clear_blacklist_endpoint(self):
|
||||
"""Test POST /api/recovery/clear-blacklist endpoint."""
|
||||
with patch('src.server.app.error_recovery_manager') as mock_manager:
|
||||
mock_manager.blacklisted_urls = {'url1': True, 'url2': True}
|
||||
mock_manager.blacklisted_urls.clear()
|
||||
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'message': 'URL blacklist cleared successfully'
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'success')
|
||||
|
||||
def test_retry_counts_endpoint(self):
|
||||
"""Test GET /api/recovery/retry-counts endpoint."""
|
||||
mock_retry_counts = {'url1': 3, 'url2': 5}
|
||||
|
||||
with patch('src.server.app.error_recovery_manager') as mock_manager:
|
||||
mock_manager.retry_counts = mock_retry_counts
|
||||
|
||||
expected_response = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'retry_counts': mock_retry_counts,
|
||||
'total_retries': sum(mock_retry_counts.values())
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['status'], 'success')
|
||||
self.assertEqual(expected_response['data']['total_retries'], 8)
|
||||
|
||||
def test_system_status_summary_endpoint(self):
|
||||
"""Test GET /api/diagnostics/system-status endpoint."""
|
||||
mock_health_status = {'cpu_usage': 25.5, 'memory_usage': 60.2}
|
||||
mock_network_status = {'internet_connected': True}
|
||||
|
||||
with patch('src.server.app.health_monitor') as mock_health, \
|
||||
patch('src.server.app.network_health_checker') as mock_network, \
|
||||
patch('src.server.app.is_process_running') as mock_running, \
|
||||
patch('src.server.app.error_recovery_manager') as mock_error:
|
||||
|
||||
mock_health.get_current_health_status.return_value = mock_health_status
|
||||
mock_network.get_network_status.return_value = mock_network_status
|
||||
mock_running.return_value = False
|
||||
mock_error.error_history = []
|
||||
mock_error.blacklisted_urls = {}
|
||||
|
||||
expected_keys = ['health', 'network', 'processes', 'errors', 'timestamp']
|
||||
|
||||
# Test that all expected sections are present
|
||||
for key in expected_keys:
|
||||
self.assertIsNotNone(key) # Placeholder assertion
|
||||
|
||||
|
||||
class TestErrorHandling(BaseAPITest):
|
||||
"""Test suite for error handling across all endpoints."""
|
||||
|
||||
def test_api_error_decorator(self):
|
||||
"""Test that @handle_api_errors decorator works correctly."""
|
||||
def test_function():
|
||||
raise ValueError("Test error")
|
||||
|
||||
# Simulate the decorator behavior
|
||||
try:
|
||||
test_function()
|
||||
self.fail("Expected ValueError")
|
||||
except ValueError as e:
|
||||
expected_response = {
|
||||
'status': 'error',
|
||||
'message': str(e)
|
||||
}
|
||||
self.assertEqual(expected_response['status'], 'error')
|
||||
self.assertEqual(expected_response['message'], 'Test error')
|
||||
|
||||
def test_authentication_required_error(self):
|
||||
"""Test error responses when authentication is required."""
|
||||
expected_response = {
|
||||
'status': 'error',
|
||||
'message': 'Authentication required',
|
||||
'code': 401
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['code'], 401)
|
||||
self.assertEqual(expected_response['status'], 'error')
|
||||
|
||||
def test_invalid_json_error(self):
|
||||
"""Test error responses for invalid JSON input."""
|
||||
expected_response = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid JSON in request body',
|
||||
'code': 400
|
||||
}
|
||||
|
||||
self.assertEqual(expected_response['code'], 400)
|
||||
self.assertEqual(expected_response['status'], 'error')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Create test suites for different categories
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
# Authentication tests
|
||||
auth_suite = loader.loadTestsFromTestCase(TestAuthenticationEndpoints)
|
||||
|
||||
# Configuration tests
|
||||
config_suite = loader.loadTestsFromTestCase(TestConfigurationEndpoints)
|
||||
|
||||
# Series management tests
|
||||
series_suite = loader.loadTestsFromTestCase(TestSeriesEndpoints)
|
||||
|
||||
# Download tests
|
||||
download_suite = loader.loadTestsFromTestCase(TestDownloadEndpoints)
|
||||
|
||||
# Process management tests
|
||||
process_suite = loader.loadTestsFromTestCase(TestProcessManagementEndpoints)
|
||||
|
||||
# Logging tests
|
||||
logging_suite = loader.loadTestsFromTestCase(TestLoggingEndpoints)
|
||||
|
||||
# Backup tests
|
||||
backup_suite = loader.loadTestsFromTestCase(TestBackupEndpoints)
|
||||
|
||||
# Diagnostics tests
|
||||
diagnostics_suite = loader.loadTestsFromTestCase(TestDiagnosticsEndpoints)
|
||||
|
||||
# Error handling tests
|
||||
error_suite = loader.loadTestsFromTestCase(TestErrorHandling)
|
||||
|
||||
# Combine all test suites
|
||||
all_tests = unittest.TestSuite([
|
||||
auth_suite,
|
||||
config_suite,
|
||||
series_suite,
|
||||
download_suite,
|
||||
process_suite,
|
||||
logging_suite,
|
||||
backup_suite,
|
||||
diagnostics_suite,
|
||||
error_suite
|
||||
])
|
||||
|
||||
# Run the tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(all_tests)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"COMPREHENSIVE API TEST SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
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}")
|
||||
print(f"Success rate: {((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100):.1f}%")
|
||||
|
||||
if result.failures:
|
||||
print(f"\nFailures:")
|
||||
for test, traceback in result.failures:
|
||||
print(f" - {test}: {traceback.split('AssertionError: ')[-1].split('\\n')[0] if 'AssertionError:' in traceback else 'See details above'}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\nErrors:")
|
||||
for test, traceback in result.errors:
|
||||
print(f" - {test}: {traceback.split('\\n')[-2] if len(traceback.split('\\n')) > 1 else 'See details above'}")
|
||||
@@ -1,480 +0,0 @@
|
||||
"""
|
||||
Live Flask App API Tests
|
||||
|
||||
These tests actually start the Flask application and make real HTTP requests
|
||||
to test the API endpoints end-to-end.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add paths 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 LiveFlaskAPITests(unittest.TestCase):
|
||||
"""Tests that use actual Flask test client to test API endpoints."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up Flask app for testing."""
|
||||
try:
|
||||
# Mock all the complex dependencies before importing the app
|
||||
with patch('sys.modules') as mock_modules:
|
||||
# Mock modules that might not be available
|
||||
mock_modules['main'] = MagicMock()
|
||||
mock_modules['core.entities.series'] = MagicMock()
|
||||
mock_modules['core.entities'] = MagicMock()
|
||||
mock_modules['infrastructure.file_system'] = MagicMock()
|
||||
mock_modules['infrastructure.providers.provider_factory'] = MagicMock()
|
||||
mock_modules['web.controllers.auth_controller'] = MagicMock()
|
||||
mock_modules['config'] = MagicMock()
|
||||
mock_modules['application.services.queue_service'] = MagicMock()
|
||||
|
||||
# Try to import the Flask app
|
||||
try:
|
||||
from app import app
|
||||
cls.app = app
|
||||
cls.app.config['TESTING'] = True
|
||||
cls.app.config['WTF_CSRF_ENABLED'] = False
|
||||
cls.client = app.test_client()
|
||||
cls.app_available = True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not import Flask app: {e}")
|
||||
cls.app_available = False
|
||||
cls.app = None
|
||||
cls.client = None
|
||||
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not set up Flask app: {e}")
|
||||
cls.app_available = False
|
||||
cls.app = None
|
||||
cls.client = None
|
||||
|
||||
def setUp(self):
|
||||
"""Set up for each test."""
|
||||
if not self.app_available:
|
||||
self.skipTest("Flask app not available for testing")
|
||||
|
||||
def test_static_routes_exist(self):
|
||||
"""Test that static JavaScript and CSS routes exist."""
|
||||
static_routes = [
|
||||
'/static/js/keyboard-shortcuts.js',
|
||||
'/static/js/drag-drop.js',
|
||||
'/static/js/bulk-operations.js',
|
||||
'/static/js/user-preferences.js',
|
||||
'/static/js/advanced-search.js',
|
||||
'/static/css/ux-features.css'
|
||||
]
|
||||
|
||||
for route in static_routes:
|
||||
response = self.client.get(route)
|
||||
# Should return 200 (content) or 404 (route exists but no content)
|
||||
# Should NOT return 500 (server error)
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
f"Route {route} should not return server error")
|
||||
|
||||
def test_main_page_routes(self):
|
||||
"""Test that main page routes exist."""
|
||||
routes = ['/', '/login', '/setup']
|
||||
|
||||
for route in routes:
|
||||
response = self.client.get(route)
|
||||
# Should return 200, 302 (redirect), or 404
|
||||
# Should NOT return 500 (server error)
|
||||
self.assertIn(response.status_code, [200, 302, 404],
|
||||
f"Route {route} returned unexpected status: {response.status_code}")
|
||||
|
||||
def test_api_auth_status_endpoint(self):
|
||||
"""Test GET /api/auth/status endpoint."""
|
||||
response = self.client.get('/api/auth/status')
|
||||
|
||||
# Should return a valid HTTP status (not 500 error)
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Auth status endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic auth status fields
|
||||
expected_fields = ['authenticated', 'has_master_password', 'setup_required']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Auth status should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Auth status should return valid JSON")
|
||||
|
||||
def test_api_series_endpoint(self):
|
||||
"""Test GET /api/series endpoint."""
|
||||
response = self.client.get('/api/series')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Series endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic series response structure
|
||||
expected_fields = ['status', 'series', 'total_series']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Series response should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Series endpoint should return valid JSON")
|
||||
|
||||
def test_api_status_endpoint(self):
|
||||
"""Test GET /api/status endpoint."""
|
||||
response = self.client.get('/api/status')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Status endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic status fields
|
||||
expected_fields = ['success', 'directory', 'series_count']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Status response should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Status endpoint should return valid JSON")
|
||||
|
||||
def test_api_process_locks_endpoint(self):
|
||||
"""Test GET /api/process/locks/status endpoint."""
|
||||
response = self.client.get('/api/process/locks/status')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Process locks endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic lock status fields
|
||||
expected_fields = ['success', 'locks']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Lock status should include {field}")
|
||||
|
||||
if 'locks' in data:
|
||||
# Should have rescan and download lock info
|
||||
lock_types = ['rescan', 'download']
|
||||
for lock_type in lock_types:
|
||||
self.assertIn(lock_type, data['locks'],
|
||||
f"Locks should include {lock_type}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Process locks endpoint should return valid JSON")
|
||||
|
||||
def test_api_search_endpoint_with_post(self):
|
||||
"""Test POST /api/search endpoint with valid data."""
|
||||
test_data = {'query': 'test anime'}
|
||||
|
||||
response = self.client.post(
|
||||
'/api/search',
|
||||
data=json.dumps(test_data),
|
||||
content_type='application/json'
|
||||
)
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Search endpoint should not return server error")
|
||||
|
||||
# Should handle JSON input (200 success or 400 bad request)
|
||||
self.assertIn(response.status_code, [200, 400, 401, 403],
|
||||
f"Search endpoint returned unexpected status: {response.status_code}")
|
||||
|
||||
def test_api_search_endpoint_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'
|
||||
)
|
||||
|
||||
# Should return 400 bad request for empty query
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# If it processed the request, should indicate error
|
||||
if data.get('status') == 'error':
|
||||
self.assertIn('empty', data.get('message', '').lower(),
|
||||
"Should indicate query is empty")
|
||||
except json.JSONDecodeError:
|
||||
pass # OK if it's not JSON
|
||||
|
||||
def test_api_scheduler_config_endpoint(self):
|
||||
"""Test GET /api/scheduler/config endpoint."""
|
||||
response = self.client.get('/api/scheduler/config')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Scheduler config endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic config structure
|
||||
expected_fields = ['success', 'config']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Scheduler config should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Scheduler config should return valid JSON")
|
||||
|
||||
def test_api_logging_config_endpoint(self):
|
||||
"""Test GET /api/logging/config endpoint."""
|
||||
response = self.client.get('/api/logging/config')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Logging config endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic config structure
|
||||
expected_fields = ['success', 'config']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Logging config should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Logging config should return valid JSON")
|
||||
|
||||
def test_api_advanced_config_endpoint(self):
|
||||
"""Test GET /api/config/section/advanced endpoint."""
|
||||
response = self.client.get('/api/config/section/advanced')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Advanced config endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic config structure
|
||||
expected_fields = ['success', 'config']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Advanced config should include {field}")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Advanced config should return valid JSON")
|
||||
|
||||
def test_api_logging_files_endpoint(self):
|
||||
"""Test GET /api/logging/files endpoint."""
|
||||
response = self.client.get('/api/logging/files')
|
||||
|
||||
# Should return a valid HTTP status
|
||||
self.assertNotEqual(response.status_code, 500,
|
||||
"Logging files endpoint should not return server error")
|
||||
|
||||
# If it returns 200, should have JSON content
|
||||
if response.status_code == 200:
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
# Should have basic response structure
|
||||
expected_fields = ['success', 'files']
|
||||
for field in expected_fields:
|
||||
self.assertIn(field, data, f"Logging files should include {field}")
|
||||
|
||||
# Files should be a list
|
||||
self.assertIsInstance(data['files'], list,
|
||||
"Files should be a list")
|
||||
except json.JSONDecodeError:
|
||||
self.fail("Logging files should return valid JSON")
|
||||
|
||||
def test_nonexistent_api_endpoint(self):
|
||||
"""Test that non-existent API endpoints return 404."""
|
||||
response = self.client.get('/api/nonexistent/endpoint')
|
||||
|
||||
# Should return 404 not found
|
||||
self.assertEqual(response.status_code, 404,
|
||||
"Non-existent endpoints should return 404")
|
||||
|
||||
def test_api_endpoints_handle_invalid_methods(self):
|
||||
"""Test that API endpoints handle invalid HTTP methods properly."""
|
||||
# Test GET on POST-only endpoints
|
||||
post_only_endpoints = [
|
||||
'/api/auth/login',
|
||||
'/api/auth/logout',
|
||||
'/api/rescan',
|
||||
'/api/download'
|
||||
]
|
||||
|
||||
for endpoint in post_only_endpoints:
|
||||
response = self.client.get(endpoint)
|
||||
# Should return 405 Method Not Allowed or 404 Not Found
|
||||
self.assertIn(response.status_code, [404, 405],
|
||||
f"GET on POST-only endpoint {endpoint} should return 404 or 405")
|
||||
|
||||
def test_api_endpoints_content_type(self):
|
||||
"""Test that API endpoints return proper content types."""
|
||||
json_endpoints = [
|
||||
'/api/auth/status',
|
||||
'/api/series',
|
||||
'/api/status',
|
||||
'/api/scheduler/config',
|
||||
'/api/logging/config'
|
||||
]
|
||||
|
||||
for endpoint in json_endpoints:
|
||||
response = self.client.get(endpoint)
|
||||
|
||||
if response.status_code == 200:
|
||||
# Should have JSON content type or be valid JSON
|
||||
content_type = response.headers.get('Content-Type', '')
|
||||
if 'application/json' not in content_type:
|
||||
# If not explicitly JSON content type, should still be valid JSON
|
||||
try:
|
||||
json.loads(response.data)
|
||||
except json.JSONDecodeError:
|
||||
self.fail(f"Endpoint {endpoint} should return valid JSON")
|
||||
|
||||
|
||||
class APIEndpointDiscoveryTest(unittest.TestCase):
|
||||
"""Test to discover and validate all available API endpoints."""
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""Set up Flask app for endpoint discovery."""
|
||||
try:
|
||||
# Mock dependencies and import app
|
||||
with patch('sys.modules') as mock_modules:
|
||||
mock_modules['main'] = MagicMock()
|
||||
mock_modules['core.entities.series'] = MagicMock()
|
||||
mock_modules['core.entities'] = MagicMock()
|
||||
mock_modules['infrastructure.file_system'] = MagicMock()
|
||||
mock_modules['infrastructure.providers.provider_factory'] = MagicMock()
|
||||
mock_modules['web.controllers.auth_controller'] = MagicMock()
|
||||
mock_modules['config'] = MagicMock()
|
||||
mock_modules['application.services.queue_service'] = MagicMock()
|
||||
|
||||
try:
|
||||
from app import app
|
||||
cls.app = app
|
||||
cls.app_available = True
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not import Flask app for discovery: {e}")
|
||||
cls.app_available = False
|
||||
cls.app = None
|
||||
except Exception as e:
|
||||
print(f"⚠️ Could not set up Flask app for discovery: {e}")
|
||||
cls.app_available = False
|
||||
cls.app = None
|
||||
|
||||
def setUp(self):
|
||||
"""Set up for each test."""
|
||||
if not self.app_available:
|
||||
self.skipTest("Flask app not available for endpoint discovery")
|
||||
|
||||
def test_discover_api_endpoints(self):
|
||||
"""Discover all registered API endpoints in the Flask app."""
|
||||
if not self.app:
|
||||
self.skipTest("Flask app not available")
|
||||
|
||||
# Get all registered routes
|
||||
api_routes = []
|
||||
other_routes = []
|
||||
|
||||
for rule in self.app.url_map.iter_rules():
|
||||
if rule.rule.startswith('/api/'):
|
||||
methods = ', '.join(sorted(rule.methods - {'OPTIONS', 'HEAD'}))
|
||||
api_routes.append(f"{methods} {rule.rule}")
|
||||
else:
|
||||
other_routes.append(rule.rule)
|
||||
|
||||
# Print discovered routes
|
||||
print(f"\n🔍 DISCOVERED API ROUTES ({len(api_routes)} total):")
|
||||
for route in sorted(api_routes):
|
||||
print(f" ✓ {route}")
|
||||
|
||||
print(f"\n📋 DISCOVERED NON-API ROUTES ({len(other_routes)} total):")
|
||||
for route in sorted(other_routes)[:10]: # Show first 10
|
||||
print(f" - {route}")
|
||||
if len(other_routes) > 10:
|
||||
print(f" ... and {len(other_routes) - 10} more")
|
||||
|
||||
# Validate we found API routes
|
||||
self.assertGreater(len(api_routes), 0, "Should discover some API routes")
|
||||
|
||||
# Validate common endpoints exist
|
||||
expected_patterns = [
|
||||
'/api/auth/',
|
||||
'/api/series',
|
||||
'/api/status',
|
||||
'/api/config/'
|
||||
]
|
||||
|
||||
found_patterns = []
|
||||
for pattern in expected_patterns:
|
||||
for route in api_routes:
|
||||
if pattern in route:
|
||||
found_patterns.append(pattern)
|
||||
break
|
||||
|
||||
print(f"\n✅ Found {len(found_patterns)}/{len(expected_patterns)} expected API patterns:")
|
||||
for pattern in found_patterns:
|
||||
print(f" ✓ {pattern}")
|
||||
|
||||
missing_patterns = set(expected_patterns) - set(found_patterns)
|
||||
if missing_patterns:
|
||||
print(f"\n⚠️ Missing expected patterns:")
|
||||
for pattern in missing_patterns:
|
||||
print(f" - {pattern}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run the live Flask tests
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
# Load test classes
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(loader.loadTestsFromTestCase(LiveFlaskAPITests))
|
||||
suite.addTests(loader.loadTestsFromTestCase(APIEndpointDiscoveryTest))
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"LIVE FLASK API TEST SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
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}%")
|
||||
|
||||
if result.failures:
|
||||
print(f"\n🔥 FAILURES:")
|
||||
for test, traceback in result.failures:
|
||||
print(f" - {test}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\n💥 ERRORS:")
|
||||
for test, traceback in result.errors:
|
||||
print(f" - {test}")
|
||||
|
||||
# Summary message
|
||||
if result.wasSuccessful():
|
||||
print(f"\n🎉 All live Flask API tests passed!")
|
||||
print(f"✅ API endpoints are responding correctly")
|
||||
print(f"✅ JSON responses are properly formatted")
|
||||
print(f"✅ HTTP methods are handled appropriately")
|
||||
print(f"✅ Error handling is working")
|
||||
else:
|
||||
print(f"\n⚠️ Some tests failed - check the Flask app setup")
|
||||
|
||||
exit(0 if result.wasSuccessful() else 1)
|
||||
@@ -1,596 +0,0 @@
|
||||
"""
|
||||
Simplified API endpoint tests that focus on testing logic without complex imports.
|
||||
|
||||
This test suite validates API endpoint functionality using simple mocks and
|
||||
direct testing of the expected behavior patterns.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
class SimpleAPIEndpointTests(unittest.TestCase):
|
||||
"""Simplified tests for API endpoints without complex dependencies."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures."""
|
||||
self.maxDiff = None
|
||||
|
||||
def test_auth_setup_response_structure(self):
|
||||
"""Test that auth setup returns proper response structure."""
|
||||
# Mock the expected response structure
|
||||
expected_response = {
|
||||
'success': True,
|
||||
'message': 'Master password set successfully',
|
||||
'session_id': 'test-session-123'
|
||||
}
|
||||
|
||||
self.assertIn('success', expected_response)
|
||||
self.assertIn('message', expected_response)
|
||||
self.assertIn('session_id', expected_response)
|
||||
self.assertTrue(expected_response['success'])
|
||||
|
||||
def test_auth_login_response_structure(self):
|
||||
"""Test that auth login returns proper response structure."""
|
||||
# Test successful login response
|
||||
success_response = {
|
||||
'success': True,
|
||||
'session_id': 'session-123',
|
||||
'message': 'Login successful'
|
||||
}
|
||||
|
||||
self.assertTrue(success_response['success'])
|
||||
self.assertIn('session_id', success_response)
|
||||
|
||||
# Test failed login response
|
||||
failure_response = {
|
||||
'success': False,
|
||||
'error': 'Invalid password'
|
||||
}
|
||||
|
||||
self.assertFalse(failure_response['success'])
|
||||
self.assertIn('error', failure_response)
|
||||
|
||||
def test_auth_status_response_structure(self):
|
||||
"""Test that auth status returns proper response structure."""
|
||||
status_response = {
|
||||
'authenticated': True,
|
||||
'has_master_password': True,
|
||||
'setup_required': False,
|
||||
'session_info': {
|
||||
'authenticated': True,
|
||||
'session_id': 'test-session'
|
||||
}
|
||||
}
|
||||
|
||||
self.assertIn('authenticated', status_response)
|
||||
self.assertIn('has_master_password', status_response)
|
||||
self.assertIn('setup_required', status_response)
|
||||
self.assertIn('session_info', status_response)
|
||||
|
||||
def test_series_list_response_structure(self):
|
||||
"""Test that series list returns proper response structure."""
|
||||
# Test with data
|
||||
series_response = {
|
||||
'status': 'success',
|
||||
'series': [
|
||||
{
|
||||
'folder': 'test_anime',
|
||||
'name': 'Test Anime',
|
||||
'total_episodes': 12,
|
||||
'missing_episodes': 2,
|
||||
'status': 'ongoing',
|
||||
'episodes': {'Season 1': [1, 2, 3, 4, 5]}
|
||||
}
|
||||
],
|
||||
'total_series': 1
|
||||
}
|
||||
|
||||
self.assertEqual(series_response['status'], 'success')
|
||||
self.assertIn('series', series_response)
|
||||
self.assertIn('total_series', series_response)
|
||||
self.assertEqual(len(series_response['series']), 1)
|
||||
|
||||
# Test empty response
|
||||
empty_response = {
|
||||
'status': 'success',
|
||||
'series': [],
|
||||
'total_series': 0,
|
||||
'message': 'No series data available. Please perform a scan to load series.'
|
||||
}
|
||||
|
||||
self.assertEqual(empty_response['status'], 'success')
|
||||
self.assertEqual(len(empty_response['series']), 0)
|
||||
self.assertIn('message', empty_response)
|
||||
|
||||
def test_search_response_structure(self):
|
||||
"""Test that search returns proper response structure."""
|
||||
# Test successful search
|
||||
search_response = {
|
||||
'status': 'success',
|
||||
'results': [
|
||||
{'name': 'Anime 1', 'link': 'https://example.com/anime1'},
|
||||
{'name': 'Anime 2', 'link': 'https://example.com/anime2'}
|
||||
],
|
||||
'total': 2
|
||||
}
|
||||
|
||||
self.assertEqual(search_response['status'], 'success')
|
||||
self.assertIn('results', search_response)
|
||||
self.assertIn('total', search_response)
|
||||
self.assertEqual(search_response['total'], 2)
|
||||
|
||||
# Test search error
|
||||
error_response = {
|
||||
'status': 'error',
|
||||
'message': 'Search query cannot be empty'
|
||||
}
|
||||
|
||||
self.assertEqual(error_response['status'], 'error')
|
||||
self.assertIn('message', error_response)
|
||||
|
||||
def test_rescan_response_structure(self):
|
||||
"""Test that rescan returns proper response structure."""
|
||||
# Test successful rescan start
|
||||
success_response = {
|
||||
'status': 'success',
|
||||
'message': 'Rescan started'
|
||||
}
|
||||
|
||||
self.assertEqual(success_response['status'], 'success')
|
||||
self.assertIn('started', success_response['message'])
|
||||
|
||||
# Test rescan already running
|
||||
running_response = {
|
||||
'status': 'error',
|
||||
'message': 'Rescan is already running. Please wait for it to complete.',
|
||||
'is_running': True
|
||||
}
|
||||
|
||||
self.assertEqual(running_response['status'], 'error')
|
||||
self.assertTrue(running_response['is_running'])
|
||||
|
||||
def test_download_response_structure(self):
|
||||
"""Test that download returns proper response structure."""
|
||||
# Test successful download start
|
||||
success_response = {
|
||||
'status': 'success',
|
||||
'message': 'Download functionality will be implemented with queue system'
|
||||
}
|
||||
|
||||
self.assertEqual(success_response['status'], 'success')
|
||||
|
||||
# Test download already running
|
||||
running_response = {
|
||||
'status': 'error',
|
||||
'message': 'Download is already running. Please wait for it to complete.',
|
||||
'is_running': True
|
||||
}
|
||||
|
||||
self.assertEqual(running_response['status'], 'error')
|
||||
self.assertTrue(running_response['is_running'])
|
||||
|
||||
def test_process_locks_response_structure(self):
|
||||
"""Test that process locks status returns proper response structure."""
|
||||
locks_response = {
|
||||
'success': True,
|
||||
'locks': {
|
||||
'rescan': {
|
||||
'is_locked': False,
|
||||
'locked_by': None,
|
||||
'lock_time': None
|
||||
},
|
||||
'download': {
|
||||
'is_locked': True,
|
||||
'locked_by': 'system',
|
||||
'lock_time': None
|
||||
}
|
||||
},
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.assertTrue(locks_response['success'])
|
||||
self.assertIn('locks', locks_response)
|
||||
self.assertIn('rescan', locks_response['locks'])
|
||||
self.assertIn('download', locks_response['locks'])
|
||||
self.assertIn('timestamp', locks_response)
|
||||
|
||||
def test_system_status_response_structure(self):
|
||||
"""Test that system status returns proper response structure."""
|
||||
status_response = {
|
||||
'success': True,
|
||||
'directory': '/test/anime',
|
||||
'series_count': 5,
|
||||
'timestamp': datetime.now().isoformat()
|
||||
}
|
||||
|
||||
self.assertTrue(status_response['success'])
|
||||
self.assertIn('directory', status_response)
|
||||
self.assertIn('series_count', status_response)
|
||||
self.assertIn('timestamp', status_response)
|
||||
self.assertIsInstance(status_response['series_count'], int)
|
||||
|
||||
def test_logging_config_response_structure(self):
|
||||
"""Test that logging config returns proper response structure."""
|
||||
# Test GET response
|
||||
get_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'log_level': 'INFO',
|
||||
'enable_console_logging': True,
|
||||
'enable_console_progress': True,
|
||||
'enable_fail2ban_logging': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertTrue(get_response['success'])
|
||||
self.assertIn('config', get_response)
|
||||
self.assertIn('log_level', get_response['config'])
|
||||
|
||||
# Test POST response
|
||||
post_response = {
|
||||
'success': True,
|
||||
'message': 'Logging configuration saved (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(post_response['success'])
|
||||
self.assertIn('message', post_response)
|
||||
|
||||
def test_scheduler_config_response_structure(self):
|
||||
"""Test that scheduler config returns proper response structure."""
|
||||
# Test GET response
|
||||
get_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'enabled': False,
|
||||
'time': '03:00',
|
||||
'auto_download_after_rescan': False,
|
||||
'next_run': None,
|
||||
'last_run': None,
|
||||
'is_running': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertTrue(get_response['success'])
|
||||
self.assertIn('config', get_response)
|
||||
self.assertIn('enabled', get_response['config'])
|
||||
self.assertIn('time', get_response['config'])
|
||||
|
||||
# Test POST response
|
||||
post_response = {
|
||||
'success': True,
|
||||
'message': 'Scheduler configuration saved (placeholder)'
|
||||
}
|
||||
|
||||
self.assertTrue(post_response['success'])
|
||||
|
||||
def test_advanced_config_response_structure(self):
|
||||
"""Test that advanced config returns proper response structure."""
|
||||
config_response = {
|
||||
'success': True,
|
||||
'config': {
|
||||
'max_concurrent_downloads': 3,
|
||||
'provider_timeout': 30,
|
||||
'enable_debug_mode': False
|
||||
}
|
||||
}
|
||||
|
||||
self.assertTrue(config_response['success'])
|
||||
self.assertIn('config', config_response)
|
||||
self.assertIn('max_concurrent_downloads', config_response['config'])
|
||||
self.assertIn('provider_timeout', config_response['config'])
|
||||
self.assertIn('enable_debug_mode', config_response['config'])
|
||||
|
||||
def test_backup_operations_response_structure(self):
|
||||
"""Test that backup operations return proper response structure."""
|
||||
# Test create backup
|
||||
create_response = {
|
||||
'success': True,
|
||||
'message': 'Configuration backup created successfully',
|
||||
'filename': 'config_backup_20231201_143000.json'
|
||||
}
|
||||
|
||||
self.assertTrue(create_response['success'])
|
||||
self.assertIn('filename', create_response)
|
||||
self.assertIn('config_backup_', create_response['filename'])
|
||||
|
||||
# Test list backups
|
||||
list_response = {
|
||||
'success': True,
|
||||
'backups': []
|
||||
}
|
||||
|
||||
self.assertTrue(list_response['success'])
|
||||
self.assertIn('backups', list_response)
|
||||
self.assertIsInstance(list_response['backups'], list)
|
||||
|
||||
# Test restore backup
|
||||
restore_response = {
|
||||
'success': True,
|
||||
'message': 'Configuration restored from config_backup_20231201_143000.json'
|
||||
}
|
||||
|
||||
self.assertTrue(restore_response['success'])
|
||||
self.assertIn('restored', restore_response['message'])
|
||||
|
||||
def test_diagnostics_response_structure(self):
|
||||
"""Test that diagnostics endpoints return proper response structure."""
|
||||
# Test network diagnostics
|
||||
network_response = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'internet_connected': True,
|
||||
'dns_working': True,
|
||||
'aniworld_reachable': True
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(network_response['status'], 'success')
|
||||
self.assertIn('data', network_response)
|
||||
|
||||
# Test error history
|
||||
error_response = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'recent_errors': [],
|
||||
'total_errors': 0,
|
||||
'blacklisted_urls': []
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(error_response['status'], 'success')
|
||||
self.assertIn('recent_errors', error_response['data'])
|
||||
self.assertIn('total_errors', error_response['data'])
|
||||
self.assertIn('blacklisted_urls', error_response['data'])
|
||||
|
||||
# Test retry counts
|
||||
retry_response = {
|
||||
'status': 'success',
|
||||
'data': {
|
||||
'retry_counts': {'url1': 3, 'url2': 5},
|
||||
'total_retries': 8
|
||||
}
|
||||
}
|
||||
|
||||
self.assertEqual(retry_response['status'], 'success')
|
||||
self.assertIn('retry_counts', retry_response['data'])
|
||||
self.assertIn('total_retries', retry_response['data'])
|
||||
|
||||
def test_error_handling_patterns(self):
|
||||
"""Test common error handling patterns across endpoints."""
|
||||
# Test authentication error
|
||||
auth_error = {
|
||||
'status': 'error',
|
||||
'message': 'Authentication required',
|
||||
'code': 401
|
||||
}
|
||||
|
||||
self.assertEqual(auth_error['status'], 'error')
|
||||
self.assertEqual(auth_error['code'], 401)
|
||||
|
||||
# Test validation error
|
||||
validation_error = {
|
||||
'status': 'error',
|
||||
'message': 'Invalid input data',
|
||||
'code': 400
|
||||
}
|
||||
|
||||
self.assertEqual(validation_error['code'], 400)
|
||||
|
||||
# Test server error
|
||||
server_error = {
|
||||
'status': 'error',
|
||||
'message': 'Internal server error',
|
||||
'code': 500
|
||||
}
|
||||
|
||||
self.assertEqual(server_error['code'], 500)
|
||||
|
||||
def test_input_validation_patterns(self):
|
||||
"""Test input validation patterns."""
|
||||
# Test empty query validation
|
||||
def validate_search_query(query):
|
||||
if not query or not query.strip():
|
||||
return {
|
||||
'status': 'error',
|
||||
'message': 'Search query cannot be empty'
|
||||
}
|
||||
return {'status': 'success'}
|
||||
|
||||
# Test empty query
|
||||
result = validate_search_query('')
|
||||
self.assertEqual(result['status'], 'error')
|
||||
|
||||
result = validate_search_query(' ')
|
||||
self.assertEqual(result['status'], 'error')
|
||||
|
||||
# Test valid query
|
||||
result = validate_search_query('anime name')
|
||||
self.assertEqual(result['status'], 'success')
|
||||
|
||||
# Test directory validation
|
||||
def validate_directory(directory):
|
||||
if not directory:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Directory is required'
|
||||
}
|
||||
return {'success': True}
|
||||
|
||||
result = validate_directory('')
|
||||
self.assertFalse(result['success'])
|
||||
|
||||
result = validate_directory('/valid/path')
|
||||
self.assertTrue(result['success'])
|
||||
|
||||
def test_authentication_flow_patterns(self):
|
||||
"""Test authentication flow patterns."""
|
||||
# Simulate session manager behavior
|
||||
class MockSessionManager:
|
||||
def __init__(self):
|
||||
self.sessions = {}
|
||||
|
||||
def login(self, password):
|
||||
if password == 'correct_password':
|
||||
session_id = 'session-123'
|
||||
self.sessions[session_id] = {
|
||||
'authenticated': True,
|
||||
'created_at': 1234567890
|
||||
}
|
||||
return {
|
||||
'success': True,
|
||||
'session_id': session_id
|
||||
}
|
||||
else:
|
||||
return {
|
||||
'success': False,
|
||||
'error': 'Invalid password'
|
||||
}
|
||||
|
||||
def logout(self, session_id):
|
||||
if session_id in self.sessions:
|
||||
del self.sessions[session_id]
|
||||
return {'success': True}
|
||||
|
||||
def is_authenticated(self, session_id):
|
||||
return session_id in self.sessions
|
||||
|
||||
# Test the flow
|
||||
session_manager = MockSessionManager()
|
||||
|
||||
# Test login with correct password
|
||||
result = session_manager.login('correct_password')
|
||||
self.assertTrue(result['success'])
|
||||
self.assertIn('session_id', result)
|
||||
|
||||
session_id = result['session_id']
|
||||
self.assertTrue(session_manager.is_authenticated(session_id))
|
||||
|
||||
# Test logout
|
||||
result = session_manager.logout(session_id)
|
||||
self.assertTrue(result['success'])
|
||||
self.assertFalse(session_manager.is_authenticated(session_id))
|
||||
|
||||
# Test login with wrong password
|
||||
result = session_manager.login('wrong_password')
|
||||
self.assertFalse(result['success'])
|
||||
self.assertIn('error', result)
|
||||
|
||||
|
||||
class APIEndpointCoverageTest(unittest.TestCase):
|
||||
"""Test to verify we have coverage for all known API endpoints."""
|
||||
|
||||
def test_endpoint_coverage(self):
|
||||
"""Verify we have identified all API endpoints for testing."""
|
||||
# List all known API endpoints from the app.py analysis
|
||||
expected_endpoints = [
|
||||
# Authentication
|
||||
'POST /api/auth/setup',
|
||||
'POST /api/auth/login',
|
||||
'POST /api/auth/logout',
|
||||
'GET /api/auth/status',
|
||||
|
||||
# Configuration
|
||||
'POST /api/config/directory',
|
||||
'GET /api/scheduler/config',
|
||||
'POST /api/scheduler/config',
|
||||
'GET /api/config/section/advanced',
|
||||
'POST /api/config/section/advanced',
|
||||
|
||||
# Series Management
|
||||
'GET /api/series',
|
||||
'POST /api/search',
|
||||
'POST /api/rescan',
|
||||
|
||||
# Download Management
|
||||
'POST /api/download',
|
||||
|
||||
# System Status
|
||||
'GET /api/process/locks/status',
|
||||
'GET /api/status',
|
||||
|
||||
# Logging
|
||||
'GET /api/logging/config',
|
||||
'POST /api/logging/config',
|
||||
'GET /api/logging/files',
|
||||
'POST /api/logging/test',
|
||||
'POST /api/logging/cleanup',
|
||||
'GET /api/logging/files/<filename>/tail',
|
||||
|
||||
# Backup Management
|
||||
'POST /api/config/backup',
|
||||
'GET /api/config/backups',
|
||||
'POST /api/config/backup/<filename>/restore',
|
||||
'GET /api/config/backup/<filename>/download',
|
||||
|
||||
# Diagnostics
|
||||
'GET /api/diagnostics/network',
|
||||
'GET /api/diagnostics/errors',
|
||||
'POST /api/recovery/clear-blacklist',
|
||||
'GET /api/recovery/retry-counts',
|
||||
'GET /api/diagnostics/system-status'
|
||||
]
|
||||
|
||||
# Verify we have a reasonable number of endpoints
|
||||
self.assertGreater(len(expected_endpoints), 25,
|
||||
"Should have identified more than 25 API endpoints")
|
||||
|
||||
# Verify endpoint format consistency
|
||||
for endpoint in expected_endpoints:
|
||||
self.assertRegex(endpoint, r'^(GET|POST|PUT|DELETE) /api/',
|
||||
f"Endpoint {endpoint} should follow proper format")
|
||||
|
||||
print(f"\n✅ Verified {len(expected_endpoints)} API endpoints for testing:")
|
||||
for endpoint in sorted(expected_endpoints):
|
||||
print(f" - {endpoint}")
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run the simplified tests
|
||||
loader = unittest.TestLoader()
|
||||
|
||||
# Load all test classes
|
||||
suite = unittest.TestSuite()
|
||||
suite.addTests(loader.loadTestsFromTestCase(SimpleAPIEndpointTests))
|
||||
suite.addTests(loader.loadTestsFromTestCase(APIEndpointCoverageTest))
|
||||
|
||||
# Run tests
|
||||
runner = unittest.TextTestRunner(verbosity=2)
|
||||
result = runner.run(suite)
|
||||
|
||||
# Print summary
|
||||
print(f"\n{'='*60}")
|
||||
print(f"SIMPLIFIED API TEST SUMMARY")
|
||||
print(f"{'='*60}")
|
||||
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}%")
|
||||
|
||||
if result.failures:
|
||||
print(f"\n🔥 FAILURES:")
|
||||
for test, traceback in result.failures[:5]: # Show first 5
|
||||
print(f" - {test}")
|
||||
|
||||
if result.errors:
|
||||
print(f"\n💥 ERRORS:")
|
||||
for test, traceback in result.errors[:5]: # Show first 5
|
||||
print(f" - {test}")
|
||||
|
||||
# Summary message
|
||||
if result.wasSuccessful():
|
||||
print(f"\n🎉 All simplified API tests passed!")
|
||||
print(f"✅ API response structures are properly defined")
|
||||
print(f"✅ Input validation patterns are working")
|
||||
print(f"✅ Authentication flows are validated")
|
||||
print(f"✅ Error handling patterns are consistent")
|
||||
else:
|
||||
print(f"\n⚠️ Some tests failed - review the patterns above")
|
||||
|
||||
exit(0 if result.wasSuccessful() else 1)
|
||||
@@ -1,42 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to verify Flask app structure without initializing SeriesApp
|
||||
"""
|
||||
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Test if we can import Flask modules
|
||||
try:
|
||||
from flask import Flask
|
||||
from flask_socketio import SocketIO
|
||||
print("✅ Flask and SocketIO imports successful")
|
||||
except ImportError as e:
|
||||
print(f"❌ Flask import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test if we can import our modules
|
||||
try:
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..', '..', '..'))
|
||||
from src.server.core.entities.series import Serie
|
||||
from src.server.core.entities.SerieList import SerieList
|
||||
print("✅ Core modules import successful")
|
||||
except ImportError as e:
|
||||
print(f"❌ Core module import failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
# Test Flask app creation
|
||||
try:
|
||||
app = Flask(__name__)
|
||||
app.config['SECRET_KEY'] = 'test-key'
|
||||
socketio = SocketIO(app, cors_allowed_origins="*")
|
||||
print("✅ Flask app creation successful")
|
||||
except Exception as e:
|
||||
print(f"❌ Flask app creation failed: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
print("🎉 All tests passed! Flask app structure is valid.")
|
||||
print("\nTo run the server:")
|
||||
print("1. Set ANIME_DIRECTORY environment variable to your anime directory")
|
||||
print("2. Run: python app.py")
|
||||
print("3. Open browser to http://localhost:5000")
|
||||
@@ -1,127 +0,0 @@
|
||||
"""
|
||||
Test suite for authentication and session management.
|
||||
|
||||
This test module validates the authentication system, session management,
|
||||
and security features.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestAuthenticationSystem(unittest.TestCase):
|
||||
"""Test class for authentication and session management."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
# Mock Flask app for testing
|
||||
self.mock_app = MagicMock()
|
||||
self.mock_app.config = {'SECRET_KEY': 'test_secret'}
|
||||
|
||||
def test_session_manager_initialization(self):
|
||||
"""Test SessionManager initialization."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
self.assertIsNotNone(manager)
|
||||
self.assertTrue(hasattr(manager, 'login'))
|
||||
self.assertTrue(hasattr(manager, 'check_password'))
|
||||
|
||||
print('✓ SessionManager initialization successful')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager initialization failed: {e}')
|
||||
|
||||
def test_login_method_exists(self):
|
||||
"""Test that login method exists and returns proper response."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test login method exists
|
||||
self.assertTrue(hasattr(manager, 'login'))
|
||||
|
||||
# Test login with invalid credentials returns dict
|
||||
result = manager.login('wrong_password')
|
||||
self.assertIsInstance(result, dict)
|
||||
self.assertIn('success', result)
|
||||
self.assertFalse(result['success'])
|
||||
|
||||
print('✓ SessionManager login method validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager login method test failed: {e}')
|
||||
|
||||
def test_password_checking(self):
|
||||
"""Test password validation functionality."""
|
||||
try:
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test check_password method exists
|
||||
self.assertTrue(hasattr(manager, 'check_password'))
|
||||
|
||||
# Test with empty/invalid password
|
||||
result = manager.check_password('')
|
||||
self.assertFalse(result)
|
||||
|
||||
result = manager.check_password('wrong_password')
|
||||
self.assertFalse(result)
|
||||
|
||||
print('✓ SessionManager password checking validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'SessionManager password checking test failed: {e}')
|
||||
|
||||
|
||||
class TestConfigurationSystem(unittest.TestCase):
|
||||
"""Test class for configuration management."""
|
||||
|
||||
def test_config_manager_initialization(self):
|
||||
"""Test ConfigManager initialization."""
|
||||
try:
|
||||
from config import ConfigManager
|
||||
|
||||
manager = ConfigManager()
|
||||
self.assertIsNotNone(manager)
|
||||
self.assertTrue(hasattr(manager, 'anime_directory'))
|
||||
|
||||
print('✓ ConfigManager initialization successful')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ConfigManager initialization failed: {e}')
|
||||
|
||||
def test_anime_directory_property(self):
|
||||
"""Test anime_directory property getter and setter."""
|
||||
try:
|
||||
from config import ConfigManager
|
||||
|
||||
manager = ConfigManager()
|
||||
|
||||
# Test getter
|
||||
initial_dir = manager.anime_directory
|
||||
self.assertIsInstance(initial_dir, str)
|
||||
|
||||
# Test setter exists
|
||||
test_dir = 'C:\\TestAnimeDir'
|
||||
manager.anime_directory = test_dir
|
||||
|
||||
# Verify setter worked
|
||||
self.assertEqual(manager.anime_directory, test_dir)
|
||||
|
||||
print('✓ ConfigManager anime_directory property validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ConfigManager anime_directory property test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
@@ -1,288 +0,0 @@
|
||||
"""
|
||||
Focused test suite for manager JavaScript and CSS generation.
|
||||
|
||||
This test module validates the core functionality that we know is working.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestManagerGenerationCore(unittest.TestCase):
|
||||
"""Test class for validating core manager JavaScript/CSS generation functionality."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.managers_tested = 0
|
||||
self.total_js_chars = 0
|
||||
self.total_css_chars = 0
|
||||
print("\n" + "="*50)
|
||||
|
||||
def test_keyboard_shortcut_manager_generation(self):
|
||||
"""Test KeyboardShortcutManager JavaScript generation."""
|
||||
print("Testing KeyboardShortcutManager...")
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
js = manager.get_shortcuts_js()
|
||||
|
||||
# Validate JS generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 1000) # Should be substantial
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ KeyboardShortcutManager: {len(js):,} JS characters generated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager test failed: {e}')
|
||||
|
||||
def test_drag_drop_manager_generation(self):
|
||||
"""Test DragDropManager JavaScript and CSS generation."""
|
||||
print("Testing DragDropManager...")
|
||||
try:
|
||||
from drag_drop import DragDropManager
|
||||
manager = DragDropManager()
|
||||
|
||||
js = manager.get_drag_drop_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper JSON serialization (no Python booleans)
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
self.assertNotIn('None', js)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ DragDropManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'DragDropManager test failed: {e}')
|
||||
|
||||
def test_accessibility_manager_generation(self):
|
||||
"""Test AccessibilityManager JavaScript and CSS generation."""
|
||||
print("Testing AccessibilityManager...")
|
||||
try:
|
||||
from accessibility_features import AccessibilityManager
|
||||
manager = AccessibilityManager()
|
||||
|
||||
js = manager.get_accessibility_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper JSON serialization
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AccessibilityManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AccessibilityManager test failed: {e}')
|
||||
|
||||
def test_user_preferences_manager_generation(self):
|
||||
"""Test UserPreferencesManager JavaScript and CSS generation."""
|
||||
print("Testing UserPreferencesManager...")
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
manager = UserPreferencesManager()
|
||||
|
||||
# Verify preferences attribute exists (this was the main fix)
|
||||
self.assertTrue(hasattr(manager, 'preferences'))
|
||||
self.assertIsInstance(manager.preferences, dict)
|
||||
|
||||
js = manager.get_preferences_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UserPreferencesManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager test failed: {e}')
|
||||
|
||||
def test_advanced_search_manager_generation(self):
|
||||
"""Test AdvancedSearchManager JavaScript and CSS generation."""
|
||||
print("Testing AdvancedSearchManager...")
|
||||
try:
|
||||
from advanced_search import AdvancedSearchManager
|
||||
manager = AdvancedSearchManager()
|
||||
|
||||
js = manager.get_search_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AdvancedSearchManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AdvancedSearchManager test failed: {e}')
|
||||
|
||||
def test_undo_redo_manager_generation(self):
|
||||
"""Test UndoRedoManager JavaScript and CSS generation."""
|
||||
print("Testing UndoRedoManager...")
|
||||
try:
|
||||
from undo_redo_manager import UndoRedoManager
|
||||
manager = UndoRedoManager()
|
||||
|
||||
js = manager.get_undo_redo_js()
|
||||
css = manager.get_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UndoRedoManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UndoRedoManager test failed: {e}')
|
||||
|
||||
def test_multi_screen_manager_generation(self):
|
||||
"""Test MultiScreenManager JavaScript and CSS generation."""
|
||||
print("Testing MultiScreenManager...")
|
||||
try:
|
||||
from multi_screen_support import MultiScreenManager
|
||||
manager = MultiScreenManager()
|
||||
|
||||
js = manager.get_multiscreen_js()
|
||||
css = manager.get_multiscreen_css()
|
||||
|
||||
# Validate generation
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 1000)
|
||||
self.assertGreater(len(css), 100)
|
||||
|
||||
# Check for proper f-string escaping (no Python syntax)
|
||||
self.assertNotIn('True', js)
|
||||
self.assertNotIn('False', js)
|
||||
self.assertNotIn('None', js)
|
||||
|
||||
# Verify JavaScript is properly formatted
|
||||
self.assertIn('class', js) # Should contain JavaScript class syntax
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ MultiScreenManager: {len(js):,} JS chars, {len(css):,} CSS chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'MultiScreenManager test failed: {e}')
|
||||
|
||||
|
||||
class TestComprehensiveSuite(unittest.TestCase):
|
||||
"""Comprehensive test to verify all fixes are working."""
|
||||
|
||||
def test_all_manager_fixes_comprehensive(self):
|
||||
"""Run comprehensive test of all manager fixes."""
|
||||
print("\n" + "="*60)
|
||||
print("COMPREHENSIVE MANAGER VALIDATION")
|
||||
print("="*60)
|
||||
|
||||
managers_tested = 0
|
||||
total_js = 0
|
||||
total_css = 0
|
||||
|
||||
# Test each manager
|
||||
test_cases = [
|
||||
('KeyboardShortcutManager', 'keyboard_shortcuts', 'get_shortcuts_js', None),
|
||||
('DragDropManager', 'drag_drop', 'get_drag_drop_js', 'get_css'),
|
||||
('AccessibilityManager', 'accessibility_features', 'get_accessibility_js', 'get_css'),
|
||||
('UserPreferencesManager', 'user_preferences', 'get_preferences_js', 'get_css'),
|
||||
('AdvancedSearchManager', 'advanced_search', 'get_search_js', 'get_css'),
|
||||
('UndoRedoManager', 'undo_redo_manager', 'get_undo_redo_js', 'get_css'),
|
||||
('MultiScreenManager', 'multi_screen_support', 'get_multiscreen_js', 'get_multiscreen_css'),
|
||||
]
|
||||
|
||||
for class_name, module_name, js_method, css_method in test_cases:
|
||||
try:
|
||||
# Dynamic import
|
||||
module = __import__(module_name, fromlist=[class_name])
|
||||
manager_class = getattr(module, class_name)
|
||||
manager = manager_class()
|
||||
|
||||
# Get JS
|
||||
js_func = getattr(manager, js_method)
|
||||
js = js_func()
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
total_js += len(js)
|
||||
|
||||
# Get CSS if available
|
||||
css_chars = 0
|
||||
if css_method:
|
||||
css_func = getattr(manager, css_method)
|
||||
css = css_func()
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(css), 0)
|
||||
css_chars = len(css)
|
||||
total_css += css_chars
|
||||
|
||||
managers_tested += 1
|
||||
print(f'✓ {class_name}: JS={len(js):,} chars' +
|
||||
(f', CSS={css_chars:,} chars' if css_chars > 0 else ' (JS only)'))
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'{class_name} failed: {e}')
|
||||
|
||||
# Final validation
|
||||
expected_managers = 7
|
||||
self.assertEqual(managers_tested, expected_managers)
|
||||
self.assertGreater(total_js, 100000) # Should have substantial JS
|
||||
self.assertGreater(total_css, 10000) # Should have substantial CSS
|
||||
|
||||
print(f'\n{"="*60}')
|
||||
print(f'🎉 ALL {managers_tested} MANAGERS PASSED!')
|
||||
print(f'📊 Total JavaScript: {total_js:,} characters')
|
||||
print(f'🎨 Total CSS: {total_css:,} characters')
|
||||
print(f'✅ No JavaScript or CSS generation issues found!')
|
||||
print(f'{"="*60}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run with high verbosity
|
||||
unittest.main(verbosity=2, buffer=False)
|
||||
@@ -1,131 +0,0 @@
|
||||
"""
|
||||
Test suite for Flask application routes and API endpoints.
|
||||
|
||||
This test module validates the main Flask application functionality,
|
||||
route handling, and API responses.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestFlaskApplication(unittest.TestCase):
|
||||
"""Test class for Flask application and routes."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
pass
|
||||
|
||||
def test_app_imports(self):
|
||||
"""Test that main app module can be imported without errors."""
|
||||
try:
|
||||
import app
|
||||
self.assertIsNotNone(app)
|
||||
print('✓ Main app module imports successfully')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'App import failed: {e}')
|
||||
|
||||
@patch('app.Flask')
|
||||
def test_app_initialization_components(self, mock_flask):
|
||||
"""Test that app initialization components are available."""
|
||||
try:
|
||||
# Test manager imports
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
from drag_drop import DragDropManager
|
||||
from accessibility_features import AccessibilityManager
|
||||
from user_preferences import UserPreferencesManager
|
||||
|
||||
# Verify managers can be instantiated
|
||||
keyboard_manager = KeyboardShortcutManager()
|
||||
drag_manager = DragDropManager()
|
||||
accessibility_manager = AccessibilityManager()
|
||||
preferences_manager = UserPreferencesManager()
|
||||
|
||||
self.assertIsNotNone(keyboard_manager)
|
||||
self.assertIsNotNone(drag_manager)
|
||||
self.assertIsNotNone(accessibility_manager)
|
||||
self.assertIsNotNone(preferences_manager)
|
||||
|
||||
print('✓ App manager components available')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'App component test failed: {e}')
|
||||
|
||||
|
||||
class TestAPIEndpoints(unittest.TestCase):
|
||||
"""Test class for API endpoint validation."""
|
||||
|
||||
def test_api_response_structure(self):
|
||||
"""Test that API endpoints return proper JSON structure."""
|
||||
try:
|
||||
# Test that we can import the auth module for API responses
|
||||
from auth import SessionManager
|
||||
|
||||
manager = SessionManager()
|
||||
|
||||
# Test login API response structure
|
||||
response = manager.login('test_password')
|
||||
self.assertIsInstance(response, dict)
|
||||
self.assertIn('success', response)
|
||||
|
||||
print('✓ API response structure validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'API endpoint test failed: {e}')
|
||||
|
||||
|
||||
class TestJavaScriptGeneration(unittest.TestCase):
|
||||
"""Test class for dynamic JavaScript generation."""
|
||||
|
||||
def test_javascript_generation_no_syntax_errors(self):
|
||||
"""Test that generated JavaScript doesn't contain Python syntax."""
|
||||
try:
|
||||
from multi_screen_support import MultiScreenSupportManager
|
||||
|
||||
manager = MultiScreenSupportManager()
|
||||
js_code = manager.get_multiscreen_js()
|
||||
|
||||
# Check for Python-specific syntax that shouldn't be in JS
|
||||
self.assertNotIn('True', js_code, 'JavaScript should use "true", not "True"')
|
||||
self.assertNotIn('False', js_code, 'JavaScript should use "false", not "False"')
|
||||
self.assertNotIn('None', js_code, 'JavaScript should use "null", not "None"')
|
||||
|
||||
# Check for proper JSON serialization indicators
|
||||
self.assertIn('true', js_code.lower())
|
||||
self.assertIn('false', js_code.lower())
|
||||
|
||||
print('✓ JavaScript generation syntax validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'JavaScript generation test failed: {e}')
|
||||
|
||||
def test_f_string_escaping(self):
|
||||
"""Test that f-strings are properly escaped in JavaScript generation."""
|
||||
try:
|
||||
from multi_screen_support import MultiScreenSupportManager
|
||||
|
||||
manager = MultiScreenSupportManager()
|
||||
js_code = manager.get_multiscreen_js()
|
||||
|
||||
# Ensure JavaScript object literals use proper syntax
|
||||
# Look for proper JavaScript object/function syntax
|
||||
self.assertGreater(len(js_code), 0)
|
||||
|
||||
# Check that braces are properly used (not bare Python f-string braces)
|
||||
brace_count = js_code.count('{')
|
||||
self.assertGreater(brace_count, 0)
|
||||
|
||||
print('✓ F-string escaping validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'F-string escaping test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
@@ -1,242 +0,0 @@
|
||||
"""
|
||||
Test suite for manager JavaScript and CSS generation.
|
||||
|
||||
This test module validates that all manager classes can successfully generate
|
||||
their JavaScript and CSS code without runtime errors.
|
||||
"""
|
||||
|
||||
import unittest
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add parent directory to path for imports
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
|
||||
class TestManagerGeneration(unittest.TestCase):
|
||||
"""Test class for validating manager JavaScript/CSS generation."""
|
||||
|
||||
def setUp(self):
|
||||
"""Set up test fixtures before each test method."""
|
||||
self.managers_tested = 0
|
||||
self.total_js_chars = 0
|
||||
self.total_css_chars = 0
|
||||
|
||||
def test_keyboard_shortcut_manager(self):
|
||||
"""Test KeyboardShortcutManager JavaScript generation."""
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
js = manager.get_shortcuts_js()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.total_js_chars += len(js)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ KeyboardShortcutManager: JS={len(js)} chars (no CSS method)')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager failed: {e}')
|
||||
|
||||
def test_drag_drop_manager(self):
|
||||
"""Test DragDropManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from drag_drop import DragDropManager
|
||||
manager = DragDropManager()
|
||||
|
||||
js = manager.get_drag_drop_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ DragDropManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'DragDropManager failed: {e}')
|
||||
|
||||
def test_accessibility_manager(self):
|
||||
"""Test AccessibilityManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from accessibility_features import AccessibilityManager
|
||||
manager = AccessibilityManager()
|
||||
|
||||
js = manager.get_accessibility_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AccessibilityManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AccessibilityManager failed: {e}')
|
||||
|
||||
def test_user_preferences_manager(self):
|
||||
"""Test UserPreferencesManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
manager = UserPreferencesManager()
|
||||
|
||||
js = manager.get_preferences_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UserPreferencesManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager failed: {e}')
|
||||
|
||||
def test_advanced_search_manager(self):
|
||||
"""Test AdvancedSearchManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from advanced_search import AdvancedSearchManager
|
||||
manager = AdvancedSearchManager()
|
||||
|
||||
js = manager.get_search_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ AdvancedSearchManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'AdvancedSearchManager failed: {e}')
|
||||
|
||||
def test_undo_redo_manager(self):
|
||||
"""Test UndoRedoManager JavaScript and CSS generation."""
|
||||
try:
|
||||
from undo_redo_manager import UndoRedoManager
|
||||
manager = UndoRedoManager()
|
||||
|
||||
js = manager.get_undo_redo_js()
|
||||
css = manager.get_css()
|
||||
|
||||
self.assertIsInstance(js, str)
|
||||
self.assertIsInstance(css, str)
|
||||
self.assertGreater(len(js), 0)
|
||||
self.assertGreater(len(css), 0)
|
||||
|
||||
self.total_js_chars += len(js)
|
||||
self.total_css_chars += len(css)
|
||||
self.managers_tested += 1
|
||||
|
||||
print(f'✓ UndoRedoManager: JS={len(js)} chars, CSS={len(css)} chars')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UndoRedoManager failed: {e}')
|
||||
|
||||
def test_all_managers_comprehensive(self):
|
||||
"""Comprehensive test to ensure all managers work together."""
|
||||
expected_managers = 6 # Total number of managers we expect to test
|
||||
|
||||
# Run all individual tests first
|
||||
self.test_keyboard_shortcut_manager()
|
||||
self.test_drag_drop_manager()
|
||||
self.test_accessibility_manager()
|
||||
self.test_user_preferences_manager()
|
||||
self.test_advanced_search_manager()
|
||||
self.test_undo_redo_manager()
|
||||
|
||||
# Validate overall results
|
||||
self.assertEqual(self.managers_tested, expected_managers)
|
||||
self.assertGreater(self.total_js_chars, 0)
|
||||
self.assertGreater(self.total_css_chars, 0)
|
||||
|
||||
print(f'\n=== COMPREHENSIVE TEST SUMMARY ===')
|
||||
print(f'Managers tested: {self.managers_tested}/{expected_managers}')
|
||||
print(f'Total JavaScript generated: {self.total_js_chars:,} characters')
|
||||
print(f'Total CSS generated: {self.total_css_chars:,} characters')
|
||||
print('🎉 All manager JavaScript/CSS generation tests passed!')
|
||||
|
||||
def tearDown(self):
|
||||
"""Clean up after each test method."""
|
||||
pass
|
||||
|
||||
|
||||
class TestManagerMethods(unittest.TestCase):
|
||||
"""Test class for validating specific manager methods."""
|
||||
|
||||
def test_keyboard_shortcuts_methods(self):
|
||||
"""Test that KeyboardShortcutManager has required methods."""
|
||||
try:
|
||||
from keyboard_shortcuts import KeyboardShortcutManager
|
||||
manager = KeyboardShortcutManager()
|
||||
|
||||
# Test that required methods exist
|
||||
self.assertTrue(hasattr(manager, 'get_shortcuts_js'))
|
||||
self.assertTrue(hasattr(manager, 'setEnabled'))
|
||||
self.assertTrue(hasattr(manager, 'updateShortcuts'))
|
||||
|
||||
# Test method calls
|
||||
self.assertIsNotNone(manager.get_shortcuts_js())
|
||||
|
||||
print('✓ KeyboardShortcutManager methods validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'KeyboardShortcutManager method test failed: {e}')
|
||||
|
||||
def test_screen_reader_methods(self):
|
||||
"""Test that ScreenReaderSupportManager has required methods."""
|
||||
try:
|
||||
from screen_reader_support import ScreenReaderManager
|
||||
manager = ScreenReaderManager()
|
||||
|
||||
# Test that required methods exist
|
||||
self.assertTrue(hasattr(manager, 'get_screen_reader_js'))
|
||||
self.assertTrue(hasattr(manager, 'enhanceFormElements'))
|
||||
self.assertTrue(hasattr(manager, 'generateId'))
|
||||
|
||||
print('✓ ScreenReaderSupportManager methods validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'ScreenReaderSupportManager method test failed: {e}')
|
||||
|
||||
def test_user_preferences_initialization(self):
|
||||
"""Test that UserPreferencesManager initializes correctly."""
|
||||
try:
|
||||
from user_preferences import UserPreferencesManager
|
||||
|
||||
# Test initialization without Flask app
|
||||
manager = UserPreferencesManager()
|
||||
self.assertTrue(hasattr(manager, 'preferences'))
|
||||
self.assertIsInstance(manager.preferences, dict)
|
||||
self.assertGreater(len(manager.preferences), 0)
|
||||
|
||||
print('✓ UserPreferencesManager initialization validated')
|
||||
|
||||
except Exception as e:
|
||||
self.fail(f'UserPreferencesManager initialization test failed: {e}')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Configure test runner
|
||||
unittest.main(verbosity=2, buffer=True)
|
||||
Reference in New Issue
Block a user