mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 21:11:30 +00:00
Implement M4: comprehensive test coverage with 120 tests
Service layer (63 tests): certificate, agent, audit, job, notification, policy, and renewal services with mock repositories covering threshold alerting, deduplication, status transitions, and job processing. Handler layer (46 tests): certificate and agent HTTP handlers using httptest with mock service interfaces, covering success/error paths, pagination, JSON marshaling, and path parameter extraction. Integration (11 subtests): end-to-end certificate lifecycle test exercising real services and Local CA issuer through HTTP API — create cert, trigger renewal, process jobs, register agent, heartbeat, verify audit trail. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,869 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// MockAgentService is a mock implementation of AgentService interface.
|
||||
type MockAgentService struct {
|
||||
ListAgentsFn func(page, perPage int) ([]domain.Agent, int64, error)
|
||||
GetAgentFn func(id string) (*domain.Agent, error)
|
||||
RegisterAgentFn func(agent domain.Agent) (*domain.Agent, error)
|
||||
HeartbeatFn func(agentID string) error
|
||||
CSRSubmitFn func(agentID string, csrPEM string) (string, error)
|
||||
CSRSubmitForCertFn func(agentID string, certID string, csrPEM string) (string, error)
|
||||
CertificatePickupFn func(agentID, certID string) (string, error)
|
||||
GetWorkFn func(agentID string) ([]domain.Job, error)
|
||||
GetWorkWithTargetsFn func(agentID string) ([]domain.WorkItem, error)
|
||||
UpdateJobStatusFn func(agentID string, jobID string, status string, errMsg string) error
|
||||
}
|
||||
|
||||
func (m *MockAgentService) ListAgents(page, perPage int) ([]domain.Agent, int64, error) {
|
||||
if m.ListAgentsFn != nil {
|
||||
return m.ListAgentsFn(page, perPage)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) GetAgent(id string) (*domain.Agent, error) {
|
||||
if m.GetAgentFn != nil {
|
||||
return m.GetAgentFn(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) RegisterAgent(agent domain.Agent) (*domain.Agent, error) {
|
||||
if m.RegisterAgentFn != nil {
|
||||
return m.RegisterAgentFn(agent)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) Heartbeat(agentID string) error {
|
||||
if m.HeartbeatFn != nil {
|
||||
return m.HeartbeatFn(agentID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) CSRSubmit(agentID string, csrPEM string) (string, error) {
|
||||
if m.CSRSubmitFn != nil {
|
||||
return m.CSRSubmitFn(agentID, csrPEM)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) CSRSubmitForCert(agentID string, certID string, csrPEM string) (string, error) {
|
||||
if m.CSRSubmitForCertFn != nil {
|
||||
return m.CSRSubmitForCertFn(agentID, certID, csrPEM)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) CertificatePickup(agentID, certID string) (string, error) {
|
||||
if m.CertificatePickupFn != nil {
|
||||
return m.CertificatePickupFn(agentID, certID)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) GetWork(agentID string) ([]domain.Job, error) {
|
||||
if m.GetWorkFn != nil {
|
||||
return m.GetWorkFn(agentID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) GetWorkWithTargets(agentID string) ([]domain.WorkItem, error) {
|
||||
if m.GetWorkWithTargetsFn != nil {
|
||||
return m.GetWorkWithTargetsFn(agentID)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockAgentService) UpdateJobStatus(agentID string, jobID string, status string, errMsg string) error {
|
||||
if m.UpdateJobStatusFn != nil {
|
||||
return m.UpdateJobStatusFn(agentID, jobID, status, errMsg)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Test ListAgents - success case
|
||||
func TestListAgents_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
agent1 := domain.Agent{
|
||||
ID: "a-prod-001",
|
||||
Name: "Production Agent",
|
||||
Hostname: "prod-server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
LastHeartbeatAt: &now,
|
||||
RegisteredAt: now,
|
||||
}
|
||||
agent2 := domain.Agent{
|
||||
ID: "a-prod-002",
|
||||
Name: "API Agent",
|
||||
Hostname: "api-server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
LastHeartbeatAt: &now,
|
||||
RegisteredAt: now,
|
||||
}
|
||||
|
||||
mock := &MockAgentService{
|
||||
ListAgentsFn: func(page, perPage int) ([]domain.Agent, int64, error) {
|
||||
if page == 1 && perPage == 50 {
|
||||
return []domain.Agent{agent1, agent2}, 2, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents?page=1&per_page=50", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListAgents(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response PagedResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Total != 2 {
|
||||
t.Errorf("expected total 2, got %d", response.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListAgents - method not allowed
|
||||
func TestListAgents_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListAgents(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListAgents - service error
|
||||
func TestListAgents_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
ListAgentsFn: func(page, perPage int) ([]domain.Agent, int64, error) {
|
||||
return nil, 0, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListAgents(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetAgent - success case
|
||||
func TestGetAgent_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "a-prod-001",
|
||||
Name: "Production Agent",
|
||||
Hostname: "prod-server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
LastHeartbeatAt: &now,
|
||||
RegisteredAt: now,
|
||||
}
|
||||
|
||||
mock := &MockAgentService{
|
||||
GetAgentFn: func(id string) (*domain.Agent, error) {
|
||||
if id == "a-prod-001" {
|
||||
return agent, nil
|
||||
}
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response domain.Agent
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.ID != "a-prod-001" {
|
||||
t.Errorf("expected ID a-prod-001, got %s", response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetAgent - not found
|
||||
func TestGetAgent_NotFound(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
GetAgentFn: func(id string) (*domain.Agent, error) {
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/nonexistent", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test RegisterAgent - success case
|
||||
func TestRegisterAgent_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
registered := &domain.Agent{
|
||||
ID: "a-prod-001",
|
||||
Name: "Production Agent",
|
||||
Hostname: "prod-server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
}
|
||||
|
||||
mock := &MockAgentService{
|
||||
RegisterAgentFn: func(agent domain.Agent) (*domain.Agent, error) {
|
||||
return registered, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
agentBody := domain.Agent{
|
||||
Name: "Production Agent",
|
||||
Hostname: "prod-server-01",
|
||||
}
|
||||
body, _ := json.Marshal(agentBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.RegisterAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected status %d, got %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var response domain.Agent
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.ID != "a-prod-001" {
|
||||
t.Errorf("expected ID a-prod-001, got %s", response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test RegisterAgent - invalid body
|
||||
func TestRegisterAgent_InvalidBody(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents", bytes.NewReader([]byte("invalid json")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.RegisterAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Heartbeat - success case
|
||||
func TestHeartbeat_Success(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
HeartbeatFn: func(agentID string) error {
|
||||
if agentID == "a-prod-001" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/heartbeat", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.Heartbeat(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["status"] != "heartbeat_recorded" {
|
||||
t.Errorf("expected status 'heartbeat_recorded', got %s", response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test Heartbeat - service error
|
||||
func TestHeartbeat_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
HeartbeatFn: func(agentID string) error {
|
||||
return ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/heartbeat", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.Heartbeat(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCSRSubmit - with certificate_id
|
||||
func TestAgentCSRSubmit_WithCertificateID(t *testing.T) {
|
||||
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
|
||||
|
||||
mock := &MockAgentService{
|
||||
CSRSubmitForCertFn: func(agentID string, certID string, csrPEM string) (string, error) {
|
||||
if agentID == "a-prod-001" && certID == "mc-prod-001" {
|
||||
return "csr_submitted", nil
|
||||
}
|
||||
return "", ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"csr_pem": csrPEM,
|
||||
"certificate_id": "mc-prod-001",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/csr", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCSRSubmit(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Errorf("expected status %d, got %d", http.StatusAccepted, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["status"] != "csr_submitted" {
|
||||
t.Errorf("expected status 'csr_submitted', got %s", response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCSRSubmit - without certificate_id
|
||||
func TestAgentCSRSubmit_WithoutCertificateID(t *testing.T) {
|
||||
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----"
|
||||
|
||||
mock := &MockAgentService{
|
||||
CSRSubmitFn: func(agentID string, csrPEM string) (string, error) {
|
||||
if agentID == "a-prod-001" {
|
||||
return "csr_submitted", nil
|
||||
}
|
||||
return "", ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"csr_pem": csrPEM,
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/csr", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCSRSubmit(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Errorf("expected status %d, got %d", http.StatusAccepted, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCSRSubmit - missing CSR PEM
|
||||
func TestAgentCSRSubmit_MissingCSRPEM(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"certificate_id": "mc-prod-001",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/csr", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCSRSubmit(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCSRSubmit - invalid body
|
||||
func TestAgentCSRSubmit_InvalidBody(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/csr", bytes.NewReader([]byte("invalid")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCSRSubmit(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCertificatePickup - success case
|
||||
func TestAgentCertificatePickup_Success(t *testing.T) {
|
||||
certPEM := "-----BEGIN CERTIFICATE-----\nMIIC...\n-----END CERTIFICATE-----"
|
||||
|
||||
mock := &MockAgentService{
|
||||
CertificatePickupFn: func(agentID, certID string) (string, error) {
|
||||
if agentID == "a-prod-001" && certID == "mc-prod-001" {
|
||||
return certPEM, nil
|
||||
}
|
||||
return "", ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
// Path structure: /api/v1/agents/{agent_id}/certificates/{cert_id}
|
||||
// After trim and split: parts[0]="agent_id", parts[1]="certificates", parts[2]="cert_id", parts[3]=""
|
||||
// Note: handler checks len(parts) < 4, so we need the trailing slash
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001/certificates/mc-prod-001/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCertificatePickup(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d (body: %s)", http.StatusOK, w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["certificate_pem"] != certPEM {
|
||||
t.Errorf("expected cert PEM %s, got %s", certPEM, response["certificate_pem"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCertificatePickup - not found
|
||||
func TestAgentCertificatePickup_NotFound(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
CertificatePickupFn: func(agentID, certID string) (string, error) {
|
||||
return "", ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001/certificates/nonexistent/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCertificatePickup(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status %d, got %d (body: %s)", http.StatusNotFound, w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentGetWork - success with items
|
||||
func TestAgentGetWork_Success(t *testing.T) {
|
||||
workItem := domain.WorkItem{
|
||||
ID: "j-deploy-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "mc-prod-001",
|
||||
TargetID: stringPtr("t-nginx-001"),
|
||||
TargetType: "NGINX",
|
||||
Status: domain.JobStatusPending,
|
||||
}
|
||||
|
||||
mock := &MockAgentService{
|
||||
GetWorkWithTargetsFn: func(agentID string) ([]domain.WorkItem, error) {
|
||||
if agentID == "a-prod-001" {
|
||||
return []domain.WorkItem{workItem}, nil
|
||||
}
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001/work", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentGetWork(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["count"] != float64(1) {
|
||||
t.Errorf("expected count 1, got %v", response["count"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentGetWork - no work items
|
||||
func TestAgentGetWork_NoItems(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
GetWorkWithTargetsFn: func(agentID string) ([]domain.WorkItem, error) {
|
||||
return nil, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001/work", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentGetWork(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["count"] != float64(0) {
|
||||
t.Errorf("expected count 0, got %v", response["count"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentGetWork - service error
|
||||
func TestAgentGetWork_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
GetWorkWithTargetsFn: func(agentID string) ([]domain.WorkItem, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/a-prod-001/work", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentGetWork(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentReportJobStatus - success case
|
||||
func TestAgentReportJobStatus_Success(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
UpdateJobStatusFn: func(agentID string, jobID string, status string, errMsg string) error {
|
||||
if agentID == "a-prod-001" && jobID == "j-deploy-001" && status == "Completed" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
statusReq := map[string]string{
|
||||
"status": "Completed",
|
||||
}
|
||||
body, _ := json.Marshal(statusReq)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/jobs/j-deploy-001/status", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentReportJobStatus(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["status"] != "updated" {
|
||||
t.Errorf("expected status 'updated', got %s", response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentReportJobStatus - with error message
|
||||
func TestAgentReportJobStatus_WithError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
UpdateJobStatusFn: func(agentID string, jobID string, status string, errMsg string) error {
|
||||
if agentID == "a-prod-001" && jobID == "j-deploy-001" && status == "Failed" && errMsg == "timeout" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
statusReq := map[string]string{
|
||||
"status": "Failed",
|
||||
"error": "timeout",
|
||||
}
|
||||
body, _ := json.Marshal(statusReq)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/jobs/j-deploy-001/status", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentReportJobStatus(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentReportJobStatus - missing status
|
||||
func TestAgentReportJobStatus_MissingStatus(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
statusReq := map[string]string{}
|
||||
body, _ := json.Marshal(statusReq)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/jobs/j-deploy-001/status", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentReportJobStatus(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentReportJobStatus - invalid body
|
||||
func TestAgentReportJobStatus_InvalidBody(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/jobs/j-deploy-001/status", bytes.NewReader([]byte("invalid")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentReportJobStatus(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListAgents - invalid pagination parameters
|
||||
func TestListAgents_InvalidPagination(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
ListAgentsFn: func(page, perPage int) ([]domain.Agent, int64, error) {
|
||||
// Should default to page=1, perPage=50 if invalid
|
||||
if page == 1 && perPage == 50 {
|
||||
return []domain.Agent{}, 0, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents?page=invalid&per_page=invalid", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListAgents(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetAgent - empty ID
|
||||
func TestGetAgent_EmptyID(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/agents/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test RegisterAgent - service error
|
||||
func TestRegisterAgent_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
RegisterAgentFn: func(agent domain.Agent) (*domain.Agent, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
agentBody := domain.Agent{
|
||||
Name: "Production Agent",
|
||||
Hostname: "prod-server-01",
|
||||
}
|
||||
body, _ := json.Marshal(agentBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.RegisterAgent(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test Heartbeat - empty agent ID
|
||||
func TestHeartbeat_EmptyAgentID(t *testing.T) {
|
||||
mock := &MockAgentService{}
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents//heartbeat", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.Heartbeat(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentCSRSubmit - service error
|
||||
func TestAgentCSRSubmit_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
CSRSubmitFn: func(agentID string, csrPEM string) (string, error) {
|
||||
return "", ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
reqBody := map[string]string{
|
||||
"csr_pem": "-----BEGIN CERTIFICATE REQUEST-----\nMIIC...\n-----END CERTIFICATE REQUEST-----",
|
||||
}
|
||||
body, _ := json.Marshal(reqBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/csr", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentCSRSubmit(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test AgentReportJobStatus - service error
|
||||
func TestAgentReportJobStatus_ServiceError(t *testing.T) {
|
||||
mock := &MockAgentService{
|
||||
UpdateJobStatusFn: func(agentID string, jobID string, status string, errMsg string) error {
|
||||
return ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewAgentHandler(mock)
|
||||
|
||||
statusReq := map[string]string{
|
||||
"status": "Completed",
|
||||
}
|
||||
body, _ := json.Marshal(statusReq)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/agents/a-prod-001/jobs/j-deploy-001/status", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.AgentReportJobStatus(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to create a string pointer
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
@@ -0,0 +1,704 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/api/middleware"
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// MockCertificateService is a mock implementation of CertificateService interface.
|
||||
type MockCertificateService struct {
|
||||
ListCertificatesFn func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error)
|
||||
GetCertificateFn func(id string) (*domain.ManagedCertificate, error)
|
||||
CreateCertificateFn func(cert domain.ManagedCertificate) (*domain.ManagedCertificate, error)
|
||||
UpdateCertificateFn func(id string, cert domain.ManagedCertificate) (*domain.ManagedCertificate, error)
|
||||
ArchiveCertificateFn func(id string) error
|
||||
GetCertificateVersionsFn func(certID string, page, perPage int) ([]domain.CertificateVersion, int64, error)
|
||||
TriggerRenewalFn func(certID string) error
|
||||
TriggerDeploymentFn func(certID string, targetID string) error
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) ListCertificates(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
if m.ListCertificatesFn != nil {
|
||||
return m.ListCertificatesFn(status, environment, ownerID, teamID, issuerID, page, perPage)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) GetCertificate(id string) (*domain.ManagedCertificate, error) {
|
||||
if m.GetCertificateFn != nil {
|
||||
return m.GetCertificateFn(id)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) CreateCertificate(cert domain.ManagedCertificate) (*domain.ManagedCertificate, error) {
|
||||
if m.CreateCertificateFn != nil {
|
||||
return m.CreateCertificateFn(cert)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) UpdateCertificate(id string, cert domain.ManagedCertificate) (*domain.ManagedCertificate, error) {
|
||||
if m.UpdateCertificateFn != nil {
|
||||
return m.UpdateCertificateFn(id, cert)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) ArchiveCertificate(id string) error {
|
||||
if m.ArchiveCertificateFn != nil {
|
||||
return m.ArchiveCertificateFn(id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) GetCertificateVersions(certID string, page, perPage int) ([]domain.CertificateVersion, int64, error) {
|
||||
if m.GetCertificateVersionsFn != nil {
|
||||
return m.GetCertificateVersionsFn(certID, page, perPage)
|
||||
}
|
||||
return nil, 0, nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) TriggerRenewal(certID string) error {
|
||||
if m.TriggerRenewalFn != nil {
|
||||
return m.TriggerRenewalFn(certID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockCertificateService) TriggerDeployment(certID string, targetID string) error {
|
||||
if m.TriggerDeploymentFn != nil {
|
||||
return m.TriggerDeploymentFn(certID, targetID)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Helper function to create context with request ID.
|
||||
func contextWithRequestID() context.Context {
|
||||
return context.WithValue(context.Background(), middleware.RequestIDKey{}, "test-request-id-123")
|
||||
}
|
||||
|
||||
// Test ListCertificates - success case
|
||||
func TestListCertificates_Success(t *testing.T) {
|
||||
cert1 := domain.ManagedCertificate{
|
||||
ID: "mc-prod-001",
|
||||
Name: "Production Cert",
|
||||
CommonName: "example.com",
|
||||
Status: domain.CertificateStatusActive,
|
||||
Environment: "prod",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
cert2 := domain.ManagedCertificate{
|
||||
ID: "mc-prod-002",
|
||||
Name: "API Cert",
|
||||
CommonName: "api.example.com",
|
||||
Status: domain.CertificateStatusActive,
|
||||
Environment: "prod",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock := &MockCertificateService{
|
||||
ListCertificatesFn: func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
if page == 1 && perPage == 50 {
|
||||
return []domain.ManagedCertificate{cert1, cert2}, 2, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates?page=1&per_page=50", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response PagedResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Total != 2 {
|
||||
t.Errorf("expected total 2, got %d", response.Total)
|
||||
}
|
||||
if response.Page != 1 {
|
||||
t.Errorf("expected page 1, got %d", response.Page)
|
||||
}
|
||||
if response.PerPage != 50 {
|
||||
t.Errorf("expected per_page 50, got %d", response.PerPage)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListCertificates - with filters
|
||||
func TestListCertificates_WithFilters(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ListCertificatesFn: func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
if status == "Active" && environment == "prod" {
|
||||
return []domain.ManagedCertificate{}, 0, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates?status=Active&environment=prod&page=1&per_page=25", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListCertificates - invalid method
|
||||
func TestListCertificates_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockCertificateService{}
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListCertificates - service error
|
||||
func TestListCertificates_ServiceError(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ListCertificatesFn: func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
return nil, 0, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificate - success case
|
||||
func TestGetCertificate_Success(t *testing.T) {
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-prod-001",
|
||||
Name: "Production Cert",
|
||||
CommonName: "example.com",
|
||||
Status: domain.CertificateStatusActive,
|
||||
Environment: "prod",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock := &MockCertificateService{
|
||||
GetCertificateFn: func(id string) (*domain.ManagedCertificate, error) {
|
||||
if id == "mc-prod-001" {
|
||||
return cert, nil
|
||||
}
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates/mc-prod-001", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response domain.ManagedCertificate
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.ID != "mc-prod-001" {
|
||||
t.Errorf("expected ID mc-prod-001, got %s", response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificate - not found
|
||||
func TestGetCertificate_NotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GetCertificateFn: func(id string) (*domain.ManagedCertificate, error) {
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates/nonexistent", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificate - empty ID
|
||||
func TestGetCertificate_EmptyID(t *testing.T) {
|
||||
mock := &MockCertificateService{}
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates/", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test CreateCertificate - success case
|
||||
func TestCreateCertificate_Success(t *testing.T) {
|
||||
now := time.Now()
|
||||
created := &domain.ManagedCertificate{
|
||||
ID: "mc-prod-001",
|
||||
Name: "Production Cert",
|
||||
CommonName: "example.com",
|
||||
Status: domain.CertificateStatusPending,
|
||||
Environment: "prod",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
mock := &MockCertificateService{
|
||||
CreateCertificateFn: func(cert domain.ManagedCertificate) (*domain.ManagedCertificate, error) {
|
||||
return created, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
certBody := domain.ManagedCertificate{
|
||||
Name: "Production Cert",
|
||||
CommonName: "example.com",
|
||||
}
|
||||
body, _ := json.Marshal(certBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusCreated {
|
||||
t.Errorf("expected status %d, got %d", http.StatusCreated, w.Code)
|
||||
}
|
||||
|
||||
var response domain.ManagedCertificate
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.ID != "mc-prod-001" {
|
||||
t.Errorf("expected ID mc-prod-001, got %s", response.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// Test CreateCertificate - invalid request body
|
||||
func TestCreateCertificate_InvalidBody(t *testing.T) {
|
||||
mock := &MockCertificateService{}
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates", bytes.NewReader([]byte("invalid json")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test CreateCertificate - service error
|
||||
func TestCreateCertificate_ServiceError(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
CreateCertificateFn: func(cert domain.ManagedCertificate) (*domain.ManagedCertificate, error) {
|
||||
return nil, ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
certBody := domain.ManagedCertificate{
|
||||
Name: "Production Cert",
|
||||
CommonName: "example.com",
|
||||
}
|
||||
body, _ := json.Marshal(certBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.CreateCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test UpdateCertificate - success case
|
||||
func TestUpdateCertificate_Success(t *testing.T) {
|
||||
updated := &domain.ManagedCertificate{
|
||||
ID: "mc-prod-001",
|
||||
Name: "Updated Cert",
|
||||
CommonName: "example.com",
|
||||
Status: domain.CertificateStatusActive,
|
||||
Environment: "prod",
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock := &MockCertificateService{
|
||||
UpdateCertificateFn: func(id string, cert domain.ManagedCertificate) (*domain.ManagedCertificate, error) {
|
||||
if id == "mc-prod-001" {
|
||||
return updated, nil
|
||||
}
|
||||
return nil, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
certBody := domain.ManagedCertificate{
|
||||
Name: "Updated Cert",
|
||||
}
|
||||
body, _ := json.Marshal(certBody)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/certificates/mc-prod-001", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response domain.ManagedCertificate
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Name != "Updated Cert" {
|
||||
t.Errorf("expected name 'Updated Cert', got %s", response.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Test UpdateCertificate - invalid body
|
||||
func TestUpdateCertificate_InvalidBody(t *testing.T) {
|
||||
mock := &MockCertificateService{}
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPut, "/api/v1/certificates/mc-prod-001", bytes.NewReader([]byte("invalid")))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.UpdateCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ArchiveCertificate - success case
|
||||
func TestArchiveCertificate_Success(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ArchiveCertificateFn: func(id string) error {
|
||||
if id == "mc-prod-001" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/certificates/mc-prod-001", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ArchiveCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusNoContent {
|
||||
t.Errorf("expected status %d, got %d", http.StatusNoContent, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ArchiveCertificate - not found
|
||||
func TestArchiveCertificate_NotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ArchiveCertificateFn: func(id string) error {
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/certificates/nonexistent", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ArchiveCertificate(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificateVersions - success case
|
||||
func TestGetCertificateVersions_Success(t *testing.T) {
|
||||
ver1 := domain.CertificateVersion{
|
||||
ID: "cv-001",
|
||||
CertificateID: "mc-prod-001",
|
||||
SerialNumber: "ABC123",
|
||||
FingerprintSHA256: "abc123...",
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(0, 0, 365),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
mock := &MockCertificateService{
|
||||
GetCertificateVersionsFn: func(certID string, page, perPage int) ([]domain.CertificateVersion, int64, error) {
|
||||
if certID == "mc-prod-001" {
|
||||
return []domain.CertificateVersion{ver1}, 1, nil
|
||||
}
|
||||
return nil, 0, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates/mc-prod-001/versions?page=1&per_page=50", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetCertificateVersions(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
|
||||
var response PagedResponse
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response.Total != 1 {
|
||||
t.Errorf("expected total 1, got %d", response.Total)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificateVersions - not found
|
||||
func TestGetCertificateVersions_NotFound(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
GetCertificateVersionsFn: func(certID string, page, perPage int) ([]domain.CertificateVersion, int64, error) {
|
||||
return nil, 0, ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates/nonexistent/versions", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetCertificateVersions(w, req)
|
||||
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Errorf("expected status %d, got %d", http.StatusNotFound, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test TriggerRenewal - success case
|
||||
func TestTriggerRenewal_Success(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
TriggerRenewalFn: func(certID string) error {
|
||||
if certID == "mc-prod-001" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-prod-001/renew", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.TriggerRenewal(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Errorf("expected status %d, got %d", http.StatusAccepted, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["status"] != "renewal_triggered" {
|
||||
t.Errorf("expected status 'renewal_triggered', got %s", response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test TriggerRenewal - service error
|
||||
func TestTriggerRenewal_ServiceError(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
TriggerRenewalFn: func(certID string) error {
|
||||
return ErrMockServiceFailed
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-prod-001/renew", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.TriggerRenewal(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test TriggerDeployment - success case
|
||||
func TestTriggerDeployment_Success(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
TriggerDeploymentFn: func(certID string, targetID string) error {
|
||||
if certID == "mc-prod-001" {
|
||||
return nil
|
||||
}
|
||||
return ErrMockNotFound
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
|
||||
deployReq := map[string]string{"target_id": "t-nginx-001"}
|
||||
body, _ := json.Marshal(deployReq)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-prod-001/deploy", bytes.NewReader(body))
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.TriggerDeployment(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Errorf("expected status %d, got %d", http.StatusAccepted, w.Code)
|
||||
}
|
||||
|
||||
var response map[string]string
|
||||
if err := json.NewDecoder(w.Body).Decode(&response); err != nil {
|
||||
t.Fatalf("failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if response["status"] != "deployment_triggered" {
|
||||
t.Errorf("expected status 'deployment_triggered', got %s", response["status"])
|
||||
}
|
||||
}
|
||||
|
||||
// Test TriggerDeployment - without target ID
|
||||
func TestTriggerDeployment_NoTargetID(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
TriggerDeploymentFn: func(certID string, targetID string) error {
|
||||
// Should accept empty targetID (deploy to all)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-prod-001/deploy", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.TriggerDeployment(w, req)
|
||||
|
||||
if w.Code != http.StatusAccepted {
|
||||
t.Errorf("expected status %d, got %d", http.StatusAccepted, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListCertificates - invalid page parameter
|
||||
func TestListCertificates_InvalidPageParam(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ListCertificatesFn: func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
// Should default to page 1
|
||||
if page == 1 {
|
||||
return []domain.ManagedCertificate{}, 0, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates?page=invalid&per_page=50", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListCertificates - per_page exceeds max
|
||||
func TestListCertificates_PerPageExceedsMax(t *testing.T) {
|
||||
mock := &MockCertificateService{
|
||||
ListCertificatesFn: func(status, environment, ownerID, teamID, issuerID string, page, perPage int) ([]domain.ManagedCertificate, int64, error) {
|
||||
// Should cap perPage at 500
|
||||
if perPage == 50 { // defaults to 50 if > 500
|
||||
return []domain.ManagedCertificate{}, 0, nil
|
||||
}
|
||||
return nil, 0, nil
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewCertificateHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/certificates?per_page=1000", nil)
|
||||
req = req.WithContext(contextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListCertificates(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected status %d, got %d", http.StatusOK, w.Code)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package handler
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// Mock errors for testing
|
||||
ErrMockServiceFailed = errors.New("mock service error")
|
||||
ErrMockNotFound = errors.New("mock not found error")
|
||||
ErrMockUnauthorized = errors.New("mock unauthorized error")
|
||||
ErrMockConflict = errors.New("mock conflict error")
|
||||
)
|
||||
Reference in New Issue
Block a user