clean-tracks/tests/integration/test_api_endpoints.py

525 lines
17 KiB
Python

"""
Integration tests for API endpoints.
"""
import pytest
import json
import io
import time
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
from werkzeug.datastructures import FileStorage
from src.api import create_app
class TestHealthEndpoints:
"""Test health check endpoints."""
def test_health_check(self, client):
"""Test basic health check endpoint."""
response = client.get('/api/health')
assert response.status_code == 200
data = response.get_json()
assert data['status'] == 'healthy'
assert 'timestamp' in data
assert 'version' in data
def test_readiness_check(self, client):
"""Test readiness check endpoint."""
response = client.get('/api/ready')
assert response.status_code == 200
data = response.get_json()
assert data['ready'] is True
assert 'checks' in data
assert 'database' in data['checks']
assert 'storage' in data['checks']
class TestUploadEndpoints:
"""Test file upload endpoints."""
def test_upload_audio_file(self, client, temp_dir):
"""Test uploading an audio file."""
# Create a test audio file
audio_content = b'ID3' + b'\x00' * 1000 # Minimal MP3
audio_file = FileStorage(
stream=io.BytesIO(audio_content),
filename='test.mp3',
content_type='audio/mpeg'
)
response = client.post(
'/api/upload',
data={'file': audio_file},
content_type='multipart/form-data'
)
assert response.status_code == 200
data = response.get_json()
assert 'job_id' in data
assert 'filename' in data
assert data['filename'] == 'test.mp3'
assert 'status' in data
assert data['status'] == 'uploaded'
def test_upload_invalid_file(self, client):
"""Test uploading an invalid file type."""
# Create a text file
text_file = FileStorage(
stream=io.BytesIO(b'This is not audio'),
filename='test.txt',
content_type='text/plain'
)
response = client.post(
'/api/upload',
data={'file': text_file},
content_type='multipart/form-data'
)
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
assert 'Invalid file type' in data['error']
def test_upload_oversized_file(self, client):
"""Test uploading a file that exceeds size limit."""
# Create a large file (over limit)
large_content = b'ID3' + b'\x00' * (501 * 1024 * 1024) # 501MB
large_file = FileStorage(
stream=io.BytesIO(large_content),
filename='large.mp3',
content_type='audio/mpeg'
)
response = client.post(
'/api/upload',
data={'file': large_file},
content_type='multipart/form-data'
)
assert response.status_code == 413
def test_upload_without_file(self, client):
"""Test upload endpoint without a file."""
response = client.post('/api/upload')
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
assert 'No file provided' in data['error']
class TestProcessingEndpoints:
"""Test audio processing endpoints."""
@patch('src.api.routes.processing.AudioProcessor')
def test_process_job(self, mock_processor, client):
"""Test starting processing for a job."""
# Mock processor
mock_instance = Mock()
mock_instance.process_file.return_value = {
'words_detected': 5,
'words_censored': 5,
'audio_duration': 30.0,
'output_file': 'output.mp3',
'detected_words': []
}
mock_processor.return_value = mock_instance
# Start processing
response = client.post(
'/api/jobs/test-job-123/process',
json={
'word_list_id': 'default',
'censor_method': 'beep',
'min_severity': 'medium',
'whisper_model': 'base'
}
)
assert response.status_code in [200, 202]
data = response.get_json()
assert 'job_id' in data
assert 'status' in data
def test_process_invalid_job(self, client):
"""Test processing with invalid job ID."""
response = client.post(
'/api/jobs/invalid-job/process',
json={'word_list_id': 'default'}
)
assert response.status_code == 404
data = response.get_json()
assert 'error' in data
assert 'Job not found' in data['error']
@patch('src.api.routes.processing.JobManager')
def test_get_job_status(self, mock_manager, client):
"""Test getting job status."""
# Mock job manager
mock_job = Mock()
mock_job.to_dict.return_value = {
'job_id': 'test-job-123',
'status': 'processing',
'progress': 45.0,
'current_stage': 'transcription'
}
mock_manager.get_job.return_value = mock_job
response = client.get('/api/jobs/test-job-123/status')
assert response.status_code == 200
data = response.get_json()
assert data['job_id'] == 'test-job-123'
assert data['status'] == 'processing'
assert data['progress'] == 45.0
def test_cancel_job(self, client):
"""Test canceling a processing job."""
response = client.post('/api/jobs/test-job-123/cancel')
# Should return 200 or 204
assert response.status_code in [200, 204]
@patch('src.api.routes.processing.send_file')
def test_download_processed_file(self, mock_send_file, client):
"""Test downloading processed audio file."""
# Mock send_file
mock_send_file.return_value = Mock(status_code=200)
response = client.get('/api/jobs/test-job-123/download')
# Note: actual response depends on send_file implementation
assert mock_send_file.called
class TestWordListEndpoints:
"""Test word list management endpoints."""
def test_get_word_lists(self, client):
"""Test getting all word lists."""
response = client.get('/api/word-lists')
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)
# Should have at least default list
assert len(data) >= 1
if data:
first_list = data[0]
assert 'id' in first_list
assert 'name' in first_list
assert 'word_count' in first_list
def test_get_specific_word_list(self, client):
"""Test getting a specific word list."""
response = client.get('/api/word-lists/default')
assert response.status_code == 200
data = response.get_json()
assert 'id' in data
assert 'name' in data
assert 'words' in data
assert isinstance(data['words'], list)
def test_create_word_list(self, client):
"""Test creating a new word list."""
response = client.post(
'/api/word-lists',
json={
'name': 'Test List',
'description': 'A test word list',
'words': [
{'word': 'testword1', 'severity': 'high', 'category': 'test'},
{'word': 'testword2', 'severity': 'medium', 'category': 'test'}
]
}
)
assert response.status_code in [200, 201]
data = response.get_json()
assert 'id' in data
assert data['name'] == 'Test List'
assert data['word_count'] == 2
def test_update_word_list(self, client):
"""Test updating a word list."""
response = client.put(
'/api/word-lists/test-list',
json={
'name': 'Updated Test List',
'words': [
{'word': 'newword', 'severity': 'low', 'category': 'test'}
]
}
)
assert response.status_code in [200, 404]
def test_delete_word_list(self, client):
"""Test deleting a word list."""
response = client.delete('/api/word-lists/test-list')
assert response.status_code in [204, 404]
def test_add_word_to_list(self, client):
"""Test adding a word to a list."""
response = client.post(
'/api/word-lists/default/words',
json={
'word': 'newbadword',
'severity': 'high',
'category': 'profanity'
}
)
assert response.status_code in [200, 201]
def test_remove_word_from_list(self, client):
"""Test removing a word from a list."""
response = client.delete('/api/word-lists/default/words/testword')
assert response.status_code in [204, 404]
class TestHistoryEndpoints:
"""Test processing history endpoints."""
def test_get_processing_history(self, client):
"""Test getting processing history."""
response = client.get('/api/history')
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)
# Check structure if there are items
if data:
item = data[0]
assert 'job_id' in item
assert 'filename' in item
assert 'timestamp' in item
assert 'status' in item
def test_get_history_with_filters(self, client):
"""Test getting filtered history."""
response = client.get('/api/history?status=completed&limit=10')
assert response.status_code == 200
data = response.get_json()
assert isinstance(data, list)
assert len(data) <= 10
def test_get_history_item(self, client):
"""Test getting specific history item."""
response = client.get('/api/history/test-job-123')
# Could be 200 or 404 depending on whether job exists
assert response.status_code in [200, 404]
def test_delete_history_item(self, client):
"""Test deleting history item."""
response = client.delete('/api/history/test-job-123')
assert response.status_code in [204, 404]
class TestSettingsEndpoints:
"""Test user settings endpoints."""
def test_get_user_settings(self, client):
"""Test getting user settings."""
response = client.get('/api/settings')
assert response.status_code == 200
data = response.get_json()
assert 'processing' in data
assert 'privacy' in data
assert 'ui' in data
def test_update_user_settings(self, client):
"""Test updating user settings."""
response = client.put(
'/api/settings',
json={
'processing': {
'whisper_model_size': 'large',
'default_censor_method': 'silence'
},
'ui': {
'theme': 'dark',
'notifications_enabled': True
}
}
)
assert response.status_code == 200
data = response.get_json()
assert data['processing']['whisper_model_size'] == 'large'
assert data['ui']['theme'] == 'dark'
def test_reset_settings(self, client):
"""Test resetting settings to defaults."""
response = client.post('/api/settings/reset')
assert response.status_code == 200
data = response.get_json()
assert 'message' in data
assert 'reset' in data['message'].lower()
class TestBatchEndpoints:
"""Test batch processing endpoints."""
def test_create_batch_job(self, client):
"""Test creating a batch processing job."""
response = client.post(
'/api/batch',
json={
'job_ids': ['job1', 'job2', 'job3'],
'processing_options': {
'word_list_id': 'default',
'censor_method': 'beep'
}
}
)
assert response.status_code in [200, 201]
data = response.get_json()
assert 'batch_id' in data
assert 'total_jobs' in data
assert data['total_jobs'] == 3
def test_get_batch_status(self, client):
"""Test getting batch job status."""
response = client.get('/api/batch/batch-123/status')
assert response.status_code in [200, 404]
if response.status_code == 200:
data = response.get_json()
assert 'batch_id' in data
assert 'progress' in data
assert 'completed_jobs' in data
assert 'total_jobs' in data
class TestWebSocketIntegration:
"""Test WebSocket integration with API."""
def test_websocket_connection(self, socketio_client):
"""Test WebSocket connection."""
# Connect to WebSocket
received = socketio_client.get_received()
# Should receive connection confirmation
assert any(msg['name'] == 'connected' for msg in received)
def test_join_job_room(self, socketio_client):
"""Test joining a job-specific room."""
socketio_client.emit('join_job', {'job_id': 'test-job-123'})
received = socketio_client.get_received()
# Should receive room join confirmation
assert any(
msg['name'] == 'joined_job' and
msg['args'][0]['job_id'] == 'test-job-123'
for msg in received
)
def test_receive_progress_updates(self, socketio_client):
"""Test receiving progress updates."""
# Join a job room
socketio_client.emit('join_job', {'job_id': 'test-job-123'})
# Simulate progress update
socketio_client.emit('processing_progress', {
'job_id': 'test-job-123',
'progress': 50.0,
'stage': 'transcription'
})
received = socketio_client.get_received()
# Should receive progress update
progress_msgs = [
msg for msg in received
if msg['name'] == 'processing_progress'
]
assert len(progress_msgs) > 0
class TestErrorHandling:
"""Test API error handling."""
def test_404_error(self, client):
"""Test 404 error handling."""
response = client.get('/api/nonexistent')
assert response.status_code == 404
data = response.get_json()
assert 'error' in data
def test_method_not_allowed(self, client):
"""Test 405 error handling."""
response = client.post('/api/health') # Health is GET only
assert response.status_code == 405
data = response.get_json()
assert 'error' in data
def test_invalid_json(self, client):
"""Test handling of invalid JSON."""
response = client.post(
'/api/word-lists',
data='invalid json {',
content_type='application/json'
)
assert response.status_code == 400
data = response.get_json()
assert 'error' in data
def test_rate_limiting(self, client):
"""Test rate limiting (if implemented)."""
# Make many rapid requests
responses = []
for _ in range(100):
responses.append(client.get('/api/health'))
# Check if any were rate limited (429)
# Note: This depends on rate limiting being configured
status_codes = [r.status_code for r in responses]
# All should be successful if no rate limiting
assert all(code == 200 for code in status_codes)
class TestCORSHeaders:
"""Test CORS header configuration."""
def test_cors_headers_present(self, client):
"""Test that CORS headers are present."""
response = client.get('/api/health')
# Check for CORS headers
assert 'Access-Control-Allow-Origin' in response.headers
def test_cors_preflight(self, client):
"""Test CORS preflight request."""
response = client.options('/api/upload', headers={
'Origin': 'http://localhost:3000',
'Access-Control-Request-Method': 'POST',
'Access-Control-Request-Headers': 'Content-Type'
})
assert response.status_code == 200
assert 'Access-Control-Allow-Methods' in response.headers
if __name__ == '__main__':
pytest.main([__file__, '-v'])