mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:51:29 +00:00
283ec27ca4
PR-E of 6 in the M-2 end-to-end remediation sequence. Collapses the
HeartbeatWithContext wrapper into a single ctx-first Heartbeat method,
matching D-1 (ctx-only signatures, no dual forms). The handler-facing
method name is preserved (D-4) — internal/api/handler/agents.go already
declares `Heartbeat(ctx, ...)` on its local service interface, and the
handler mock at internal/api/handler/agent_handler_test.go already
takes `_ context.Context` as its first param, so no handler churn.
Changes
-------
internal/service/agent.go
- Delete the zero-body Heartbeat wrapper that forwarded to
HeartbeatWithContext with context.Background().
- Rename HeartbeatWithContext → Heartbeat (ctx-bearing body
folded directly into the canonical method).
internal/service/agent_test.go
- TestHeartbeat (L95) and TestHeartbeat_NotFound (L128):
agentService.HeartbeatWithContext(ctx, ...) → .Heartbeat(ctx, ...).
internal/service/concurrent_test.go
- L162: agentSvc.HeartbeatWithContext(ctx, agentID, metadata)
→ .Heartbeat(ctx, agentID, metadata).
internal/service/context_test.go
- L179 + L232: agentSvc.HeartbeatWithContext(ctx, ...) → .Heartbeat(...)
- L185 + L238 t.Logf strings: "HeartbeatWithContext with ..." →
"Heartbeat with ..." to match the collapsed method name.
Verification (Go 1.25.9 linux/arm64, CI-parity caches)
------------------------------------------------------
go build ./... clean
go vet ./... clean
go test -short ./internal/service/... ./internal/api/handler/... \
./internal/integration/... all ok
go test -race -short same set all ok
go test -short ./... all packages ok
golangci-lint run ./... 0 issues
Locked decisions from the M-2 plan:
D-1 ctx-only signatures (no dual forms)
D-4 preserve handler method names facing the router
D-5 domain types stay ctx-free
Audit complete. Commit: 1f6cf0eafa. Sections: 12. Findings: 2/7/10/4/6.
240 lines
7.2 KiB
Go
240 lines
7.2 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
"github.com/shankar0123/certctl/internal/repository"
|
|
)
|
|
|
|
// TestCertificateService_ListWithCancelledContext verifies that List respects a cancelled context
|
|
func TestCertificateService_ListWithCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mockCertRepo := newMockCertificateRepository()
|
|
certSvc := NewCertificateService(mockCertRepo, nil, nil)
|
|
|
|
_, _, err := certSvc.List(ctx, &repository.CertificateFilter{})
|
|
|
|
// The service should propagate context cancellation errors
|
|
// even though our mock may not check context, we verify the call goes through
|
|
// and the context error becomes part of the error chain
|
|
if err == nil || ctx.Err() == context.Canceled {
|
|
// Either the service respects context and returns an error,
|
|
// or the context was cancelled. Both are valid findings.
|
|
return
|
|
}
|
|
t.Logf("List with cancelled context returned: %v", err)
|
|
}
|
|
|
|
// TestCertificateService_GetWithCancelledContext verifies that Get respects a cancelled context
|
|
func TestCertificateService_GetWithCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mockCertRepo := newMockCertificateRepository()
|
|
mockCertRepo.AddCert(&domain.ManagedCertificate{ID: "mc-test-1", CommonName: "test.example.com"})
|
|
certSvc := NewCertificateService(mockCertRepo, nil, nil)
|
|
|
|
_, err := certSvc.Get(ctx, "mc-test-1")
|
|
|
|
// Service should handle cancelled context
|
|
if err == nil || ctx.Err() == context.Canceled {
|
|
return
|
|
}
|
|
t.Logf("Get with cancelled context returned: %v", err)
|
|
}
|
|
|
|
// TestRenewalService_ProcessWithCancelledContext verifies that renewal processing respects a cancelled context
|
|
func TestRenewalService_ProcessWithCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mockCertRepo := newMockCertificateRepository()
|
|
mockJobRepo := newMockJobRepository()
|
|
mockPolicyRepo := newMockRenewalPolicyRepository()
|
|
mockProfileRepo := &mockCertificateProfileRepository{
|
|
Profiles: make(map[string]*domain.CertificateProfile),
|
|
}
|
|
mockAuditSvc := &AuditService{auditRepo: newMockAuditRepository()}
|
|
mockNotifSvc := &NotificationService{
|
|
notifRepo: newMockNotificationRepository(),
|
|
ownerRepo: nil,
|
|
notifierRegistry: make(map[string]Notifier),
|
|
}
|
|
|
|
issuerRegistry := NewIssuerRegistry(slog.Default())
|
|
renewalSvc := NewRenewalService(
|
|
mockCertRepo,
|
|
mockJobRepo,
|
|
mockPolicyRepo,
|
|
mockProfileRepo,
|
|
mockAuditSvc,
|
|
mockNotifSvc,
|
|
issuerRegistry,
|
|
"agent",
|
|
)
|
|
|
|
// Attempt to check expiring certificates with cancelled context
|
|
err := renewalSvc.CheckExpiringCertificates(ctx)
|
|
|
|
// Should handle cancelled context gracefully
|
|
if err == nil || ctx.Err() == context.Canceled {
|
|
return
|
|
}
|
|
t.Logf("CheckExpiringCertificates with cancelled context returned: %v", err)
|
|
}
|
|
|
|
// mockCertificateProfileRepository is a mock for testing
|
|
type mockCertificateProfileRepository struct {
|
|
Profiles map[string]*domain.CertificateProfile
|
|
GetErr error
|
|
ListErr error
|
|
}
|
|
|
|
func (m *mockCertificateProfileRepository) List(ctx context.Context) ([]*domain.CertificateProfile, error) {
|
|
if m.ListErr != nil {
|
|
return nil, m.ListErr
|
|
}
|
|
var profiles []*domain.CertificateProfile
|
|
for _, p := range m.Profiles {
|
|
profiles = append(profiles, p)
|
|
}
|
|
return profiles, nil
|
|
}
|
|
|
|
func (m *mockCertificateProfileRepository) Get(ctx context.Context, id string) (*domain.CertificateProfile, error) {
|
|
if m.GetErr != nil {
|
|
return nil, m.GetErr
|
|
}
|
|
profile, ok := m.Profiles[id]
|
|
if !ok {
|
|
return nil, errNotFound
|
|
}
|
|
return profile, nil
|
|
}
|
|
|
|
func (m *mockCertificateProfileRepository) Create(ctx context.Context, profile *domain.CertificateProfile) error {
|
|
m.Profiles[profile.ID] = profile
|
|
return nil
|
|
}
|
|
|
|
func (m *mockCertificateProfileRepository) Update(ctx context.Context, profile *domain.CertificateProfile) error {
|
|
m.Profiles[profile.ID] = profile
|
|
return nil
|
|
}
|
|
|
|
func (m *mockCertificateProfileRepository) Delete(ctx context.Context, id string) error {
|
|
delete(m.Profiles, id)
|
|
return nil
|
|
}
|
|
|
|
// TestTargetService_ListWithCancelledContext verifies that target listing respects a cancelled context
|
|
func TestTargetService_ListWithCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mockTargetRepo := &mockTargetRepo{
|
|
Targets: make(map[string]*domain.DeploymentTarget),
|
|
}
|
|
targetSvc := NewTargetService(mockTargetRepo, nil, nil, "", slog.New(slog.NewTextHandler(os.Stderr, nil)))
|
|
|
|
_, _, err := targetSvc.List(ctx, 1, 50)
|
|
|
|
// Service should handle cancelled context
|
|
if err == nil || ctx.Err() == context.Canceled {
|
|
return
|
|
}
|
|
t.Logf("TargetService.List with cancelled context returned: %v", err)
|
|
}
|
|
|
|
// TestAgentService_HeartbeatWithCancelledContext verifies that heartbeat respects a cancelled context
|
|
func TestAgentService_HeartbeatWithCancelledContext(t *testing.T) {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mockAgentRepo := newMockAgentRepository()
|
|
mockAgentRepo.AddAgent(&domain.Agent{
|
|
ID: "agent-1",
|
|
Name: "test-agent",
|
|
Hostname: "localhost",
|
|
})
|
|
|
|
issuerRegistry := NewIssuerRegistry(slog.Default())
|
|
agentSvc := NewAgentService(
|
|
mockAgentRepo,
|
|
nil, // certRepo
|
|
nil, // jobRepo
|
|
nil, // targetRepo
|
|
nil, // auditService
|
|
issuerRegistry,
|
|
nil, // renewalService
|
|
)
|
|
|
|
err := agentSvc.Heartbeat(ctx, "agent-1", &domain.AgentMetadata{})
|
|
|
|
// Service should handle cancelled context
|
|
if err == nil || ctx.Err() == context.Canceled {
|
|
return
|
|
}
|
|
t.Logf("Heartbeat with cancelled context returned: %v", err)
|
|
}
|
|
|
|
// Test with timeout context (should trigger deadline exceeded)
|
|
func TestCertificateService_ListWithDeadlineExceeded(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 0) // Immediate timeout
|
|
defer cancel()
|
|
|
|
mockCertRepo := newMockCertificateRepository()
|
|
certSvc := NewCertificateService(mockCertRepo, nil, nil)
|
|
|
|
time.Sleep(10 * time.Millisecond) // Ensure deadline is exceeded
|
|
|
|
_, _, err := certSvc.List(ctx, &repository.CertificateFilter{})
|
|
|
|
// Should handle deadline exceeded gracefully
|
|
if err == nil || ctx.Err() == context.DeadlineExceeded {
|
|
return
|
|
}
|
|
t.Logf("List with deadline exceeded returned: %v", err)
|
|
}
|
|
|
|
// Test with timeout context on agent heartbeat
|
|
func TestAgentService_HeartbeatWithDeadlineExceeded(t *testing.T) {
|
|
ctx, cancel := context.WithTimeout(context.Background(), 0) // Immediate timeout
|
|
defer cancel()
|
|
|
|
mockAgentRepo := newMockAgentRepository()
|
|
mockAgentRepo.AddAgent(&domain.Agent{
|
|
ID: "agent-1",
|
|
Name: "test-agent",
|
|
Hostname: "localhost",
|
|
})
|
|
|
|
issuerRegistry := NewIssuerRegistry(slog.Default())
|
|
agentSvc := NewAgentService(
|
|
mockAgentRepo,
|
|
nil, // certRepo
|
|
nil, // jobRepo
|
|
nil, // targetRepo
|
|
nil, // auditService
|
|
issuerRegistry,
|
|
nil, // renewalService
|
|
)
|
|
|
|
time.Sleep(10 * time.Millisecond) // Ensure deadline is exceeded
|
|
|
|
err := agentSvc.Heartbeat(ctx, "agent-1", &domain.AgentMetadata{})
|
|
|
|
// Service should handle deadline exceeded
|
|
if err == nil || ctx.Err() == context.DeadlineExceeded {
|
|
return
|
|
}
|
|
t.Logf("Heartbeat with deadline exceeded returned: %v", err)
|
|
}
|