mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 16:31:33 +00:00
test + docs: close 12 test gaps (~250 new tests) and expand testing guide to 34 parts
Implements all P0-P2 test gaps from docs/test-gap-prompt.md: - Deployment service tests (20), target service tests (18), scheduler tests (8) - Agent binary tests (48), CSR renewal tests (8), short-lived cert tests (7) - Domain model tests (25), context cancellation tests (9), concurrency tests (7) - Handler negative-path tests (23 across 5 files) - Frontend error handling tests (86) and API client tests (7) Expands testing-guide.md from 28 to 34 parts covering certificate export, S/MIME/EKU, OCSP/DER CRL, body size limits, Apache/HAProxy connectors, and sub-CA mode. Fixes stale profile count (4->5) and updates sign-off table. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -610,3 +610,122 @@ func TestGetDiscoverySummary_MethodNotAllowed(t *testing.T) {
|
||||
t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test DismissDiscovered - service error
|
||||
func TestDismissDiscovered_ServiceError(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
DismissDiscoveredFn: func(ctx context.Context, id string) error {
|
||||
return fmt.Errorf("database error")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/discovered-certificates/dcert-1/dismiss", nil)
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
req.SetPathValue("id", "dcert-1")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.DismissDiscovered(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ClaimDiscovered - invalid body (malformed JSON)
|
||||
func TestClaimDiscovered_InvalidJSON(t *testing.T) {
|
||||
mock := &MockDiscoveryService{}
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/discovered-certificates/dcert-1/claim", bytes.NewReader([]byte("invalid json")))
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
req.SetPathValue("id", "dcert-1")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ClaimDiscovered(w, req)
|
||||
|
||||
if w.Code != http.StatusBadRequest {
|
||||
t.Errorf("expected status %d, got %d", http.StatusBadRequest, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ClaimDiscovered - method not allowed
|
||||
func TestClaimDiscovered_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockDiscoveryService{}
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/discovered-certificates/dcert-1/claim", nil)
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
req.SetPathValue("id", "dcert-1")
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ClaimDiscovered(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected status %d, got %d", http.StatusMethodNotAllowed, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListDiscovered - service error
|
||||
func TestListDiscovered_ServiceError(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
ListDiscoveredFn: func(ctx context.Context, agentID, status string, page, perPage int) ([]*domain.DiscoveredCertificate, int, error) {
|
||||
return nil, 0, fmt.Errorf("database error")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/discovered-certificates", nil)
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListDiscovered(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test ListScans - service error
|
||||
func TestListScans_ServiceError(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
ListScansFn: func(ctx context.Context, agentID string, page, perPage int) ([]*domain.DiscoveryScan, int, error) {
|
||||
return nil, 0, fmt.Errorf("database error")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/discovery-scans", nil)
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.ListScans(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetDiscoverySummary - service error
|
||||
func TestGetDiscoverySummary_ServiceError(t *testing.T) {
|
||||
mock := &MockDiscoveryService{
|
||||
GetDiscoverySummaryFn: func(ctx context.Context) (map[string]int, error) {
|
||||
return nil, fmt.Errorf("database error")
|
||||
},
|
||||
}
|
||||
|
||||
handler := NewDiscoveryHandler(mock)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/discovery-summary", nil)
|
||||
req = req.WithContext(discoveryContextWithRequestID())
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
handler.GetDiscoverySummary(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected status %d, got %d", http.StatusInternalServerError, w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -396,3 +396,49 @@ func TestASN1EncodeLength(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestESTCSRAttrs_ServiceError(t *testing.T) {
|
||||
svc := &mockESTService{
|
||||
CSRAttrsErr: errors.New("service error"),
|
||||
}
|
||||
h := NewESTHandler(svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/.well-known/est/csrattrs", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.CSRAttrs(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestESTSimpleReEnroll_ServiceError(t *testing.T) {
|
||||
csrPEM := generateTestCSRPEM(t)
|
||||
svc := &mockESTService{
|
||||
EnrollErr: errors.New("renewal failed"),
|
||||
}
|
||||
h := NewESTHandler(svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/.well-known/est/simplereenroll", strings.NewReader(csrPEM))
|
||||
w := httptest.NewRecorder()
|
||||
h.SimpleReEnroll(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestESTCACerts_UnableToGetCerts(t *testing.T) {
|
||||
svc := &mockESTService{
|
||||
CACertErr: errors.New("CA unavailable"),
|
||||
}
|
||||
h := NewESTHandler(svc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/.well-known/est/cacerts", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.CACerts(w, req)
|
||||
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"github.com/shankar0123/certctl/internal/service"
|
||||
)
|
||||
|
||||
// Add context import was already there — verify import is present above
|
||||
|
||||
// MockExportService is a mock implementation of ExportService interface.
|
||||
type MockExportService struct {
|
||||
ExportPEMFn func(ctx context.Context, certID string) (*service.ExportPEMResult, error)
|
||||
@@ -280,3 +282,38 @@ func TestExtractCertIDFromExportPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportPKCS12_InvalidJSON(t *testing.T) {
|
||||
mockSvc := &MockExportService{
|
||||
ExportPKCS12Fn: func(_ context.Context, _ string, password string) ([]byte, error) {
|
||||
// Invalid JSON is silently ignored, defaults to empty password
|
||||
if password != "" {
|
||||
t.Errorf("expected empty password (invalid JSON ignored), got %s", password)
|
||||
}
|
||||
return []byte{0x30}, nil
|
||||
},
|
||||
}
|
||||
h := NewExportHandler(mockSvc)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/certificates/mc-test-1/export/pkcs12", strings.NewReader(`{"invalid json`))
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ExportPKCS12(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200 (invalid JSON ignored), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExportPEM_MethodNotAllowedDelete(t *testing.T) {
|
||||
h := NewExportHandler(&MockExportService{})
|
||||
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/certificates/mc-test-1/export/pem", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
h.ExportPEM(w, req)
|
||||
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Fatalf("expected 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,3 +316,115 @@ func TestGetPrometheusMetrics_ZeroValues(t *testing.T) {
|
||||
func containsLine(text, substr string) bool {
|
||||
return strings.Contains(text, substr)
|
||||
}
|
||||
|
||||
// Test GetCertificatesByStatus - method not allowed
|
||||
func TestGetCertificatesByStatus_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockStatsService{}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/stats/certificates-by-status", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetCertificatesByStatus(w, req)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetCertificatesByStatus - service error
|
||||
func TestGetCertificatesByStatus_ServiceError(t *testing.T) {
|
||||
mock := &MockStatsService{
|
||||
GetCertificatesByStatusFn: func(ctx context.Context) (interface{}, error) {
|
||||
return nil, fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats/certificates-by-status", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetCertificatesByStatus(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetExpirationTimeline - method not allowed
|
||||
func TestGetExpirationTimeline_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockStatsService{}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/stats/expiration-timeline", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetExpirationTimeline(w, req)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetExpirationTimeline - service error
|
||||
func TestGetExpirationTimeline_ServiceError(t *testing.T) {
|
||||
mock := &MockStatsService{
|
||||
GetExpirationTimelineFn: func(ctx context.Context, days int) (interface{}, error) {
|
||||
return nil, fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats/expiration-timeline?days=30", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetExpirationTimeline(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetJobTrends - method not allowed
|
||||
func TestGetJobTrends_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockStatsService{}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/stats/job-trends", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetJobTrends(w, req)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetJobTrends - service error
|
||||
func TestGetJobTrends_ServiceError(t *testing.T) {
|
||||
mock := &MockStatsService{
|
||||
GetJobStatsFn: func(ctx context.Context, days int) (interface{}, error) {
|
||||
return nil, fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats/job-trends?days=14", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetJobTrends(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetIssuanceRate - method not allowed
|
||||
func TestGetIssuanceRate_MethodNotAllowed(t *testing.T) {
|
||||
mock := &MockStatsService{}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/stats/issuance-rate", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetIssuanceRate(w, req)
|
||||
if w.Code != http.StatusMethodNotAllowed {
|
||||
t.Errorf("expected 405, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// Test GetIssuanceRate - service error
|
||||
func TestGetIssuanceRate_ServiceError(t *testing.T) {
|
||||
mock := &MockStatsService{
|
||||
GetIssuanceRateFn: func(ctx context.Context, days int) (interface{}, error) {
|
||||
return nil, fmt.Errorf("db error")
|
||||
},
|
||||
}
|
||||
h := NewStatsHandler(mock)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/stats/issuance-rate?days=7", nil)
|
||||
w := httptest.NewRecorder()
|
||||
h.GetIssuanceRate(w, req)
|
||||
if w.Code != http.StatusInternalServerError {
|
||||
t.Errorf("expected 500, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -249,6 +249,58 @@ func TestVerifyDeployment_ServiceError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user