Files
certctl/internal/service/audit_test.go
T
Shankar 6daf0cd33d Implement M4: comprehensive test coverage with 120 tests
Service layer (63 tests): certificate, agent, audit, job, notification,
policy, and renewal services with mock repositories covering threshold
alerting, deduplication, status transitions, and job processing.

Handler layer (46 tests): certificate and agent HTTP handlers using
httptest with mock service interfaces, covering success/error paths,
pagination, JSON marshaling, and path parameter extraction.

Integration (11 subtests): end-to-end certificate lifecycle test
exercising real services and Local CA issuer through HTTP API —
create cert, trigger renewal, process jobs, register agent, heartbeat,
verify audit trail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 00:25:01 -04:00

330 lines
8.0 KiB
Go

package service
import (
"context"
"encoding/json"
"testing"
"time"
"github.com/shankar0123/certctl/internal/domain"
"github.com/shankar0123/certctl/internal/repository"
)
func TestRecordEvent(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
err := service.RecordEvent(ctx, "user123", domain.ActorTypeUser, "certificate_created", "certificate", "cert-001", map[string]interface{}{"common_name": "example.com"})
if err != nil {
t.Fatalf("RecordEvent failed: %v", err)
}
if len(auditRepo.Events) != 1 {
t.Errorf("expected 1 event, got %d", len(auditRepo.Events))
}
event := auditRepo.Events[0]
if event.Actor != "user123" {
t.Errorf("expected actor user123, got %s", event.Actor)
}
if event.ActorType != domain.ActorTypeUser {
t.Errorf("expected actor type User, got %s", event.ActorType)
}
if event.Action != "certificate_created" {
t.Errorf("expected action certificate_created, got %s", event.Action)
}
if event.ResourceType != "certificate" {
t.Errorf("expected resource type certificate, got %s", event.ResourceType)
}
if event.ResourceID != "cert-001" {
t.Errorf("expected resource ID cert-001, got %s", event.ResourceID)
}
}
func TestRecordEvent_RepoError(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
CreateErr: errNotFound,
}
service := NewAuditService(auditRepo)
err := service.RecordEvent(ctx, "user123", domain.ActorTypeUser, "test_action", "resource", "res-001", map[string]interface{}{})
if err == nil {
t.Fatal("expected error, got nil")
}
}
func TestListByResource(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
event1 := &domain.AuditEvent{
ID: "audit-1",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "created",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: time.Now(),
}
event2 := &domain.AuditEvent{
ID: "audit-2",
Actor: "user2",
ActorType: domain.ActorTypeUser,
Action: "updated",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: time.Now(),
}
event3 := &domain.AuditEvent{
ID: "audit-3",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "created",
ResourceType: "certificate",
ResourceID: "cert-002",
Timestamp: time.Now(),
}
auditRepo.AddEvent(event1)
auditRepo.AddEvent(event2)
auditRepo.AddEvent(event3)
events, err := service.ListByResource(ctx, "certificate", "cert-001")
if err != nil {
t.Fatalf("ListByResource failed: %v", err)
}
if len(events) != 2 {
t.Errorf("expected 2 events, got %d", len(events))
}
}
func TestListByActor(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
event1 := &domain.AuditEvent{
ID: "audit-1",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "created",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: time.Now(),
}
event2 := &domain.AuditEvent{
ID: "audit-2",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "updated",
ResourceType: "certificate",
ResourceID: "cert-002",
Timestamp: time.Now(),
}
event3 := &domain.AuditEvent{
ID: "audit-3",
Actor: "user2",
ActorType: domain.ActorTypeUser,
Action: "created",
ResourceType: "certificate",
ResourceID: "cert-003",
Timestamp: time.Now(),
}
auditRepo.AddEvent(event1)
auditRepo.AddEvent(event2)
auditRepo.AddEvent(event3)
events, err := service.ListByActor(ctx, "user1")
if err != nil {
t.Fatalf("ListByActor failed: %v", err)
}
if len(events) != 2 {
t.Errorf("expected 2 events, got %d", len(events))
}
}
func TestListByAction(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
now := time.Now()
from := now.Add(-1 * time.Hour)
to := now.Add(1 * time.Hour)
event1 := &domain.AuditEvent{
ID: "audit-1",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "certificate_created",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: now.Add(-30 * time.Minute),
}
event2 := &domain.AuditEvent{
ID: "audit-2",
Actor: "user2",
ActorType: domain.ActorTypeUser,
Action: "certificate_created",
ResourceType: "certificate",
ResourceID: "cert-002",
Timestamp: now.Add(-20 * time.Minute),
}
event3 := &domain.AuditEvent{
ID: "audit-3",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "certificate_updated",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: now.Add(-10 * time.Minute),
}
auditRepo.AddEvent(event1)
auditRepo.AddEvent(event2)
auditRepo.AddEvent(event3)
events, err := service.ListByAction(ctx, "certificate_created", from, to)
if err != nil {
t.Fatalf("ListByAction failed: %v", err)
}
if len(events) != 2 {
t.Errorf("expected 2 events, got %d", len(events))
}
for _, e := range events {
if e.Action != "certificate_created" {
t.Errorf("expected action certificate_created, got %s", e.Action)
}
}
}
func TestListByAction_EmptyRange(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
now := time.Now()
from := now.Add(1 * time.Hour)
to := now.Add(2 * time.Hour)
event := &domain.AuditEvent{
ID: "audit-1",
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "certificate_created",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: now.Add(-30 * time.Minute),
}
auditRepo.AddEvent(event)
events, err := service.ListByAction(ctx, "certificate_created", from, to)
if err != nil {
t.Fatalf("ListByAction failed: %v", err)
}
if len(events) != 0 {
t.Errorf("expected 0 events, got %d", len(events))
}
}
func TestRecordEvent_ComplexDetails(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
details := map[string]interface{}{
"common_name": "example.com",
"sans": []string{"www.example.com", "api.example.com"},
"issuer_id": "iss-123",
"count": 5,
}
err := service.RecordEvent(ctx, "user1", domain.ActorTypeUser, "certificate_created", "certificate", "cert-001", details)
if err != nil {
t.Fatalf("RecordEvent failed: %v", err)
}
event := auditRepo.Events[0]
var decoded map[string]interface{}
err = json.Unmarshal(event.Details, &decoded)
if err != nil {
t.Fatalf("failed to unmarshal details: %v", err)
}
if decoded["common_name"] != "example.com" {
t.Errorf("expected common_name example.com, got %v", decoded["common_name"])
}
}
func TestList(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
Events: []*domain.AuditEvent{},
}
service := NewAuditService(auditRepo)
for i := 0; i < 5; i++ {
event := &domain.AuditEvent{
ID: "audit-" + string(rune(i)),
Actor: "user1",
ActorType: domain.ActorTypeUser,
Action: "test",
ResourceType: "certificate",
ResourceID: "cert-001",
Timestamp: time.Now(),
}
auditRepo.AddEvent(event)
}
filter := &repository.AuditFilter{
Page: 1,
PerPage: 10,
}
events, err := service.List(ctx, filter)
if err != nil {
t.Fatalf("List failed: %v", err)
}
if len(events) != 5 {
t.Errorf("expected 5 events, got %d", len(events))
}
}
func TestList_RepoError(t *testing.T) {
ctx := context.Background()
auditRepo := &mockAuditRepo{
ListErr: errNotFound,
}
service := NewAuditService(auditRepo)
filter := &repository.AuditFilter{}
_, err := service.List(ctx, filter)
if err == nil {
t.Fatal("expected error, got nil")
}
}