mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 17:31:30 +00:00
8b75e0311b
Mechanical sed across the main go.mod's module declaration, the f5-mock-icontrol
sub-module's go.mod, every Go file's import path (361 files), and a rebuild of
the checked-in f5-mock-icontrol binary so its embedded build-info reflects the
new module path. No behavior change.
Choice B from cowork/transfer-certctl-to-org.md, executed 2026-05-04. Choice A
(keep module path declared as github.com/shankar0123/certctl regardless of
repo URL) shipped on the day of the org transfer (2026-05-03) since we had no
external Go consumers; this commit closes that deferral.
Backward-compat: GitHub HTTP redirects continue to forward
github.com/shankar0123/certctl → github.com/certctl-io/certctl at the URL
level, but Go's module proxy uses the path declared in go.mod as the
canonical name. Pre-fix, anyone trying `go get github.com/certctl-io/certctl/...`
hit a "module path mismatch" error because go.mod said
github.com/shankar0123/certctl and the URL they fetched it from said
certctl-io/certctl. Post-fix, the canonical name and the URL agree, so
go get / go install / external Go consumers / Go-tooling integrations
work cleanly via either the new path (preferred) or the old path (which
redirects and Go follows the redirect for source fetch).
Anyone still importing the old path inside their own code keeps working
provided they update their go.mod's `require` line to match — the module
path declared in their consumer's go.sum / go.mod is the authoritative
import name, so a mass sed across their import statements is the migration
on the consumer side. No external consumers exist today.
Diff shape:
361 *.go files — import path replacement only
2 go.mod — module declaration replacement only
1 binary — deploy/test/f5-mock-icontrol/f5-mock-icontrol rebuilt
so embedded build-info reflects the new path (8618965 vs
8618933 bytes; 32-byte diff is the build-info change)
Total: 364 files, 730 insertions / 730 deletions, net-zero size, pure
mechanical substitution.
Verification:
gofmt: 17 files needed re-alignment after sed (the new path is one char
shorter than the old, so column-aligned import groups drifted). Applied
`gofmt -w` to fix.
go mod tidy: clean exit on both modules.
go vet ./...: clean exit.
go build ./...: clean exit.
go test -short -count=1 on representative packages: all green
(internal/domain, internal/validation, internal/crypto, internal/crypto/signer,
cmd/agent). Test output now reads `ok github.com/certctl-io/certctl/...`
confirming the module path resolves correctly.
binary: f5-mock-icontrol rebuilt; `strings | grep shankar0123` returns
nothing; `strings | grep certctl-io/certctl` shows the new module path
embedded in build-info.
Files intentionally NOT touched in this commit:
README.md / CHANGELOG.md / docs/ / etc. — already swept to certctl-io
URLs in commit 0729ee4 (the post-transfer URL refresh). This commit is
purely the Go-tooling layer.
Scarf pixels (`shankar0123.docker.scarf.sh/...`) — Scarf-account
namespace, not a Go import or GitHub repo URL. Stays.
This is a non-blocking, non-customer-impacting change. Operators pulling
container images, running `make verify`, hitting the API, or installing the
agent see no functional difference. Only Go-tooling consumers (none today)
are affected, and they're enabled — not broken — by this commit.
641 lines
19 KiB
Go
641 lines
19 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"log/slog"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
)
|
|
|
|
// helper to create a test CertificateService wired for revocation tests
|
|
func newRevocationTestService() (*CertificateService, *mockCertRepo, *mockRevocationRepo, *mockAuditRepo) {
|
|
certRepo := newMockCertificateRepository()
|
|
auditRepo := newMockAuditRepository()
|
|
policyRepo := newMockPolicyRepository()
|
|
revocationRepo := newMockRevocationRepository()
|
|
profileRepo := newMockProfileRepository()
|
|
|
|
auditService := NewAuditService(auditRepo)
|
|
policyService := NewPolicyService(policyRepo, auditService)
|
|
|
|
// Create RevocationSvc
|
|
revSvc := NewRevocationSvc(certRepo, revocationRepo, auditService)
|
|
registry := NewIssuerRegistry(slog.Default())
|
|
registry.Set("iss-local", &mockIssuerConnector{})
|
|
revSvc.SetIssuerRegistry(registry)
|
|
|
|
// Create CAOperationsSvc
|
|
caSvc := NewCAOperationsSvc(revocationRepo, certRepo, profileRepo)
|
|
caSvc.SetIssuerRegistry(registry)
|
|
|
|
certService := NewCertificateService(certRepo, policyService, auditService)
|
|
certService.SetRevocationSvc(revSvc)
|
|
certService.SetCAOperationsSvc(caSvc)
|
|
|
|
return certService, certRepo, revocationRepo, auditRepo
|
|
}
|
|
|
|
func TestRevokeCertificate_Success(t *testing.T) {
|
|
svc, certRepo, revocationRepo, auditRepo := newRevocationTestService()
|
|
|
|
// 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 := svc.RevokeCertificate(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")
|
|
}
|
|
foundRevocationAudit := false
|
|
for _, e := range auditRepo.Events {
|
|
if e.Action == "certificate_revoked" {
|
|
foundRevocationAudit = true
|
|
}
|
|
}
|
|
if !foundRevocationAudit {
|
|
t.Error("expected certificate_revoked audit event")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_DefaultReason(t *testing.T) {
|
|
svc, certRepo, revocationRepo, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-2",
|
|
CommonName: "default-reason.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
certRepo.Versions["cert-2"] = []*domain.CertificateVersion{
|
|
{ID: "ver-2", CertificateID: "cert-2", SerialNumber: "DEF456", CreatedAt: time.Now()},
|
|
}
|
|
|
|
// Revoke with empty reason — should default to "unspecified"
|
|
err := svc.RevokeCertificate(context.Background(), "cert-2", "", "api")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
updated, _ := certRepo.Get(context.Background(), "cert-2")
|
|
if updated.RevocationReason != "unspecified" {
|
|
t.Errorf("expected default reason 'unspecified', got %s", updated.RevocationReason)
|
|
}
|
|
|
|
if len(revocationRepo.Revocations) != 1 {
|
|
t.Fatalf("expected 1 revocation, got %d", len(revocationRepo.Revocations))
|
|
}
|
|
if revocationRepo.Revocations[0].Reason != "unspecified" {
|
|
t.Errorf("expected revocation reason 'unspecified', got %s", revocationRepo.Revocations[0].Reason)
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_AlreadyRevoked(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
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 := svc.RevokeCertificate(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 TestRevokeCertificate_ArchivedCert(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-4",
|
|
CommonName: "archived.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusArchived,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-4", "keyCompromise", "admin")
|
|
if err == nil {
|
|
t.Fatal("expected error for archived certificate")
|
|
}
|
|
if err.Error() != "cannot revoke archived certificate" {
|
|
t.Errorf("expected 'cannot revoke archived' error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_InvalidReason(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-5",
|
|
CommonName: "invalid-reason.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-5", "notAValidReason", "admin")
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid reason")
|
|
}
|
|
if err.Error() != "invalid revocation reason: notAValidReason" {
|
|
t.Errorf("unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NotFound(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "nonexistent-cert", "keyCompromise", "admin")
|
|
if err == nil {
|
|
t.Fatal("expected error for nonexistent certificate")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_NoVersion(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-6",
|
|
CommonName: "no-version.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
// No versions added — should fail
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-6", "keyCompromise", "admin")
|
|
if err == nil {
|
|
t.Fatal("expected error when no certificate version exists")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_WithIssuerNotification(t *testing.T) {
|
|
svc, certRepo, revocationRepo, _ := newRevocationTestService()
|
|
|
|
// Wire up issuer registry on RevocationSvc with mock
|
|
mockIssuer := &mockIssuerConnector{}
|
|
registry := NewIssuerRegistry(slog.Default())
|
|
registry.Set("iss-local", mockIssuer)
|
|
svc.revSvc.SetIssuerRegistry(registry)
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-7",
|
|
CommonName: "issuer-notify.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
certRepo.Versions["cert-7"] = []*domain.CertificateVersion{
|
|
{ID: "ver-7", CertificateID: "cert-7", SerialNumber: "GHI789", CreatedAt: time.Now()},
|
|
}
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-7", "cessationOfOperation", "admin")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Verify revocation was recorded and issuer was notified
|
|
if len(revocationRepo.Revocations) != 1 {
|
|
t.Fatalf("expected 1 revocation, got %d", len(revocationRepo.Revocations))
|
|
}
|
|
if !revocationRepo.Revocations[0].IssuerNotified {
|
|
t.Error("expected issuer to be marked as notified")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_WithNotificationService(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
// Wire up notification service on RevocationSvc
|
|
notifRepo := newMockNotificationRepository()
|
|
notifService := NewNotificationService(notifRepo, make(map[string]Notifier))
|
|
svc.revSvc.SetNotificationService(notifService)
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-8",
|
|
CommonName: "with-notify.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
OwnerID: "owner-alice",
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
certRepo.Versions["cert-8"] = []*domain.CertificateVersion{
|
|
{ID: "ver-8", CertificateID: "cert-8", SerialNumber: "JKL012", CreatedAt: time.Now()},
|
|
}
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-8", "keyCompromise", "admin")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
// Should have created revocation notifications (webhook + email)
|
|
if len(notifRepo.Notifications) < 1 {
|
|
t.Error("expected at least one revocation notification to be created")
|
|
}
|
|
|
|
foundRevocationNotif := false
|
|
for _, n := range notifRepo.Notifications {
|
|
if n.Type == domain.NotificationTypeRevocation {
|
|
foundRevocationNotif = true
|
|
}
|
|
}
|
|
if !foundRevocationNotif {
|
|
t.Error("expected Revocation type notification")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_AllValidReasons(t *testing.T) {
|
|
reasons := []string{
|
|
"unspecified", "keyCompromise", "caCompromise", "affiliationChanged",
|
|
"superseded", "cessationOfOperation", "certificateHold", "privilegeWithdrawn",
|
|
}
|
|
|
|
for _, reason := range reasons {
|
|
t.Run(reason, func(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-" + reason,
|
|
CommonName: reason + ".com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
certRepo.Versions["cert-"+reason] = []*domain.CertificateVersion{
|
|
{ID: "ver-" + reason, CertificateID: "cert-" + reason, SerialNumber: "SER-" + reason, CreatedAt: time.Now()},
|
|
}
|
|
|
|
err := svc.RevokeCertificate(context.Background(), "cert-"+reason, reason, "admin")
|
|
if err != nil {
|
|
t.Fatalf("expected no error for reason %s, got: %v", reason, err)
|
|
}
|
|
|
|
updated, _ := certRepo.Get(context.Background(), "cert-"+reason)
|
|
if updated.Status != domain.CertificateStatusRevoked {
|
|
t.Errorf("expected Revoked status, got %s", updated.Status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGetRevokedCertificates_Success(t *testing.T) {
|
|
svc, _, revocationRepo, _ := newRevocationTestService()
|
|
|
|
// 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 := svc.GetRevokedCertificates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if len(revocations) != 2 {
|
|
t.Errorf("expected 2 revocations, got %d", len(revocations))
|
|
}
|
|
}
|
|
|
|
func TestGetRevokedCertificates_Empty(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
revocations, err := svc.GetRevokedCertificates(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
if revocations == nil {
|
|
// nil is acceptable for empty
|
|
} else if len(revocations) != 0 {
|
|
t.Errorf("expected 0 revocations, got %d", len(revocations))
|
|
}
|
|
}
|
|
|
|
func TestGetRevokedCertificates_NoRepo(t *testing.T) {
|
|
certRepo := newMockCertificateRepository()
|
|
auditRepo := newMockAuditRepository()
|
|
policyRepo := newMockPolicyRepository()
|
|
auditService := NewAuditService(auditRepo)
|
|
policyService := NewPolicyService(policyRepo, auditService)
|
|
svc := NewCertificateService(certRepo, policyService, auditService)
|
|
// Do NOT set revocation repo
|
|
|
|
_, err := svc.GetRevokedCertificates(context.Background())
|
|
if err == nil {
|
|
t.Fatal("expected error when revocation repo not configured")
|
|
}
|
|
}
|
|
|
|
func TestRevokeCertificate_HandlerInterfaceMethod(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-handler",
|
|
CommonName: "handler-test.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(0, 6, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
certRepo.Versions["cert-handler"] = []*domain.CertificateVersion{
|
|
{ID: "ver-handler", CertificateID: "cert-handler", SerialNumber: "SER-HANDLER", CreatedAt: time.Now()},
|
|
}
|
|
|
|
// Test the handler interface method (actor collapsed to required positional arg per D-2)
|
|
err := svc.RevokeCertificate(context.Background(), "cert-handler", "superseded", "api")
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
updated, _ := certRepo.Get(context.Background(), "cert-handler")
|
|
if updated.Status != domain.CertificateStatusRevoked {
|
|
t.Errorf("expected Revoked status, got %s", updated.Status)
|
|
}
|
|
}
|
|
|
|
// M15b: CRL and OCSP Service Tests
|
|
|
|
func TestGenerateDERCRL_Success(t *testing.T) {
|
|
svc, _, revocationRepo, _ := newRevocationTestService()
|
|
|
|
// Add some revoked certificates to the repo
|
|
now := time.Now()
|
|
revocationRepo.Revocations = []*domain.CertificateRevocation{
|
|
{
|
|
SerialNumber: "SERIAL-001",
|
|
CertificateID: "cert-1",
|
|
IssuerID: "iss-local",
|
|
Reason: "keyCompromise",
|
|
RevokedAt: now.Add(-24 * time.Hour),
|
|
RevokedBy: "admin",
|
|
},
|
|
{
|
|
SerialNumber: "SERIAL-002",
|
|
CertificateID: "cert-2",
|
|
IssuerID: "iss-local",
|
|
Reason: "superseded",
|
|
RevokedAt: now.Add(-12 * time.Hour),
|
|
RevokedBy: "admin",
|
|
},
|
|
}
|
|
|
|
crl, err := svc.GenerateDERCRL(context.Background(), "iss-local")
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
if crl == nil {
|
|
t.Fatal("expected non-nil CRL")
|
|
}
|
|
|
|
if len(crl) == 0 {
|
|
t.Fatal("expected non-empty CRL")
|
|
}
|
|
|
|
t.Logf("DER CRL generated successfully: %d bytes", len(crl))
|
|
}
|
|
|
|
func TestGenerateDERCRL_EmptyCRL(t *testing.T) {
|
|
svc, _, revocationRepo, _ := newRevocationTestService()
|
|
|
|
// No revoked certs for this issuer
|
|
revocationRepo.Revocations = []*domain.CertificateRevocation{}
|
|
|
|
crl, err := svc.GenerateDERCRL(context.Background(), "iss-local")
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
if crl == nil {
|
|
t.Fatal("expected non-nil CRL even when empty")
|
|
}
|
|
|
|
if len(crl) == 0 {
|
|
t.Fatal("expected non-empty CRL bytes (at least the CRL structure)")
|
|
}
|
|
|
|
t.Logf("Empty DER CRL generated successfully: %d bytes", len(crl))
|
|
}
|
|
|
|
func TestGenerateDERCRL_IssuerNotFound(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
// Try to generate CRL for unknown issuer
|
|
crl, err := svc.GenerateDERCRL(context.Background(), "iss-unknown")
|
|
|
|
// Should return error or nil CRL depending on implementation
|
|
if crl != nil && err == nil {
|
|
t.Error("expected error or nil CRL for unknown issuer")
|
|
}
|
|
|
|
t.Logf("GenerateDERCRL correctly handles unknown issuer")
|
|
}
|
|
|
|
func TestGetOCSPResponse_Good(t *testing.T) {
|
|
svc, certRepo, _, _ := newRevocationTestService()
|
|
|
|
// Add a non-revoked certificate
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-ocsp-good",
|
|
CommonName: "good.example.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusActive,
|
|
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
version := &domain.CertificateVersion{
|
|
ID: "ver-ocsp-good",
|
|
CertificateID: "cert-ocsp-good",
|
|
SerialNumber: "OCSP-GOOD-001",
|
|
NotBefore: time.Now(),
|
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
certRepo.Versions["cert-ocsp-good"] = []*domain.CertificateVersion{version}
|
|
|
|
// Request OCSP response for good cert
|
|
resp, err := svc.GetOCSPResponse(context.Background(), "iss-local", "OCSP-GOOD-001")
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
if resp == nil || len(resp) == 0 {
|
|
t.Fatal("expected non-empty OCSP response for good cert")
|
|
}
|
|
|
|
t.Logf("OCSP response for good cert generated: %d bytes", len(resp))
|
|
}
|
|
|
|
func TestGetOCSPResponse_Revoked(t *testing.T) {
|
|
svc, certRepo, revocationRepo, _ := newRevocationTestService()
|
|
|
|
now := time.Now()
|
|
|
|
// Add a revoked certificate
|
|
cert := &domain.ManagedCertificate{
|
|
ID: "cert-ocsp-revoked",
|
|
CommonName: "revoked.example.com",
|
|
IssuerID: "iss-local",
|
|
Status: domain.CertificateStatusRevoked,
|
|
RevokedAt: &now,
|
|
RevocationReason: "keyCompromise",
|
|
ExpiresAt: time.Now().AddDate(1, 0, 0),
|
|
}
|
|
certRepo.AddCert(cert)
|
|
|
|
version := &domain.CertificateVersion{
|
|
ID: "ver-ocsp-revoked",
|
|
CertificateID: "cert-ocsp-revoked",
|
|
SerialNumber: "OCSP-REVOKED-001",
|
|
NotBefore: time.Now().Add(-24 * time.Hour),
|
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
|
CreatedAt: time.Now(),
|
|
}
|
|
certRepo.Versions["cert-ocsp-revoked"] = []*domain.CertificateVersion{version}
|
|
|
|
// Add revocation record
|
|
revocationRepo.Revocations = []*domain.CertificateRevocation{
|
|
{
|
|
SerialNumber: "OCSP-REVOKED-001",
|
|
CertificateID: "cert-ocsp-revoked",
|
|
IssuerID: "iss-local",
|
|
Reason: "keyCompromise",
|
|
RevokedAt: now.Add(-24 * time.Hour),
|
|
RevokedBy: "admin",
|
|
},
|
|
}
|
|
|
|
// Request OCSP response for revoked cert
|
|
resp, err := svc.GetOCSPResponse(context.Background(), "iss-local", "OCSP-REVOKED-001")
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error, got: %v", err)
|
|
}
|
|
|
|
if resp == nil || len(resp) == 0 {
|
|
t.Fatal("expected non-empty OCSP response for revoked cert")
|
|
}
|
|
|
|
t.Logf("OCSP response for revoked cert generated: %d bytes", len(resp))
|
|
}
|
|
|
|
func TestGetOCSPResponse_Unknown(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
// Request OCSP response for unknown cert
|
|
resp, err := svc.GetOCSPResponse(context.Background(), "iss-local", "UNKNOWN-SERIAL")
|
|
|
|
if err != nil {
|
|
t.Fatalf("expected no error (should return unknown status), got: %v", err)
|
|
}
|
|
|
|
// Response should indicate unknown status
|
|
if resp == nil || len(resp) == 0 {
|
|
t.Fatal("expected non-empty OCSP response even for unknown cert")
|
|
}
|
|
|
|
t.Logf("OCSP response for unknown cert generated: %d bytes", len(resp))
|
|
}
|
|
|
|
func TestGetOCSPResponse_IssuerNotFound(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
// Request OCSP response for unknown issuer
|
|
resp, err := svc.GetOCSPResponse(context.Background(), "iss-unknown", "SOME-SERIAL")
|
|
|
|
// Should return error since issuer doesn't exist
|
|
if err == nil && resp != nil {
|
|
t.Error("expected error for unknown issuer")
|
|
}
|
|
|
|
t.Logf("GetOCSPResponse correctly handles unknown issuer")
|
|
}
|
|
|
|
func TestGetOCSPResponse_InvalidSerial(t *testing.T) {
|
|
svc, _, _, _ := newRevocationTestService()
|
|
|
|
// Request OCSP response with invalid serial format
|
|
resp, err := svc.GetOCSPResponse(context.Background(), "iss-local", "")
|
|
|
|
if err == nil && resp != nil {
|
|
// Empty serial might return unknown status; that's ok
|
|
t.Logf("Empty serial handled gracefully")
|
|
} else if err != nil {
|
|
t.Logf("Empty serial rejected with error: %v", err)
|
|
}
|
|
}
|