Files
certctl/internal/service/deployment_test.go
T
shankar0123 11173a74c6 feat(M31): agent work routing — scope jobs to assigned agents
Deployment jobs now set agent_id from target→agent relationship at
creation time. GetPendingWork() uses ListPendingByAgentID() with a
3-way UNION query (direct match, legacy NULL fallback via target JOIN,
AwaitingCSR via cert→target→agent chain) so each agent only receives
its own jobs.

- Added AgentID *string to Job domain struct
- Added agent_id to all job SQL queries (5 SELECTs, INSERT, UPDATE, scanJob)
- New ListPendingByAgentID() repository method
- Rewrote GetPendingWork() from ~25 lines to single scoped query
- 4 new Go tests (3 agent routing + 1 deployment agent_id)
- Frontend: agent_id/target_id on Job type

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 14:10:42 -04:00

832 lines
23 KiB
Go

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")
}
// M31: Verify AgentID is set from target's agent assignment
if job.AgentID == nil {
t.Errorf("expected job to have AgentID set (M31 agent routing)")
}
}
}
// TestDeploymentService_CreateDeploymentJobs_SetsAgentID verifies AgentID is populated from target.
func TestDeploymentService_CreateDeploymentJobs_SetsAgentID(t *testing.T) {
ctx := context.Background()
svc, jobRepo, targetRepo, _, _, _, _ := newTestDeploymentService()
target := &domain.DeploymentTarget{
ID: "tgt-nginx-1",
Name: "NGINX Server 1",
Type: domain.TargetTypeNGINX,
AgentID: "agent-web-01",
Enabled: true,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
targetRepo.AddTarget(target)
jobIDs, err := svc.CreateDeploymentJobs(ctx, "mc-cert-1")
if err != nil {
t.Fatalf("CreateDeploymentJobs failed: %v", err)
}
if len(jobIDs) != 1 {
t.Fatalf("expected 1 job, got %d", len(jobIDs))
}
job := jobRepo.Jobs[jobIDs[0]]
if job.AgentID == nil {
t.Fatal("expected AgentID to be set on deployment job")
}
if *job.AgentID != "agent-web-01" {
t.Errorf("expected AgentID 'agent-web-01', got '%s'", *job.AgentID)
}
}
// 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")
}
}