package service import ( "context" "log/slog" "os" "testing" "time" "github.com/shankar0123/certctl/internal/domain" ) // helper to build job service with proper constructor signatures func newTestJobService(jobRepo *mockJobRepo) *JobService { logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})) certRepo := &mockCertRepo{ Certs: make(map[string]*domain.ManagedCertificate), Versions: make(map[string][]*domain.CertificateVersion), } renewalPolicyRepo := &mockRenewalPolicyRepo{ Policies: make(map[string]*domain.RenewalPolicy), } auditRepo := &mockAuditRepo{} auditService := NewAuditService(auditRepo) notifRepo := newMockNotificationRepository() notifService := NewNotificationService(notifRepo, make(map[string]Notifier)) targetRepo := &mockTargetRepo{Targets: make(map[string]*domain.DeploymentTarget)} agentRepo := &mockAgentRepo{Agents: make(map[string]*domain.Agent)} renewalService := NewRenewalService(certRepo, jobRepo, renewalPolicyRepo, nil, auditService, notifService, make(map[string]IssuerConnector), "server") deploymentService := NewDeploymentService(jobRepo, targetRepo, agentRepo, certRepo, auditService, notifService) return NewJobService(jobRepo, renewalService, deploymentService, logger) } func TestProcessPendingJobs_Renewal(t *testing.T) { ctx := context.Background() now := time.Now() job := &domain.Job{ ID: "job-001", Type: domain.JobTypeRenewal, CertificateID: "cert-001", Status: domain.JobStatusPending, Attempts: 0, MaxAttempts: 3, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) err := jobService.ProcessPendingJobs(ctx) if err != nil { t.Logf("ProcessPendingJobs returned error (expected for renewal without cert): %v", err) } } func TestProcessPendingJobs_NoJobs(t *testing.T) { ctx := context.Background() jobRepo := &mockJobRepo{ Jobs: make(map[string]*domain.Job), StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) err := jobService.ProcessPendingJobs(ctx) if err != nil { t.Fatalf("ProcessPendingJobs failed: %v", err) } } func TestCancelJob(t *testing.T) { ctx := context.Background() now := time.Now() job := &domain.Job{ ID: "job-001", Type: domain.JobTypeDeployment, CertificateID: "cert-001", Status: domain.JobStatusPending, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) err := jobService.CancelJobWithContext(ctx, "job-001") if err != nil { t.Fatalf("CancelJob failed: %v", err) } if jobRepo.StatusUpdates["job-001"] != domain.JobStatusCancelled { t.Errorf("expected status Cancelled, got %s", jobRepo.StatusUpdates["job-001"]) } } func TestCancelJob_AlreadyCompleted(t *testing.T) { ctx := context.Background() now := time.Now() job := &domain.Job{ ID: "job-001", Type: domain.JobTypeDeployment, CertificateID: "cert-001", Status: domain.JobStatusCompleted, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) err := jobService.CancelJobWithContext(ctx, "job-001") if err == nil { t.Fatal("expected error for completed job") } } func TestGetJob(t *testing.T) { now := time.Now() job := &domain.Job{ ID: "job-001", Type: domain.JobTypeDeployment, CertificateID: "cert-001", Status: domain.JobStatusPending, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) retrieved, err := jobService.GetJob("job-001") if err != nil { t.Fatalf("GetJob failed: %v", err) } if retrieved.ID != "job-001" { t.Errorf("expected job ID job-001, got %s", retrieved.ID) } if retrieved.Type != domain.JobTypeDeployment { t.Errorf("expected job type Deployment, got %s", retrieved.Type) } } func TestListJobs(t *testing.T) { now := time.Now() job1 := &domain.Job{ ID: "job-001", Type: domain.JobTypeDeployment, CertificateID: "cert-001", Status: domain.JobStatusCompleted, CreatedAt: now, ScheduledAt: now, } job2 := &domain.Job{ ID: "job-002", Type: domain.JobTypeRenewal, CertificateID: "cert-002", Status: domain.JobStatusPending, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job1, "job-002": job2}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) jobs, total, err := jobService.ListJobs("", "", 1, 50) if err != nil { t.Fatalf("ListJobs failed: %v", err) } if len(jobs) != 2 { t.Errorf("expected 2 jobs, got %d", len(jobs)) } if total != 2 { t.Errorf("expected total 2, got %d", total) } } func TestListJobs_FilterByStatus(t *testing.T) { now := time.Now() job1 := &domain.Job{ ID: "job-001", Type: domain.JobTypeDeployment, CertificateID: "cert-001", Status: domain.JobStatusCompleted, CreatedAt: now, ScheduledAt: now, } job2 := &domain.Job{ ID: "job-002", Type: domain.JobTypeRenewal, CertificateID: "cert-002", Status: domain.JobStatusPending, CreatedAt: now, ScheduledAt: now, } jobRepo := &mockJobRepo{ Jobs: map[string]*domain.Job{"job-001": job1, "job-002": job2}, StatusUpdates: make(map[string]domain.JobStatus), } jobService := newTestJobService(jobRepo) jobs, total, err := jobService.ListJobs(string(domain.JobStatusPending), "", 1, 50) if err != nil { t.Fatalf("ListJobs failed: %v", err) } if len(jobs) != 1 { t.Errorf("expected 1 pending job, got %d", len(jobs)) } if total != 1 { t.Errorf("expected total 1, got %d", total) } }