use application::processing::{FailJobCommand, FailJobHandler}; use application::testing::{InMemoryJobBatchRepository, InMemoryJobRepository, StubEventPublisher}; use domain::entities::{Job, JobBatch, JobStatus, JobType}; use domain::events::DomainEvent; use domain::ports::{JobBatchRepository, JobRepository}; use domain::value_objects::StructuredData; use std::sync::Arc; fn make_handler( job_repo: Arc, batch_repo: Arc, event_pub: Arc, ) -> FailJobHandler { FailJobHandler::new(job_repo, batch_repo, event_pub) } #[tokio::test] async fn retries_on_failure() { let job_repo = Arc::new(InMemoryJobRepository::new()); let batch_repo = Arc::new(InMemoryJobBatchRepository::new()); let event_pub = Arc::new(StubEventPublisher::new()); let job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new()); let job_id = job.job_id; assert_eq!(job.retry_count, 0); job_repo.save(&job).await.unwrap(); let handler = make_handler(job_repo.clone(), batch_repo.clone(), event_pub.clone()); let result = handler .execute(FailJobCommand { job_id, error: "transient error".into(), }) .await .unwrap(); assert_eq!(result.status, JobStatus::Queued); assert_eq!(result.retry_count, 1); let events = event_pub.published().await; assert!(matches!(&events[0], DomainEvent::JobEnqueued { .. })); } #[tokio::test] async fn fails_permanently_after_max_retries() { let job_repo = Arc::new(InMemoryJobRepository::new()); let batch_repo = Arc::new(InMemoryJobBatchRepository::new()); let event_pub = Arc::new(StubEventPublisher::new()); let mut job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new()); // Exhaust retries (max_retries=3, so fail 3 times) job.fail("err1"); job.fail("err2"); assert_eq!(job.retry_count, 2); assert_eq!(job.status, JobStatus::Queued); // still retryable let job_id = job.job_id; job_repo.save(&job).await.unwrap(); let handler = make_handler(job_repo.clone(), batch_repo.clone(), event_pub.clone()); let result = handler .execute(FailJobCommand { job_id, error: "fatal".into(), }) .await .unwrap(); assert_eq!(result.status, JobStatus::Failed); assert_eq!(result.retry_count, 3); let events = event_pub.published().await; assert!(matches!(&events[0], DomainEvent::JobFailed { .. })); } #[tokio::test] async fn updates_batch_on_permanent_failure() { let job_repo = Arc::new(InMemoryJobRepository::new()); let batch_repo = Arc::new(InMemoryJobBatchRepository::new()); let event_pub = Arc::new(StubEventPublisher::new()); let batch = JobBatch::new("test-batch", 2); let batch_id = batch.batch_id; batch_repo.save(&batch).await.unwrap(); let mut job = Job::new(JobType::ExtractMetadata, 5, StructuredData::new()).with_batch(batch_id); // Exhaust retries job.fail("err1"); job.fail("err2"); let job_id = job.job_id; job_repo.save(&job).await.unwrap(); let handler = make_handler(job_repo.clone(), batch_repo.clone(), event_pub.clone()); handler .execute(FailJobCommand { job_id, error: "permanent failure".into(), }) .await .unwrap(); let updated_batch = batch_repo.find_by_id(&batch_id).await.unwrap().unwrap(); assert_eq!(updated_batch.failed_count, 1); }