package handler import ( "bytes" "context" "encoding/json" "net/http" "net/http/httptest" "testing" "time" "github.com/certctl-io/certctl/internal/domain" ) // mockVerificationService is a test double for VerificationService. type mockVerificationService struct { recordErr error getErr error results map[string]*domain.VerificationResult } func (m *mockVerificationService) RecordVerificationResult(ctx context.Context, result *domain.VerificationResult) error { if m.recordErr != nil { return m.recordErr } if m.results == nil { m.results = make(map[string]*domain.VerificationResult) } m.results[result.JobID] = result return nil } func (m *mockVerificationService) GetVerificationResult(ctx context.Context, jobID string) (*domain.VerificationResult, error) { if m.getErr != nil { return nil, m.getErr } if m.results == nil { m.results = make(map[string]*domain.VerificationResult) } return m.results[jobID], nil } func TestVerifyDeployment_Success(t *testing.T) { mockSvc := &mockVerificationService{ results: make(map[string]*domain.VerificationResult), } handler := NewVerificationHandler(mockSvc) req := VerifyDeploymentRequest{ TargetID: "t-nginx1", ExpectedFingerprint: "abc123", ActualFingerprint: "abc123", Verified: true, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test1/verify", bytes.NewReader(body)) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } // Verify result was recorded result := mockSvc.results["j-test1"] if result == nil { t.Fatal("expected verification result to be recorded") } if !result.Verified { t.Error("expected Verified to be true") } } func TestVerifyDeployment_FingerPrintMismatch(t *testing.T) { mockSvc := &mockVerificationService{ results: make(map[string]*domain.VerificationResult), } handler := NewVerificationHandler(mockSvc) req := VerifyDeploymentRequest{ TargetID: "t-apache1", ExpectedFingerprint: "aaa111", ActualFingerprint: "bbb222", Verified: false, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test2/verify", bytes.NewReader(body)) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } result := mockSvc.results["j-test2"] if result == nil { t.Fatal("expected verification result to be recorded") } if result.Verified { t.Error("expected Verified to be false") } } func TestVerifyDeployment_MissingTargetID(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) req := VerifyDeploymentRequest{ ExpectedFingerprint: "abc123", ActualFingerprint: "abc123", Verified: true, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test3/verify", bytes.NewReader(body)) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestVerifyDeployment_MissingExpectedFingerprint(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) req := VerifyDeploymentRequest{ TargetID: "t-nginx1", ActualFingerprint: "abc123", Verified: true, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test4/verify", bytes.NewReader(body)) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestVerifyDeployment_InvalidMethod(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("GET", "/api/v1/jobs/j-test5/verify", nil) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected status 405, got %d", w.Code) } } func TestVerifyDeployment_InvalidJSON(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test6/verify", bytes.NewBufferString("invalid json")) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestGetVerificationStatus_Success(t *testing.T) { now := time.Now().UTC() fp := "xyz789" mockSvc := &mockVerificationService{ results: map[string]*domain.VerificationResult{ "j-test7": { JobID: "j-test7", TargetID: "t-haproxy1", ExpectedFingerprint: "xyz789", ActualFingerprint: fp, Verified: true, VerifiedAt: now, }, }, } handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("GET", "/api/v1/jobs/j-test7/verification", nil) w := httptest.NewRecorder() handler.GetVerificationStatus(w, httpReq) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var result domain.VerificationResult json.NewDecoder(w.Body).Decode(&result) if result.JobID != "j-test7" { t.Errorf("expected job ID j-test7, got %s", result.JobID) } if !result.Verified { t.Error("expected Verified to be true") } } func TestGetVerificationStatus_InvalidMethod(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test8/verification", nil) w := httptest.NewRecorder() handler.GetVerificationStatus(w, httpReq) if w.Code != http.StatusMethodNotAllowed { t.Errorf("expected status 405, got %d", w.Code) } } func TestVerifyDeployment_ServiceError(t *testing.T) { mockSvc := &mockVerificationService{ recordErr: ErrServiceUnavailable, } handler := NewVerificationHandler(mockSvc) req := VerifyDeploymentRequest{ TargetID: "t-nginx1", ExpectedFingerprint: "abc123", ActualFingerprint: "abc123", Verified: true, } body, _ := json.Marshal(req) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test9/verify", bytes.NewReader(body)) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusInternalServerError { t.Errorf("expected status 500, got %d", w.Code) } } func TestVerifyDeployment_EmptyBody(t *testing.T) { mockSvc := &mockVerificationService{} handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("POST", "/api/v1/jobs/j-test10/verify", bytes.NewBufferString("")) w := httptest.NewRecorder() handler.VerifyDeployment(w, httpReq) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestGetVerificationStatus_ServiceError(t *testing.T) { mockSvc := &mockVerificationService{ getErr: ErrServiceUnavailable, } handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("GET", "/api/v1/jobs/j-test11/verification", nil) w := httptest.NewRecorder() handler.GetVerificationStatus(w, httpReq) if w.Code != http.StatusInternalServerError { t.Errorf("expected status 500, got %d", w.Code) } } func TestGetVerificationStatus_NotFound(t *testing.T) { mockSvc := &mockVerificationService{ results: make(map[string]*domain.VerificationResult), } handler := NewVerificationHandler(mockSvc) httpReq := httptest.NewRequest("GET", "/api/v1/jobs/j-nonexistent/verification", nil) w := httptest.NewRecorder() handler.GetVerificationStatus(w, httpReq) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var result *domain.VerificationResult json.NewDecoder(w.Body).Decode(&result) if result != nil { t.Error("expected nil result for nonexistent job") } } var ErrServiceUnavailable = NewServiceError("service unavailable") func NewServiceError(msg string) error { return &serviceError{msg: msg} } type serviceError struct { msg string } func (e *serviceError) Error() string { return e.msg }