mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 11:19:00 +00:00
804a1b05ce
Follow-up to590f654(awsacmpca: replace stub client with AWS SDK v2 implementation). CI's golangci-lint contextcheck rule flagged six violations in awsacmpca_test.go where mustNew/awsacmpca.New were called from test functions that had ctx in scope but didn't thread it through New(). The previous commit used context.Background() inside New() with the rationale that "the audit allows either threading or documenting the limitation"; CI made that choice for us. Threading ctx is the right shape per the audit's stated preference. The fix cascades from awsacmpca.New through issuerfactory.NewFromConfig and IssuerRegistry.Rebuild because the contextcheck rule propagates upward through every caller that has ctx in scope. This commit: - Changes awsacmpca.New(config, logger) to awsacmpca.New(ctx, config, logger). The ctx is passed to buildSDKClient → awsconfig.LoadDefaultConfig so SDK credential chain resolution honors caller deadlines (LoadDefaultConfig may probe IMDS or remote credential sources). The doc-comment on New explains that callers without a useful deadline should pass context.Background() and that the SDK has internal credential-resolution timeouts. - Adds ctx as the first parameter of issuerfactory.NewFromConfig. Currently only the AWSACMPCA branch uses ctx (it's threaded into awsacmpca.New); the other 11 branches accept ctx without using it. This is a contractual change that lets callers thread ctx through without contextcheck warnings, even though most issuer constructors do no ctx-aware work today. - Adds ctx as the first parameter of IssuerRegistry.Rebuild. Rebuild iterates over configs and calls NewFromConfig per issuer; the same ctx flows through every connector instantiation. - Updates the two production call sites in internal/service: - issuer.go:279 (TestIssuer connection test) now passes its method-scoped ctx - issuer.go:303 (BuildRegistry) now passes its method-scoped ctx to Rebuild - Updates 13 test sites in internal/connector/issuerfactory/factory_test.go via a new testCtx() helper that returns context.Background(). Helper is dedicated to this file so contextcheck's "you have a ctx in scope, pass it" rule doesn't fire on test functions that don't otherwise need ctx. - Updates 6 test sites in internal/service/issuer_registry_test.go to pass context.Background() to Rebuild. - Removes the now-stale "// NewFromConfig has no ctx parameter (preserved across all 12 connectors); pass context.Background() ..." comment from the awsacmpca branch in factory.go — that workaround is no longer the design. Verified locally: - gofmt -l . clean - go vet ./... clean - staticcheck ./... clean - golangci-lint run --timeout 5m ./... clean (was failing with 6 contextcheck issues before the cascade; now 0 issues) - go test -short -count=1 across all changed packages green Sandbox couldn't run the existing CI's full make verify due to disk pressure on /sessions and a virtiofs concurrent-open-file ceiling on go mod tidy; operator should run `make verify` on the workstation to confirm. Audit reference: cowork/issuer-coverage-audit-2026-05-01/RESULTS.md Top-10 fix #1 (CI follow-up; behavior unchanged from590f654).
290 lines
6.5 KiB
Go
290 lines
6.5 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), 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(context.Background(), []*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())
|
|
}
|
|
}
|