mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 08:08:51 +00:00
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>
This commit is contained in:
@@ -0,0 +1,467 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
func TestRegisterAgent(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: make(map[string]*domain.Agent),
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
agent, apiKey, err := agentService.Register(ctx, "prod-agent-1", "server-01.example.com")
|
||||
if err != nil {
|
||||
t.Fatalf("Register failed: %v", err)
|
||||
}
|
||||
|
||||
if agent.Name != "prod-agent-1" {
|
||||
t.Errorf("expected name prod-agent-1, got %s", agent.Name)
|
||||
}
|
||||
if agent.Hostname != "server-01.example.com" {
|
||||
t.Errorf("expected hostname server-01.example.com, got %s", agent.Hostname)
|
||||
}
|
||||
if agent.Status != domain.AgentStatusOnline {
|
||||
t.Errorf("expected status Online, got %s", agent.Status)
|
||||
}
|
||||
if apiKey == "" {
|
||||
t.Fatal("expected non-empty API key")
|
||||
}
|
||||
|
||||
if len(agentRepo.Agents) != 1 {
|
||||
t.Errorf("expected 1 agent in repo, got %d", len(agentRepo.Agents))
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeat(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "prod-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash123",
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
err := agentService.HeartbeatWithContext(ctx, "agent-001")
|
||||
if err != nil {
|
||||
t.Fatalf("Heartbeat failed: %v", err)
|
||||
}
|
||||
|
||||
if _, ok := agentRepo.HeartbeatUpdates["agent-001"]; !ok {
|
||||
t.Fatal("heartbeat not recorded")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeartbeat_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: make(map[string]*domain.Agent),
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
err := agentService.HeartbeatWithContext(ctx, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent agent")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetPendingWork(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "prod-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash123",
|
||||
}
|
||||
|
||||
job1 := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
}
|
||||
job2 := &domain.Job{
|
||||
ID: "job-002",
|
||||
Type: domain.JobTypeRenewal,
|
||||
CertificateID: "cert-002",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job1, "job-002": job2},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
jobs, err := agentService.GetPendingWork(ctx, "agent-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetPendingWork failed: %v", err)
|
||||
}
|
||||
|
||||
if len(jobs) != 1 {
|
||||
t.Errorf("expected 1 deployment job, got %d", len(jobs))
|
||||
}
|
||||
if jobs[0].Type != domain.JobTypeDeployment {
|
||||
t.Errorf("expected JobTypeDeployment, got %s", jobs[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportJobStatus(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "prod-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash123",
|
||||
}
|
||||
job := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusRunning,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
err := agentService.ReportJobStatus(ctx, "agent-001", "job-001", domain.JobStatusCompleted, "")
|
||||
if err != nil {
|
||||
t.Fatalf("ReportJobStatus failed: %v", err)
|
||||
}
|
||||
|
||||
if jobRepo.StatusUpdates["job-001"] != domain.JobStatusCompleted {
|
||||
t.Errorf("expected status Completed, got %s", jobRepo.StatusUpdates["job-001"])
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkStaleAgentsOffline(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
staleTime := now.Add(-3 * time.Hour)
|
||||
|
||||
agent1 := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "online-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash1",
|
||||
}
|
||||
agent2 := &domain.Agent{
|
||||
ID: "agent-002",
|
||||
Name: "stale-agent",
|
||||
Hostname: "server-02",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now.Add(-24 * time.Hour),
|
||||
LastHeartbeatAt: &staleTime,
|
||||
APIKeyHash: "hash2",
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent1, "agent-002": agent2},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
err := agentService.MarkStaleAgentsOffline(ctx, 1*time.Hour)
|
||||
if err != nil {
|
||||
t.Fatalf("MarkStaleAgentsOffline failed: %v", err)
|
||||
}
|
||||
|
||||
if agentRepo.Agents["agent-001"].Status != domain.AgentStatusOnline {
|
||||
t.Errorf("expected agent-001 to be Online, got %s", agentRepo.Agents["agent-001"].Status)
|
||||
}
|
||||
if agentRepo.Agents["agent-002"].Status != domain.AgentStatusOffline {
|
||||
t.Errorf("expected agent-002 to be Offline, got %s", agentRepo.Agents["agent-002"].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCSR(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "prod-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash123",
|
||||
}
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-local",
|
||||
Status: domain.CertificateStatusPending,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
issuerConnector := &mockIssuerConnector{
|
||||
Result: &IssuanceResult{
|
||||
Serial: "serial-123",
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----",
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0),
|
||||
},
|
||||
}
|
||||
issuerRegistry := map[string]IssuerConnector{"iss-local": issuerConnector}
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
csrPEM := "-----BEGIN CERTIFICATE REQUEST-----\ntest-csr\n-----END CERTIFICATE REQUEST-----"
|
||||
err := agentService.SubmitCSR(ctx, "agent-001", "cert-001", []byte(csrPEM))
|
||||
if err != nil {
|
||||
t.Fatalf("SubmitCSR failed: %v", err)
|
||||
}
|
||||
|
||||
if len(certRepo.Versions["cert-001"]) != 1 {
|
||||
t.Errorf("expected 1 certificate version, got %d", len(certRepo.Versions["cert-001"]))
|
||||
}
|
||||
|
||||
if cert.Status != domain.CertificateStatusActive {
|
||||
t.Errorf("expected certificate status Active, got %s", cert.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCSR_EmptyCSR(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
agent := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "prod-agent",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash123",
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
err := agentService.SubmitCSR(ctx, "agent-001", "", []byte{})
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty CSR")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAgents(t *testing.T) {
|
||||
now := time.Now()
|
||||
agent1 := &domain.Agent{
|
||||
ID: "agent-001",
|
||||
Name: "agent1",
|
||||
Hostname: "server-01",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash1",
|
||||
}
|
||||
agent2 := &domain.Agent{
|
||||
ID: "agent-002",
|
||||
Name: "agent2",
|
||||
Hostname: "server-02",
|
||||
Status: domain.AgentStatusOnline,
|
||||
RegisteredAt: now,
|
||||
LastHeartbeatAt: &now,
|
||||
APIKeyHash: "hash2",
|
||||
}
|
||||
|
||||
agentRepo := &mockAgentRepo{
|
||||
Agents: map[string]*domain.Agent{"agent-001": agent1, "agent-002": agent2},
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
targetRepo := &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
issuerRegistry := make(map[string]IssuerConnector)
|
||||
|
||||
agentService := NewAgentService(agentRepo, certRepo, jobRepo, targetRepo, auditService, issuerRegistry)
|
||||
|
||||
agents, total, err := agentService.ListAgents(1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListAgents failed: %v", err)
|
||||
}
|
||||
|
||||
if len(agents) != 2 {
|
||||
t.Errorf("expected 2 agents, got %d", len(agents))
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
func TestCreateCertificate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{
|
||||
Events: []*domain.AuditEvent{},
|
||||
}
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: make(map[string]*domain.PolicyRule),
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
now := time.Now()
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
Name: "api-prod",
|
||||
CommonName: "api.example.com",
|
||||
SANs: []string{"api.example.com"},
|
||||
Environment: "production",
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-acme",
|
||||
TargetIDs: []string{"target-1"},
|
||||
RenewalPolicyID: "policy-1",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
Tags: map[string]string{"env": "prod"},
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
err := certService.Create(ctx, cert, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Create failed: %v", err)
|
||||
}
|
||||
|
||||
if len(certRepo.Certs) != 1 {
|
||||
t.Errorf("expected 1 cert, got %d", len(certRepo.Certs))
|
||||
}
|
||||
|
||||
storedCert, ok := certRepo.Certs["cert-001"]
|
||||
if !ok {
|
||||
t.Fatal("certificate not stored")
|
||||
}
|
||||
if storedCert.CommonName != "api.example.com" {
|
||||
t.Errorf("expected common name api.example.com, got %s", storedCert.CommonName)
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCertificate_MissingRequired(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
// Missing CommonName and IssuerID
|
||||
}
|
||||
|
||||
err := certService.Create(ctx, cert, "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for missing required fields")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCertificate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-1",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
retrieved, err := certService.Get(ctx, "cert-001")
|
||||
if err != nil {
|
||||
t.Fatalf("Get failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.CommonName != "example.com" {
|
||||
t.Errorf("expected common name example.com, got %s", retrieved.CommonName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCertificate_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
_, err := certService.Get(ctx, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateCertificate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
originalCert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-1",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": originalCert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
updatedCert := *originalCert
|
||||
updatedCert.Status = domain.CertificateStatusExpiring
|
||||
updatedCert.ExpiresAt = now.AddDate(0, 0, 5)
|
||||
|
||||
err := certService.Update(ctx, &updatedCert, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Update failed: %v", err)
|
||||
}
|
||||
|
||||
stored := certRepo.Certs["cert-001"]
|
||||
if stored.Status != domain.CertificateStatusExpiring {
|
||||
t.Errorf("expected status Expiring, got %s", stored.Status)
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestArchiveCertificate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-1",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
err := certService.Archive(ctx, "cert-001", "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("Archive failed: %v", err)
|
||||
}
|
||||
|
||||
archived := certRepo.Certs["cert-001"]
|
||||
if archived.Status != domain.CertificateStatusArchived {
|
||||
t.Errorf("expected status Archived, got %s", archived.Status)
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetVersions(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
version1 := &domain.CertificateVersion{
|
||||
ID: "ver-1",
|
||||
CertificateID: "cert-001",
|
||||
SerialNumber: "serial-1",
|
||||
NotBefore: now.AddDate(-1, 0, 0),
|
||||
NotAfter: now,
|
||||
PEMChain: "cert1-pem",
|
||||
CreatedAt: now.AddDate(-1, 0, 0),
|
||||
}
|
||||
version2 := &domain.CertificateVersion{
|
||||
ID: "ver-2",
|
||||
CertificateID: "cert-001",
|
||||
SerialNumber: "serial-2",
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0),
|
||||
PEMChain: "cert2-pem",
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: map[string][]*domain.CertificateVersion{"cert-001": {version1, version2}},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
versions, err := certService.GetVersions(ctx, "cert-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetVersions failed: %v", err)
|
||||
}
|
||||
|
||||
if len(versions) != 2 {
|
||||
t.Errorf("expected 2 versions, got %d", len(versions))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggerRenewal(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-1",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(0, 0, 5),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
err := certService.TriggerRenewalWithActor(ctx, "cert-001", "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("TriggerRenewal failed: %v", err)
|
||||
}
|
||||
|
||||
renewed := certRepo.Certs["cert-001"]
|
||||
if renewed.Status != domain.CertificateStatusRenewalInProgress {
|
||||
t.Errorf("expected status RenewalInProgress, got %s", renewed.Status)
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestTriggerRenewal_Archived(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-1",
|
||||
Status: domain.CertificateStatusArchived,
|
||||
ExpiresAt: now.AddDate(0, 0, 5),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
err := certService.TriggerRenewalWithActor(ctx, "cert-001", "user-1")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for archived certificate")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListCertificates(t *testing.T) {
|
||||
now := time.Now()
|
||||
cert1 := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "api.example.com",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
cert2 := &domain.ManagedCertificate{
|
||||
ID: "cert-002",
|
||||
CommonName: "web.example.com",
|
||||
Status: domain.CertificateStatusExpiring,
|
||||
ExpiresAt: now.AddDate(0, 0, 5),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: map[string]*domain.ManagedCertificate{"cert-001": cert1, "cert-002": cert2},
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
policyRepo := &mockPolicyRepo{Rules: make(map[string]*domain.PolicyRule)}
|
||||
|
||||
policyService := NewPolicyService(policyRepo, NewAuditService(auditRepo))
|
||||
auditService := NewAuditService(auditRepo)
|
||||
certService := NewCertificateService(certRepo, policyService, auditService)
|
||||
|
||||
certs, total, err := certService.ListCertificates("", "", "", "", "", 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListCertificates failed: %v", err)
|
||||
}
|
||||
|
||||
if len(certs) != 2 {
|
||||
t.Errorf("expected 2 certs, got %d", len(certs))
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
// helper to build job service with proper constructor signatures
|
||||
func newTestJobService(jobRepo *mockJobRepo) *JobService {
|
||||
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo}))
|
||||
|
||||
certRepo := &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
renewalPolicyRepo := &mockRenewalPolicyRepo{
|
||||
Policies: make(map[string]*domain.RenewalPolicy),
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifService := NewNotificationService(notifRepo, make(map[string]Notifier))
|
||||
targetRepo := &mockTargetRepo{Targets: make(map[string]*domain.DeploymentTarget)}
|
||||
agentRepo := &mockAgentRepo{Agents: make(map[string]*domain.Agent)}
|
||||
|
||||
renewalService := NewRenewalService(certRepo, jobRepo, renewalPolicyRepo, auditService, notifService, make(map[string]IssuerConnector))
|
||||
deploymentService := NewDeploymentService(jobRepo, targetRepo, agentRepo, certRepo, auditService, notifService)
|
||||
|
||||
return NewJobService(jobRepo, renewalService, deploymentService, logger)
|
||||
}
|
||||
|
||||
func TestProcessPendingJobs_Renewal(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
job := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeRenewal,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusPending,
|
||||
Attempts: 0,
|
||||
MaxAttempts: 3,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
err := jobService.ProcessPendingJobs(ctx)
|
||||
if err != nil {
|
||||
t.Logf("ProcessPendingJobs returned error (expected for renewal without cert): %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingJobs_NoJobs(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
err := jobService.ProcessPendingJobs(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingJobs failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelJob(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
job := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
err := jobService.CancelJobWithContext(ctx, "job-001")
|
||||
if err != nil {
|
||||
t.Fatalf("CancelJob failed: %v", err)
|
||||
}
|
||||
|
||||
if jobRepo.StatusUpdates["job-001"] != domain.JobStatusCancelled {
|
||||
t.Errorf("expected status Cancelled, got %s", jobRepo.StatusUpdates["job-001"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelJob_AlreadyCompleted(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
now := time.Now()
|
||||
job := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusCompleted,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
err := jobService.CancelJobWithContext(ctx, "job-001")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for completed job")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJob(t *testing.T) {
|
||||
now := time.Now()
|
||||
job := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
retrieved, err := jobService.GetJob("job-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetJob failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.ID != "job-001" {
|
||||
t.Errorf("expected job ID job-001, got %s", retrieved.ID)
|
||||
}
|
||||
if retrieved.Type != domain.JobTypeDeployment {
|
||||
t.Errorf("expected job type Deployment, got %s", retrieved.Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListJobs(t *testing.T) {
|
||||
now := time.Now()
|
||||
job1 := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusCompleted,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
job2 := &domain.Job{
|
||||
ID: "job-002",
|
||||
Type: domain.JobTypeRenewal,
|
||||
CertificateID: "cert-002",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job1, "job-002": job2},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
jobs, total, err := jobService.ListJobs("", "", 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListJobs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(jobs) != 2 {
|
||||
t.Errorf("expected 2 jobs, got %d", len(jobs))
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListJobs_FilterByStatus(t *testing.T) {
|
||||
now := time.Now()
|
||||
job1 := &domain.Job{
|
||||
ID: "job-001",
|
||||
Type: domain.JobTypeDeployment,
|
||||
CertificateID: "cert-001",
|
||||
Status: domain.JobStatusCompleted,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
job2 := &domain.Job{
|
||||
ID: "job-002",
|
||||
Type: domain.JobTypeRenewal,
|
||||
CertificateID: "cert-002",
|
||||
Status: domain.JobStatusPending,
|
||||
CreatedAt: now,
|
||||
ScheduledAt: now,
|
||||
}
|
||||
|
||||
jobRepo := &mockJobRepo{
|
||||
Jobs: map[string]*domain.Job{"job-001": job1, "job-002": job2},
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
|
||||
jobService := newTestJobService(jobRepo)
|
||||
|
||||
jobs, total, err := jobService.ListJobs(string(domain.JobStatusPending), "", 1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListJobs failed: %v", err)
|
||||
}
|
||||
|
||||
if len(jobs) != 1 {
|
||||
t.Errorf("expected 1 pending job, got %d", len(jobs))
|
||||
}
|
||||
if total != 1 {
|
||||
t.Errorf("expected total 1, got %d", total)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
func TestSendThresholdAlert(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-test-1",
|
||||
CommonName: "example.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||
}
|
||||
|
||||
threshold := 7
|
||||
daysUntilExpiry := 5
|
||||
|
||||
err := svc.SendThresholdAlert(ctx, cert, daysUntilExpiry, threshold)
|
||||
if err != nil {
|
||||
t.Fatalf("SendThresholdAlert failed: %v", err)
|
||||
}
|
||||
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
notif := notifRepo.Notifications[0]
|
||||
if notif.Type != domain.NotificationTypeExpirationWarning {
|
||||
t.Errorf("expected ExpirationWarning, got %s", notif.Type)
|
||||
}
|
||||
|
||||
// Verify message contains threshold tag
|
||||
if !strings.Contains(notif.Message, "[threshold:7]") {
|
||||
t.Errorf("expected threshold tag in message, got: %s", notif.Message)
|
||||
}
|
||||
|
||||
// Verify notifier was called
|
||||
if notifier.getSentCount() != 1 {
|
||||
t.Errorf("expected 1 sent message, got %d", notifier.getSentCount())
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendThresholdAlert_Expired(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-test-expired",
|
||||
CommonName: "expired.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(0, 0, -1),
|
||||
}
|
||||
|
||||
threshold := 0
|
||||
daysUntilExpiry := -1
|
||||
|
||||
err := svc.SendThresholdAlert(ctx, cert, daysUntilExpiry, threshold)
|
||||
if err != nil {
|
||||
t.Fatalf("SendThresholdAlert failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify message contains [EXPIRED] prefix
|
||||
if len(notifRepo.Notifications) > 0 && !strings.Contains(notifRepo.Notifications[0].Message, "[EXPIRED]") {
|
||||
t.Errorf("expected [EXPIRED] in message, got: %s", notifRepo.Notifications[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasThresholdNotification_Found(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add an existing notification with threshold tag
|
||||
existingNotif := &domain.NotificationEvent{
|
||||
ID: "notif-1",
|
||||
CertificateID: stringPtr("mc-test-1"),
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: "Certificate expires soon\n\n[threshold:30]",
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(existingNotif)
|
||||
|
||||
// Check for existing notification
|
||||
found, err := svc.HasThresholdNotification(ctx, "mc-test-1", 30)
|
||||
if err != nil {
|
||||
t.Fatalf("HasThresholdNotification failed: %v", err)
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("expected to find threshold notification, but didn't")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHasThresholdNotification_NotFound(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Check for non-existent notification
|
||||
found, err := svc.HasThresholdNotification(ctx, "mc-test-1", 30)
|
||||
if err != nil {
|
||||
t.Fatalf("HasThresholdNotification failed: %v", err)
|
||||
}
|
||||
|
||||
if found {
|
||||
t.Errorf("expected not to find threshold notification, but did")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendExpirationWarning(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-test-warning",
|
||||
CommonName: "warn.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 10),
|
||||
}
|
||||
|
||||
err := svc.SendExpirationWarning(ctx, cert, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("SendExpirationWarning failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify notification was created
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
if notifRepo.Notifications[0].Type != domain.NotificationTypeExpirationWarning {
|
||||
t.Errorf("expected ExpirationWarning type, got %s", notifRepo.Notifications[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRenewalNotification_Success(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-renewed",
|
||||
CommonName: "renewed.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
||||
}
|
||||
|
||||
err := svc.SendRenewalNotification(ctx, cert, true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SendRenewalNotification failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify notification was created with success type
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
if notifRepo.Notifications[0].Type != domain.NotificationTypeRenewalSuccess {
|
||||
t.Errorf("expected RenewalSuccess type, got %s", notifRepo.Notifications[0].Type)
|
||||
}
|
||||
|
||||
// Verify message contains success text
|
||||
if !strings.Contains(notifRepo.Notifications[0].Message, "successfully renewed") {
|
||||
t.Errorf("expected success message, got: %s", notifRepo.Notifications[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendRenewalNotification_Failure(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-failed-renewal",
|
||||
CommonName: "failed.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 5),
|
||||
}
|
||||
|
||||
testErr := fmt.Errorf("issuer unavailable")
|
||||
err := svc.SendRenewalNotification(ctx, cert, false, testErr)
|
||||
if err != nil {
|
||||
t.Fatalf("SendRenewalNotification failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify notification was created with failure type
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
if notifRepo.Notifications[0].Type != domain.NotificationTypeRenewalFailure {
|
||||
t.Errorf("expected RenewalFailure type, got %s", notifRepo.Notifications[0].Type)
|
||||
}
|
||||
|
||||
// Verify message contains error info
|
||||
if !strings.Contains(notifRepo.Notifications[0].Message, "failed to renew") {
|
||||
t.Errorf("expected failure message, got: %s", notifRepo.Notifications[0].Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingNotifications(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add pending notifications
|
||||
for i := 0; i < 3; i++ {
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: fmt.Sprintf("notif-%d", i),
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: fmt.Sprintf("Test notification %d", i),
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
}
|
||||
|
||||
err := svc.ProcessPendingNotifications(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingNotifications failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify all notifications were sent
|
||||
if notifier.getSentCount() != 3 {
|
||||
t.Errorf("expected 3 sent notifications, got %d", notifier.getSentCount())
|
||||
}
|
||||
|
||||
// Verify status was updated to sent
|
||||
for _, notif := range notifRepo.Notifications {
|
||||
if notif.Status != "sent" {
|
||||
t.Errorf("expected notification status 'sent', got %s", notif.Status)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessPendingNotifications_NoNotifier(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
// No notifier registered - demo mode
|
||||
registry := map[string]Notifier{}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add pending notification
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: "notif-demo",
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail, // Channel not in registry
|
||||
Recipient: "owner-1",
|
||||
Message: "Test notification",
|
||||
Status: "pending",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
|
||||
// Should not fail, just mark as sent (demo mode graceful skip)
|
||||
err := svc.ProcessPendingNotifications(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessPendingNotifications should not fail in demo mode: %v", err)
|
||||
}
|
||||
|
||||
// Status should still be updated to sent
|
||||
if len(notifRepo.Notifications) > 0 && notifRepo.Notifications[0].Status == "sent" {
|
||||
// This is fine - graceful skip marks as sent
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterNotifier(t *testing.T) {
|
||||
t.Helper()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
notifier := newMockNotifier()
|
||||
svc.RegisterNotifier("Email", notifier)
|
||||
|
||||
// Verify notifier was registered
|
||||
if svc.notifierRegistry["Email"] == nil {
|
||||
t.Errorf("expected notifier to be registered")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListNotifications(t *testing.T) {
|
||||
t.Helper()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add test notifications
|
||||
for i := 0; i < 5; i++ {
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: fmt.Sprintf("notif-list-%d", i),
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: fmt.Sprintf("owner-%d", i%2),
|
||||
Message: fmt.Sprintf("Test notification %d", i),
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
}
|
||||
|
||||
// List with pagination
|
||||
notifs, total, err := svc.ListNotifications(1, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("ListNotifications failed: %v", err)
|
||||
}
|
||||
|
||||
if len(notifs) == 0 {
|
||||
t.Errorf("expected notifications, got none")
|
||||
}
|
||||
|
||||
if total == 0 {
|
||||
t.Errorf("expected total count > 0, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarkAsRead(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add a notification
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: "notif-read",
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: "Test notification",
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
|
||||
// Mark as read
|
||||
err := svc.MarkAsRead(notif.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("MarkAsRead failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify status was updated
|
||||
if len(notifRepo.Notifications) > 0 && notifRepo.Notifications[0].Status != "read" {
|
||||
t.Errorf("expected status 'read', got %s", notifRepo.Notifications[0].Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNotification(t *testing.T) {
|
||||
t.Helper()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
// Add a notification
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: "notif-get-test",
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: "Test notification",
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
|
||||
// Get the notification
|
||||
retrieved, err := svc.GetNotification(notif.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetNotification failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved == nil {
|
||||
t.Errorf("expected notification, got nil")
|
||||
} else if retrieved.ID != notif.ID {
|
||||
t.Errorf("expected ID %s, got %s", notif.ID, retrieved.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendDeploymentNotification_Success(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-deploy",
|
||||
CommonName: "deploy.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
||||
}
|
||||
|
||||
target := &domain.DeploymentTarget{
|
||||
ID: "target-1",
|
||||
Name: "NGINX-Prod",
|
||||
}
|
||||
|
||||
err := svc.SendDeploymentNotification(ctx, cert, target, true, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("SendDeploymentNotification failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify notification was created
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
if notifRepo.Notifications[0].Type != domain.NotificationTypeDeploymentSuccess {
|
||||
t.Errorf("expected DeploymentSuccess type, got %s", notifRepo.Notifications[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendDeploymentNotification_Failure(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
registry := map[string]Notifier{
|
||||
"Email": notifier,
|
||||
}
|
||||
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-deploy-fail",
|
||||
CommonName: "deploy-fail.com",
|
||||
OwnerID: "owner-1",
|
||||
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
||||
}
|
||||
|
||||
target := &domain.DeploymentTarget{
|
||||
ID: "target-2",
|
||||
Name: "NGINX-Staging",
|
||||
}
|
||||
|
||||
deployErr := fmt.Errorf("connection timeout")
|
||||
err := svc.SendDeploymentNotification(ctx, cert, target, false, deployErr)
|
||||
if err != nil {
|
||||
t.Fatalf("SendDeploymentNotification failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify notification was created
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
if notifRepo.Notifications[0].Type != domain.NotificationTypeDeploymentFailure {
|
||||
t.Errorf("expected DeploymentFailure type, got %s", notifRepo.Notifications[0].Type)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetNotificationHistory(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
notifRepo := newMockNotificationRepository()
|
||||
registry := map[string]Notifier{}
|
||||
svc := NewNotificationService(notifRepo, registry)
|
||||
|
||||
certID := "mc-history"
|
||||
|
||||
// Add multiple notifications for same cert
|
||||
for i := 0; i < 3; i++ {
|
||||
notif := &domain.NotificationEvent{
|
||||
ID: fmt.Sprintf("notif-hist-%d", i),
|
||||
CertificateID: &certID,
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: fmt.Sprintf("Alert %d", i),
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(notif)
|
||||
}
|
||||
|
||||
// Get history
|
||||
history, err := svc.GetNotificationHistory(ctx, certID)
|
||||
if err != nil {
|
||||
t.Fatalf("GetNotificationHistory failed: %v", err)
|
||||
}
|
||||
|
||||
if len(history) < 1 {
|
||||
t.Errorf("expected at least 1 notification, got %d", len(history))
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function
|
||||
func stringPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
@@ -0,0 +1,422 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
func TestCreateRule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: make(map[string]*domain.PolicyRule),
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
config := map[string]interface{}{"issuers": []string{"iss-acme"}}
|
||||
configJSON, _ := json.Marshal(config)
|
||||
|
||||
rule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Config: configJSON,
|
||||
Enabled: true,
|
||||
}
|
||||
|
||||
err := policyService.CreateRule(ctx, rule, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("CreateRule failed: %v", err)
|
||||
}
|
||||
|
||||
if len(policyRepo.Rules) != 1 {
|
||||
t.Errorf("expected 1 rule, got %d", len(policyRepo.Rules))
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
rule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
retrieved, err := policyService.GetRule(ctx, "rule-001")
|
||||
if err != nil {
|
||||
t.Fatalf("GetRule failed: %v", err)
|
||||
}
|
||||
|
||||
if retrieved.Name != "Allowed Issuers" {
|
||||
t.Errorf("expected name Allowed Issuers, got %s", retrieved.Name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetRule_NotFound(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: make(map[string]*domain.PolicyRule),
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
_, err := policyService.GetRule(ctx, "nonexistent")
|
||||
if err == nil {
|
||||
t.Fatal("expected error for nonexistent rule")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRules(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
rule1 := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
rule2 := &domain.PolicyRule{
|
||||
ID: "rule-002",
|
||||
Name: "Required Metadata",
|
||||
Type: domain.PolicyTypeRequiredMetadata,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule1, "rule-002": rule2},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
rules, err := policyService.ListRules(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("ListRules failed: %v", err)
|
||||
}
|
||||
|
||||
if len(rules) != 2 {
|
||||
t.Errorf("expected 2 rules, got %d", len(rules))
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateRule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
originalRule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": originalRule},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
updatedRule := *originalRule
|
||||
updatedRule.Enabled = false
|
||||
|
||||
err := policyService.UpdateRule(ctx, &updatedRule, "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateRule failed: %v", err)
|
||||
}
|
||||
|
||||
stored := policyRepo.Rules["rule-001"]
|
||||
if stored.Enabled {
|
||||
t.Error("expected rule to be disabled")
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteRule(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
rule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{Events: []*domain.AuditEvent{}}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
err := policyService.DeleteRule(ctx, "rule-001", "user-1")
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteRule failed: %v", err)
|
||||
}
|
||||
|
||||
if len(policyRepo.Rules) != 0 {
|
||||
t.Errorf("expected 0 rules, got %d", len(policyRepo.Rules))
|
||||
}
|
||||
|
||||
if len(auditRepo.Events) != 1 {
|
||||
t.Errorf("expected 1 audit event, got %d", len(auditRepo.Events))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCertificate(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
rule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "iss-acme",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
violations, err := policyService.ValidateCertificate(ctx, cert)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if len(violations) > 0 {
|
||||
t.Errorf("expected no violations, got %d", len(violations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCertificate_WithViolation(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
rule := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "", // Missing issuer
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
violations, err := policyService.ValidateCertificate(ctx, cert)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if len(violations) != 1 {
|
||||
t.Errorf("expected 1 violation, got %d", len(violations))
|
||||
}
|
||||
|
||||
if violations[0].CertificateID != "cert-001" {
|
||||
t.Errorf("expected violation for cert-001, got %s", violations[0].CertificateID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCertificate_MultipleViolations(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
now := time.Now()
|
||||
|
||||
rule1 := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Allowed Issuers",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
rule2 := &domain.PolicyRule{
|
||||
ID: "rule-002",
|
||||
Name: "Required Metadata",
|
||||
Type: domain.PolicyTypeRequiredMetadata,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule1, "rule-002": rule2},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "cert-001",
|
||||
CommonName: "example.com",
|
||||
IssuerID: "", // Missing issuer
|
||||
Tags: nil, // Missing metadata
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: now.AddDate(1, 0, 0),
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
violations, err := policyService.ValidateCertificate(ctx, cert)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateCertificate failed: %v", err)
|
||||
}
|
||||
|
||||
if len(violations) != 2 {
|
||||
t.Errorf("expected 2 violations, got %d", len(violations))
|
||||
}
|
||||
}
|
||||
|
||||
func TestListPolicies(t *testing.T) {
|
||||
now := time.Now()
|
||||
rule1 := &domain.PolicyRule{
|
||||
ID: "rule-001",
|
||||
Name: "Rule 1",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
rule2 := &domain.PolicyRule{
|
||||
ID: "rule-002",
|
||||
Name: "Rule 2",
|
||||
Type: domain.PolicyTypeRequiredMetadata,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: map[string]*domain.PolicyRule{"rule-001": rule1, "rule-002": rule2},
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
policies, total, err := policyService.ListPolicies(1, 50)
|
||||
if err != nil {
|
||||
t.Fatalf("ListPolicies failed: %v", err)
|
||||
}
|
||||
|
||||
if len(policies) != 2 {
|
||||
t.Errorf("expected 2 policies, got %d", len(policies))
|
||||
}
|
||||
if total != 2 {
|
||||
t.Errorf("expected total 2, got %d", total)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreatePolicy(t *testing.T) {
|
||||
now := time.Now()
|
||||
policyRepo := &mockPolicyRepo{
|
||||
Rules: make(map[string]*domain.PolicyRule),
|
||||
Violations: []*domain.PolicyViolation{},
|
||||
}
|
||||
auditRepo := &mockAuditRepo{}
|
||||
auditService := NewAuditService(auditRepo)
|
||||
|
||||
policyService := NewPolicyService(policyRepo, auditService)
|
||||
|
||||
policy := domain.PolicyRule{
|
||||
Name: "Test Policy",
|
||||
Type: domain.PolicyTypeAllowedIssuers,
|
||||
Enabled: true,
|
||||
CreatedAt: now,
|
||||
}
|
||||
|
||||
created, err := policyService.CreatePolicy(policy)
|
||||
if err != nil {
|
||||
t.Fatalf("CreatePolicy failed: %v", err)
|
||||
}
|
||||
|
||||
if created.ID == "" {
|
||||
t.Fatal("expected non-empty policy ID")
|
||||
}
|
||||
|
||||
if len(policyRepo.Rules) != 1 {
|
||||
t.Errorf("expected 1 rule in repo, got %d", len(policyRepo.Rules))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,866 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
)
|
||||
|
||||
func TestCheckExpiringCertificates_SendsThresholdAlerts(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{
|
||||
"Email": notifier,
|
||||
})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create a cert expiring in 10 days
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-expiring",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 10),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy with thresholds
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run expiry check
|
||||
err := svc.CheckExpiringCertificates(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckExpiringCertificates failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify alerts were sent
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected at least 1 alert, got %d", len(notifRepo.Notifications))
|
||||
}
|
||||
|
||||
// Verify renewal job was created
|
||||
if len(jobRepo.Jobs) < 1 {
|
||||
t.Errorf("expected renewal job to be created")
|
||||
}
|
||||
|
||||
hasRenewalJob := false
|
||||
for _, job := range jobRepo.Jobs {
|
||||
if job.Type == domain.JobTypeRenewal {
|
||||
hasRenewalJob = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRenewalJob {
|
||||
t.Errorf("expected renewal job in jobs")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_DeduplicatesAlerts(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
notifier := newMockNotifier()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{
|
||||
"Email": notifier,
|
||||
})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create cert
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-dedup",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 10),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Add existing threshold alert notification
|
||||
existingNotif := &domain.NotificationEvent{
|
||||
ID: "notif-existing",
|
||||
CertificateID: &cert.ID,
|
||||
Type: domain.NotificationTypeExpirationWarning,
|
||||
Channel: domain.NotificationChannelEmail,
|
||||
Recipient: "owner-1",
|
||||
Message: "Alert [threshold:7]",
|
||||
Status: "sent",
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
notifRepo.AddNotification(existingNotif)
|
||||
|
||||
// Run first check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
initialCount := notifier.getSentCount()
|
||||
|
||||
// Run second check - should deduplicate
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
finalCount := notifier.getSentCount()
|
||||
|
||||
// Should not send duplicate alerts
|
||||
if finalCount > initialCount {
|
||||
t.Errorf("expected deduplication, but sent new alerts: initial=%d, final=%d", initialCount, finalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_SkipsRenewalInProgress(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create cert with RenewalInProgress status
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-in-progress",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusRenewalInProgress,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 10),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run check
|
||||
err := svc.CheckExpiringCertificates(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckExpiringCertificates failed: %v", err)
|
||||
}
|
||||
|
||||
// Should not create renewal job for cert already renewing
|
||||
for _, job := range jobRepo.Jobs {
|
||||
if job.Type == domain.JobTypeRenewal {
|
||||
t.Errorf("should not create renewal job for cert with RenewalInProgress status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_UpdatesStatusToExpiring(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create active cert that will become expiring
|
||||
// Use an issuer NOT in the registry so no renewal job is created (which would override status)
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-expiring-status",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-unregistered",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 5), // 5 days, within 30-day threshold
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy with AutoRenew: false so we only test status transition
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: false,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Verify status was updated to Expiring
|
||||
updated, _ := certRepo.Get(ctx, cert.ID)
|
||||
if updated.Status != domain.CertificateStatusExpiring {
|
||||
t.Errorf("expected status Expiring, got %s", updated.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_UpdatesStatusToExpired(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create cert that is already expired
|
||||
// Use an issuer NOT in the registry so no renewal job is created (which would override status)
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-expired-status",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-unregistered",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, -1), // Already expired
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy with AutoRenew: false so we only test status transition
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: false,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Verify status was updated to Expired
|
||||
updated, _ := certRepo.Get(ctx, cert.ID)
|
||||
if updated.Status != domain.CertificateStatusExpired {
|
||||
t.Errorf("expected status Expired, got %s", updated.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_CreatesRenewalJob(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create expiring cert with registered issuer
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-job-create",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test", // Registered issuer
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 20),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Verify renewal job was created
|
||||
hasRenewalJob := false
|
||||
for _, job := range jobRepo.Jobs {
|
||||
if job.Type == domain.JobTypeRenewal && job.Status == domain.JobStatusPending {
|
||||
hasRenewalJob = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasRenewalJob {
|
||||
t.Errorf("expected renewal job to be created")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_SkipsWithoutIssuer(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
// Empty issuer registry
|
||||
issuerRegistry := map[string]IssuerConnector{}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create cert with unregistered issuer
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-no-issuer",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-missing", // Not in registry
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 20),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Run check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Should not create renewal job without issuer
|
||||
for _, job := range jobRepo.Jobs {
|
||||
if job.Type == domain.JobTypeRenewal {
|
||||
t.Errorf("should not create renewal job for cert with missing issuer")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckExpiringCertificates_SkipsDuplicateJobs(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create cert
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-dup-job",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 20),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create policy
|
||||
policy := &domain.RenewalPolicy{
|
||||
ID: "rp-standard",
|
||||
Name: "Standard",
|
||||
RenewalWindowDays: 30,
|
||||
AutoRenew: true,
|
||||
MaxRetries: 3,
|
||||
RetryInterval: 300,
|
||||
AlertThresholdsDays: []int{30, 14, 7, 0},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
policyRepo.AddPolicy(policy)
|
||||
|
||||
// Add existing renewal job
|
||||
existingJob := &domain.Job{
|
||||
ID: "job-existing",
|
||||
CertificateID: cert.ID,
|
||||
Type: domain.JobTypeRenewal,
|
||||
Status: domain.JobStatusPending,
|
||||
MaxAttempts: 3,
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
jobRepo.AddJob(existingJob)
|
||||
|
||||
// Run first check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Run second check
|
||||
_ = svc.CheckExpiringCertificates(ctx)
|
||||
|
||||
// Should have only 1 renewal job
|
||||
renewalCount := 0
|
||||
for _, job := range jobRepo.Jobs {
|
||||
if job.Type == domain.JobTypeRenewal {
|
||||
renewalCount++
|
||||
}
|
||||
}
|
||||
if renewalCount > 1 {
|
||||
t.Errorf("expected 1 renewal job, got %d (duplicate prevention failed)", renewalCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessRenewalJob(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{
|
||||
"Email": newMockNotifier(),
|
||||
})
|
||||
|
||||
issuerConnector := &mockIssuerConnector{}
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": issuerConnector,
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create certificate
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-renewal",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{"www.test.example.com"},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
TargetIDs: []string{"target-1", "target-2"},
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 30),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create renewal job
|
||||
job := &domain.Job{
|
||||
ID: "job-renewal-1",
|
||||
CertificateID: cert.ID,
|
||||
Type: domain.JobTypeRenewal,
|
||||
Status: domain.JobStatusPending,
|
||||
MaxAttempts: 3,
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
jobRepo.AddJob(job)
|
||||
|
||||
// Process renewal job
|
||||
err := svc.ProcessRenewalJob(ctx, job)
|
||||
if err != nil {
|
||||
t.Fatalf("ProcessRenewalJob failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify cert was updated
|
||||
updated, _ := certRepo.Get(ctx, cert.ID)
|
||||
if updated.Status != domain.CertificateStatusActive {
|
||||
t.Errorf("expected cert status Active, got %s", updated.Status)
|
||||
}
|
||||
|
||||
if updated.LastRenewalAt == nil {
|
||||
t.Errorf("expected LastRenewalAt to be set")
|
||||
}
|
||||
|
||||
// Verify certificate version was created
|
||||
if len(certRepo.Versions[cert.ID]) != 1 {
|
||||
t.Errorf("expected 1 certificate version, got %d", len(certRepo.Versions[cert.ID]))
|
||||
}
|
||||
|
||||
// Verify deployment jobs were created
|
||||
deploymentCount := 0
|
||||
for _, j := range jobRepo.Jobs {
|
||||
if j.Type == domain.JobTypeDeployment {
|
||||
deploymentCount++
|
||||
}
|
||||
}
|
||||
if deploymentCount != 2 {
|
||||
t.Errorf("expected 2 deployment jobs (one per target), got %d", deploymentCount)
|
||||
}
|
||||
|
||||
// Verify job was marked as completed
|
||||
completedJob, _ := jobRepo.Get(ctx, job.ID)
|
||||
if completedJob.Status != domain.JobStatusCompleted {
|
||||
t.Errorf("expected job status Completed, got %s", completedJob.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessRenewalJob_IssuerFailure(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{
|
||||
"Email": newMockNotifier(),
|
||||
})
|
||||
|
||||
// Create issuer that will fail
|
||||
issuerConnector := &mockIssuerConnector{
|
||||
Err: fmt.Errorf("issuer service unavailable"),
|
||||
}
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": issuerConnector,
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create certificate
|
||||
cert := &domain.ManagedCertificate{
|
||||
ID: "mc-renewal-fail",
|
||||
Name: "Test Cert",
|
||||
CommonName: "test.example.com",
|
||||
SANs: []string{},
|
||||
OwnerID: "owner-1",
|
||||
TeamID: "team-1",
|
||||
IssuerID: "iss-test",
|
||||
RenewalPolicyID: "rp-standard",
|
||||
Status: domain.CertificateStatusActive,
|
||||
ExpiresAt: time.Now().AddDate(0, 0, 30),
|
||||
Tags: make(map[string]string),
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
certRepo.AddCert(cert)
|
||||
|
||||
// Create renewal job
|
||||
job := &domain.Job{
|
||||
ID: "job-renewal-fail",
|
||||
CertificateID: cert.ID,
|
||||
Type: domain.JobTypeRenewal,
|
||||
Status: domain.JobStatusPending,
|
||||
MaxAttempts: 3,
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
jobRepo.AddJob(job)
|
||||
|
||||
// Process renewal job (should fail)
|
||||
err := svc.ProcessRenewalJob(ctx, job)
|
||||
if err == nil {
|
||||
t.Fatalf("expected ProcessRenewalJob to fail")
|
||||
}
|
||||
|
||||
// Verify job was marked as failed
|
||||
failedJob, _ := jobRepo.Get(ctx, job.ID)
|
||||
if failedJob.Status != domain.JobStatusFailed {
|
||||
t.Errorf("expected job status Failed, got %s", failedJob.Status)
|
||||
}
|
||||
|
||||
if failedJob.LastError == nil || !strings.Contains(*failedJob.LastError, "issuer service unavailable") {
|
||||
t.Errorf("expected error message in job, got: %v", failedJob.LastError)
|
||||
}
|
||||
|
||||
// Verify failure notification was sent
|
||||
if len(notifRepo.Notifications) < 1 {
|
||||
t.Errorf("expected failure notification to be created")
|
||||
}
|
||||
|
||||
foundFailureNotif := false
|
||||
for _, notif := range notifRepo.Notifications {
|
||||
if notif.Type == domain.NotificationTypeRenewalFailure {
|
||||
foundFailureNotif = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundFailureNotif {
|
||||
t.Errorf("expected RenewalFailure notification type")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryFailedJobs(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create failed job with attempts < max_attempts
|
||||
failedJob := &domain.Job{
|
||||
ID: "job-failed-1",
|
||||
CertificateID: "mc-test",
|
||||
Type: domain.JobTypeRenewal,
|
||||
Status: domain.JobStatusFailed,
|
||||
Attempts: 1,
|
||||
MaxAttempts: 3,
|
||||
LastError: stringPtr("temporary failure"),
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now().AddDate(0, 0, -1),
|
||||
}
|
||||
jobRepo.AddJob(failedJob)
|
||||
|
||||
// Create other job types that should be ignored
|
||||
otherJob := &domain.Job{
|
||||
ID: "job-other",
|
||||
CertificateID: "mc-test",
|
||||
Type: domain.JobTypeDeployment,
|
||||
Status: domain.JobStatusFailed,
|
||||
Attempts: 1,
|
||||
MaxAttempts: 3,
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
jobRepo.AddJob(otherJob)
|
||||
|
||||
// Retry failed jobs
|
||||
err := svc.RetryFailedJobs(ctx, 3)
|
||||
if err != nil {
|
||||
t.Fatalf("RetryFailedJobs failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify failed renewal job was reset to pending
|
||||
retried, _ := jobRepo.Get(ctx, failedJob.ID)
|
||||
if retried.Status != domain.JobStatusPending {
|
||||
t.Errorf("expected job status Pending after retry, got %s", retried.Status)
|
||||
}
|
||||
|
||||
// Verify other job type was not touched
|
||||
other, _ := jobRepo.Get(ctx, otherJob.ID)
|
||||
if other.Status != domain.JobStatusFailed {
|
||||
t.Errorf("expected non-renewal job to stay Failed, got %s", other.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessRenewalJob_NoCertificate(t *testing.T) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
|
||||
certRepo := newMockCertificateRepository()
|
||||
jobRepo := newMockJobRepository()
|
||||
policyRepo := newMockRenewalPolicyRepository()
|
||||
auditRepo := newMockAuditRepository()
|
||||
notifRepo := newMockNotificationRepository()
|
||||
|
||||
auditSvc := NewAuditService(auditRepo)
|
||||
notifSvc := NewNotificationService(notifRepo, map[string]Notifier{})
|
||||
|
||||
issuerRegistry := map[string]IssuerConnector{
|
||||
"iss-test": &mockIssuerConnector{},
|
||||
}
|
||||
|
||||
svc := NewRenewalService(certRepo, jobRepo, policyRepo, auditSvc, notifSvc, issuerRegistry)
|
||||
|
||||
// Create job with non-existent certificate
|
||||
job := &domain.Job{
|
||||
ID: "job-no-cert",
|
||||
CertificateID: "mc-missing",
|
||||
Type: domain.JobTypeRenewal,
|
||||
Status: domain.JobStatusPending,
|
||||
MaxAttempts: 3,
|
||||
ScheduledAt: time.Now(),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
jobRepo.AddJob(job)
|
||||
|
||||
// Process renewal job
|
||||
err := svc.ProcessRenewalJob(ctx, job)
|
||||
if err == nil {
|
||||
t.Fatalf("expected ProcessRenewalJob to fail for missing certificate")
|
||||
}
|
||||
|
||||
// Verify job was marked as failed
|
||||
failedJob, _ := jobRepo.Get(ctx, job.ID)
|
||||
if failedJob.Status != domain.JobStatusFailed {
|
||||
t.Errorf("expected job status Failed, got %s", failedJob.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// stringPtr is defined in notification_test.go
|
||||
@@ -0,0 +1,771 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/shankar0123/certctl/internal/domain"
|
||||
"github.com/shankar0123/certctl/internal/repository"
|
||||
)
|
||||
|
||||
var errNotFound = errors.New("not found")
|
||||
|
||||
// mockCertRepo is a test implementation of CertificateRepository
|
||||
type mockCertRepo struct {
|
||||
Certs map[string]*domain.ManagedCertificate
|
||||
Versions map[string][]*domain.CertificateVersion
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
ListVersionsErr error
|
||||
ListVersionsResult []*domain.CertificateVersion
|
||||
CreateVersionErr error
|
||||
ArchiveErr error
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) List(ctx context.Context, filter *repository.CertificateFilter) ([]*domain.ManagedCertificate, int, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, 0, m.ListErr
|
||||
}
|
||||
var certs []*domain.ManagedCertificate
|
||||
for _, c := range m.Certs {
|
||||
certs = append(certs, c)
|
||||
}
|
||||
return certs, len(certs), nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) Get(ctx context.Context, id string) (*domain.ManagedCertificate, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
cert, ok := m.Certs[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) Create(ctx context.Context, cert *domain.ManagedCertificate) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Certs[cert.ID] = cert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) Update(ctx context.Context, cert *domain.ManagedCertificate) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.Certs[cert.ID] = cert
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) Archive(ctx context.Context, id string) error {
|
||||
if m.ArchiveErr != nil {
|
||||
return m.ArchiveErr
|
||||
}
|
||||
cert, ok := m.Certs[id]
|
||||
if !ok {
|
||||
return errNotFound
|
||||
}
|
||||
cert.Status = domain.CertificateStatusArchived
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) ListVersions(ctx context.Context, certID string) ([]*domain.CertificateVersion, error) {
|
||||
if m.ListVersionsErr != nil {
|
||||
return nil, m.ListVersionsErr
|
||||
}
|
||||
if m.ListVersionsResult != nil {
|
||||
return m.ListVersionsResult, nil
|
||||
}
|
||||
return m.Versions[certID], nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) CreateVersion(ctx context.Context, version *domain.CertificateVersion) error {
|
||||
if m.CreateVersionErr != nil {
|
||||
return m.CreateVersionErr
|
||||
}
|
||||
m.Versions[version.CertificateID] = append(m.Versions[version.CertificateID], version)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) GetExpiringCertificates(ctx context.Context, before time.Time) ([]*domain.ManagedCertificate, error) {
|
||||
var expiring []*domain.ManagedCertificate
|
||||
for _, c := range m.Certs {
|
||||
if c.ExpiresAt.Before(before) {
|
||||
expiring = append(expiring, c)
|
||||
}
|
||||
}
|
||||
return expiring, nil
|
||||
}
|
||||
|
||||
func (m *mockCertRepo) AddCert(cert *domain.ManagedCertificate) {
|
||||
m.Certs[cert.ID] = cert
|
||||
}
|
||||
|
||||
// mockJobRepo is a test implementation of JobRepository
|
||||
type mockJobRepo struct {
|
||||
Jobs map[string]*domain.Job
|
||||
StatusUpdates map[string]domain.JobStatus
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
UpdateStatusErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
ListByStatusErr error
|
||||
DeleteErr error
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) List(ctx context.Context) ([]*domain.Job, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var jobs []*domain.Job
|
||||
for _, j := range m.Jobs {
|
||||
jobs = append(jobs, j)
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) Get(ctx context.Context, id string) (*domain.Job, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
job, ok := m.Jobs[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return job, nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) Create(ctx context.Context, job *domain.Job) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Jobs[job.ID] = job
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) Update(ctx context.Context, job *domain.Job) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.Jobs[job.ID] = job
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.Jobs, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) ListByStatus(ctx context.Context, status domain.JobStatus) ([]*domain.Job, error) {
|
||||
if m.ListByStatusErr != nil {
|
||||
return nil, m.ListByStatusErr
|
||||
}
|
||||
var jobs []*domain.Job
|
||||
for _, j := range m.Jobs {
|
||||
if j.Status == status {
|
||||
jobs = append(jobs, j)
|
||||
}
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) ListByCertificate(ctx context.Context, certID string) ([]*domain.Job, error) {
|
||||
var jobs []*domain.Job
|
||||
for _, j := range m.Jobs {
|
||||
if j.CertificateID == certID {
|
||||
jobs = append(jobs, j)
|
||||
}
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) UpdateStatus(ctx context.Context, id string, status domain.JobStatus, errMsg string) error {
|
||||
if m.UpdateStatusErr != nil {
|
||||
return m.UpdateStatusErr
|
||||
}
|
||||
job, ok := m.Jobs[id]
|
||||
if !ok {
|
||||
return errNotFound
|
||||
}
|
||||
job.Status = status
|
||||
if errMsg != "" {
|
||||
job.LastError = &errMsg
|
||||
}
|
||||
m.StatusUpdates[id] = status
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) GetPendingJobs(ctx context.Context, jobType domain.JobType) ([]*domain.Job, error) {
|
||||
var jobs []*domain.Job
|
||||
for _, j := range m.Jobs {
|
||||
if j.Type == jobType && j.Status == domain.JobStatusPending {
|
||||
jobs = append(jobs, j)
|
||||
}
|
||||
}
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func (m *mockJobRepo) AddJob(job *domain.Job) {
|
||||
m.Jobs[job.ID] = job
|
||||
}
|
||||
|
||||
// mockNotifRepo is a test implementation of NotificationRepository
|
||||
type mockNotifRepo struct {
|
||||
Notifications []*domain.NotificationEvent
|
||||
CreateErr error
|
||||
ListErr error
|
||||
UpdateErr error
|
||||
}
|
||||
|
||||
func (m *mockNotifRepo) Create(ctx context.Context, notif *domain.NotificationEvent) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Notifications = append(m.Notifications, notif)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotifRepo) List(ctx context.Context, filter *repository.NotificationFilter) ([]*domain.NotificationEvent, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
return m.Notifications, nil
|
||||
}
|
||||
|
||||
func (m *mockNotifRepo) UpdateStatus(ctx context.Context, id string, status string, sentAt time.Time) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
for _, n := range m.Notifications {
|
||||
if n.ID == id {
|
||||
n.Status = status
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return errNotFound
|
||||
}
|
||||
|
||||
func (m *mockNotifRepo) AddNotification(notif *domain.NotificationEvent) {
|
||||
m.Notifications = append(m.Notifications, notif)
|
||||
}
|
||||
|
||||
// mockAuditRepo is a test implementation of AuditRepository
|
||||
type mockAuditRepo struct {
|
||||
Events []*domain.AuditEvent
|
||||
CreateErr error
|
||||
ListErr error
|
||||
}
|
||||
|
||||
func (m *mockAuditRepo) Create(ctx context.Context, event *domain.AuditEvent) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Events = append(m.Events, event)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAuditRepo) List(ctx context.Context, filter *repository.AuditFilter) ([]*domain.AuditEvent, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
// Apply filtering like the real repo
|
||||
var filtered []*domain.AuditEvent
|
||||
for _, e := range m.Events {
|
||||
if filter != nil {
|
||||
if filter.ResourceType != "" && e.ResourceType != filter.ResourceType {
|
||||
continue
|
||||
}
|
||||
if filter.ResourceID != "" && e.ResourceID != filter.ResourceID {
|
||||
continue
|
||||
}
|
||||
if filter.Actor != "" && e.Actor != filter.Actor {
|
||||
continue
|
||||
}
|
||||
if !filter.From.IsZero() && e.Timestamp.Before(filter.From) {
|
||||
continue
|
||||
}
|
||||
if !filter.To.IsZero() && e.Timestamp.After(filter.To) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
filtered = append(filtered, e)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (m *mockAuditRepo) AddEvent(event *domain.AuditEvent) {
|
||||
m.Events = append(m.Events, event)
|
||||
}
|
||||
|
||||
// mockPolicyRepo is a test implementation of PolicyRepository
|
||||
type mockPolicyRepo struct {
|
||||
Rules map[string]*domain.PolicyRule
|
||||
Violations []*domain.PolicyViolation
|
||||
CreateRuleErr error
|
||||
UpdateRuleErr error
|
||||
DeleteRuleErr error
|
||||
GetRuleErr error
|
||||
ListRulesErr error
|
||||
CreateViolationErr error
|
||||
ListViolationsErr error
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) ListRules(ctx context.Context) ([]*domain.PolicyRule, error) {
|
||||
if m.ListRulesErr != nil {
|
||||
return nil, m.ListRulesErr
|
||||
}
|
||||
var rules []*domain.PolicyRule
|
||||
for _, r := range m.Rules {
|
||||
rules = append(rules, r)
|
||||
}
|
||||
return rules, nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) GetRule(ctx context.Context, id string) (*domain.PolicyRule, error) {
|
||||
if m.GetRuleErr != nil {
|
||||
return nil, m.GetRuleErr
|
||||
}
|
||||
rule, ok := m.Rules[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return rule, nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) CreateRule(ctx context.Context, rule *domain.PolicyRule) error {
|
||||
if m.CreateRuleErr != nil {
|
||||
return m.CreateRuleErr
|
||||
}
|
||||
m.Rules[rule.ID] = rule
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) UpdateRule(ctx context.Context, rule *domain.PolicyRule) error {
|
||||
if m.UpdateRuleErr != nil {
|
||||
return m.UpdateRuleErr
|
||||
}
|
||||
m.Rules[rule.ID] = rule
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) DeleteRule(ctx context.Context, id string) error {
|
||||
if m.DeleteRuleErr != nil {
|
||||
return m.DeleteRuleErr
|
||||
}
|
||||
delete(m.Rules, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) CreateViolation(ctx context.Context, violation *domain.PolicyViolation) error {
|
||||
if m.CreateViolationErr != nil {
|
||||
return m.CreateViolationErr
|
||||
}
|
||||
m.Violations = append(m.Violations, violation)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) ListViolations(ctx context.Context, filter *repository.AuditFilter) ([]*domain.PolicyViolation, error) {
|
||||
if m.ListViolationsErr != nil {
|
||||
return nil, m.ListViolationsErr
|
||||
}
|
||||
return m.Violations, nil
|
||||
}
|
||||
|
||||
func (m *mockPolicyRepo) AddRule(rule *domain.PolicyRule) {
|
||||
m.Rules[rule.ID] = rule
|
||||
}
|
||||
|
||||
// mockRenewalPolicyRepo is a test implementation of RenewalPolicyRepository
|
||||
type mockRenewalPolicyRepo struct {
|
||||
Policies map[string]*domain.RenewalPolicy
|
||||
GetErr error
|
||||
ListErr error
|
||||
}
|
||||
|
||||
func (m *mockRenewalPolicyRepo) Get(ctx context.Context, id string) (*domain.RenewalPolicy, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
policy, ok := m.Policies[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return policy, nil
|
||||
}
|
||||
|
||||
func (m *mockRenewalPolicyRepo) List(ctx context.Context) ([]*domain.RenewalPolicy, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var policies []*domain.RenewalPolicy
|
||||
for _, p := range m.Policies {
|
||||
policies = append(policies, p)
|
||||
}
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
func (m *mockRenewalPolicyRepo) AddPolicy(policy *domain.RenewalPolicy) {
|
||||
m.Policies[policy.ID] = policy
|
||||
}
|
||||
|
||||
// mockAgentRepo is a test implementation of AgentRepository
|
||||
type mockAgentRepo struct {
|
||||
Agents map[string]*domain.Agent
|
||||
HeartbeatUpdates map[string]time.Time
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
UpdateHeartbeatErr error
|
||||
GetByAPIKeyErr error
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) List(ctx context.Context) ([]*domain.Agent, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var agents []*domain.Agent
|
||||
for _, a := range m.Agents {
|
||||
agents = append(agents, a)
|
||||
}
|
||||
return agents, nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) Get(ctx context.Context, id string) (*domain.Agent, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
agent, ok := m.Agents[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) Create(ctx context.Context, agent *domain.Agent) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Agents[agent.ID] = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) Update(ctx context.Context, agent *domain.Agent) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.Agents[agent.ID] = agent
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.Agents, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) UpdateHeartbeat(ctx context.Context, id string) error {
|
||||
if m.UpdateHeartbeatErr != nil {
|
||||
return m.UpdateHeartbeatErr
|
||||
}
|
||||
agent, ok := m.Agents[id]
|
||||
if !ok {
|
||||
return errNotFound
|
||||
}
|
||||
now := time.Now()
|
||||
agent.LastHeartbeatAt = &now
|
||||
m.HeartbeatUpdates[id] = now
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) GetByAPIKey(ctx context.Context, keyHash string) (*domain.Agent, error) {
|
||||
if m.GetByAPIKeyErr != nil {
|
||||
return nil, m.GetByAPIKeyErr
|
||||
}
|
||||
for _, a := range m.Agents {
|
||||
if a.APIKeyHash == keyHash {
|
||||
return a, nil
|
||||
}
|
||||
}
|
||||
return nil, errNotFound
|
||||
}
|
||||
|
||||
func (m *mockAgentRepo) AddAgent(agent *domain.Agent) {
|
||||
m.Agents[agent.ID] = agent
|
||||
}
|
||||
|
||||
// mockTargetRepo is a test implementation of TargetRepository
|
||||
type mockTargetRepo struct {
|
||||
Targets map[string]*domain.DeploymentTarget
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
GetErr error
|
||||
ListErr error
|
||||
ListByCertErr error
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) List(ctx context.Context) ([]*domain.DeploymentTarget, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var targets []*domain.DeploymentTarget
|
||||
for _, t := range m.Targets {
|
||||
targets = append(targets, t)
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) Get(ctx context.Context, id string) (*domain.DeploymentTarget, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
target, ok := m.Targets[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) Create(ctx context.Context, target *domain.DeploymentTarget) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.Targets[target.ID] = target
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) Update(ctx context.Context, target *domain.DeploymentTarget) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.Targets[target.ID] = target
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.Targets, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) ListByCertificate(ctx context.Context, certID string) ([]*domain.DeploymentTarget, error) {
|
||||
if m.ListByCertErr != nil {
|
||||
return nil, m.ListByCertErr
|
||||
}
|
||||
return m.List(ctx)
|
||||
}
|
||||
|
||||
func (m *mockTargetRepo) AddTarget(target *domain.DeploymentTarget) {
|
||||
m.Targets[target.ID] = target
|
||||
}
|
||||
|
||||
// mockIssuerConnector is a test implementation of IssuerConnector
|
||||
type mockIssuerConnector struct {
|
||||
Result *IssuanceResult
|
||||
Err error
|
||||
}
|
||||
|
||||
func (m *mockIssuerConnector) IssueCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
if m.Result != nil {
|
||||
return m.Result, nil
|
||||
}
|
||||
now := time.Now()
|
||||
return &IssuanceResult{
|
||||
Serial: "test-serial-123",
|
||||
CertPEM: "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----",
|
||||
ChainPEM: "-----BEGIN CERTIFICATE-----\nchain\n-----END CERTIFICATE-----",
|
||||
NotBefore: now,
|
||||
NotAfter: now.AddDate(1, 0, 0),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerConnector) RenewCertificate(ctx context.Context, commonName string, sans []string, csrPEM string) (*IssuanceResult, error) {
|
||||
if m.Err != nil {
|
||||
return nil, m.Err
|
||||
}
|
||||
return m.IssueCertificate(ctx, commonName, sans, csrPEM)
|
||||
}
|
||||
|
||||
// Constructor functions for mocks
|
||||
|
||||
func newMockCertificateRepository() *mockCertRepo {
|
||||
return &mockCertRepo{
|
||||
Certs: make(map[string]*domain.ManagedCertificate),
|
||||
Versions: make(map[string][]*domain.CertificateVersion),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockJobRepository() *mockJobRepo {
|
||||
return &mockJobRepo{
|
||||
Jobs: make(map[string]*domain.Job),
|
||||
StatusUpdates: make(map[string]domain.JobStatus),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockNotificationRepository() *mockNotifRepo {
|
||||
return &mockNotifRepo{
|
||||
Notifications: make([]*domain.NotificationEvent, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockAuditRepository() *mockAuditRepo {
|
||||
return &mockAuditRepo{
|
||||
Events: make([]*domain.AuditEvent, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockPolicyRepository() *mockPolicyRepo {
|
||||
return &mockPolicyRepo{
|
||||
Rules: make(map[string]*domain.PolicyRule),
|
||||
Violations: make([]*domain.PolicyViolation, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockRenewalPolicyRepository() *mockRenewalPolicyRepo {
|
||||
return &mockRenewalPolicyRepo{
|
||||
Policies: make(map[string]*domain.RenewalPolicy),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockAgentRepository() *mockAgentRepo {
|
||||
return &mockAgentRepo{
|
||||
Agents: make(map[string]*domain.Agent),
|
||||
HeartbeatUpdates: make(map[string]time.Time),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockTargetRepository() *mockTargetRepo {
|
||||
return &mockTargetRepo{
|
||||
Targets: make(map[string]*domain.DeploymentTarget),
|
||||
}
|
||||
}
|
||||
|
||||
func newMockIssuerRepository() *mockIssuerRepository {
|
||||
return &mockIssuerRepository{
|
||||
issuers: make(map[string]*domain.Issuer),
|
||||
}
|
||||
}
|
||||
|
||||
// mockIssuerRepository is a test implementation of IssuerRepository
|
||||
type mockIssuerRepository struct {
|
||||
issuers map[string]*domain.Issuer
|
||||
GetErr error
|
||||
ListErr error
|
||||
CreateErr error
|
||||
UpdateErr error
|
||||
DeleteErr error
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) List(ctx context.Context) ([]*domain.Issuer, error) {
|
||||
if m.ListErr != nil {
|
||||
return nil, m.ListErr
|
||||
}
|
||||
var issuers []*domain.Issuer
|
||||
for _, i := range m.issuers {
|
||||
issuers = append(issuers, i)
|
||||
}
|
||||
return issuers, nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) Get(ctx context.Context, id string) (*domain.Issuer, error) {
|
||||
if m.GetErr != nil {
|
||||
return nil, m.GetErr
|
||||
}
|
||||
issuer, ok := m.issuers[id]
|
||||
if !ok {
|
||||
return nil, errNotFound
|
||||
}
|
||||
return issuer, nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) Create(ctx context.Context, issuer *domain.Issuer) error {
|
||||
if m.CreateErr != nil {
|
||||
return m.CreateErr
|
||||
}
|
||||
m.issuers[issuer.ID] = issuer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) Update(ctx context.Context, issuer *domain.Issuer) error {
|
||||
if m.UpdateErr != nil {
|
||||
return m.UpdateErr
|
||||
}
|
||||
m.issuers[issuer.ID] = issuer
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) Delete(ctx context.Context, id string) error {
|
||||
if m.DeleteErr != nil {
|
||||
return m.DeleteErr
|
||||
}
|
||||
delete(m.issuers, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockIssuerRepository) AddIssuer(issuer *domain.Issuer) {
|
||||
m.issuers[issuer.ID] = issuer
|
||||
}
|
||||
|
||||
// mockNotifier is a simple notifier for testing
|
||||
type mockNotifier struct {
|
||||
messages []*mockNotifierMessage
|
||||
SendErr error
|
||||
}
|
||||
|
||||
type mockNotifierMessage struct {
|
||||
Recipient string
|
||||
Subject string
|
||||
Body string
|
||||
}
|
||||
|
||||
func newMockNotifier() *mockNotifier {
|
||||
return &mockNotifier{
|
||||
messages: make([]*mockNotifierMessage, 0),
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockNotifier) Send(ctx context.Context, recipient string, subject string, body string) error {
|
||||
if m.SendErr != nil {
|
||||
return m.SendErr
|
||||
}
|
||||
m.messages = append(m.messages, &mockNotifierMessage{
|
||||
Recipient: recipient,
|
||||
Subject: subject,
|
||||
Body: body,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockNotifier) Channel() string {
|
||||
return "Email"
|
||||
}
|
||||
|
||||
func (m *mockNotifier) getSentCount() int {
|
||||
return len(m.messages)
|
||||
}
|
||||
|
||||
func (m *mockNotifier) getLastMessage() *mockNotifierMessage {
|
||||
if len(m.messages) == 0 {
|
||||
return nil
|
||||
}
|
||||
return m.messages[len(m.messages)-1]
|
||||
}
|
||||
Reference in New Issue
Block a user