mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 20:11:31 +00:00
chore: remove obsolete testing.md and test-gap-prompt.md
These files are superseded by the comprehensive 34-section docs/testing-guide.md. Removing to avoid confusion. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,480 +0,0 @@
|
||||
# certctl Test Gap Attack Prompt
|
||||
|
||||
**Purpose:** Self-contained prompt for a future Claude session to systematically close all identified test gaps. Copy this entire document into a new session along with CLAUDE.md.
|
||||
|
||||
**Estimated effort:** 250-350 new test functions across 12-15 new/modified test files.
|
||||
|
||||
---
|
||||
|
||||
## Context
|
||||
|
||||
You are working on certctl, a self-hosted certificate lifecycle platform. The project has ~1100 tests but a comprehensive audit identified 12 gaps across 4 priority tiers. Your job is to close ALL of them in order (P0 first, then P1, then P2). After each file you create or modify, run the specific test file to verify it passes, then run `go vet ./...` to catch issues early.
|
||||
|
||||
**Key conventions:**
|
||||
- Package-level tests (e.g., `package service` not `package service_test`) so you can access unexported fields
|
||||
- Mock repositories use function-field injection pattern (see `internal/service/testutil_test.go` for all mocks)
|
||||
- Mocks available: `mockCertRepo`, `mockJobRepo`, `mockNotifRepo`, `mockAuditRepo`, `mockPolicyRepo`, `mockRenewalPolicyRepo`, `mockAgentRepo`, `mockTargetRepo`, `mockIssuerConnector`, `mockIssuerRepository`, `mockRevocationRepo`, `mockNotifier`
|
||||
- Constructor helpers: `newMockCertificateRepository()`, `newMockJobRepository()`, etc.
|
||||
- Test naming: `TestServiceName_MethodName_Scenario` (e.g., `TestDeploymentService_CreateDeploymentJobs_Success`)
|
||||
- All tests use `context.Background()` unless testing cancellation
|
||||
- The `generateID(prefix)` function exists in the service package for creating IDs
|
||||
|
||||
---
|
||||
|
||||
## P0-1: `internal/service/deployment_test.go` (NEW FILE)
|
||||
|
||||
**File to test:** `internal/service/deployment.go`
|
||||
|
||||
Create `internal/service/deployment_test.go` in `package service`.
|
||||
|
||||
### DeploymentService struct dependencies:
|
||||
```go
|
||||
type DeploymentService struct {
|
||||
jobRepo repository.JobRepository // mockJobRepo
|
||||
targetRepo repository.TargetRepository // mockTargetRepo
|
||||
agentRepo repository.AgentRepository // mockAgentRepo
|
||||
certRepo repository.CertificateRepository // mockCertRepo
|
||||
auditService *AuditService // real AuditService with mockAuditRepo
|
||||
notificationSvc *NotificationService // real NotificationService with mockNotifRepo + mockNotifier
|
||||
}
|
||||
```
|
||||
|
||||
### Setup helper:
|
||||
```go
|
||||
func newTestDeploymentService() (*DeploymentService, *mockJobRepo, *mockTargetRepo, *mockAgentRepo, *mockCertRepo, *mockAuditRepo) {
|
||||
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, auditSvc)
|
||||
notifSvc.RegisterNotifier(notifier)
|
||||
|
||||
svc := NewDeploymentService(jobRepo, targetRepo, agentRepo, certRepo, auditSvc, notifSvc)
|
||||
return svc, jobRepo, targetRepo, agentRepo, certRepo, auditRepo
|
||||
}
|
||||
```
|
||||
|
||||
### Required tests (~20 functions):
|
||||
|
||||
**CreateDeploymentJobs:**
|
||||
1. `TestDeploymentService_CreateDeploymentJobs_Success` — 2 targets for cert, verify 2 jobs created with correct CertificateID, Type=Deployment, Status=Pending, TargetID set
|
||||
2. `TestDeploymentService_CreateDeploymentJobs_NoTargets` — empty targets list, expect error "no targets found"
|
||||
3. `TestDeploymentService_CreateDeploymentJobs_TargetListError` — targetRepo.ListByCertErr set, expect wrapped error
|
||||
4. `TestDeploymentService_CreateDeploymentJobs_AllJobCreationsFail` — jobRepo.CreateErr set, expect error "failed to create any deployment jobs"
|
||||
5. `TestDeploymentService_CreateDeploymentJobs_PartialFailure` — first job create fails (use a counter-based mock or accept that current mock fails all), verify at least error handling
|
||||
6. `TestDeploymentService_CreateDeploymentJobs_AuditEvent` — verify auditRepo.Events contains "deployment_jobs_created" event with target_count and job_count
|
||||
|
||||
**ProcessDeploymentJob:**
|
||||
7. `TestDeploymentService_ProcessDeploymentJob_Success` — job with TargetID, target has AgentID, agent has recent heartbeat. Verify job status updated to Running, audit event recorded
|
||||
8. `TestDeploymentService_ProcessDeploymentJob_CertNotFound` — certRepo.GetErr set, verify job marked Failed
|
||||
9. `TestDeploymentService_ProcessDeploymentJob_NoTargetID` — job.TargetID is nil, verify job marked Failed with "target_id not found"
|
||||
10. `TestDeploymentService_ProcessDeploymentJob_TargetNotFound` — targetRepo.GetErr set, verify job marked Failed
|
||||
11. `TestDeploymentService_ProcessDeploymentJob_AgentNotFound` — agentRepo.GetErr set, verify job marked Failed
|
||||
12. `TestDeploymentService_ProcessDeploymentJob_AgentOffline` — agent.LastHeartbeatAt is 10 minutes ago, verify job marked Failed with "agent is offline", notification sent
|
||||
|
||||
**ValidateDeployment:**
|
||||
13. `TestDeploymentService_ValidateDeployment_Completed` — deployment job exists with Status=Completed, expect (true, nil)
|
||||
14. `TestDeploymentService_ValidateDeployment_Failed` — deployment job with Status=Failed and LastError, expect (false, error with message)
|
||||
15. `TestDeploymentService_ValidateDeployment_InProgress` — deployment job with Status=Running, expect (false, "deployment in progress")
|
||||
16. `TestDeploymentService_ValidateDeployment_NoJob` — no matching deployment job, expect (false, "no deployment job found")
|
||||
17. `TestDeploymentService_ValidateDeployment_ListError` — jobRepo returns error
|
||||
|
||||
**MarkDeploymentComplete:**
|
||||
18. `TestDeploymentService_MarkDeploymentComplete_Success` — verify job status -> Completed, notification sent (success=true), audit event
|
||||
19. `TestDeploymentService_MarkDeploymentComplete_JobNotFound` — jobRepo.GetErr set
|
||||
20. `TestDeploymentService_MarkDeploymentComplete_NoTargetID` — job.TargetID is nil, still completes without notification
|
||||
|
||||
**MarkDeploymentFailed:**
|
||||
21. `TestDeploymentService_MarkDeploymentFailed_Success` — verify job status -> Failed, error message stored, notification sent (success=false), audit event
|
||||
22. `TestDeploymentService_MarkDeploymentFailed_JobNotFound` — jobRepo.GetErr set
|
||||
|
||||
---
|
||||
|
||||
## P0-2: `internal/service/target_test.go` (NEW FILE)
|
||||
|
||||
**File to test:** `internal/service/target.go`
|
||||
|
||||
### Setup:
|
||||
```go
|
||||
func newTestTargetService() (*TargetService, *mockTargetRepo, *mockAuditRepo) {
|
||||
targetRepo := &mockTargetRepo{Targets: make(map[string]*domain.DeploymentTarget)}
|
||||
auditRepo := newMockAuditRepository()
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
return NewTargetService(targetRepo, auditSvc), targetRepo, auditRepo
|
||||
}
|
||||
```
|
||||
|
||||
### Required tests (~15 functions):
|
||||
|
||||
**Context-aware methods (List, Get, Create, Update, Delete):**
|
||||
1. `TestTargetService_List_Success` — 3 targets, page=1 perPage=2, expect 2 returned with total=3
|
||||
2. `TestTargetService_List_DefaultPagination` — page=0 perPage=0, expect defaults to 1/50
|
||||
3. `TestTargetService_List_EmptyPage` — page=2 perPage=10 with only 3 targets, expect empty slice, total=3
|
||||
4. `TestTargetService_List_RepoError` — ListErr set
|
||||
5. `TestTargetService_Get_Success` — target exists
|
||||
6. `TestTargetService_Get_NotFound` — target doesn't exist
|
||||
7. `TestTargetService_Create_Success` — verify target stored, ID generated, timestamps set, audit event
|
||||
8. `TestTargetService_Create_MissingName` — empty name, expect error
|
||||
9. `TestTargetService_Create_RepoError` — CreateErr set
|
||||
10. `TestTargetService_Update_Success` — verify target updated, audit event
|
||||
11. `TestTargetService_Update_MissingName` — empty name, expect error
|
||||
12. `TestTargetService_Delete_Success` — verify target removed, audit event
|
||||
13. `TestTargetService_Delete_RepoError` — DeleteErr set
|
||||
|
||||
**Legacy handler interface methods:**
|
||||
14. `TestTargetService_ListTargets_Success` — verify returns dereferenced targets
|
||||
15. `TestTargetService_GetTarget_Success`
|
||||
16. `TestTargetService_CreateTarget_Success` — verify ID generation
|
||||
17. `TestTargetService_UpdateTarget_Success`
|
||||
18. `TestTargetService_DeleteTarget_Success`
|
||||
|
||||
---
|
||||
|
||||
## P0-3: Scheduler Loop Execution Tests
|
||||
|
||||
**File to modify:** `internal/scheduler/scheduler_test.go`
|
||||
|
||||
The existing tests cover idempotency and graceful shutdown. Add tests that verify each loop actually calls its service method.
|
||||
|
||||
### Required tests (~8 functions):
|
||||
|
||||
1. `TestSchedulerRenewalLoopCallsService` — start scheduler with 50ms interval, wait 150ms, verify renewalMock.callCount >= 1
|
||||
2. `TestSchedulerJobProcessorLoopCallsService` — same pattern for jobMock
|
||||
3. `TestSchedulerAgentHealthCheckLoopCallsService` — same for agentMock
|
||||
4. `TestSchedulerNotificationLoopCallsService` — same for notificationMock
|
||||
5. `TestSchedulerNetworkScanLoopCallsService` — same for networkMock
|
||||
6. `TestSchedulerShortLivedExpiryLoopCallsService` — verify ExpireShortLivedCertificates is called (need to add callCount tracking to mockRenewalService.ExpireShortLivedCertificates)
|
||||
7. `TestSchedulerLoopErrorRecovery` — set shouldError=true on renewalMock, verify scheduler continues (doesn't crash), subsequent calls still happen
|
||||
8. `TestSchedulerLoopContextCancellation` — cancel context mid-execution, verify no panics, WaitForCompletion succeeds
|
||||
|
||||
**Note:** You'll need to add `expireCallCount` and `expireCallTimes` fields to `mockRenewalService` and track calls in `ExpireShortLivedCertificates`.
|
||||
|
||||
---
|
||||
|
||||
## P0-4: Agent Binary Tests
|
||||
|
||||
**File to create:** `cmd/agent/agent_test.go` (NEW FILE, `package main`)
|
||||
|
||||
This is the hardest gap. The agent binary's methods (`executeCSRJob`, `executeDeploymentJob`, heartbeat loop, discovery loop) need a mock HTTP server.
|
||||
|
||||
### Setup:
|
||||
```go
|
||||
func newTestServer(t *testing.T) *httptest.Server {
|
||||
mux := http.NewServeMux()
|
||||
// Register mock endpoints
|
||||
mux.HandleFunc("/api/v1/agents/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// Handle heartbeat (POST /agents/{id}/heartbeat), work (GET /agents/{id}/work),
|
||||
// CSR submission (POST /agents/{id}/csr), job status (POST /agents/{id}/jobs/{job_id}/status),
|
||||
// discoveries (POST /agents/{id}/discoveries)
|
||||
})
|
||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
||||
json.NewEncoder(w).Encode(map[string]string{"status": "healthy"})
|
||||
})
|
||||
return httptest.NewServer(mux)
|
||||
}
|
||||
```
|
||||
|
||||
### Required tests (~10 functions):
|
||||
|
||||
1. `TestAgentHeartbeat_Success` — mock server returns 200, verify request has correct headers
|
||||
2. `TestAgentHeartbeat_ServerDown` — connection refused, verify error handling (no panic)
|
||||
3. `TestAgentCSRGeneration` — verify ECDSA P-256 key generation, CSR contains correct CN and SANs
|
||||
4. `TestAgentCSRGeneration_EmailSAN` — verify email SANs route to EmailAddresses (not DNSNames)
|
||||
5. `TestAgentWorkPolling_NoWork` — server returns empty work list
|
||||
6. `TestAgentWorkPolling_DeploymentJob` — server returns deployment work item
|
||||
7. `TestAgentWorkPolling_CSRJob` — server returns AwaitingCSR work item
|
||||
8. `TestAgentKeyStorage` — verify keys written to temp dir with 0600 permissions
|
||||
9. `TestAgentDiscoveryScan` — scan a temp directory with a test PEM file, verify correct extraction
|
||||
10. `TestAgentDiscoveryScan_EmptyDir` — scan empty directory, verify empty results (no error)
|
||||
|
||||
**Important:** The agent code uses global variables and `main()` package patterns. You may need to extract testable functions or use `TestMain` for setup. If the agent's methods are on a struct, mock the HTTP client. If they're standalone functions, use httptest.
|
||||
|
||||
---
|
||||
|
||||
## P1-1: `CompleteAgentCSRRenewal` Tests
|
||||
|
||||
**File to modify:** `internal/service/renewal_test.go`
|
||||
|
||||
### Required tests (~8 functions):
|
||||
|
||||
The method signature is:
|
||||
```go
|
||||
func (s *RenewalService) CompleteAgentCSRRenewal(ctx context.Context, job *domain.Job, cert *domain.ManagedCertificate, csrPEM string) error
|
||||
```
|
||||
|
||||
You need a `RenewalService` with: certRepo, jobRepo, auditService, notificationSvc, issuerConnector (mock), profileRepo (mock), keygenMode="agent".
|
||||
|
||||
1. `TestCompleteAgentCSRRenewal_Success` — valid job (AwaitingCSR), valid cert, valid CSR PEM. Verify: issuer.IssueCertificate called, cert version created, job status -> Completed, deployment jobs created
|
||||
2. `TestCompleteAgentCSRRenewal_IssuerError` — issuerConnector.Err set, verify job status -> Failed
|
||||
3. `TestCompleteAgentCSRRenewal_InvalidCSR` — garbage CSR PEM, verify error
|
||||
4. `TestCompleteAgentCSRRenewal_WithEKUs` — cert has certificate_profile_id, profile has allowed_ekus=["emailProtection"], verify EKUs forwarded to issuer
|
||||
5. `TestCompleteAgentCSRRenewal_NoProfile` — cert has no profile ID, verify default EKUs (nil)
|
||||
6. `TestCompleteAgentCSRRenewal_CreateVersionError` — certRepo.CreateVersionErr set
|
||||
7. `TestCompleteAgentCSRRenewal_AuditRecorded` — verify audit event with correct details
|
||||
8. `TestCompleteAgentCSRRenewal_DeploymentJobsCreated` — after successful signing, verify deployment jobs exist in jobRepo
|
||||
|
||||
**Note:** You'll need a `mockProfileRepo` if one doesn't exist in testutil_test.go. Check if `internal/repository/interfaces.go` has `ProfileRepository` and create a mock.
|
||||
|
||||
---
|
||||
|
||||
## P1-2: `ExpireShortLivedCertificates` Tests
|
||||
|
||||
**File to modify:** `internal/service/renewal_test.go`
|
||||
|
||||
```go
|
||||
func (s *RenewalService) ExpireShortLivedCertificates(ctx context.Context) error
|
||||
```
|
||||
|
||||
1. `TestExpireShortLivedCertificates_NoShortLived` — no active certs with short-lived profiles, no changes
|
||||
2. `TestExpireShortLivedCertificates_ExpiresActiveCert` — cert with profile TTL < 1h, cert active, cert's NotAfter is in the past. Verify status -> Expired
|
||||
3. `TestExpireShortLivedCertificates_SkipsNonExpired` — cert with short-lived profile but NotAfter is in the future, no change
|
||||
4. `TestExpireShortLivedCertificates_SkipsNonShortLived` — cert with normal profile (TTL > 1h), even if expired. Verify not touched by this method
|
||||
5. `TestExpireShortLivedCertificates_RepoError` — certRepo.ListErr set
|
||||
|
||||
**Note:** This method needs access to profiles to determine TTL. Read the actual implementation to understand how it queries — it may iterate all active certs and check their profile's max_ttl.
|
||||
|
||||
---
|
||||
|
||||
## P1-3: Domain Model Tests
|
||||
|
||||
### `internal/domain/job_test.go` (NEW FILE)
|
||||
|
||||
```go
|
||||
package domain
|
||||
|
||||
import "testing"
|
||||
```
|
||||
|
||||
1. `TestJobType_Constants` — verify all 4 JobType constants have expected string values
|
||||
2. `TestJobStatus_Constants` — verify all 7 JobStatus constants
|
||||
3. `TestVerificationStatus_Constants` — verify all 4 VerificationStatus constants (pending, success, failed, skipped)
|
||||
|
||||
### `internal/domain/certificate_test.go` (NEW FILE)
|
||||
|
||||
1. `TestCertificateStatus_Constants` — verify all 8 CertificateStatus constants
|
||||
2. `TestRenewalPolicy_EffectiveAlertThresholds_Custom` — policy with custom thresholds returns them
|
||||
3. `TestRenewalPolicy_EffectiveAlertThresholds_Default` — policy with nil thresholds returns DefaultAlertThresholds()
|
||||
4. `TestDefaultAlertThresholds` — returns [30, 14, 7, 0]
|
||||
|
||||
### `internal/domain/agent_group_test.go` (NEW FILE)
|
||||
|
||||
1. `TestAgentGroup_HasDynamicCriteria_True` — group with MatchOS set
|
||||
2. `TestAgentGroup_HasDynamicCriteria_False` — all criteria empty
|
||||
3. `TestAgentGroup_MatchesAgent_AllMatch` — all 4 criteria set, agent matches all
|
||||
4. `TestAgentGroup_MatchesAgent_OSMismatch` — MatchOS="linux", agent.OS="windows"
|
||||
5. `TestAgentGroup_MatchesAgent_ArchMismatch` — MatchArchitecture="amd64", agent.Architecture="arm64"
|
||||
6. `TestAgentGroup_MatchesAgent_VersionMismatch` — MatchVersion="1.0", agent.Version="2.0"
|
||||
7. `TestAgentGroup_MatchesAgent_IPMismatch` — MatchIPCIDR doesn't match agent.IPAddress
|
||||
8. `TestAgentGroup_MatchesAgent_EmptyCriteriaMatchesAll` — all criteria empty, any agent matches
|
||||
9. `TestAgentGroup_MatchesAgent_PartialCriteria` — only MatchOS set, agent matches OS, other fields irrelevant
|
||||
10. `TestAgentGroup_MatchesAgent_NilAgent` — if agent is nil, should not panic (add nil guard or verify behavior)
|
||||
|
||||
### `internal/domain/notification_test.go` (NEW FILE)
|
||||
|
||||
1. `TestNotificationType_Constants` — verify all 7 types
|
||||
2. `TestNotificationChannel_Constants` — verify all 6 channels
|
||||
3. `TestNotificationEvent_ZeroValue` — default struct has empty strings, nil pointers
|
||||
|
||||
### `internal/domain/policy_test.go` (NEW FILE)
|
||||
|
||||
1. `TestPolicyType_Constants` — verify all 5 policy types
|
||||
2. `TestPolicySeverity_Constants` — verify all 3 severities
|
||||
3. `TestPolicyViolation_Fields` — create a violation, verify all fields accessible
|
||||
|
||||
---
|
||||
|
||||
## P1-4: Handler Gap Tests
|
||||
|
||||
### Modify `internal/api/handler/agent_group_handler_test.go`
|
||||
|
||||
Add:
|
||||
1. `TestUpdateAgentGroup_Success` — PUT with valid body, verify 200
|
||||
2. `TestUpdateAgentGroup_InvalidJSON` — malformed body, verify 400
|
||||
3. `TestUpdateAgentGroup_MissingName` — empty name field, verify 400
|
||||
4. `TestUpdateAgentGroup_NotFound` — service returns not found error, verify 404
|
||||
|
||||
### Modify `internal/api/handler/issuer_handler_test.go`
|
||||
|
||||
Add:
|
||||
1. `TestUpdateIssuer_Success` — PUT with valid body, verify 200
|
||||
2. `TestUpdateIssuer_InvalidJSON` — verify 400
|
||||
3. `TestUpdateIssuer_NotFound` — verify 404
|
||||
|
||||
### Modify `internal/api/handler/network_scan_handler_test.go`
|
||||
|
||||
Add:
|
||||
1. `TestGetNetworkScanTarget_Success` — GET by ID, verify 200
|
||||
2. `TestGetNetworkScanTarget_NotFound` — verify 404
|
||||
3. `TestUpdateNetworkScanTarget_Success` — PUT with valid body, verify 200
|
||||
4. `TestUpdateNetworkScanTarget_InvalidJSON` — verify 400
|
||||
5. `TestUpdateNetworkScanTarget_NotFound` — verify 404
|
||||
|
||||
---
|
||||
|
||||
## P2-1: Frontend Error Handling Tests
|
||||
|
||||
**File to modify:** `web/src/api/client.test.ts`
|
||||
|
||||
Add error scenario tests for the 65+ API functions that lack them. Group by resource:
|
||||
|
||||
### Pattern:
|
||||
```typescript
|
||||
it('listCertificates handles 500 error', async () => {
|
||||
fetchMock.mockResponseOnce('', { status: 500 });
|
||||
await expect(listCertificates()).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('getCertificate handles 404 error', async () => {
|
||||
fetchMock.mockResponseOnce('', { status: 404 });
|
||||
await expect(getCertificate('nonexistent')).rejects.toThrow();
|
||||
});
|
||||
```
|
||||
|
||||
### Required (~40 tests):
|
||||
|
||||
Add at minimum a 500 error test and a 404 test (where applicable) for each resource group:
|
||||
- Certificates (list 500, get 404, renew 404, revoke 404, export 404)
|
||||
- Agents (list 500, get 404)
|
||||
- Jobs (list 500, get 404, cancel 404, approve 404, reject 404)
|
||||
- Policies (list 500, get 404, create 400, update 404, delete 404)
|
||||
- Profiles (list 500, get 404, create 400)
|
||||
- Owners (list 500, get 404)
|
||||
- Teams (list 500, get 404)
|
||||
- Agent Groups (list 500, get 404)
|
||||
- Issuers (list 500, get 404)
|
||||
- Targets (list 500, get 404, create 400)
|
||||
- Discovery (list 500, claim 404, dismiss 404)
|
||||
- Network Scans (list 500, create 400, trigger 404)
|
||||
- Stats/Metrics (500 errors)
|
||||
- Health (500 error)
|
||||
|
||||
---
|
||||
|
||||
## P2-2: Context Cancellation Tests
|
||||
|
||||
**File to create:** `internal/service/context_test.go` (NEW FILE)
|
||||
|
||||
Test that long-running service methods respect context cancellation.
|
||||
|
||||
### Pattern:
|
||||
```go
|
||||
func TestDeploymentService_CreateDeploymentJobs_ContextCancelled(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
svc, _, targetRepo, _, _, _ := newTestDeploymentService()
|
||||
targetRepo.AddTarget(&domain.DeploymentTarget{ID: "t1", Name: "test"})
|
||||
|
||||
_, err := svc.CreateDeploymentJobs(ctx, "cert-1")
|
||||
// Depending on implementation, may get context.Canceled or proceed normally
|
||||
// The key assertion: no panic, no goroutine leak
|
||||
t.Logf("result with cancelled context: %v", err)
|
||||
}
|
||||
```
|
||||
|
||||
### Required (~8 tests):
|
||||
|
||||
1. `TestDeploymentService_ProcessDeploymentJob_ContextTimeout` — context with 1ms timeout
|
||||
2. `TestNetworkScanService_ScanAllTargets_ContextCancelled` — cancel mid-scan
|
||||
3. `TestDiscoveryService_ProcessDiscoveryReport_ContextCancelled`
|
||||
4. `TestESTService_SimpleEnroll_ContextCancelled`
|
||||
5. `TestExportService_ExportPKCS12_ContextCancelled`
|
||||
6. `TestRenewalService_ProcessRenewalJob_ContextTimeout`
|
||||
7. `TestCertificateService_RevokeCertificateWithActor_ContextCancelled`
|
||||
8. `TestVerificationService_RecordVerificationResult_ContextCancelled`
|
||||
|
||||
---
|
||||
|
||||
## P2-3: Concurrent Operation Tests
|
||||
|
||||
**File to create:** `internal/service/concurrent_test.go` (NEW FILE)
|
||||
|
||||
Use `sync.WaitGroup` and goroutines to test concurrent access patterns.
|
||||
|
||||
### Required (~6 tests):
|
||||
|
||||
```go
|
||||
func TestConcurrentRevocation(t *testing.T) {
|
||||
// Setup service with a certificate
|
||||
// Launch 5 goroutines all trying to revoke the same cert simultaneously
|
||||
// Verify: exactly 1 succeeds (or all succeed idempotently), no panics, no data corruption
|
||||
var wg sync.WaitGroup
|
||||
errors := make([]error, 5)
|
||||
for i := 0; i < 5; i++ {
|
||||
wg.Add(1)
|
||||
go func(idx int) {
|
||||
defer wg.Done()
|
||||
errors[idx] = svc.RevokeCertificateWithActor(ctx, certID, "keyCompromise", "test-actor")
|
||||
}(i)
|
||||
}
|
||||
wg.Wait()
|
||||
// Assert at most 1 "already revoked" error
|
||||
}
|
||||
```
|
||||
|
||||
1. `TestConcurrentRevocation` — 5 goroutines revoke same cert
|
||||
2. `TestConcurrentDeploymentJobCreation` — 3 goroutines create deployment jobs for same cert
|
||||
3. `TestConcurrentDiscoveryReports` — 3 goroutines submit discovery reports simultaneously
|
||||
4. `TestConcurrentCertificateList` — 10 goroutines list certificates simultaneously (no race)
|
||||
5. `TestConcurrentJobStatusUpdate` — 5 goroutines update same job status
|
||||
6. `TestConcurrentTargetCRUD` — create, update, delete targets concurrently
|
||||
|
||||
---
|
||||
|
||||
## Execution Order
|
||||
|
||||
Run these in order, verifying each step:
|
||||
|
||||
```bash
|
||||
# P0 — Critical
|
||||
go test ./internal/service/ -run TestDeploymentService -v -count=1
|
||||
go test ./internal/service/ -run TestTargetService -v -count=1
|
||||
go test ./internal/scheduler/ -run TestScheduler -v -count=1
|
||||
|
||||
# P1 — High Priority
|
||||
go test ./internal/service/ -run TestCompleteAgentCSR -v -count=1
|
||||
go test ./internal/service/ -run TestExpireShortLived -v -count=1
|
||||
go test ./internal/domain/ -v -count=1
|
||||
go test ./internal/api/handler/ -run "TestUpdateAgentGroup|TestUpdateIssuer|TestGetNetworkScan|TestUpdateNetworkScan" -v -count=1
|
||||
|
||||
# P2 — Medium Priority
|
||||
cd web && npx vitest run
|
||||
go test ./internal/service/ -run TestContext -v -count=1
|
||||
go test ./internal/service/ -run TestConcurrent -v -count=1
|
||||
|
||||
# Full suite verification
|
||||
go test -race ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/scheduler/... ./internal/connector/... ./internal/domain/... ./internal/validation/... -count=1 -timeout 300s
|
||||
go vet ./...
|
||||
cd web && npx vitest run
|
||||
```
|
||||
|
||||
## Final CI Gate
|
||||
|
||||
After all tests pass locally, verify the full CI pipeline would pass:
|
||||
|
||||
```bash
|
||||
# Coverage check
|
||||
go test ./internal/service/... ./internal/api/handler/... ./internal/api/middleware/... ./internal/integration/... ./internal/connector/issuer/... ./internal/connector/target/... ./internal/connector/notifier/... ./internal/mcp/... ./internal/cli/... ./internal/domain/... ./internal/validation/... -count=1 -cover -coverprofile=coverage.out
|
||||
|
||||
# Check thresholds
|
||||
go tool cover -func=coverage.out | grep 'internal/service' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {printf "Service: %.1f%%\n", sum/n}'
|
||||
go tool cover -func=coverage.out | grep 'internal/api/handler' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {printf "Handler: %.1f%%\n", sum/n}'
|
||||
go tool cover -func=coverage.out | grep 'internal/domain' | awk '{print $NF}' | sed 's/%//' | awk '{sum+=$1; n++} END {printf "Domain: %.1f%%\n", sum/n}'
|
||||
|
||||
# Targets: service >= 60%, handler >= 60%, domain >= 40%
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## What NOT To Do
|
||||
|
||||
- Do NOT modify any production code (only test files)
|
||||
- Do NOT add new dependencies to go.mod
|
||||
- Do NOT create mocks that duplicate existing ones in testutil_test.go — reuse them
|
||||
- Do NOT use `testing.Short()` skips — all these tests should run in CI
|
||||
- Do NOT use `time.Sleep` for synchronization — use channels, WaitGroups, or atomic counters
|
||||
- Do NOT write tests that are flaky due to timing — if testing scheduler loops, use generous timeouts and verify "at least 1 call" rather than exact counts
|
||||
-481
@@ -1,481 +0,0 @@
|
||||
# certctl Test Suite Audit & Manual Testing Guide
|
||||
|
||||
Last updated: March 28, 2026
|
||||
|
||||
This document covers the automated test suite inventory, identified gaps, and a complete manual testing guide for v2.1 release validation.
|
||||
|
||||
## Contents
|
||||
|
||||
1. [Automated Test Suite Inventory](#automated-test-suite-inventory)
|
||||
2. [Test Gap Analysis](#test-gap-analysis)
|
||||
3. [Manual Testing Guide](#manual-testing-guide)
|
||||
4. [Pre-Release Checklist](#pre-release-checklist)
|
||||
|
||||
---
|
||||
|
||||
## Automated Test Suite Inventory
|
||||
|
||||
### Summary
|
||||
|
||||
| Layer | Test Files | Test Functions | Subtests | Coverage Target | Notes |
|
||||
|-------|-----------|---------------|----------|-----------------|-------|
|
||||
| Service | 12 | ~120 | ~185 | 60% (CI gate) | Best-covered layer |
|
||||
| Handler | 12 | ~140 | ~145 | 60% (CI gate) | Near-complete endpoint coverage |
|
||||
| Domain | 3 | ~16 | ~12 | 40% (CI gate) | Only revocation, discovery, verification tested |
|
||||
| Middleware | 2 | ~14 | ~10 | 50% (CI gate) | Audit + CORS tested |
|
||||
| Integration | 2 | ~15 | ~25 | — | Lifecycle + negative paths |
|
||||
| Connector (Issuer) | 4 | ~41 | — | — | Local CA, ACME DNS, step-ca, OpenSSL |
|
||||
| Connector (Target) | 2 | ~12 | — | — | Traefik, Caddy |
|
||||
| Connector (Notifier) | 4 | ~20 | — | — | Slack, Teams, PagerDuty, OpsGenie |
|
||||
| Validation | 2 | ~10 | ~80 | — | command.go + fuzz tests |
|
||||
| Scheduler | 1 | ~5 | — | — | Startup/shutdown only |
|
||||
| CLI | 1 | ~14 | — | — | All 10 subcommands |
|
||||
| Repository | 2 | ~24 | ~50 | — | testcontainers-go, skipped in CI |
|
||||
| Agent | 1 | ~5 | — | — | verify.go only |
|
||||
| Frontend (API) | 1 | 89 | — | — | 96% API function coverage |
|
||||
| Frontend (Utils) | 1 | 18 | — | — | 100% utility coverage |
|
||||
| **Total** | **~50** | **~835+** | **~200+** | — | **1100+ total test points** |
|
||||
|
||||
### CI Pipeline
|
||||
|
||||
Every push runs (`.github/workflows/ci.yml`):
|
||||
|
||||
- `go vet ./...`
|
||||
- `golangci-lint` (11 linters including gosec, bodyclose, errcheck)
|
||||
- `govulncheck` (dependency CVE scanning)
|
||||
- `go test -race` (race detection across service, handler, middleware, scheduler, connector, domain, validation)
|
||||
- `go test -cover` with per-layer thresholds (service 55%, handler 60%, domain 40%, middleware 30%)
|
||||
- Frontend: `tsc --noEmit`, `vitest run`, `vite build`
|
||||
|
||||
### What's Well-Tested
|
||||
|
||||
**Service layer** — renewal flows (server + agent keygen modes), revocation (all 8 RFC 5280 reasons), CRL/OCSP generation, discovery (process report, claim, dismiss, summary), network scan (CIDR expansion, validation, CRUD), stats (5 aggregations), EST enrollment (GetCACerts, SimpleEnroll/ReEnroll, CSRAttrs), export (PEM split, PKCS#12 encoding), verification (record/get results), issuer adapter (issue, renew, revoke with EKU forwarding).
|
||||
|
||||
**Handler layer** — all 12 resource handlers tested with success paths, 404/400/405/500 error paths, input validation (required fields, type checks, JSON parsing), query parameter parsing (pagination, filters, sort, cursor, sparse fields). CRUD endpoints, revocation, CRL, OCSP, EST, export, verification handlers all covered.
|
||||
|
||||
**Connectors** — Local CA (self-signed, sub-CA with RSA/ECDSA, renewal, config validation), ACME DNS solver (present, cleanup, DNS-PERSIST-01), step-ca (issue, renew, revoke via mock HTTP), OpenSSL (config validation, script execution, timeout), Traefik (file write, directory validation), Caddy (API mode, file mode, config validation), all 4 notifiers (webhook payloads, HTTP errors, auth headers, config defaults).
|
||||
|
||||
**Validation** — shell injection prevention with 80+ adversarial patterns (fuzz tests), domain validation, ACME token validation.
|
||||
|
||||
**Frontend** — 107 Vitest tests: all API client functions (certificates, agents, jobs, policies, profiles, owners, teams, agent groups, discovery, network scans, stats, metrics, export, health), utility functions (date formatting, time-ago, expiry color), both happy path and some error scenarios.
|
||||
|
||||
---
|
||||
|
||||
## Test Gap Analysis
|
||||
|
||||
### P0 — Critical Gaps (Production Risk)
|
||||
|
||||
**1. No tests for `service/deployment.go`** — deployment orchestration (creating deployment jobs, target resolution, deployment execution) is completely untested. This is the core path that actually puts certificates onto servers.
|
||||
- Missing: `CreateDeploymentJobs`, `ProcessDeploymentJob`, target connector dispatch
|
||||
- Risk: silent deployment failures, wrong cert deployed to wrong target
|
||||
- Effort: 15-20 test functions, 1-2 days
|
||||
|
||||
**2. Agent binary (`cmd/agent/main.go`) largely untested** — only `verify.go` has tests. The agent's registration, heartbeat loop, work polling, CSR generation, discovery scanning, and deployment execution have no automated tests.
|
||||
- Missing: heartbeat error handling, CSR generation edge cases, deployment with local keys, discovery scan error paths
|
||||
- Risk: agent fails silently in production, key material handling bugs
|
||||
- Effort: significant — needs mock control plane HTTP server, 3-5 days
|
||||
- Mitigation: the manual testing guide below covers these flows
|
||||
|
||||
**3. `service/target.go` untested** — target CRUD operations (Create, List, Get, Update, Delete) have service-layer tests missing.
|
||||
- Risk: target configuration errors not caught
|
||||
- Effort: 8-10 test functions, 0.5 days
|
||||
|
||||
**4. Scheduler loop execution untested** — `scheduler_test.go` only tests startup and graceful shutdown. The 6 actual loops (renewal check, job processing, health check, notifications, short-lived expiry, network scanning) are not tested for correct execution behavior.
|
||||
- Risk: scheduler silently stops processing without detection
|
||||
- Effort: complex — needs time manipulation and mock services, 2-3 days
|
||||
|
||||
### P1 — High-Priority Gaps
|
||||
|
||||
**5. `CompleteAgentCSRRenewal()` not tested** — this is the critical path where agent-submitted CSRs are signed by the issuer. EKU resolution from profiles, deployment job creation after signing, and CSR validation are all untested at the service layer.
|
||||
- Effort: 5-8 test functions, 1 day
|
||||
|
||||
**6. `ExpireShortLivedCertificates()` not tested** — scheduler operation that marks short-lived certs as expired. No test coverage.
|
||||
- Effort: 3-4 test functions, 0.5 days
|
||||
|
||||
**7. Domain models mostly untested** — only `revocation.go`, `discovery.go`, and `verification.go` have test files. Missing: `job.go` (state machine transitions), `certificate.go` (status validation), `agent_group.go` (MatchesAgent criteria), `notification.go`, `policy.go`.
|
||||
- Effort: 20-30 test functions across 5 files, 2-3 days
|
||||
|
||||
**8. Handler gaps** — `UpdateAgentGroup`, `UpdateIssuer`, `GetNetworkScanTarget`, `UpdateNetworkScanTarget` are untested handler methods.
|
||||
- Effort: ~12 test functions, 0.5 days
|
||||
|
||||
### P2 — Medium-Priority Gaps
|
||||
|
||||
**9. Frontend: zero component/page render tests** — no React component tests exist. All 22 pages and 8 shared components are untested for rendering, user interaction, modal behavior, and form validation.
|
||||
- Risk: UI regressions go undetected
|
||||
- Effort: significant — needs React Testing Library setup, 3-5 days for core pages
|
||||
|
||||
**10. Frontend: weak error handling tests** — only 13 of 78 API functions have error scenario tests. Missing: 404 errors, network timeouts, 429 rate limiting, malformed JSON responses.
|
||||
- Effort: 1-2 days
|
||||
|
||||
**11. Context cancellation / timeout tests** — no service or handler tests verify correct behavior when contexts are cancelled or time out. Long-running operations (network scan, EST enrollment) should gracefully handle cancellation.
|
||||
- Effort: 1-2 days
|
||||
|
||||
**12. Concurrent operation tests** — two simultaneous revocations of the same certificate, concurrent discovery reports from multiple agents, parallel deployment jobs. Race detector catches some of this but not logic bugs.
|
||||
- Effort: 1-2 days
|
||||
|
||||
### Docker Compose Bug Found During Audit
|
||||
|
||||
**`migrations/000008_verification.up.sql` is NOT mounted in `deploy/docker-compose.yml`**. The verification migration exists on disk but the Docker Compose file only mounts migrations 000001-000007. This means the demo environment is missing the `verification_status`, `verified_at`, `verification_fingerprint`, and `verification_error` columns on the jobs table.
|
||||
|
||||
Fix: add to docker-compose.yml:
|
||||
```yaml
|
||||
- ../migrations/000008_verification.up.sql:/docker-entrypoint-initdb.d/008_verification.sql
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Manual Testing Guide
|
||||
|
||||
This guide covers end-to-end manual validation of all certctl features against the Docker Compose demo environment. Use this for v2.1 release validation.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Clean start (removes old data)
|
||||
docker compose -f deploy/docker-compose.yml down -v
|
||||
docker compose -f deploy/docker-compose.yml up -d --build
|
||||
|
||||
# Wait for healthy
|
||||
docker compose -f deploy/docker-compose.yml ps
|
||||
# All three services should show "Up (healthy)" or "Up"
|
||||
|
||||
# Verify
|
||||
curl -s http://localhost:8443/health | jq .
|
||||
# {"status":"healthy"}
|
||||
```
|
||||
|
||||
### 1. Dashboard & Navigation
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 1.1 | Dashboard loads | Open http://localhost:8443 | Stats cards show (total certs, expiring, expired, agents). 4 charts render (heatmap, trends, distribution, issuance rate) |
|
||||
| 1.2 | Sidebar navigation | Click each sidebar item | All 16 nav items load without errors: Dashboard, Certificates, Agents, Fleet Overview, Jobs, Notifications, Policies, Profiles, Issuers, Targets, Owners, Teams, Agent Groups, Audit Trail, Short-Lived, Discovery, Network Scans |
|
||||
| 1.3 | Auth disabled notice | Check for login prompt | No login screen (demo runs with `CERTCTL_AUTH_TYPE=none`) |
|
||||
|
||||
### 2. Certificate Lifecycle
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 2.1 | List certificates | Certificates page | 15 demo certificates with status badges, names, expiry dates |
|
||||
| 2.2 | Certificate detail | Click any certificate | Detail page shows: Certificate Details card, Lifecycle card, Lifecycle Timeline (4 steps), Policy & Profile editor, Version History, Tags |
|
||||
| 2.3 | Trigger renewal | Click "Trigger Renewal" on `mc-api-prod` | Success banner. Jobs page shows new Renewal job |
|
||||
| 2.4 | Trigger deployment | Click "Deploy" → select a target → "Deploy" | Success banner. Jobs page shows new Deployment job |
|
||||
| 2.5 | Revoke certificate | Click "Revoke" on an active cert → select "Key Compromise" → confirm | Red revocation banner appears on cert detail. Status changes to "Revoked" |
|
||||
| 2.6 | Archive certificate | Click "Archive" → confirm | Redirect to certificates list. Cert no longer shows (or shows as Archived) |
|
||||
| 2.7 | Export PEM | Click "Export PEM" on cert detail | Browser downloads a .pem file. File contains valid PEM certificate |
|
||||
| 2.8 | Export PKCS#12 | Click "Export PKCS#12" → enter password → download | Browser downloads a .p12 file |
|
||||
| 2.9 | Deployment timeline | View cert detail for a cert with deployment jobs | Timeline shows: Requested (green) → Issued (green) → Deploying (status) → Active |
|
||||
| 2.10 | Version history | View cert detail with multiple versions | Version list with "Current" badge on latest. Rollback button on previous versions |
|
||||
| 2.11 | Inline policy editor | Click "Edit" on Policy & Profile card → change policy → Save | Policy updates. Card shows new values |
|
||||
|
||||
### 3. Bulk Operations
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 3.1 | Multi-select | On Certificates page, check 3 certificates | Bulk action bar appears with count |
|
||||
| 3.2 | Bulk renew | Select 3 certs → "Renew Selected" | Progress bar. 3 renewal jobs created |
|
||||
| 3.3 | Bulk revoke | Select 2 certs → "Revoke Selected" → choose reason → confirm | Progress bar. Both certs revoked |
|
||||
| 3.4 | Bulk reassign | Select 2 certs → "Reassign Owner" → enter new owner ID → confirm | Owner updated on both certificates |
|
||||
| 3.5 | Select all | Click header checkbox | All visible certs selected |
|
||||
|
||||
### 4. Agent & Fleet
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 4.1 | Agent list | Agents page | 5 demo agents with status (Online/Offline), OS, Architecture, IP |
|
||||
| 4.2 | Agent detail | Click an agent | System Information card (OS, arch, IP, version), recent jobs, capabilities |
|
||||
| 4.3 | Fleet overview | Fleet Overview page | OS distribution chart, architecture chart, version breakdown, per-platform agent listing |
|
||||
| 4.4 | Agent heartbeat | Check docker-agent status | `docker-agent` shows recent heartbeat timestamp, status Online |
|
||||
|
||||
### 5. Jobs & Approval Workflows
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 5.1 | Job list | Jobs page | Jobs with status badges. Type and status filters work |
|
||||
| 5.2 | Pending approval banner | Jobs page (if AwaitingApproval jobs exist) | Amber banner: "N jobs awaiting approval" with "Show only" link |
|
||||
| 5.3 | Approve renewal | Click "Approve" on an AwaitingApproval job | Job status changes to Pending or Running |
|
||||
| 5.4 | Reject renewal | Click "Reject" → enter reason → confirm | Job status changes to Cancelled. Reason recorded |
|
||||
| 5.5 | Cancel job | Click "Cancel" on a Pending/Running job | Job status changes to Cancelled |
|
||||
| 5.6 | Status filter | Select "AwaitingApproval" from status dropdown | Only AwaitingApproval jobs shown |
|
||||
| 5.7 | Type filter | Select "Deployment" from type dropdown | Only Deployment jobs shown |
|
||||
|
||||
### 6. Discovery & Network Scanning
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 6.1 | Discovery page | Discovery nav item | Summary stats bar (Unmanaged/Managed/Dismissed counts), certificate table |
|
||||
| 6.2 | Claim cert | Click "Claim" on an unmanaged cert → enter managed cert ID → confirm | Status changes to Managed |
|
||||
| 6.3 | Dismiss cert | Click "Dismiss" on an unmanaged cert | Status changes to Dismissed |
|
||||
| 6.4 | Discovery filters | Filter by status (Unmanaged) | Only unmanaged certs shown |
|
||||
| 6.5 | Scan history | Expand scan history panel | List of past scans with timestamps, cert counts |
|
||||
| 6.6 | Network scan list | Network Scans page | Demo scan targets with CIDRs, ports, intervals |
|
||||
| 6.7 | Create scan target | Click "+ New Target" → fill form → create | New target appears in list |
|
||||
| 6.8 | Trigger scan | Click "Scan Now" on a target | Scan triggered (may timeout in demo if targets unreachable — that's OK) |
|
||||
| 6.9 | Delete scan target | Click "Delete" on a target → confirm | Target removed from list |
|
||||
|
||||
### 7. Target Connector Wizard
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 7.1 | Open wizard | Targets page → "+ New Target" | 3-step wizard opens: Select Type → Configure → Review |
|
||||
| 7.2 | NGINX type | Select NGINX → Next | Config fields: Certificate Path*, Key Path*, Chain Path, Reload Command |
|
||||
| 7.3 | Apache type | Select Apache → Next | Config fields: Certificate Path*, Key Path*, Chain Path, Reload Command |
|
||||
| 7.4 | HAProxy type | Select HAProxy → Next | Config fields: Combined PEM Path*, Reload Command, Validate Command |
|
||||
| 7.5 | Traefik type | Select Traefik → Next | Config fields: Certificate Directory*, Certificate Filename, Key Filename |
|
||||
| 7.6 | Caddy type | Select Caddy → Next | Config fields: Deployment Mode*, Admin API URL, Certificate Directory, Certificate Filename, Key Filename |
|
||||
| 7.7 | F5 BIG-IP type | Select F5 BIG-IP → Next | Config fields: Management IP*, Partition, Proxy Agent ID |
|
||||
| 7.8 | IIS type | Select IIS → Next | Config fields: IIS Site Name*, Binding IP, Binding Port, Certificate Store |
|
||||
| 7.9 | Review & create | Fill required fields → Review → Create Target | Target appears in list with correct type and config |
|
||||
| 7.10 | Validation | Leave required fields empty → try to proceed | "Next" / "Review" button disabled |
|
||||
|
||||
### 8. Policies, Profiles & Ownership
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 8.1 | Policy list | Policies page | 5 demo policies with severity bar |
|
||||
| 8.2 | Create policy | Create a new policy with name, type, severity, config | Policy appears in list |
|
||||
| 8.3 | Profile list | Profiles page | Demo profiles with allowed key types, max TTL, EKUs |
|
||||
| 8.4 | S/MIME profile | Check `prof-smime` profile | Shows `emailProtection` EKU, 365-day max TTL |
|
||||
| 8.5 | Owner list | Owners page | Demo owners with email and team assignment |
|
||||
| 8.6 | Team list | Teams page | Demo teams |
|
||||
| 8.7 | Agent groups | Agent Groups page | Demo groups with dynamic criteria badges (OS, arch, CIDR, version) |
|
||||
|
||||
### 9. Observability
|
||||
|
||||
| # | Test | Steps | Expected |
|
||||
|---|------|-------|----------|
|
||||
| 9.1 | Audit trail | Audit Trail page | Events with actor, action, resource, timestamp. Time range filter works |
|
||||
| 9.2 | Audit export CSV | Click "Export CSV" | Downloads .csv file with filtered audit events |
|
||||
| 9.3 | Audit export JSON | Click "Export JSON" | Downloads .json file with filtered audit events |
|
||||
| 9.4 | Short-lived creds | Short-Lived page | Filtered view of certs with TTL < 1 hour. Live countdown timers |
|
||||
| 9.5 | Notifications | Notifications page | Grouped by certificate. Read/unread state. Mark as read works |
|
||||
| 9.6 | JSON metrics | `curl http://localhost:8443/api/v1/metrics \| jq .` | Returns gauges (cert totals, agent counts), counters (jobs), uptime |
|
||||
| 9.7 | Prometheus metrics | `curl http://localhost:8443/api/v1/metrics/prometheus` | Returns text/plain with `certctl_` prefixed metrics, `# HELP` and `# TYPE` lines |
|
||||
| 9.8 | Stats summary | `curl http://localhost:8443/api/v1/stats/summary \| jq .` | Returns total_certificates, expiring, expired, agent counts, job counts |
|
||||
|
||||
### 10. API Endpoints (curl)
|
||||
|
||||
Run these against the demo environment to verify the API layer:
|
||||
|
||||
```bash
|
||||
# Health
|
||||
curl -s http://localhost:8443/health | jq .
|
||||
|
||||
# Certificate CRUD
|
||||
curl -s http://localhost:8443/api/v1/certificates | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/certificates/mc-api-prod | jq '.common_name'
|
||||
curl -s "http://localhost:8443/api/v1/certificates?status=Active&sort=-notAfter&fields=id,common_name,status,expires_at" | jq .
|
||||
curl -s "http://localhost:8443/api/v1/certificates?page_size=3" | jq '.next_cursor'
|
||||
curl -s "http://localhost:8443/api/v1/certificates?expires_before=2026-05-01T00:00:00Z" | jq '.total'
|
||||
|
||||
# Certificate deployments
|
||||
curl -s http://localhost:8443/api/v1/certificates/mc-api-prod/deployments | jq .
|
||||
|
||||
# Renewal
|
||||
curl -s -X POST http://localhost:8443/api/v1/certificates/mc-api-prod/renew | jq .
|
||||
|
||||
# Revocation
|
||||
curl -s -X POST http://localhost:8443/api/v1/certificates/mc-internal-staging/revoke \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"reason": "superseded"}' | jq .
|
||||
|
||||
# CRL (JSON)
|
||||
curl -s http://localhost:8443/api/v1/crl | jq .
|
||||
|
||||
# Export PEM
|
||||
curl -s http://localhost:8443/api/v1/certificates/mc-api-prod/export/pem | jq .
|
||||
curl -s "http://localhost:8443/api/v1/certificates/mc-api-prod/export/pem?download=true" -o cert.pem
|
||||
|
||||
# Export PKCS#12
|
||||
curl -s -X POST http://localhost:8443/api/v1/certificates/mc-api-prod/export/pkcs12 \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"password": "test123"}' -o cert.p12
|
||||
|
||||
# Agents
|
||||
curl -s http://localhost:8443/api/v1/agents | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/agents/ag-web-prod | jq '.os, .architecture, .ip_address'
|
||||
curl -s http://localhost:8443/api/v1/agents/ag-web-prod/work | jq .
|
||||
|
||||
# Jobs
|
||||
curl -s http://localhost:8443/api/v1/jobs | jq '.total'
|
||||
curl -s "http://localhost:8443/api/v1/jobs?status=AwaitingApproval" | jq '.total'
|
||||
|
||||
# Approval
|
||||
curl -s -X POST http://localhost:8443/api/v1/jobs/JOB_ID_HERE/approve | jq .
|
||||
curl -s -X POST http://localhost:8443/api/v1/jobs/JOB_ID_HERE/reject \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"reason": "Not approved for this window"}' | jq .
|
||||
|
||||
# Discovery
|
||||
curl -s http://localhost:8443/api/v1/discovered-certificates | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/discovery-summary | jq .
|
||||
curl -s http://localhost:8443/api/v1/discovery-scans | jq '.total'
|
||||
|
||||
# Network scan targets
|
||||
curl -s http://localhost:8443/api/v1/network-scan-targets | jq '.total'
|
||||
curl -s -X POST http://localhost:8443/api/v1/network-scan-targets \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name": "test-scan", "cidrs": ["192.168.1.0/24"], "ports": [443, 8443]}' | jq .
|
||||
|
||||
# Policies, profiles, teams, owners, agent groups
|
||||
curl -s http://localhost:8443/api/v1/policies | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/profiles | jq '.data[] | {id, name, allowed_ekus}'
|
||||
curl -s http://localhost:8443/api/v1/teams | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/owners | jq '.total'
|
||||
curl -s http://localhost:8443/api/v1/agent-groups | jq '.total'
|
||||
|
||||
# Stats
|
||||
curl -s http://localhost:8443/api/v1/stats/summary | jq .
|
||||
curl -s http://localhost:8443/api/v1/stats/certificates-by-status | jq .
|
||||
curl -s "http://localhost:8443/api/v1/stats/expiration-timeline?days=90" | jq .
|
||||
curl -s "http://localhost:8443/api/v1/stats/job-trends?days=30" | jq .
|
||||
curl -s "http://localhost:8443/api/v1/stats/issuance-rate?days=30" | jq .
|
||||
|
||||
# Metrics
|
||||
curl -s http://localhost:8443/api/v1/metrics | jq .
|
||||
curl -s http://localhost:8443/api/v1/metrics/prometheus
|
||||
|
||||
# Audit
|
||||
curl -s http://localhost:8443/api/v1/audit | jq '.total'
|
||||
curl -s "http://localhost:8443/api/v1/audit?resource_type=certificate&action=revoke" | jq .
|
||||
|
||||
# Notifications
|
||||
curl -s http://localhost:8443/api/v1/notifications | jq '.total'
|
||||
|
||||
# Issuers and targets
|
||||
curl -s http://localhost:8443/api/v1/issuers | jq '.data[] | {id, name, type}'
|
||||
curl -s http://localhost:8443/api/v1/targets | jq '.data[] | {id, name, type, hostname}'
|
||||
```
|
||||
|
||||
### 11. EST Server (RFC 7030)
|
||||
|
||||
EST requires `CERTCTL_EST_ENABLED=true` in the server environment. Add it to docker-compose and restart:
|
||||
|
||||
```bash
|
||||
# Get CA certs (PKCS#7)
|
||||
curl -s http://localhost:8443/.well-known/est/cacerts
|
||||
|
||||
# Get CSR attributes
|
||||
curl -s http://localhost:8443/.well-known/est/csrattrs
|
||||
|
||||
# Simple enroll (requires a valid CSR in base64 DER or PEM format)
|
||||
# Generate a test CSR:
|
||||
openssl req -new -newkey rsa:2048 -nodes -keyout /tmp/test.key -subj "/CN=test.example.com" | \
|
||||
base64 -w0 | \
|
||||
curl -s -X POST http://localhost:8443/.well-known/est/simpleenroll \
|
||||
-H "Content-Type: application/pkcs10" \
|
||||
-d @-
|
||||
```
|
||||
|
||||
### 12. CLI Tool
|
||||
|
||||
```bash
|
||||
# Build CLI (requires Go)
|
||||
go build -o certctl-cli ./cmd/cli/
|
||||
|
||||
# Configure
|
||||
export CERTCTL_SERVER_URL=http://localhost:8443
|
||||
|
||||
# Test all subcommands
|
||||
./certctl-cli health
|
||||
./certctl-cli metrics
|
||||
./certctl-cli certs list
|
||||
./certctl-cli certs list --format json
|
||||
./certctl-cli certs get mc-api-prod
|
||||
./certctl-cli certs renew mc-api-prod
|
||||
./certctl-cli certs revoke mc-internal-staging --reason superseded
|
||||
./certctl-cli agents list
|
||||
./certctl-cli jobs list
|
||||
|
||||
# Bulk import
|
||||
echo "-----BEGIN CERTIFICATE-----
|
||||
... (paste a valid PEM cert) ...
|
||||
-----END CERTIFICATE-----" > /tmp/test-import.pem
|
||||
./certctl-cli import /tmp/test-import.pem
|
||||
```
|
||||
|
||||
### 13. Auth Flow (requires restart with auth enabled)
|
||||
|
||||
```bash
|
||||
# Restart with auth
|
||||
docker compose -f deploy/docker-compose.yml down
|
||||
CERTCTL_AUTH_TYPE=api-key CERTCTL_AUTH_SECRET=test-secret-key \
|
||||
docker compose -f deploy/docker-compose.yml up -d --build
|
||||
|
||||
# API should reject without key
|
||||
curl -s http://localhost:8443/api/v1/certificates
|
||||
# 401 Unauthorized
|
||||
|
||||
# API works with key
|
||||
curl -s -H "Authorization: Bearer test-secret-key" http://localhost:8443/api/v1/certificates | jq '.total'
|
||||
|
||||
# GUI should show login screen
|
||||
# Open http://localhost:8443 — enter "test-secret-key" — dashboard loads
|
||||
# Logout button in sidebar should clear auth and redirect to login
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Pre-Release Checklist
|
||||
|
||||
### Automated (CI must pass)
|
||||
|
||||
- [ ] `go vet ./...` — no issues
|
||||
- [ ] `golangci-lint run ./...` — no issues
|
||||
- [ ] `govulncheck ./...` — no known vulnerabilities
|
||||
- [ ] `go test -race` — no race conditions detected
|
||||
- [ ] Coverage thresholds met (service 55%+, handler 60%+, domain 40%+, middleware 30%+)
|
||||
- [ ] `npx tsc --noEmit` — no TypeScript errors
|
||||
- [ ] `npx vitest run` — all frontend tests pass (107+)
|
||||
- [ ] `npx vite build` — production build succeeds
|
||||
|
||||
### Manual (v2.1 release gate)
|
||||
|
||||
- [ ] Docker Compose starts cleanly from scratch (`down -v` then `up --build`)
|
||||
- [ ] All 16 sidebar navigation items load without console errors
|
||||
- [ ] Dashboard charts render with demo data
|
||||
- [ ] Certificate CRUD: list, detail, renew, deploy, revoke, archive all work
|
||||
- [ ] Bulk operations: multi-select, bulk renew, bulk revoke with progress bars
|
||||
- [ ] Export: PEM download and PKCS#12 download both produce valid files
|
||||
- [ ] Target wizard: all 7 target types show correct config fields (NGINX, Apache, HAProxy, Traefik, Caddy, F5, IIS)
|
||||
- [ ] Deployment timeline shows correct step progression
|
||||
- [ ] Jobs page: status/type filters, approval workflow (approve/reject with reason)
|
||||
- [ ] Discovery page: summary stats, claim/dismiss, scan history
|
||||
- [ ] Network scans: CRUD, trigger scan
|
||||
- [ ] Audit trail: time range filter, CSV export, JSON export
|
||||
- [ ] Prometheus endpoint returns valid exposition format
|
||||
- [ ] CLI: `health`, `certs list`, `certs get`, `agents list` all return data
|
||||
- [ ] Auth flow: login screen appears with auth enabled, API rejects without key
|
||||
|
||||
### Known Limitations
|
||||
|
||||
- EST enrollment requires `CERTCTL_EST_ENABLED=true` (off by default in demo)
|
||||
- Network scans will timeout scanning demo CIDRs (no real hosts) — this is expected
|
||||
- Agent keygen mode is `server` in demo (production uses `agent` for key isolation)
|
||||
- OCSP/CRL endpoints require the Local CA to have been used for issuance (demo uses seeded certs, not issued via Local CA — OCSP/CRL may return empty results)
|
||||
- Post-deployment TLS verification requires a real TLS endpoint to probe — not testable in basic demo setup
|
||||
- Verification migration (000008) needs to be added to docker-compose.yml for full feature availability
|
||||
|
||||
---
|
||||
|
||||
## Prioritized Test Backlog
|
||||
|
||||
For the engineering team to close gaps over the next 2-3 sprints:
|
||||
|
||||
**Sprint 1 (1 week):**
|
||||
1. Fix docker-compose migration gap (000008_verification)
|
||||
2. Add `service/deployment_test.go` — 15 tests for deployment orchestration
|
||||
3. Add `service/target_test.go` — 8 tests for target CRUD
|
||||
4. Add missing handler tests: UpdateAgentGroup, UpdateIssuer, Get/UpdateNetworkScanTarget
|
||||
|
||||
**Sprint 2 (1 week):**
|
||||
5. Add `CompleteAgentCSRRenewal` service tests — 8 tests
|
||||
6. Add `ExpireShortLivedCertificates` service tests — 4 tests
|
||||
7. Add domain model tests for `job.go`, `certificate.go`, `agent_group.go` — 20 tests
|
||||
8. Frontend: add error scenario tests for API client (404, 429, timeout) — 15 tests
|
||||
|
||||
**Sprint 3 (1-2 weeks):**
|
||||
9. Expand scheduler tests — test loop execution with mocked time
|
||||
10. Add agent binary tests — mock HTTP control plane, test heartbeat + CSR + deploy flows
|
||||
11. Frontend: add React component tests for LoginPage, CertificateDetailPage, TargetsPage wizard
|
||||
12. Context cancellation tests for long-running service operations
|
||||
Reference in New Issue
Block a user