Spaces:
Sleeping
Sleeping
| """Unit tests for API endpoints with authentication and external job ID support.""" | |
| import pytest | |
| import base64 | |
| import json | |
| from fastapi.testclient import TestClient | |
| from unittest.mock import Mock, AsyncMock, patch | |
| from fastapi import HTTPException | |
| # Test the authentication logic separately to avoid complex setup | |
| class TestAPIEndpointAuthentication: | |
| """Test authentication logic for API endpoints.""" | |
| def setup_method(self): | |
| """Set up test fixtures.""" | |
| # Create a valid JWT token for testing | |
| header = {"alg": "HS256", "typ": "JWT"} | |
| payload = {"sub": "1234567890", "name": "John Doe", "iat": 1516239022} | |
| header_b64 = base64.urlsafe_b64encode( | |
| json.dumps(header).encode() | |
| ).decode().rstrip('=') | |
| payload_b64 = base64.urlsafe_b64encode( | |
| json.dumps(payload).encode() | |
| ).decode().rstrip('=') | |
| self.valid_token = f"{header_b64}.{payload_b64}.test_signature" | |
| self.valid_auth_header = f"Bearer {self.valid_token}" | |
| def test_extract_endpoint_requires_authentication(self): | |
| """Test that extract endpoint requires authentication.""" | |
| # Simulate the authentication logic that would happen in the endpoint | |
| def simulate_extract_auth_check(authorization_header): | |
| """Simulate the authentication check for extract endpoint.""" | |
| if not authorization_header: | |
| raise HTTPException(status_code=401, detail="Missing Authorization header") | |
| if not authorization_header.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Invalid Authorization header format") | |
| return True | |
| # Test successful authentication | |
| result = simulate_extract_auth_check(self.valid_auth_header) | |
| assert result is True | |
| # Test missing authentication | |
| with pytest.raises(HTTPException) as exc_info: | |
| simulate_extract_auth_check(None) | |
| assert exc_info.value.status_code == 401 | |
| assert "Missing Authorization header" in str(exc_info.value.detail) | |
| # Test invalid format | |
| with pytest.raises(HTTPException) as exc_info: | |
| simulate_extract_auth_check("Basic dXNlcjpwYXNz") | |
| assert exc_info.value.status_code == 401 | |
| assert "Invalid Authorization header format" in str(exc_info.value.detail) | |
| def test_job_status_endpoint_requires_authentication(self): | |
| """Test that job status endpoint requires authentication.""" | |
| def simulate_job_status_auth_check(authorization_header): | |
| """Simulate the authentication check for job status endpoint.""" | |
| if not authorization_header: | |
| raise HTTPException(status_code=401, detail="Missing Authorization header") | |
| return True | |
| # Test successful authentication | |
| result = simulate_job_status_auth_check(self.valid_auth_header) | |
| assert result is True | |
| # Test missing authentication | |
| with pytest.raises(HTTPException) as exc_info: | |
| simulate_job_status_auth_check(None) | |
| assert exc_info.value.status_code == 401 | |
| def test_download_endpoint_requires_authentication(self): | |
| """Test that download endpoint requires authentication.""" | |
| def simulate_download_auth_check(authorization_header): | |
| """Simulate the authentication check for download endpoint.""" | |
| if not authorization_header: | |
| raise HTTPException(status_code=401, detail="Missing Authorization header") | |
| return True | |
| # Test successful authentication | |
| result = simulate_download_auth_check(self.valid_auth_header) | |
| assert result is True | |
| # Test missing authentication | |
| with pytest.raises(HTTPException) as exc_info: | |
| simulate_download_auth_check(None) | |
| assert exc_info.value.status_code == 401 | |
| def test_info_endpoint_public(self): | |
| """Test that info endpoint doesn't require authentication.""" | |
| def simulate_info_endpoint(): | |
| """Simulate the info endpoint (no auth required).""" | |
| return { | |
| "version": "1.0.0", | |
| "supported_video_formats": ['.mp4', '.avi'], | |
| "supported_audio_formats": ['mp3', 'aac'], | |
| "quality_levels": ['high', 'medium', 'low'] | |
| } | |
| # Should work without any authentication | |
| result = simulate_info_endpoint() | |
| assert "version" in result | |
| assert result["version"] == "1.0.0" | |
| def test_health_endpoint_public(self): | |
| """Test that health endpoint doesn't require authentication.""" | |
| def simulate_health_endpoint(): | |
| """Simulate the health endpoint (no auth required).""" | |
| return {"status": "healthy", "service": "audio-extractor-api"} | |
| # Should work without any authentication | |
| result = simulate_health_endpoint() | |
| assert result["status"] == "healthy" | |
| assert result["service"] == "audio-extractor-api" | |
| class TestExternalJobIdValidation: | |
| """Test external job ID validation in API endpoints.""" | |
| def test_valid_external_job_ids(self): | |
| """Test validation of valid external job IDs.""" | |
| def validate_external_job_id(job_id): | |
| """Simulate external job ID validation.""" | |
| if job_id is None or job_id == "": | |
| return True # Optional field | |
| if len(job_id) > 50: | |
| raise HTTPException(status_code=400, detail="External job ID must be 50 characters or less") | |
| import re | |
| if not re.match(r'^[a-zA-Z0-9_-]+$', job_id): | |
| raise HTTPException(status_code=400, detail="External job ID must contain only alphanumeric characters, underscores, and hyphens") | |
| return True | |
| # Valid cases | |
| assert validate_external_job_id(None) is True | |
| assert validate_external_job_id("") is True | |
| assert validate_external_job_id("job123") is True | |
| assert validate_external_job_id("job_123-abc") is True | |
| assert validate_external_job_id("a" * 50) is True # Max length | |
| # Invalid cases | |
| with pytest.raises(HTTPException) as exc_info: | |
| validate_external_job_id("a" * 51) # Too long | |
| assert exc_info.value.status_code == 400 | |
| assert "50 characters or less" in str(exc_info.value.detail) | |
| with pytest.raises(HTTPException) as exc_info: | |
| validate_external_job_id("job@123") # Invalid character | |
| assert exc_info.value.status_code == 400 | |
| assert "alphanumeric characters" in str(exc_info.value.detail) | |
| with pytest.raises(HTTPException) as exc_info: | |
| validate_external_job_id("job 123") # Space | |
| assert exc_info.value.status_code == 400 | |
| class TestAPIResponseUpdates: | |
| """Test that API responses include external job IDs when provided.""" | |
| def test_job_creation_response_includes_external_job_id(self): | |
| """Test that job creation response includes external job ID.""" | |
| # Simulate JobCreationDTO | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| class MockJobCreationDTO: | |
| job_id: str | |
| external_job_id: Optional[str] = None | |
| status: str = "processing" | |
| message: str = "Job created" | |
| check_url: str = "/api/v1/jobs/123" | |
| file_size_mb: float = 10.0 | |
| # With external job ID | |
| dto_with_external = MockJobCreationDTO( | |
| job_id="internal-123", | |
| external_job_id="ext-job-456" | |
| ) | |
| assert dto_with_external.job_id == "internal-123" | |
| assert dto_with_external.external_job_id == "ext-job-456" | |
| # Without external job ID | |
| dto_without_external = MockJobCreationDTO( | |
| job_id="internal-789" | |
| ) | |
| assert dto_without_external.job_id == "internal-789" | |
| assert dto_without_external.external_job_id is None | |
| def test_job_status_response_includes_external_job_id(self): | |
| """Test that job status response includes external job ID.""" | |
| from dataclasses import dataclass | |
| from typing import Optional | |
| from datetime import datetime | |
| class MockJobStatusDTO: | |
| job_id: str | |
| external_job_id: Optional[str] = None | |
| status: str = "processing" | |
| created_at: datetime = None | |
| updated_at: datetime = None | |
| # With external job ID | |
| now = datetime.utcnow() | |
| dto_with_external = MockJobStatusDTO( | |
| job_id="internal-123", | |
| external_job_id="ext-job-456", | |
| status="completed", | |
| created_at=now, | |
| updated_at=now | |
| ) | |
| assert dto_with_external.job_id == "internal-123" | |
| assert dto_with_external.external_job_id == "ext-job-456" | |
| assert dto_with_external.status == "completed" | |
| def test_extract_endpoint_form_parameter_handling(self): | |
| """Test that extract endpoint handles job_id form parameter correctly.""" | |
| def simulate_extract_form_handling(form_data): | |
| """Simulate form data handling for extract endpoint.""" | |
| # Extract job_id from form data | |
| job_id = form_data.get("job_id") | |
| # Validate if provided | |
| if job_id and job_id != "": | |
| # Simulate validation | |
| if len(job_id) > 50: | |
| raise HTTPException(status_code=400, detail="External job ID too long") | |
| return {"extracted_job_id": job_id, "valid": True} | |
| else: | |
| return {"extracted_job_id": None, "valid": True} | |
| # Test with job_id provided | |
| form_with_job_id = {"video": "test.mp4", "job_id": "ext-job-123"} | |
| result = simulate_extract_form_handling(form_with_job_id) | |
| assert result["extracted_job_id"] == "ext-job-123" | |
| assert result["valid"] is True | |
| # Test without job_id | |
| form_without_job_id = {"video": "test.mp4"} | |
| result = simulate_extract_form_handling(form_without_job_id) | |
| assert result["extracted_job_id"] is None | |
| assert result["valid"] is True | |
| # Test with empty job_id | |
| form_with_empty_job_id = {"video": "test.mp4", "job_id": ""} | |
| result = simulate_extract_form_handling(form_with_empty_job_id) | |
| assert result["extracted_job_id"] is None # Empty string should be treated as None | |
| assert result["valid"] is True | |
| class TestDuplicateExternalJobIdHandling: | |
| """Test handling of duplicate external job IDs in API endpoints.""" | |
| def test_duplicate_external_job_id_error_handling(self): | |
| """Test that duplicate external job ID errors are handled properly.""" | |
| from domain.exceptions.domain_exceptions import DuplicateExternalJobIdError | |
| def simulate_job_creation_with_duplicate_check(external_job_id): | |
| """Simulate job creation with duplicate external ID check.""" | |
| # Simulate existing external job IDs | |
| existing_external_ids = ["existing-job-1", "existing-job-2"] | |
| if external_job_id in existing_external_ids: | |
| raise DuplicateExternalJobIdError(external_job_id) | |
| return {"job_id": "new-internal-id", "external_job_id": external_job_id} | |
| # Test successful creation with unique external ID | |
| result = simulate_job_creation_with_duplicate_check("unique-job-123") | |
| assert result["external_job_id"] == "unique-job-123" | |
| # Test duplicate external ID error | |
| with pytest.raises(DuplicateExternalJobIdError) as exc_info: | |
| simulate_job_creation_with_duplicate_check("existing-job-1") | |
| assert exc_info.value.external_job_id == "existing-job-1" | |
| assert "already exists" in str(exc_info.value) | |
| class TestEndpointIntegrationLogic: | |
| """Test the integration logic of endpoints with authentication and external job IDs.""" | |
| def test_extract_endpoint_complete_flow(self): | |
| """Test the complete flow of the extract endpoint.""" | |
| def simulate_complete_extract_flow(auth_header, form_data): | |
| """Simulate the complete extract endpoint flow.""" | |
| # 1. Authentication check | |
| if not auth_header or not auth_header.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Authentication required") | |
| # 2. Extract external job ID | |
| external_job_id = form_data.get("job_id") | |
| # 3. Validate external job ID format | |
| if external_job_id and len(external_job_id) > 50: | |
| raise HTTPException(status_code=400, detail="Invalid external job ID") | |
| # 4. Check for duplicates | |
| existing_ids = ["existing-1", "existing-2"] | |
| if external_job_id in existing_ids: | |
| raise HTTPException(status_code=400, detail="Duplicate external job ID") | |
| # 5. Create job | |
| internal_job_id = "internal-12345" | |
| # 6. Return response | |
| return { | |
| "job_id": internal_job_id, | |
| "external_job_id": external_job_id, | |
| "status": "processing", | |
| "message": "Job created successfully" | |
| } | |
| # Test successful flow | |
| auth_header = "Bearer valid.jwt.token" | |
| form_data = {"video": "test.mp4", "job_id": "my-job-123"} | |
| result = simulate_complete_extract_flow(auth_header, form_data) | |
| assert result["job_id"] == "internal-12345" | |
| assert result["external_job_id"] == "my-job-123" | |
| assert result["status"] == "processing" | |
| # Test without external job ID | |
| form_data_no_job_id = {"video": "test.mp4"} | |
| result = simulate_complete_extract_flow(auth_header, form_data_no_job_id) | |
| assert result["job_id"] == "internal-12345" | |
| assert result["external_job_id"] is None | |
| def test_job_status_endpoint_complete_flow(self): | |
| """Test the complete flow of the job status endpoint.""" | |
| def simulate_complete_job_status_flow(auth_header, job_id): | |
| """Simulate the complete job status endpoint flow.""" | |
| # 1. Authentication check | |
| if not auth_header or not auth_header.startswith("Bearer "): | |
| raise HTTPException(status_code=401, detail="Authentication required") | |
| # 2. Look up job | |
| mock_jobs = { | |
| "internal-123": { | |
| "job_id": "internal-123", | |
| "external_job_id": "ext-job-456", | |
| "status": "completed", | |
| "filename": "test.mp4" | |
| }, | |
| "internal-789": { | |
| "job_id": "internal-789", | |
| "external_job_id": None, | |
| "status": "processing", | |
| "filename": "test2.mp4" | |
| } | |
| } | |
| if job_id not in mock_jobs: | |
| raise HTTPException(status_code=404, detail="Job not found") | |
| # 3. Return job status | |
| return mock_jobs[job_id] | |
| # Test successful lookup with external job ID | |
| auth_header = "Bearer valid.jwt.token" | |
| result = simulate_complete_job_status_flow(auth_header, "internal-123") | |
| assert result["job_id"] == "internal-123" | |
| assert result["external_job_id"] == "ext-job-456" | |
| assert result["status"] == "completed" | |
| # Test successful lookup without external job ID | |
| result = simulate_complete_job_status_flow(auth_header, "internal-789") | |
| assert result["job_id"] == "internal-789" | |
| assert result["external_job_id"] is None | |
| assert result["status"] == "processing" | |
| # Test job not found | |
| with pytest.raises(HTTPException) as exc_info: | |
| simulate_complete_job_status_flow(auth_header, "non-existent") | |
| assert exc_info.value.status_code == 404 | |
| assert "Job not found" in str(exc_info.value.detail) |