Files
certctl/internal/service/ocsp_counters_test.go
T
shankar0123 9e6c57673e test(service): coverage uplift for production hardening II + adjacent helpers (R-CI-extended floor)
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/.
2026-04-30 06:22:06 +00:00

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)
}
}