| """Unit tests for Job entity and repository with external job ID support.""" |
| import pytest |
| import asyncio |
| from datetime import datetime, timedelta |
| from domain.entities.job import Job |
| from domain.exceptions.domain_exceptions import DuplicateExternalJobIdError, ValidationError |
| from domain.services.validation_service import ValidationService |
| from infrastructure.repositories.job_repository import InMemoryJobRepository, JobRecord |
|
|
|
|
| class TestJobEntityWithExternalId: |
| """Test Job entity with external job ID and bearer token support.""" |
| |
| def test_job_creation_with_external_id(self): |
| """Test creating a job with external job ID.""" |
| job = Job.create_new( |
| video_filename="test.mp4", |
| file_size_bytes=1000000, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="ext-job-123", |
| bearer_token="bearer-token-xyz" |
| ) |
| |
| assert job.external_job_id == "ext-job-123" |
| assert job.bearer_token == "bearer-token-xyz" |
| assert job.has_external_job_id is True |
| assert job.id is not None |
| |
| def test_job_creation_without_external_id(self): |
| """Test creating a job without external job ID (backwards compatibility).""" |
| job = Job.create_new( |
| video_filename="test.mp4", |
| file_size_bytes=1000000, |
| output_format="mp3", |
| quality="medium" |
| ) |
| |
| assert job.external_job_id is None |
| assert job.bearer_token is None |
| assert job.has_external_job_id is False |
| assert job.id is not None |
| |
| def test_job_creation_with_empty_external_id(self): |
| """Test creating a job with empty external job ID.""" |
| job = Job.create_new( |
| video_filename="test.mp4", |
| file_size_bytes=1000000, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="", |
| bearer_token=None |
| ) |
| |
| assert job.external_job_id == "" |
| assert job.has_external_job_id is False |
| |
| def test_clear_bearer_token(self): |
| """Test clearing bearer token for security.""" |
| job = Job.create_new( |
| video_filename="test.mp4", |
| file_size_bytes=1000000, |
| output_format="mp3", |
| quality="medium", |
| bearer_token="secret-token" |
| ) |
| |
| assert job.bearer_token == "secret-token" |
| |
| original_updated_at = job.updated_at |
| job.clear_bearer_token() |
| |
| assert job.bearer_token is None |
| assert job.updated_at > original_updated_at |
| |
| def test_has_external_job_id_property(self): |
| """Test the has_external_job_id property.""" |
| |
| job1 = Job.create_new("test.mp4", 1000, "mp3", "medium", external_job_id="ext-123") |
| assert job1.has_external_job_id is True |
| |
| |
| job2 = Job.create_new("test.mp4", 1000, "mp3", "medium", external_job_id=None) |
| assert job2.has_external_job_id is False |
| |
| |
| job3 = Job.create_new("test.mp4", 1000, "mp3", "medium", external_job_id="") |
| assert job3.has_external_job_id is False |
|
|
|
|
| class TestValidationServiceExternalJobId: |
| """Test ValidationService external job ID validation.""" |
| |
| def setup_method(self): |
| """Set up test fixtures.""" |
| self.validation_service = ValidationService( |
| max_file_size_mb=100.0, |
| supported_video_formats=['.mp4', '.avi'], |
| supported_audio_formats=['mp3', 'aac'] |
| ) |
| |
| def test_validate_external_job_id_valid(self): |
| """Test validation of valid external job IDs.""" |
| |
| self.validation_service.validate_external_job_id("job123") |
| |
| |
| self.validation_service.validate_external_job_id("job_123-abc") |
| |
| |
| self.validation_service.validate_external_job_id("a") |
| |
| |
| long_id = "a" * 50 |
| self.validation_service.validate_external_job_id(long_id) |
| |
| |
| self.validation_service.validate_external_job_id(None) |
| |
| |
| self.validation_service.validate_external_job_id("") |
| |
| def test_validate_external_job_id_invalid(self): |
| """Test validation of invalid external job IDs.""" |
| |
| with pytest.raises(ValidationError, match="must be 50 characters or less"): |
| long_id = "a" * 51 |
| self.validation_service.validate_external_job_id(long_id) |
| |
| |
| with pytest.raises(ValidationError, match="must contain only alphanumeric"): |
| self.validation_service.validate_external_job_id("job@123") |
| |
| with pytest.raises(ValidationError, match="must contain only alphanumeric"): |
| self.validation_service.validate_external_job_id("job 123") |
| |
| with pytest.raises(ValidationError, match="must contain only alphanumeric"): |
| self.validation_service.validate_external_job_id("job.123") |
| |
| with pytest.raises(ValidationError, match="must contain only alphanumeric"): |
| self.validation_service.validate_external_job_id("job/123") |
|
|
|
|
| class TestJobRepositoryWithExternalId: |
| """Test InMemoryJobRepository with external job ID support.""" |
| |
| def setup_method(self): |
| """Set up test fixtures.""" |
| self.repo = InMemoryJobRepository() |
| |
| @pytest.mark.asyncio |
| async def test_create_job_with_external_id(self): |
| """Test creating a job with external job ID.""" |
| job = await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="ext-job-456", |
| bearer_token="bearer-xyz" |
| ) |
| |
| assert job.id == "internal-123" |
| assert job.external_job_id == "ext-job-456" |
| assert job.bearer_token == "bearer-xyz" |
| assert job.filename == "test.mp4" |
| |
| @pytest.mark.asyncio |
| async def test_create_job_without_external_id(self): |
| """Test creating a job without external job ID (backwards compatibility).""" |
| job = await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium" |
| ) |
| |
| assert job.id == "internal-123" |
| assert job.external_job_id is None |
| assert job.bearer_token is None |
| |
| @pytest.mark.asyncio |
| async def test_duplicate_external_job_id_raises_error(self): |
| """Test that duplicate external job IDs raise an error.""" |
| |
| await self.repo.create( |
| job_id="internal-1", |
| filename="test1.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="duplicate-id" |
| ) |
| |
| |
| with pytest.raises(DuplicateExternalJobIdError) as exc_info: |
| await self.repo.create( |
| job_id="internal-2", |
| filename="test2.mp4", |
| file_size_mb=20.0, |
| output_format="aac", |
| quality="high", |
| external_job_id="duplicate-id" |
| ) |
| |
| assert exc_info.value.external_job_id == "duplicate-id" |
| assert "already exists" in str(exc_info.value) |
| |
| @pytest.mark.asyncio |
| async def test_get_by_external_id_success(self): |
| """Test retrieving a job by external job ID.""" |
| |
| await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="ext-job-456" |
| ) |
| |
| |
| job = await self.repo.get_by_external_id("ext-job-456") |
| |
| assert job is not None |
| assert job.id == "internal-123" |
| assert job.external_job_id == "ext-job-456" |
| |
| @pytest.mark.asyncio |
| async def test_get_by_external_id_not_found(self): |
| """Test retrieving a job by non-existent external job ID.""" |
| job = await self.repo.get_by_external_id("non-existent-id") |
| assert job is None |
| |
| @pytest.mark.asyncio |
| async def test_get_by_internal_id_still_works(self): |
| """Test that retrieving by internal ID still works.""" |
| |
| await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="ext-job-456" |
| ) |
| |
| |
| job = await self.repo.get("internal-123") |
| |
| assert job is not None |
| assert job.id == "internal-123" |
| assert job.external_job_id == "ext-job-456" |
| |
| @pytest.mark.asyncio |
| async def test_clear_bearer_token(self): |
| """Test clearing bearer token from repository.""" |
| |
| await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| bearer_token="secret-token" |
| ) |
| |
| |
| job = await self.repo.get("internal-123") |
| assert job.bearer_token == "secret-token" |
| |
| |
| result = await self.repo.clear_bearer_token("internal-123") |
| assert result is True |
| |
| |
| job = await self.repo.get("internal-123") |
| assert job.bearer_token is None |
| |
| @pytest.mark.asyncio |
| async def test_clear_bearer_token_nonexistent_job(self): |
| """Test clearing bearer token for non-existent job.""" |
| result = await self.repo.clear_bearer_token("non-existent-id") |
| assert result is False |
| |
| @pytest.mark.asyncio |
| async def test_delete_job_with_external_id(self): |
| """Test deleting a job removes it from external ID index.""" |
| |
| await self.repo.create( |
| job_id="internal-123", |
| filename="test.mp4", |
| file_size_mb=10.0, |
| output_format="mp3", |
| quality="medium", |
| external_job_id="ext-job-456" |
| ) |
| |
| |
| job = await self.repo.get_by_external_id("ext-job-456") |
| assert job is not None |
| |
| |
| result = await self.repo.delete("internal-123") |
| assert result is True |
| |
| |
| job = await self.repo.get_by_external_id("ext-job-456") |
| assert job is None |
| |
| |
| job = await self.repo.get("internal-123") |
| assert job is None |
| |
| @pytest.mark.asyncio |
| async def test_multiple_jobs_with_external_ids(self): |
| """Test managing multiple jobs with different external IDs.""" |
| |
| await self.repo.create("internal-1", "test1.mp4", 10.0, "mp3", "medium", "ext-1") |
| await self.repo.create("internal-2", "test2.mp4", 20.0, "aac", "high", "ext-2") |
| await self.repo.create("internal-3", "test3.mp4", 30.0, "wav", "low") |
| |
| |
| job1 = await self.repo.get_by_external_id("ext-1") |
| assert job1.id == "internal-1" |
| |
| job2 = await self.repo.get_by_external_id("ext-2") |
| assert job2.id == "internal-2" |
| |
| job3 = await self.repo.get("internal-3") |
| assert job3.external_job_id is None |
| |
| |
| job3_by_ext = await self.repo.get_by_external_id("internal-3") |
| assert job3_by_ext is None |