mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:41:41 +00:00
03472072b8
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>
413 lines
11 KiB
Go
413 lines
11 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// newTestTargetService creates a TargetService with mock repositories for testing.
|
|
func newTestTargetService() (*TargetService, *mockTargetRepo, *mockAuditRepo) {
|
|
targetRepo := &mockTargetRepo{Targets: make(map[string]*domain.DeploymentTarget)}
|
|
auditRepo := newMockAuditRepository()
|
|
auditSvc := NewAuditService(auditRepo)
|
|
return NewTargetService(targetRepo, auditSvc), targetRepo, auditRepo
|
|
}
|
|
|
|
func TestTargetService_List_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Add 3 targets
|
|
target1 := &domain.DeploymentTarget{ID: "t-1", Name: "Target 1", Type: domain.TargetTypeNGINX}
|
|
target2 := &domain.DeploymentTarget{ID: "t-2", Name: "Target 2", Type: domain.TargetTypeApache}
|
|
target3 := &domain.DeploymentTarget{ID: "t-3", Name: "Target 3", Type: domain.TargetTypeHAProxy}
|
|
targetRepo.AddTarget(target1)
|
|
targetRepo.AddTarget(target2)
|
|
targetRepo.AddTarget(target3)
|
|
|
|
// Request page 1, perPage 2
|
|
targets, total, err := svc.List(ctx, 1, 2)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(targets) != 2 {
|
|
t.Errorf("expected 2 targets, got %d", len(targets))
|
|
}
|
|
|
|
if total != 3 {
|
|
t.Errorf("expected total=3, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_List_DefaultPagination(t *testing.T) {
|
|
svc, _, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Call with invalid pagination (page=0, perPage=0)
|
|
targets, total, err := svc.List(ctx, 0, 0)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Should not panic; should use defaults (page=1, perPage=50)
|
|
if targets != nil || total != 0 {
|
|
t.Errorf("expected empty list with defaults, got %d targets", len(targets))
|
|
}
|
|
}
|
|
|
|
func TestTargetService_List_EmptyPage(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Add 3 targets
|
|
target1 := &domain.DeploymentTarget{ID: "t-1", Name: "Target 1", Type: domain.TargetTypeNGINX}
|
|
target2 := &domain.DeploymentTarget{ID: "t-2", Name: "Target 2", Type: domain.TargetTypeApache}
|
|
target3 := &domain.DeploymentTarget{ID: "t-3", Name: "Target 3", Type: domain.TargetTypeHAProxy}
|
|
targetRepo.AddTarget(target1)
|
|
targetRepo.AddTarget(target2)
|
|
targetRepo.AddTarget(target3)
|
|
|
|
// Request page 2 with perPage 10 (beyond available data)
|
|
targets, total, err := svc.List(ctx, 2, 10)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(targets) != 0 {
|
|
t.Errorf("expected 0 targets, got %d", len(targets))
|
|
}
|
|
|
|
if total != 3 {
|
|
t.Errorf("expected total=3, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_List_RepoError(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Set repo to return error
|
|
targetRepo.ListErr = errNotFound
|
|
|
|
targets, total, err := svc.List(ctx, 1, 50)
|
|
if err == nil {
|
|
t.Fatalf("expected error, got nil")
|
|
}
|
|
|
|
if targets != nil || total != 0 {
|
|
t.Errorf("expected nil targets and zero total, got %d targets and %d total", len(targets), total)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Get_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
target := &domain.DeploymentTarget{ID: "t-1", Name: "Target 1", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(target)
|
|
|
|
result, err := svc.Get(ctx, "t-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID != "t-1" || result.Name != "Target 1" {
|
|
t.Errorf("expected target t-1/Target 1, got %s/%s", result.ID, result.Name)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Get_NotFound(t *testing.T) {
|
|
svc, _, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
result, err := svc.Get(ctx, "nonexistent")
|
|
if err == nil {
|
|
t.Fatalf("expected error for nonexistent target, got nil")
|
|
}
|
|
|
|
if result != nil {
|
|
t.Errorf("expected nil result, got %v", result)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Create_Success(t *testing.T) {
|
|
svc, targetRepo, auditRepo := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
target := &domain.DeploymentTarget{
|
|
Name: "New Target",
|
|
Type: domain.TargetTypeNGINX,
|
|
Config: json.RawMessage(`{"path": "/etc/nginx/certs"}`),
|
|
}
|
|
|
|
err := svc.Create(ctx, target, "test-actor")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify target was stored
|
|
if target.ID == "" || len(target.ID) < 7 || target.ID[:6] != "target" {
|
|
t.Errorf("expected ID to start with 'target', got %s", target.ID)
|
|
}
|
|
|
|
stored, ok := targetRepo.Targets[target.ID]
|
|
if !ok {
|
|
t.Fatalf("target not stored in repo")
|
|
}
|
|
|
|
if stored.Name != "New Target" {
|
|
t.Errorf("expected name 'New Target', got %s", stored.Name)
|
|
}
|
|
|
|
// Verify timestamps are set
|
|
if target.CreatedAt.IsZero() || target.UpdatedAt.IsZero() {
|
|
t.Errorf("expected timestamps to be set, CreatedAt=%v, UpdatedAt=%v", target.CreatedAt, target.UpdatedAt)
|
|
}
|
|
|
|
// Verify audit event
|
|
if len(auditRepo.Events) == 0 {
|
|
t.Fatalf("expected audit event, got none")
|
|
}
|
|
|
|
lastEvent := auditRepo.Events[len(auditRepo.Events)-1]
|
|
if lastEvent.Action != "create_target" {
|
|
t.Errorf("expected action 'create_target', got %s", lastEvent.Action)
|
|
}
|
|
|
|
if lastEvent.Actor != "test-actor" {
|
|
t.Errorf("expected actor 'test-actor', got %s", lastEvent.Actor)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Create_MissingName(t *testing.T) {
|
|
svc, _, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
target := &domain.DeploymentTarget{
|
|
Type: domain.TargetTypeNGINX,
|
|
}
|
|
|
|
err := svc.Create(ctx, target, "test-actor")
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing name, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Create_RepoError(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
targetRepo.CreateErr = errNotFound
|
|
|
|
target := &domain.DeploymentTarget{
|
|
Name: "New Target",
|
|
Type: domain.TargetTypeNGINX,
|
|
}
|
|
|
|
err := svc.Create(ctx, target, "test-actor")
|
|
if err == nil {
|
|
t.Fatalf("expected error from repo, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Update_Success(t *testing.T) {
|
|
svc, targetRepo, auditRepo := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Create initial target
|
|
existing := &domain.DeploymentTarget{ID: "t-1", Name: "Old Name", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(existing)
|
|
|
|
// Update it
|
|
updated := &domain.DeploymentTarget{
|
|
Name: "New Name",
|
|
Type: domain.TargetTypeApache,
|
|
}
|
|
|
|
err := svc.Update(ctx, "t-1", updated, "test-actor")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify update
|
|
stored := targetRepo.Targets["t-1"]
|
|
if stored.Name != "New Name" {
|
|
t.Errorf("expected name 'New Name', got %s", stored.Name)
|
|
}
|
|
|
|
// Verify audit event
|
|
if len(auditRepo.Events) == 0 {
|
|
t.Fatalf("expected audit event, got none")
|
|
}
|
|
|
|
lastEvent := auditRepo.Events[len(auditRepo.Events)-1]
|
|
if lastEvent.Action != "update_target" {
|
|
t.Errorf("expected action 'update_target', got %s", lastEvent.Action)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Update_MissingName(t *testing.T) {
|
|
svc, _, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
target := &domain.DeploymentTarget{
|
|
Type: domain.TargetTypeNGINX,
|
|
}
|
|
|
|
err := svc.Update(ctx, "t-1", target, "test-actor")
|
|
if err == nil {
|
|
t.Fatalf("expected error for missing name, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Delete_Success(t *testing.T) {
|
|
svc, targetRepo, auditRepo := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
// Create initial target
|
|
target := &domain.DeploymentTarget{ID: "t-1", Name: "Target To Delete", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(target)
|
|
|
|
// Delete it
|
|
err := svc.Delete(ctx, "t-1", "test-actor")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify deletion
|
|
if _, ok := targetRepo.Targets["t-1"]; ok {
|
|
t.Errorf("target should be deleted from repo")
|
|
}
|
|
|
|
// Verify audit event
|
|
if len(auditRepo.Events) == 0 {
|
|
t.Fatalf("expected audit event, got none")
|
|
}
|
|
|
|
lastEvent := auditRepo.Events[len(auditRepo.Events)-1]
|
|
if lastEvent.Action != "delete_target" {
|
|
t.Errorf("expected action 'delete_target', got %s", lastEvent.Action)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_Delete_RepoError(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
ctx := context.Background()
|
|
|
|
targetRepo.DeleteErr = errNotFound
|
|
|
|
err := svc.Delete(ctx, "t-1", "test-actor")
|
|
if err == nil {
|
|
t.Fatalf("expected error from repo, got nil")
|
|
}
|
|
}
|
|
|
|
func TestTargetService_ListTargets_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
|
|
// Add targets
|
|
target1 := &domain.DeploymentTarget{ID: "t-1", Name: "Target 1", Type: domain.TargetTypeNGINX}
|
|
target2 := &domain.DeploymentTarget{ID: "t-2", Name: "Target 2", Type: domain.TargetTypeApache}
|
|
targetRepo.AddTarget(target1)
|
|
targetRepo.AddTarget(target2)
|
|
|
|
// Call handler-interface method
|
|
targets, total, err := svc.ListTargets(1, 50)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if len(targets) != 2 {
|
|
t.Errorf("expected 2 targets, got %d", len(targets))
|
|
}
|
|
|
|
if total != 2 {
|
|
t.Errorf("expected total=2, got %d", total)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_GetTarget_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
|
|
target := &domain.DeploymentTarget{ID: "t-1", Name: "Target 1", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(target)
|
|
|
|
result, err := svc.GetTarget("t-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID != "t-1" || result.Name != "Target 1" {
|
|
t.Errorf("expected target t-1/Target 1, got %s/%s", result.ID, result.Name)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_CreateTarget_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
|
|
target := domain.DeploymentTarget{
|
|
Name: "New Target",
|
|
Type: domain.TargetTypeNGINX,
|
|
}
|
|
|
|
result, err := svc.CreateTarget(target)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.ID == "" || len(result.ID) < 7 || result.ID[:6] != "target" {
|
|
t.Errorf("expected ID to start with 'target', got %s", result.ID)
|
|
}
|
|
|
|
// Verify it was stored
|
|
if _, ok := targetRepo.Targets[result.ID]; !ok {
|
|
t.Fatalf("target not stored in repo")
|
|
}
|
|
}
|
|
|
|
func TestTargetService_UpdateTarget_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
|
|
// Create initial target
|
|
target := &domain.DeploymentTarget{ID: "t-1", Name: "Old Name", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(target)
|
|
|
|
// Update it
|
|
updated := domain.DeploymentTarget{
|
|
Name: "New Name",
|
|
Type: domain.TargetTypeApache,
|
|
}
|
|
|
|
result, err := svc.UpdateTarget("t-1", updated)
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
if result.Name != "New Name" {
|
|
t.Errorf("expected name 'New Name', got %s", result.Name)
|
|
}
|
|
}
|
|
|
|
func TestTargetService_DeleteTarget_Success(t *testing.T) {
|
|
svc, targetRepo, _ := newTestTargetService()
|
|
|
|
// Create initial target
|
|
target := &domain.DeploymentTarget{ID: "t-1", Name: "Target To Delete", Type: domain.TargetTypeNGINX}
|
|
targetRepo.AddTarget(target)
|
|
|
|
// Delete it
|
|
err := svc.DeleteTarget("t-1")
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
|
|
// Verify deletion
|
|
if _, ok := targetRepo.Targets["t-1"]; ok {
|
|
t.Errorf("target should be deleted from repo")
|
|
}
|
|
}
|