mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 12:21:31 +00:00
9e6c57673e
CI's R-CI-extended coverage gate failed on 2025-04-30: service-layer
coverage was 68.7% vs the 70% floor. The drag was from new files
(internal/service/ocsp_counters.go, ocsp_response_cache.go,
export_audit_actions.go) that shipped without enough direct tests
to keep the package above the floor.
NEW internal/service/ocsp_counters_test.go (4 tests):
- TestOCSPCounters_NewIsZero — fresh counter snapshot is all zero
- TestOCSPCounters_EveryIncTicksItsLabel — table-driven test
pinning every Inc* method to its label string + the no-cross-
bleed invariant. Critical for Phase 8 Prometheus exposer
contract: a typo in either side would silently drop the
counter from /metrics/prometheus.
- TestOCSPCounters_SnapshotIsCopy — mutating the returned map
doesn't affect the underlying counters
- TestOCSPCounters_ConcurrentTicksRace — race-detector smoke
against sync/atomic primitives
NEW internal/service/ocsp_response_cache_real_test.go (10 tests):
- HappyPath_CachesAfterMiss — first fetch live-signs + writes
cache row; second fetch hits cache
- CacheWriteFailureIsNonFatal — putErrorRepo simulates disk full;
response still returned (fail-soft contract)
- StaleEntryRegenerates — entries with next_update in the past
trigger re-sign on next fetch
- InvalidateOnRevoke — pin the load-bearing security wire
- InvalidateOnRevoke_DeleteFailureSurfacesError — error-path
coverage for the delete branch
- CountByIssuer + NilRepoReturnsEmpty
- CAOperationsSvc.GetOCSPResponseWithNonce_CacheDispatchHit pins
the nil-nonce → cache dispatch wire
- CAOperationsSvc.GetOCSPResponseWithNonce_NonceBypassesCache
pins the nonce-bearing → live-sign bypass wire (cache stays
empty)
- RevocationSvc.SetOCSPCacheInvalidator_WireConnects pins the
setter through to the wired interface
NEW internal/service/coverage_extras_test.go (~12 tests) targets the
0%-coverage chunks adjacent to the bundle's modified files so the
package as a whole stays above the floor:
- cert-export typed audit emission (Phase 7) round-trip with
detail-map inspection (has_private_key + actor_kind + cipher pin)
- PKCS12CipherModernAES256 pinned-value test (drift catches a
future go-pkcs12 default change)
- audit.ListAuditEvents + GetAuditEvent (handler-interface methods
that were at 0%)
- certificate.ListCertificatesWithFilter (M20 filter delegate)
- discovery.{ListScans,GetScan,GetDiscoverySummary} (delegates)
- health_check.{Update,SetNotificationService} delegates + audit
- est.{deterministicSerial,zeroizeBytes,zeroizeKey} pure helpers
+ the live RSA + ECDSA key-zeroize branches
Sandbox total: 67.6% → 69.9% (+2.3pp). The live keygen branches
in zeroizeKey skip in the sandbox when crypto/rand isn't available
but run on CI, so the CI total should land above the 70% floor with
a small buffer.
Pre-commit verification: go build ./... clean; go test -short
-count=1 green for ./internal/service/.
104 lines
3.1 KiB
Go
104 lines
3.1 KiB
Go
package service
|
|
|
|
import (
|
|
"sync"
|
|
"testing"
|
|
)
|
|
|
|
// Production hardening II Phase 1+8 — OCSPCounters direct tests.
|
|
//
|
|
// Pin every label name + every Inc* method + the Snapshot copy
|
|
// invariant. The labels feed the Phase 8 Prometheus exposer
|
|
// (handler/metrics.go::SetOCSPCounters); a typo in either side
|
|
// would silently drop the counter from /metrics/prometheus, so
|
|
// these tests act as the cross-package contract.
|
|
|
|
func TestOCSPCounters_NewIsZero(t *testing.T) {
|
|
c := NewOCSPCounters()
|
|
snap := c.Snapshot()
|
|
for label, v := range snap {
|
|
if v != 0 {
|
|
t.Errorf("fresh counter[%q] = %d, want 0", label, v)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestOCSPCounters_EveryIncTicksItsLabel(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
inc func(*OCSPCounters)
|
|
label string
|
|
}{
|
|
{"RequestGET", (*OCSPCounters).IncRequestGET, "request_get"},
|
|
{"RequestPOST", (*OCSPCounters).IncRequestPOST, "request_post"},
|
|
{"RequestSuccess", (*OCSPCounters).IncRequestSuccess, "request_success"},
|
|
{"RequestInvalid", (*OCSPCounters).IncRequestInvalid, "request_invalid"},
|
|
{"IssuerNotFound", (*OCSPCounters).IncIssuerNotFound, "issuer_not_found"},
|
|
{"CertNotFound", (*OCSPCounters).IncCertNotFound, "cert_not_found"},
|
|
{"SigningFailed", (*OCSPCounters).IncSigningFailed, "signing_failed"},
|
|
{"NonceEchoed", (*OCSPCounters).IncNonceEchoed, "nonce_echoed"},
|
|
{"NonceMalformed", (*OCSPCounters).IncNonceMalformed, "nonce_malformed"},
|
|
{"RateLimited", (*OCSPCounters).IncRateLimited, "rate_limited"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
c := NewOCSPCounters()
|
|
tc.inc(c)
|
|
tc.inc(c)
|
|
tc.inc(c)
|
|
snap := c.Snapshot()
|
|
if got := snap[tc.label]; got != 3 {
|
|
t.Errorf("label %q = %d after 3 ticks, want 3", tc.label, got)
|
|
}
|
|
// All other labels stay at zero — pin the no-cross-bleed invariant.
|
|
for label, v := range snap {
|
|
if label == tc.label {
|
|
continue
|
|
}
|
|
if v != 0 {
|
|
t.Errorf("Inc%s leaked into label %q (=%d)", tc.name, label, v)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestOCSPCounters_SnapshotIsCopy(t *testing.T) {
|
|
// Mutating the snapshot must NOT affect the underlying counters.
|
|
c := NewOCSPCounters()
|
|
c.IncRequestSuccess()
|
|
snap := c.Snapshot()
|
|
snap["request_success"] = 999
|
|
if again := c.Snapshot()["request_success"]; again != 1 {
|
|
t.Errorf("counter mutated through snapshot: got %d, want 1", again)
|
|
}
|
|
}
|
|
|
|
func TestOCSPCounters_ConcurrentTicksRace(t *testing.T) {
|
|
// Race-detector smoke: every Inc* method should be safe under
|
|
// concurrent callers (sync/atomic primitives are the contract).
|
|
c := NewOCSPCounters()
|
|
const goroutines = 10
|
|
const ticksPerG = 100
|
|
var wg sync.WaitGroup
|
|
for i := 0; i < goroutines; i++ {
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
for j := 0; j < ticksPerG; j++ {
|
|
c.IncRequestSuccess()
|
|
c.IncNonceEchoed()
|
|
}
|
|
}()
|
|
}
|
|
wg.Wait()
|
|
snap := c.Snapshot()
|
|
want := uint64(goroutines * ticksPerG)
|
|
if snap["request_success"] != want {
|
|
t.Errorf("request_success = %d, want %d", snap["request_success"], want)
|
|
}
|
|
if snap["nonce_echoed"] != want {
|
|
t.Errorf("nonce_echoed = %d, want %d", snap["nonce_echoed"], want)
|
|
}
|
|
}
|