Download backend/tests/test_api.py from calebhan/rescored: direct link, hf CLI and curl.
- Browser
- Download file 13 kB
-
https://huggingface.co/spaces/calebhan/rescored/resolve/main/backend/tests/test_api.py
- Command line
-
hf download hf://spaces/calebhan/rescored/backend/tests/test_api.py
-
curl -L -o test_api.py https://huggingface.co/spaces/calebhan/rescored/resolve/main/backend/tests/test_api.py
13 kB
| """Integration tests for FastAPI endpoints.""" | |
| import pytest | |
| from unittest.mock import patch, MagicMock | |
| import json | |
| class TestRootEndpoint: | |
| """Test root endpoint.""" | |
| def test_root(self, test_client): | |
| """Test root endpoint returns API info.""" | |
| response = test_client.get("/") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["name"] == "Rescored API" | |
| assert data["version"] == "1.0.0" | |
| assert data["docs"] == "/docs" | |
| class TestHealthCheck: | |
| """Test health check endpoint.""" | |
| def test_health_check_healthy(self, test_client, mock_redis): | |
| """Test health check when all services are healthy.""" | |
| mock_redis.ping.return_value = True | |
| response = test_client.get("/health") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "healthy" | |
| assert data["redis"] == "healthy" | |
| def test_health_check_redis_down(self, test_client, mock_redis): | |
| """Test health check when Redis is down.""" | |
| mock_redis.ping.side_effect = Exception("Connection failed") | |
| response = test_client.get("/health") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "degraded" | |
| assert data["redis"] == "unhealthy" | |
| class TestTranscribeEndpoint: | |
| """Test transcription submission endpoint.""" | |
| def test_submit_valid_transcription( | |
| self, | |
| mock_validate, | |
| mock_check_availability, | |
| mock_task, | |
| test_client, | |
| mock_redis | |
| ): | |
| """Test submitting valid transcription request.""" | |
| mock_validate.return_value = (True, "dQw4w9WgXcQ") | |
| mock_check_availability.return_value = { | |
| 'available': True, | |
| 'info': {'duration': 180} | |
| } | |
| mock_task.delay.return_value = MagicMock(id="task-id") | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"} | |
| ) | |
| assert response.status_code == 201 | |
| data = response.json() | |
| assert "job_id" in data | |
| assert data["status"] == "queued" | |
| assert "websocket_url" in data | |
| assert data["estimated_duration_seconds"] == 120 | |
| # Verify Redis was called to store job | |
| assert mock_redis.hset.called | |
| # Verify Celery task was queued | |
| assert mock_task.delay.called | |
| def test_submit_invalid_url(self, mock_validate, test_client): | |
| """Test submitting invalid YouTube URL.""" | |
| mock_validate.return_value = (False, "Invalid YouTube URL format") | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={"youtube_url": "https://invalid.com/video"} | |
| ) | |
| assert response.status_code == 400 | |
| assert "Invalid YouTube URL format" in response.json()["detail"] | |
| def test_submit_unavailable_video( | |
| self, | |
| mock_check_availability, | |
| mock_validate, | |
| test_client | |
| ): | |
| """Test submitting unavailable video.""" | |
| mock_validate.return_value = (True, "dQw4w9WgXcQ") | |
| mock_check_availability.return_value = { | |
| 'available': False, | |
| 'reason': 'Video too long (max 15 minutes)' | |
| } | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"} | |
| ) | |
| assert response.status_code == 422 | |
| assert "too long" in response.json()["detail"] | |
| def test_submit_with_options( | |
| self, | |
| mock_check_availability, | |
| mock_validate, | |
| test_client, | |
| mock_redis | |
| ): | |
| """Test submitting transcription with custom options.""" | |
| mock_validate.return_value = (True, "dQw4w9WgXcQ") | |
| mock_check_availability.return_value = {'available': True, 'info': {}} | |
| with patch('main.process_transcription_task') as mock_task: | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={ | |
| "youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ", | |
| "options": {"instruments": ["piano", "guitar"]} | |
| } | |
| ) | |
| assert response.status_code == 201 | |
| class TestRateLimiting: | |
| """Test rate limiting middleware.""" | |
| def test_rate_limit_enforced( | |
| self, | |
| mock_task, | |
| mock_check_availability, | |
| mock_validate, | |
| test_client, | |
| mock_redis | |
| ): | |
| """Test that rate limit is enforced after 10 requests.""" | |
| mock_validate.return_value = (True, "dQw4w9WgXcQ") | |
| mock_check_availability.return_value = {'available': True, 'info': {}} | |
| mock_task.delay.return_value = MagicMock(id="task-id") | |
| # Mock Redis counter for rate limiting | |
| mock_redis.get.return_value = "10" # Already at limit | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"} | |
| ) | |
| assert response.status_code == 429 | |
| assert "Rate limit exceeded" in response.json()["detail"] | |
| def test_rate_limit_under_limit( | |
| self, | |
| mock_task, | |
| mock_check_availability, | |
| mock_validate, | |
| test_client, | |
| mock_redis | |
| ): | |
| """Test that requests under limit succeed.""" | |
| mock_validate.return_value = (True, "dQw4w9WgXcQ") | |
| mock_check_availability.return_value = {'available': True, 'info': {}} | |
| mock_task.delay.return_value = MagicMock(id="task-id") | |
| # Mock Redis counter for rate limiting (under limit) | |
| mock_redis.get.return_value = "5" # 5 out of 10 | |
| response = test_client.post( | |
| "/api/v1/transcribe", | |
| json={"youtube_url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ"} | |
| ) | |
| assert response.status_code == 201 # Request succeeds | |
| assert mock_redis.pipeline.called # Counter incremented | |
| class TestJobStatusEndpoint: | |
| """Test job status endpoint.""" | |
| def test_get_existing_job_status(self, test_client, mock_redis, sample_job_data): | |
| """Test getting status of existing job.""" | |
| mock_redis.hgetall.return_value = sample_job_data | |
| response = test_client.get(f"/api/v1/jobs/{sample_job_data['job_id']}") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["job_id"] == sample_job_data["job_id"] | |
| assert data["status"] == "queued" | |
| assert data["progress"] == 0 | |
| assert data["current_stage"] == "queued" | |
| def test_get_nonexistent_job(self, test_client, mock_redis): | |
| """Test getting status of nonexistent job.""" | |
| mock_redis.hgetall.return_value = {} | |
| response = test_client.get("/api/v1/jobs/nonexistent-id") | |
| assert response.status_code == 404 | |
| assert "not found" in response.json()["detail"] | |
| def test_get_completed_job_status(self, test_client, mock_redis, sample_job_data): | |
| """Test getting status of completed job.""" | |
| completed_job = {**sample_job_data, "status": "completed", "progress": 100} | |
| mock_redis.hgetall.return_value = completed_job | |
| response = test_client.get(f"/api/v1/jobs/{sample_job_data['job_id']}") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "completed" | |
| assert data["progress"] == 100 | |
| assert data["result_url"] is not None | |
| def test_get_failed_job_status(self, test_client, mock_redis, sample_job_data): | |
| """Test getting status of failed job.""" | |
| error_data = {"message": "Transcription failed", "stage": "audio_download"} | |
| failed_job = { | |
| **sample_job_data, | |
| "status": "failed", | |
| "error": json.dumps(error_data) | |
| } | |
| mock_redis.hgetall.return_value = failed_job | |
| response = test_client.get(f"/api/v1/jobs/{sample_job_data['job_id']}") | |
| assert response.status_code == 200 | |
| data = response.json() | |
| assert data["status"] == "failed" | |
| assert data["error"] is not None | |
| assert data["error"]["message"] == "Transcription failed" | |
| class TestScoreDownloadEndpoint: | |
| """Test score download endpoint.""" | |
| def test_download_completed_score( | |
| self, | |
| test_client, | |
| mock_redis, | |
| sample_job_data, | |
| temp_storage_dir, | |
| sample_musicxml_content | |
| ): | |
| """Test downloading a completed score.""" | |
| # Create a real MusicXML file | |
| score_path = temp_storage_dir / "score.musicxml" | |
| score_path.write_text(sample_musicxml_content) | |
| completed_job = { | |
| **sample_job_data, | |
| "status": "completed", | |
| "output_path": str(score_path) | |
| } | |
| mock_redis.hgetall.return_value = completed_job | |
| response = test_client.get(f"/api/v1/scores/{sample_job_data['job_id']}") | |
| assert response.status_code == 200 | |
| assert response.headers["content-type"] == "application/vnd.recordare.musicxml+xml" | |
| assert "score-partwise" in response.text | |
| def test_download_nonexistent_job(self, test_client, mock_redis): | |
| """Test downloading score for nonexistent job.""" | |
| mock_redis.hgetall.return_value = {} | |
| response = test_client.get("/api/v1/scores/nonexistent-id") | |
| assert response.status_code == 404 | |
| def test_download_incomplete_job(self, test_client, mock_redis, sample_job_data): | |
| """Test downloading score for incomplete job.""" | |
| mock_redis.hgetall.return_value = sample_job_data # Still queued | |
| response = test_client.get(f"/api/v1/scores/{sample_job_data['job_id']}") | |
| assert response.status_code == 404 | |
| assert "not available" in response.json()["detail"] | |
| def test_download_missing_file(self, test_client, mock_redis, sample_job_data): | |
| """Test downloading score when file is missing.""" | |
| completed_job = { | |
| **sample_job_data, | |
| "status": "completed", | |
| "output_path": "/nonexistent/path/score.musicxml" | |
| } | |
| mock_redis.hgetall.return_value = completed_job | |
| response = test_client.get(f"/api/v1/scores/{sample_job_data['job_id']}") | |
| assert response.status_code == 404 | |
| assert "not found" in response.json()["detail"] | |
| class TestMIDIDownloadEndpoint: | |
| """Test MIDI download endpoint.""" | |
| def test_download_completed_midi(self, test_client, sample_job_id, tmp_path, mock_redis): | |
| """Test downloading MIDI from completed job.""" | |
| # Create a dummy MIDI file | |
| midi_file = tmp_path / "test.mid" | |
| midi_file.write_bytes(b"MIDI_DATA") | |
| # Set job as completed with MIDI path | |
| mock_redis.hgetall.return_value = { | |
| "status": "completed", | |
| "midi_path": str(midi_file) | |
| } | |
| response = test_client.get(f"/api/v1/scores/{sample_job_id}/midi") | |
| assert response.status_code == 200 | |
| assert response.headers["content-type"] == "audio/midi" | |
| assert response.content == b"MIDI_DATA" | |
| def test_download_nonexistent_job_midi(self, test_client, mock_redis): | |
| """Test downloading MIDI from nonexistent job.""" | |
| mock_redis.hgetall.return_value = {} | |
| response = test_client.get("/api/v1/scores/nonexistent/midi") | |
| assert response.status_code == 404 | |
| assert "not available" in response.json()["detail"] | |
| def test_download_incomplete_job_midi(self, test_client, sample_job_id, mock_redis): | |
| """Test downloading MIDI from incomplete job.""" | |
| mock_redis.hgetall.return_value = {"status": "processing"} | |
| response = test_client.get(f"/api/v1/scores/{sample_job_id}/midi") | |
| assert response.status_code == 404 | |
| def test_download_missing_midi_file(self, test_client, sample_job_id, mock_redis): | |
| """Test downloading when MIDI file doesn't exist.""" | |
| mock_redis.hgetall.return_value = { | |
| "status": "completed", | |
| "midi_path": "/nonexistent/path.mid" | |
| } | |
| response = test_client.get(f"/api/v1/scores/{sample_job_id}/midi") | |
| assert response.status_code == 404 | |
| assert "file not found" in response.json()["detail"].lower() | |