Files
certctl/internal/service/issuer_registry_test.go
T

289 lines
6.3 KiB
Go

package service
import (
"encoding/json"
"log/slog"
"os"
"sync"
"testing"
"github.com/shankar0123/certctl/internal/crypto"
"github.com/shankar0123/certctl/internal/domain"
)
func registryTestLogger() *slog.Logger {
return slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelError}))
}
func TestIssuerRegistry_GetSet(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
mock := &mockIssuerConnector{}
reg.Set("iss-test", mock)
conn, ok := reg.Get("iss-test")
if !ok {
t.Fatal("expected to find iss-test in registry")
}
if conn == nil {
t.Fatal("expected non-nil connector")
}
}
func TestIssuerRegistry_GetNotFound(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
_, ok := reg.Get("nonexistent")
if ok {
t.Fatal("expected not to find nonexistent issuer")
}
}
func TestIssuerRegistry_Remove(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
reg.Set("iss-test", &mockIssuerConnector{})
reg.Remove("iss-test")
_, ok := reg.Get("iss-test")
if ok {
t.Fatal("expected issuer to be removed")
}
}
func TestIssuerRegistry_List(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
reg.Set("iss-a", &mockIssuerConnector{})
reg.Set("iss-b", &mockIssuerConnector{})
list := reg.List()
if len(list) != 2 {
t.Fatalf("expected 2 issuers, got %d", len(list))
}
// Verify List returns a copy (modifying it doesn't affect registry)
delete(list, "iss-a")
if reg.Len() != 2 {
t.Fatal("deleting from List() copy should not affect registry")
}
}
func TestIssuerRegistry_Len(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
if reg.Len() != 0 {
t.Fatalf("expected empty registry, got %d", reg.Len())
}
reg.Set("iss-a", &mockIssuerConnector{})
if reg.Len() != 1 {
t.Fatalf("expected 1 issuer, got %d", reg.Len())
}
}
func TestIssuerRegistry_Rebuild_Enabled(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
configs := []*domain.Issuer{
{
ID: "iss-local",
Name: "Local CA",
Type: "local",
Config: json.RawMessage(`{}`),
Enabled: true,
},
{
ID: "iss-disabled",
Name: "Disabled",
Type: "local",
Config: json.RawMessage(`{}`),
Enabled: false,
},
}
err := reg.Rebuild(configs, "")
if err != nil {
t.Fatalf("Rebuild failed: %v", err)
}
if reg.Len() != 1 {
t.Fatalf("expected 1 enabled issuer, got %d", reg.Len())
}
_, ok := reg.Get("iss-local")
if !ok {
t.Fatal("expected iss-local in registry")
}
_, ok = reg.Get("iss-disabled")
if ok {
t.Fatal("disabled issuer should not be in registry")
}
}
func TestIssuerRegistry_Rebuild_WithEncryption(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
configJSON := []byte(`{"ca_common_name":"Encrypted CA"}`)
// M-8: EncryptIfKeySet now emits v2 (magic 0x02 || per-ciphertext salt || sealed).
// IssuerRegistry.Rebuild accepts the raw passphrase and delegates PBKDF2 to crypto.DecryptIfKeySet.
encrypted, _, err := crypto.EncryptIfKeySet(configJSON, "test-key")
if err != nil {
t.Fatalf("EncryptIfKeySet failed: %v", err)
}
configs := []*domain.Issuer{
{
ID: "iss-encrypted",
Name: "Encrypted Local CA",
Type: "local",
EncryptedConfig: encrypted,
Enabled: true,
},
}
err = reg.Rebuild(configs, "test-key")
if err != nil {
t.Fatalf("Rebuild with encryption failed: %v", err)
}
_, ok := reg.Get("iss-encrypted")
if !ok {
t.Fatal("expected iss-encrypted in registry")
}
}
func TestIssuerRegistry_Rebuild_NilKeyFallback(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
configs := []*domain.Issuer{
{
ID: "iss-plain",
Name: "Plain Config",
Type: "local",
Config: json.RawMessage(`{}`),
Enabled: true,
},
}
// Empty passphrase is safe when no EncryptedConfig is present — falls back to config column.
// The C-2 fail-closed sentinel only fires when EncryptedConfig is non-empty.
err := reg.Rebuild(configs, "")
if err != nil {
t.Fatalf("Rebuild with empty key failed: %v", err)
}
_, ok := reg.Get("iss-plain")
if !ok {
t.Fatal("expected iss-plain in registry")
}
}
func TestIssuerRegistry_Rebuild_InvalidConfig(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
configs := []*domain.Issuer{
{
ID: "iss-bad",
Name: "Bad Config",
Type: "UnknownType",
Config: json.RawMessage(`{}`),
Enabled: true,
},
{
ID: "iss-good",
Name: "Good Config",
Type: "local",
Config: json.RawMessage(`{}`),
Enabled: true,
},
}
// Should return an error indicating partial failure, but still load valid issuers
err := reg.Rebuild(configs, "")
if err == nil {
t.Fatal("Rebuild should return error when some issuers fail to load")
}
// Despite the error, valid issuers should be loaded
if reg.Len() != 1 {
t.Fatalf("expected 1 valid issuer, got %d", reg.Len())
}
_, ok := reg.Get("iss-good")
if !ok {
t.Fatal("expected iss-good in registry")
}
}
func TestIssuerRegistry_Rebuild_ReplacesExisting(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
// Set up initial state
reg.Set("iss-old", &mockIssuerConnector{})
configs := []*domain.Issuer{
{
ID: "iss-new",
Name: "New Issuer",
Type: "local",
Config: json.RawMessage(`{}`),
Enabled: true,
},
}
err := reg.Rebuild(configs, "")
if err != nil {
t.Fatalf("Rebuild failed: %v", err)
}
_, ok := reg.Get("iss-old")
if ok {
t.Fatal("old issuer should have been replaced")
}
_, ok = reg.Get("iss-new")
if !ok {
t.Fatal("new issuer should be present")
}
}
func TestIssuerRegistry_ConcurrentAccess(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(3)
id := "iss-concurrent"
go func() {
defer wg.Done()
reg.Set(id, &mockIssuerConnector{})
}()
go func() {
defer wg.Done()
reg.Get(id)
}()
go func() {
defer wg.Done()
reg.List()
}()
}
wg.Wait()
// No race detector panics = success
}
func TestIssuerRegistry_Rebuild_Empty(t *testing.T) {
reg := NewIssuerRegistry(registryTestLogger())
reg.Set("iss-existing", &mockIssuerConnector{})
err := reg.Rebuild([]*domain.Issuer{}, "")
if err != nil {
t.Fatalf("Rebuild with empty configs failed: %v", err)
}
if reg.Len() != 0 {
t.Fatalf("expected empty registry after rebuild with no configs, got %d", reg.Len())
}
}