package service import ( "context" "strings" "testing" "time" "github.com/shankar0123/certctl/internal/domain" ) // newTestDeploymentService creates a test deployment service with all necessary mocks. func newTestDeploymentService() (*DeploymentService, *mockJobRepo, *mockTargetRepo, *mockAgentRepo, *mockCertRepo, *mockAuditRepo, *mockNotifier) { jobRepo := newMockJobRepository() targetRepo := &mockTargetRepo{Targets: make(map[string]*domain.DeploymentTarget)} agentRepo := newMockAgentRepository() certRepo := newMockCertificateRepository() auditRepo := newMockAuditRepository() auditSvc := NewAuditService(auditRepo) notifRepo := newMockNotificationRepository() notifier := newMockNotifier() notifSvc := NewNotificationService(notifRepo, map[string]Notifier{"Email": notifier}) svc := NewDeploymentService(jobRepo, targetRepo, agentRepo, certRepo, auditSvc, notifSvc) return svc, jobRepo, targetRepo, agentRepo, certRepo, auditRepo, notifier } // TestDeploymentService_CreateDeploymentJobs_Success tests successful creation of deployment jobs. func TestDeploymentService_CreateDeploymentJobs_Success(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, _, _, _, _ := newTestDeploymentService() // Add two targets target1 := &domain.DeploymentTarget{ ID: "tgt-nginx-1", Name: "NGINX Server 1", Type: domain.TargetTypeNGINX, AgentID: "agent-1", Enabled: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } target2 := &domain.DeploymentTarget{ ID: "tgt-nginx-2", Name: "NGINX Server 2", Type: domain.TargetTypeNGINX, AgentID: "agent-2", Enabled: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } targetRepo.AddTarget(target1) targetRepo.AddTarget(target2) // Create deployment jobs jobIDs, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1") if err != nil { t.Fatalf("CreateDeploymentJobs failed: %v", err) } // Verify 2 jobs were created if len(jobIDs) != 2 { t.Errorf("expected 2 jobs, got %d", len(jobIDs)) } // Verify jobs are of correct type and status for _, jobID := range jobIDs { job, ok := jobRepo.Jobs[jobID] if !ok { t.Fatalf("job %s not found", jobID) } if job.Type != domain.JobTypeDeployment { t.Errorf("expected job type Deployment, got %v", job.Type) } if job.Status != domain.JobStatusPending { t.Errorf("expected job status Pending, got %v", job.Status) } if job.CertificateID != "mc-cert-1" { t.Errorf("expected CertificateID mc-cert-1, got %s", job.CertificateID) } if job.TargetID == nil || len(*job.TargetID) == 0 { t.Errorf("expected job to have TargetID set") } } } // TestDeploymentService_CreateDeploymentJobs_NoTargets tests error when no targets exist. func TestDeploymentService_CreateDeploymentJobs_NoTargets(t *testing.T) { ctx := context.Background() svc, _, _, _, _, _, _ := newTestDeploymentService() // No targets added, so ListByCertificate returns empty slice jobIDs, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1") if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), "no targets found") { t.Errorf("expected error containing 'no targets found', got %v", err) } if len(jobIDs) != 0 { t.Errorf("expected 0 job IDs, got %d", len(jobIDs)) } } // TestDeploymentService_CreateDeploymentJobs_TargetListError tests error from target list. func TestDeploymentService_CreateDeploymentJobs_TargetListError(t *testing.T) { ctx := context.Background() svc, _, targetRepo, _, _, _, _ := newTestDeploymentService() // Set target repo to return error targetRepo.ListByCertErr = errNotFound jobIDs, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1") if err == nil { t.Fatalf("expected error, got nil") } if len(jobIDs) != 0 { t.Errorf("expected 0 job IDs, got %d", len(jobIDs)) } } // TestDeploymentService_CreateDeploymentJobs_AllJobCreationsFail tests when all job creations fail. func TestDeploymentService_CreateDeploymentJobs_AllJobCreationsFail(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, _, _, _, _ := newTestDeploymentService() // Add targets but job creation will fail target := &domain.DeploymentTarget{ ID: "tgt-1", Name: "Test Target", Type: domain.TargetTypeNGINX, AgentID: "agent-1", } targetRepo.AddTarget(target) // Set job repo to fail all creates jobRepo.CreateErr = errNotFound jobIDs, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1") if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), "failed to create any deployment jobs") { t.Errorf("expected error containing 'failed to create any deployment jobs', got %v", err) } if len(jobIDs) != 0 { t.Errorf("expected 0 job IDs, got %d", len(jobIDs)) } } // TestDeploymentService_CreateDeploymentJobs_AuditEvent tests that audit event is recorded. func TestDeploymentService_CreateDeploymentJobs_AuditEvent(t *testing.T) { ctx := context.Background() svc, _, targetRepo, _, _, auditRepo, _ := newTestDeploymentService() // Add a target target := &domain.DeploymentTarget{ ID: "tgt-1", Name: "Test Target", Type: domain.TargetTypeNGINX, AgentID: "agent-1", } targetRepo.AddTarget(target) _, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1") if err != nil { t.Fatalf("CreateDeploymentJobs failed: %v", err) } // Check audit event if len(auditRepo.Events) == 0 { t.Errorf("expected at least 1 audit event, got %d", len(auditRepo.Events)) } found := false for _, event := range auditRepo.Events { if event.Action == "deployment_jobs_created" { found = true break } } if !found { t.Errorf("expected audit event with action 'deployment_jobs_created'") } } // TestDeploymentService_ProcessDeploymentJob_Success tests successful job processing. func TestDeploymentService_ProcessDeploymentJob_Success(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, agentRepo, certRepo, _, _ := newTestDeploymentService() // Create job with TargetID targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target with AgentID target := &domain.DeploymentTarget{ ID: targetID, Name: "Test Target", Type: domain.TargetTypeNGINX, AgentID: "agent-1", Enabled: true, CreatedAt: time.Now(), UpdatedAt: time.Now(), } targetRepo.AddTarget(target) // Add agent with recent heartbeat now := time.Now() agent := &domain.Agent{ ID: "agent-1", Name: "Test Agent", Hostname: "agent.example.com", Status: domain.AgentStatusOnline, LastHeartbeatAt: &now, RegisteredAt: time.Now(), APIKeyHash: "hash-1", OS: "linux", Architecture: "amd64", IPAddress: "192.168.1.1", Version: "1.0.0", } agentRepo.AddAgent(agent) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", CommonName: "example.com", Status: domain.CertificateStatusActive, ExpiresAt: time.Now().AddDate(1, 0, 0), CreatedAt: time.Now(), UpdatedAt: time.Now(), } certRepo.AddCert(cert) // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err != nil { t.Fatalf("ProcessDeploymentJob failed: %v", err) } // Verify job status was updated to Running if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusRunning { t.Errorf("expected job status Running, got %v", status) } } // TestDeploymentService_ProcessDeploymentJob_CertNotFound tests handling when cert is not found. func TestDeploymentService_ProcessDeploymentJob_CertNotFound(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, agentRepo, certRepo, _, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target target := &domain.DeploymentTarget{ ID: targetID, AgentID: "agent-1", } targetRepo.AddTarget(target) // Add agent now := time.Now() agent := &domain.Agent{ ID: "agent-1", Status: domain.AgentStatusOnline, LastHeartbeatAt: &now, } agentRepo.AddAgent(agent) // Set cert repo to return error certRepo.GetErr = errNotFound // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err == nil { t.Fatalf("expected error, got nil") } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } } // TestDeploymentService_ProcessDeploymentJob_NoTargetID tests handling when TargetID is missing. func TestDeploymentService_ProcessDeploymentJob_NoTargetID(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Create job without TargetID job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: nil, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err == nil { t.Fatalf("expected error, got nil") } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } } // TestDeploymentService_ProcessDeploymentJob_TargetNotFound tests handling when target is not found. func TestDeploymentService_ProcessDeploymentJob_TargetNotFound(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, agentRepo, certRepo, _, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add agent now := time.Now() agent := &domain.Agent{ ID: "agent-1", Status: domain.AgentStatusOnline, LastHeartbeatAt: &now, } agentRepo.AddAgent(agent) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Set target repo to return error targetRepo.GetErr = errNotFound // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err == nil { t.Fatalf("expected error, got nil") } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } } // TestDeploymentService_ProcessDeploymentJob_AgentNotFound tests handling when agent is not found. func TestDeploymentService_ProcessDeploymentJob_AgentNotFound(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, agentRepo, certRepo, _, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target with AgentID target := &domain.DeploymentTarget{ ID: targetID, AgentID: "agent-1", } targetRepo.AddTarget(target) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Set agent repo to return error agentRepo.GetErr = errNotFound // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err == nil { t.Fatalf("expected error, got nil") } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } } // TestDeploymentService_ProcessDeploymentJob_AgentOffline tests handling when agent is offline. func TestDeploymentService_ProcessDeploymentJob_AgentOffline(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, agentRepo, certRepo, _, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusPending, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target target := &domain.DeploymentTarget{ ID: targetID, AgentID: "agent-1", } targetRepo.AddTarget(target) // Add agent with old heartbeat (offline) oldTime := time.Now().Add(-10 * time.Minute) agent := &domain.Agent{ ID: "agent-1", Status: domain.AgentStatusOnline, LastHeartbeatAt: &oldTime, } agentRepo.AddAgent(agent) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Process the job err := svc.ProcessDeploymentJob(ctx, job) if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), "offline") { t.Errorf("expected error containing 'offline', got %v", err) } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } } // TestDeploymentService_ValidateDeployment_Completed tests successful validation. func TestDeploymentService_ValidateDeployment_Completed(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Create completed deployment job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusCompleted, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Validate deployment success, err := svc.ValidateDeployment(ctx, "mc-cert-1", "tgt-1") if err != nil { t.Fatalf("ValidateDeployment failed: %v", err) } if !success { t.Errorf("expected success=true, got %v", success) } } // TestDeploymentService_ValidateDeployment_Failed tests validation of failed deployment. func TestDeploymentService_ValidateDeployment_Failed(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Create failed deployment job targetID := "tgt-1" errMsg := "deployment failed" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusFailed, LastError: &errMsg, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Validate deployment success, err := svc.ValidateDeployment(ctx, "mc-cert-1", "tgt-1") if err == nil { t.Fatalf("expected error, got nil") } if success { t.Errorf("expected success=false, got %v", success) } } // TestDeploymentService_ValidateDeployment_InProgress tests validation of in-progress deployment. func TestDeploymentService_ValidateDeployment_InProgress(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Create running deployment job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusRunning, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Validate deployment success, err := svc.ValidateDeployment(ctx, "mc-cert-1", "tgt-1") if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), "in progress") { t.Errorf("expected error containing 'in progress', got %v", err) } if success { t.Errorf("expected success=false, got %v", success) } } // TestDeploymentService_ValidateDeployment_NoJob tests validation when no job exists. func TestDeploymentService_ValidateDeployment_NoJob(t *testing.T) { ctx := context.Background() svc, _, _, _, _, _, _ := newTestDeploymentService() // No jobs added // Validate deployment success, err := svc.ValidateDeployment(ctx, "mc-cert-1", "tgt-1") if err == nil { t.Fatalf("expected error, got nil") } if !strings.Contains(err.Error(), "no deployment job found") { t.Errorf("expected error containing 'no deployment job found', got %v", err) } if success { t.Errorf("expected success=false, got %v", success) } } // TestDeploymentService_MarkDeploymentComplete_Success tests successful completion marking. func TestDeploymentService_MarkDeploymentComplete_Success(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, _, certRepo, auditRepo, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusRunning, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target target := &domain.DeploymentTarget{ ID: targetID, Name: "Test Target", AgentID: "agent-1", } targetRepo.AddTarget(target) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Mark deployment complete err := svc.MarkDeploymentComplete(ctx, "job-1") if err != nil { t.Fatalf("MarkDeploymentComplete failed: %v", err) } // Verify job status was updated to Completed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusCompleted { t.Errorf("expected job status Completed, got %v", status) } // Verify audit event was recorded found := false for _, event := range auditRepo.Events { if event.Action == "deployment_job_completed" { found = true break } } if !found { t.Errorf("expected audit event for deployment_job_completed") } } // TestDeploymentService_MarkDeploymentComplete_JobNotFound tests error when job not found. func TestDeploymentService_MarkDeploymentComplete_JobNotFound(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Set job repo to return error jobRepo.GetErr = errNotFound // Mark deployment complete err := svc.MarkDeploymentComplete(ctx, "job-1") if err == nil { t.Fatalf("expected error, got nil") } } // TestDeploymentService_MarkDeploymentComplete_NoTargetID tests completion without target ID. func TestDeploymentService_MarkDeploymentComplete_NoTargetID(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, certRepo, _, _ := newTestDeploymentService() // Create job without TargetID job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: nil, Status: domain.JobStatusRunning, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Mark deployment complete (should succeed, just no notification) err := svc.MarkDeploymentComplete(ctx, "job-1") if err != nil { t.Fatalf("MarkDeploymentComplete failed: %v", err) } // Verify job status was updated to Completed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusCompleted { t.Errorf("expected job status Completed, got %v", status) } } // TestDeploymentService_MarkDeploymentFailed_Success tests successful failure marking. func TestDeploymentService_MarkDeploymentFailed_Success(t *testing.T) { ctx := context.Background() svc, jobRepo, targetRepo, _, certRepo, auditRepo, _ := newTestDeploymentService() // Create job targetID := "tgt-1" job := &domain.Job{ ID: "job-1", Type: domain.JobTypeDeployment, CertificateID: "mc-cert-1", TargetID: &targetID, Status: domain.JobStatusRunning, CreatedAt: time.Now(), } jobRepo.AddJob(job) // Add target target := &domain.DeploymentTarget{ ID: targetID, Name: "Test Target", AgentID: "agent-1", } targetRepo.AddTarget(target) // Add certificate cert := &domain.ManagedCertificate{ ID: "mc-cert-1", Name: "Test Cert", Status: domain.CertificateStatusActive, } certRepo.AddCert(cert) // Mark deployment failed err := svc.MarkDeploymentFailed(ctx, "job-1", "connection timeout") if err != nil { t.Fatalf("MarkDeploymentFailed failed: %v", err) } // Verify job status was updated to Failed if status, ok := jobRepo.StatusUpdates["job-1"]; !ok || status != domain.JobStatusFailed { t.Errorf("expected job status Failed, got %v", status) } // Verify LastError is set if jobRepo.Jobs["job-1"].LastError == nil || *jobRepo.Jobs["job-1"].LastError != "connection timeout" { t.Errorf("expected LastError to be 'connection timeout', got %v", jobRepo.Jobs["job-1"].LastError) } // Verify audit event was recorded found := false for _, event := range auditRepo.Events { if event.Action == "deployment_job_failed" { found = true break } } if !found { t.Errorf("expected audit event for deployment_job_failed") } } // TestDeploymentService_MarkDeploymentFailed_JobNotFound tests error when job not found. func TestDeploymentService_MarkDeploymentFailed_JobNotFound(t *testing.T) { ctx := context.Background() svc, jobRepo, _, _, _, _, _ := newTestDeploymentService() // Set job repo to return error jobRepo.GetErr = errNotFound // Mark deployment failed err := svc.MarkDeploymentFailed(ctx, "job-1", "error message") if err == nil { t.Fatalf("expected error, got nil") } }