mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 15:51:30 +00:00
de9264baf7
Implements 3 deferred security tickets (TICKET-003, TICKET-007, TICKET-010) and performs comprehensive documentation audit to eliminate drift between code and docs. Code changes: - TICKET-003: Repository integration tests with testcontainers-go (50+ subtests) - TICKET-007: CertificateService decomposition into RevocationSvc + CAOperationsSvc - TICKET-010: Request body size limits via http.MaxBytesReader middleware - Fix missing slog import in certificate.go after service decomposition Documentation updates: - README: Fix endpoint count (97→93), expand env var reference (15→39 vars) - CLAUDE.md: Fix OpenAPI operation count (85→93), update file locations - architecture.md: Add body size limits section, middleware chain ordering - CONTRIBUTING.md: New contributor guide with architecture conventions, test patterns, middleware ordering, CI thresholds - SECURITY_REMEDIATION.md: Removed from repo (moved to cowork, gitignored) - Test files: Add doc comments to all new test files Documentation that should exist but doesn't yet: - Architecture diagrams (C4 model or similar) - Threat model document - Testing philosophy guide - Disaster recovery runbook - Upgrade guide (migration between versions) - API versioning strategy document Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
// Tests for RevocationSvc, the focused sub-service that handles certificate
|
|
// revocation logic extracted from CertificateService (TICKET-007).
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/shankar0123/certctl/internal/domain"
|
|
)
|
|
|
|
// helper to create a RevocationSvc for testing
|
|
func newRevocationSvcTest() (*RevocationSvc, *mockCertRepo, *mockRevocationRepo, *mockAuditRepo) {
|
|
certRepo := newMockCertificateRepository()
|
|
revocationRepo := newMockRevocationRepository()
|
|
auditRepo := newMockAuditRepository()
|
|
|
|
auditService := NewAuditService(auditRepo)
|
|
revSvc := NewRevocationSvc(certRepo, revocationRepo, auditService)
|
|
revSvc.SetIssuerRegistry(map[string]IssuerConnector{
|
|
"iss-local": &mockIssuerConnector{},
|
|
})
|
|
|
|
return revSvc, certRepo, revocationRepo, auditRepo
|
|
}
|
|
|
|
func TestRevocationSvc_RevokeCertificateWithActor_Success(t *testing.T) {
|
|
revSvc, certRepo, revocationRepo, auditRepo := newRevocationSvcTest()
|
|
|
|
// Set up test data
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-1",
|
|
CommonName: "example.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
// Add a certificate version with a serial number
|
|
version := &domain.CertificateVersion{
|
|
ID: "ver-1",
|
|
CertificateID: "cert-1",
|
|
SerialNumber: "ABC123",
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
certRepo.Versions["cert-1"] = []*domain.CertificateVersion{version}
|
|
|
|
// Revoke
|
|
err := revSvc.RevokeCertificateWithActor(context.Background(), "cert-1", "keyCompromise", "admin")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Verify certificate status changed
|
|
updated, _ := certRepo.Get(context.Background(), "cert-1")
|
|
if updated.Status != domain.CertificateStatusRevoked {
|
|
t.Errorf("expected status Revoked, got %s", updated.Status)
|
|
}
|
|
if updated.RevokedAt == nil {
|
|
t.Error("expected RevokedAt to be set")
|
|
}
|
|
if updated.RevocationReason != "keyCompromise" {
|
|
t.Errorf("expected reason keyCompromise, got %s", updated.RevocationReason)
|
|
}
|
|
|
|
// Verify revocation record created
|
|
if len(revocationRepo.Revocations) != 1 {
|
|
t.Fatalf("expected 1 revocation record, got %d", len(revocationRepo.Revocations))
|
|
}
|
|
rev := revocationRepo.Revocations[0]
|
|
if rev.SerialNumber != "ABC123" {
|
|
t.Errorf("expected serial ABC123, got %s", rev.SerialNumber)
|
|
}
|
|
if rev.Reason != "keyCompromise" {
|
|
t.Errorf("expected reason keyCompromise, got %s", rev.Reason)
|
|
}
|
|
if rev.RevokedBy != "admin" {
|
|
t.Errorf("expected revokedBy admin, got %s", rev.RevokedBy)
|
|
}
|
|
|
|
// Verify audit event recorded
|
|
if len(auditRepo.Events) == 0 {
|
|
t.Error("expected audit event to be recorded")
|
|
}
|
|
}
|
|
|
|
func TestRevocationSvc_RevokeCertificateWithActor_AlreadyRevoked(t *testing.T) {
|
|
revSvc, certRepo, _, _ := newRevocationSvcTest()
|
|
|
|
now := time.Now()
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-3",
|
|
CommonName: "already-revoked.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusRevoked,
|
|
RevokedAt: &now,
|
|
RevocationReason: "keyCompromise",
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
err := revSvc.RevokeCertificateWithActor(context.Background(), "cert-3", "superseded", "admin")
|
|
if err == nil {
|
|
t.Fatal("expected error for already revoked certificate")
|
|
}
|
|
if err.Error() != "certificate is already revoked" {
|
|
t.Errorf("expected 'already revoked' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRevocationSvc_GetRevokedCertificates_Success(t *testing.T) {
|
|
revSvc, _, revocationRepo, _ := newRevocationSvcTest()
|
|
|
|
// Pre-populate revocation records
|
|
revocationRepo.Revocations = []*domain.CertificateRevocation{
|
|
{ID: "rev-1", CertificateID: "cert-1", SerialNumber: "SER-1", Reason: "keyCompromise", RevokedAt: time.Now()},
|
|
{ID: "rev-2", CertificateID: "cert-2", SerialNumber: "SER-2", Reason: "superseded", RevokedAt: time.Now()},
|
|
}
|
|
|
|
revocations, err := revSvc.GetRevokedCertificates()
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if len(revocations) != 2 {
|
|
t.Errorf("expected 2 revocations, got %d", len(revocations))
|
|
}
|
|
}
|