package service import ( "context" "fmt" "strings" "testing" "time" "github.com/shankar0123/certctl/internal/domain" ) func TestCheckExpiringCertificates_SendsThresholdAlerts(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() notifier := newMockNotifier() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{ "Email": notifier, }) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create a cert expiring in 10 days cert := &domain.ManagedCertificate{ ID: "mc-expiring", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 10), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy with thresholds policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run expiry check err := svc.CheckExpiringCertificates(ctx) if err != nil { t.Fatalf("CheckExpiringCertificates failed: %v", err) } // Verify alerts were sent if len(notifRepo.Notifications) < 1 { t.Errorf("expected at least 1 alert, got %d", len(notifRepo.Notifications)) } // Verify renewal job was created if len(jobRepo.Jobs) < 1 { t.Errorf("expected renewal job to be created") } hasRenewalJob := false for _, job := range jobRepo.Jobs { if job.Type == domain.JobTypeRenewal { hasRenewalJob = true break } } if !hasRenewalJob { t.Errorf("expected renewal job in jobs") } } func TestCheckExpiringCertificates_DeduplicatesAlerts(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() notifier := newMockNotifier() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{ "Email": notifier, }) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create cert cert := &domain.ManagedCertificate{ ID: "mc-dedup", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 10), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Add existing threshold alert notification existingNotif := &domain.NotificationEvent{ ID: "notif-existing", CertificateID: &cert.ID, Type: domain.NotificationTypeExpirationWarning, Channel: domain.NotificationChannelEmail, Recipient: "owner-1", Message: "Alert [threshold:7]", Status: "sent", CreatedAt: time.Now(), } notifRepo.AddNotification(existingNotif) // Run first check _ = svc.CheckExpiringCertificates(ctx) initialCount := notifier.getSentCount() // Run second check - should deduplicate _ = svc.CheckExpiringCertificates(ctx) finalCount := notifier.getSentCount() // Should not send duplicate alerts if finalCount > initialCount { t.Errorf("expected deduplication, but sent new alerts: initial=%d, final=%d", initialCount, finalCount) } } func TestCheckExpiringCertificates_SkipsRenewalInProgress(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create cert with RenewalInProgress status cert := &domain.ManagedCertificate{ ID: "mc-in-progress", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusRenewalInProgress, ExpiresAt: time.Now().AddDate(0, 0, 10), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run check err := svc.CheckExpiringCertificates(ctx) if err != nil { t.Fatalf("CheckExpiringCertificates failed: %v", err) } // Should not create renewal job for cert already renewing for _, job := range jobRepo.Jobs { if job.Type == domain.JobTypeRenewal { t.Errorf("should not create renewal job for cert with RenewalInProgress status") } } } func TestCheckExpiringCertificates_UpdatesStatusToExpiring(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create active cert that will become expiring // Use an issuer NOT in the registry so no renewal job is created (which would override status) cert := &domain.ManagedCertificate{ ID: "mc-expiring-status", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-unregistered", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 5), // 5 days, within 30-day threshold Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy with AutoRenew: false so we only test status transition policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: false, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run check _ = svc.CheckExpiringCertificates(ctx) // Verify status was updated to Expiring updated, _ := certRepo.Get(ctx, cert.ID) if updated.Status != domain.CertificateStatusExpiring { t.Errorf("expected status Expiring, got %s", updated.Status) } } func TestCheckExpiringCertificates_UpdatesStatusToExpired(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create cert that is already expired // Use an issuer NOT in the registry so no renewal job is created (which would override status) cert := &domain.ManagedCertificate{ ID: "mc-expired-status", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-unregistered", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, -1), // Already expired Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy with AutoRenew: false so we only test status transition policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: false, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run check _ = svc.CheckExpiringCertificates(ctx) // Verify status was updated to Expired updated, _ := certRepo.Get(ctx, cert.ID) if updated.Status != domain.CertificateStatusExpired { t.Errorf("expected status Expired, got %s", updated.Status) } } func TestCheckExpiringCertificates_CreatesRenewalJob(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create expiring cert with registered issuer cert := &domain.ManagedCertificate{ ID: "mc-job-create", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", // Registered issuer RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 20), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run check _ = svc.CheckExpiringCertificates(ctx) // Verify renewal job was created hasRenewalJob := false for _, job := range jobRepo.Jobs { if job.Type == domain.JobTypeRenewal && job.Status == domain.JobStatusPending { hasRenewalJob = true break } } if !hasRenewalJob { t.Errorf("expected renewal job to be created") } } func TestCheckExpiringCertificates_SkipsWithoutIssuer(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) // Empty issuer registry issuerRegistry := map[string]IssuerConnector{} svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create cert with unregistered issuer cert := &domain.ManagedCertificate{ ID: "mc-no-issuer", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-missing", // Not in registry RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 20), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Run check _ = svc.CheckExpiringCertificates(ctx) // Should not create renewal job without issuer for _, job := range jobRepo.Jobs { if job.Type == domain.JobTypeRenewal { t.Errorf("should not create renewal job for cert with missing issuer") } } } func TestCheckExpiringCertificates_SkipsDuplicateJobs(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create cert cert := &domain.ManagedCertificate{ ID: "mc-dup-job", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 20), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create policy policy := &domain.RenewalPolicy{ ID: "rp-standard", Name: "Standard", RenewalWindowDays: 30, AutoRenew: true, MaxRetries: 3, RetryInterval: 300, AlertThresholdsDays: []int{30, 14, 7, 0}, CreatedAt: time.Now(), UpdatedAt: time.Now(), } policyRepo.AddPolicy(policy) // Add existing renewal job existingJob := &domain.Job{ ID: "job-existing", CertificateID: cert.ID, Type: domain.JobTypeRenewal, Status: domain.JobStatusPending, MaxAttempts: 3, ScheduledAt: time.Now(), CreatedAt: time.Now(), } jobRepo.AddJob(existingJob) // Run first check _ = svc.CheckExpiringCertificates(ctx) // Run second check _ = svc.CheckExpiringCertificates(ctx) // Should have only 1 renewal job renewalCount := 0 for _, job := range jobRepo.Jobs { if job.Type == domain.JobTypeRenewal { renewalCount++ } } if renewalCount > 1 { t.Errorf("expected 1 renewal job, got %d (duplicate prevention failed)", renewalCount) } } func TestProcessRenewalJob(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{ "Email": newMockNotifier(), }) issuerConnector := &mockIssuerConnector{} issuerRegistry := map[string]IssuerConnector{ "iss-test": issuerConnector, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create certificate cert := &domain.ManagedCertificate{ ID: "mc-renewal", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{"www.test.example.com"}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, TargetIDs: []string{"target-1", "target-2"}, ExpiresAt: time.Now().AddDate(0, 0, 30), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create renewal job job := &domain.Job{ ID: "job-renewal-1", CertificateID: cert.ID, Type: domain.JobTypeRenewal, Status: domain.JobStatusPending, MaxAttempts: 3, ScheduledAt: time.Now(), CreatedAt: time.Now(), } jobRepo.AddJob(job) // Process renewal job err := svc.ProcessRenewalJob(ctx, job) if err != nil { t.Fatalf("ProcessRenewalJob failed: %v", err) } // Verify cert was updated updated, _ := certRepo.Get(ctx, cert.ID) if updated.Status != domain.CertificateStatusActive { t.Errorf("expected cert status Active, got %s", updated.Status) } if updated.LastRenewalAt == nil { t.Errorf("expected LastRenewalAt to be set") } // Verify certificate version was created if len(certRepo.Versions[cert.ID]) != 1 { t.Errorf("expected 1 certificate version, got %d", len(certRepo.Versions[cert.ID])) } // Verify deployment jobs were created deploymentCount := 0 for _, j := range jobRepo.Jobs { if j.Type == domain.JobTypeDeployment { deploymentCount++ } } if deploymentCount != 2 { t.Errorf("expected 2 deployment jobs (one per target), got %d", deploymentCount) } // Verify job was marked as completed completedJob, _ := jobRepo.Get(ctx, job.ID) if completedJob.Status != domain.JobStatusCompleted { t.Errorf("expected job status Completed, got %s", completedJob.Status) } } func TestProcessRenewalJob_IssuerFailure(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{ "Email": newMockNotifier(), }) // Create issuer that will fail issuerConnector := &mockIssuerConnector{ Err: fmt.Errorf("issuer service unavailable"), } issuerRegistry := map[string]IssuerConnector{ "iss-test": issuerConnector, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create certificate cert := &domain.ManagedCertificate{ ID: "mc-renewal-fail", Name: "Test Cert", CommonName: "test.example.com", SANs: []string{}, OwnerID: "owner-1", TeamID: "team-1", IssuerID: "iss-test", RenewalPolicyID: "rp-standard", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(0, 0, 30), Tags: make(map[string]string), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Create renewal job job := &domain.Job{ ID: "job-renewal-fail", CertificateID: cert.ID, Type: domain.JobTypeRenewal, Status: domain.JobStatusPending, MaxAttempts: 3, ScheduledAt: time.Now(), CreatedAt: time.Now(), } jobRepo.AddJob(job) // Process renewal job (should fail) err := svc.ProcessRenewalJob(ctx, job) if err == nil { t.Fatalf("expected ProcessRenewalJob to fail") } // Verify job was marked as failed failedJob, _ := jobRepo.Get(ctx, job.ID) if failedJob.Status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %s", failedJob.Status) } if failedJob.LastError == nil || !strings.Contains(*failedJob.LastError, "issuer service unavailable") { t.Errorf("expected error message in job, got: %v", failedJob.LastError) } // Verify failure notification was sent if len(notifRepo.Notifications) < 1 { t.Errorf("expected failure notification to be created") } foundFailureNotif := false for _, notif := range notifRepo.Notifications { if notif.Type == domain.NotificationTypeRenewalFailure { foundFailureNotif = true break } } if !foundFailureNotif { t.Errorf("expected RenewalFailure notification type") } } func TestRetryFailedJobs(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create failed job with attempts < max_attempts failedJob := &domain.Job{ ID: "job-failed-1", CertificateID: "mc-test", Type: domain.JobTypeRenewal, Status: domain.JobStatusFailed, Attempts: 1, MaxAttempts: 3, LastError: stringPtr("temporary failure"), ScheduledAt: time.Now(), CreatedAt: time.Now().AddDate(0, 0, -1), } jobRepo.AddJob(failedJob) // Create other job types that should be ignored otherJob := &domain.Job{ ID: "job-other", CertificateID: "mc-test", Type: domain.JobTypeDeployment, Status: domain.JobStatusFailed, Attempts: 1, MaxAttempts: 3, ScheduledAt: time.Now(), CreatedAt: time.Now(), } jobRepo.AddJob(otherJob) // Retry failed jobs err := svc.RetryFailedJobs(ctx, 3) if err != nil { t.Fatalf("RetryFailedJobs failed: %v", err) } // Verify failed renewal job was reset to pending retried, _ := jobRepo.Get(ctx, failedJob.ID) if retried.Status != domain.JobStatusPending { t.Errorf("expected job status Pending after retry, got %s", retried.Status) } // Verify other job type was not touched other, _ := jobRepo.Get(ctx, otherJob.ID) if other.Status != domain.JobStatusFailed { t.Errorf("expected non-renewal job to stay Failed, got %s", other.Status) } } func TestProcessRenewalJob_NoCertificate(t *testing.T) { t.Helper() ctx := context.Background() certRepo := newMockCertificateRepository() jobRepo := newMockJobRepository() policyRepo := newMockRenewalPolicyRepository() auditRepo := newMockAuditRepository() notifRepo := newMockNotificationRepository() auditSvc := NewAuditService(auditRepo) notifSvc := NewNotificationService(notifRepo, map[string]Notifier{}) issuerRegistry := map[string]IssuerConnector{ "iss-test": &mockIssuerConnector{}, } svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry, "server") // Create job with non-existent certificate job := &domain.Job{ ID: "job-no-cert", CertificateID: "mc-missing", Type: domain.JobTypeRenewal, Status: domain.JobStatusPending, MaxAttempts: 3, ScheduledAt: time.Now(), CreatedAt: time.Now(), } jobRepo.AddJob(job) // Process renewal job err := svc.ProcessRenewalJob(ctx, job) if err == nil { t.Fatalf("expected ProcessRenewalJob to fail for missing certificate") } // Verify job was marked as failed failedJob, _ := jobRepo.Get(ctx, job.ID) if failedJob.Status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %s", failedJob.Status) } } // stringPtr is defined in notification_test.go