test(coverage): backfill 5 packages to clear v2.1.0 release-gate Phase 3 floors

Phase 3 of /Users/shankar/Desktop/cowork/v2.1.0-release-gate.md surfaced
four packages below their coverage floors. All four are regressions from
new code shipped in the audit-2026-05-10/11 fix bundles that didn't get
per-function tests:

  internal/auth/breakglass    87.5% -> 93.3% (floor: 90%)
    + List (was 0%) — 3 tests (disabled, empty+populated, repo err)
    + RemoveCredential, Unlock disabled-branch tests

  internal/auth/oidc          89.4% -> 95.4% (floor: 90%)
    + JWKSStatus (was 0%) — 2 tests (unknown provider, after AuthRequest)
    + TestDiscovery (was 0%) — 5 tests (discovery failure, happy path,
      HS256 alg-downgrade detected, missing jwks_uri, JWKS 500 fetch)

  internal/auth/session       89.9% -> 94.4% (floor: 90%)
    + SetTrustedProxies (was 0%) — round-trip + clear
    + ComputeCookieHMAC (was 0%) — determinism + key/inputs differ
    + DecryptKeyMaterial (was 0%) — round-trip + wrong-passphrase

  internal/api/handler        73.2% -> 75.5% (floor: 75%)
    + 6 auth_breakglass handler funcs (were all 0%) — 14 tests
      (disabled/404, invalid JSON, empty fields, service err, happy
      path with cookies, admin endpoints, ListCredentials no
      password_hash on the wire)
    + WithPermissionChecker setter test (was 0%, Bundle 2 MED-2)
    + NewAdminCRLCacheServiceImpl + CacheRows (were 0%) — 3 tests
    + itoaForRetryAfter + challengeURLBuilder ACME helpers (were 0%) —
      4 tests

All five coverage gates green:

  internal/service                                    72.7% (floor: 70%)
  internal/api/handler                                75.5% (floor: 75%)
  internal/api/middleware                             67.9% (floor: 30%)
  internal/auth                                       93.3% (floor: 85%)
  internal/service/auth                               91.8% (floor: 85%)
  internal/auth/oidc                                  95.4% (floor: 90%)
  internal/auth/oidc/groupclaim                      100.0% (floor: 95%)
  internal/auth/oidc/domain                           97.6% (floor: 90%)
  internal/auth/session                               94.4% (floor: 90%)
  internal/auth/session/domain                        98.3% (floor: 90%)
  internal/auth/breakglass                            93.3% (floor: 90%)
  internal/auth/breakglass/domain                    100.0% (floor: 90%)
  internal/auth/user/domain                           96.2% (floor: 90%)
  (and 6 more — all green)

Per CLAUDE.md operating rule: 'Lowering a floor REQUIRES corresponding
code-side test work — never lower the gate to make CI green.' The
floors stay at their committed values; the new tests close the gap.
This commit is contained in:
shankar0123
2026-05-11 14:12:11 +00:00
parent 8aeeec93c0
commit 80cbd2db59
5 changed files with 956 additions and 0 deletions
@@ -0,0 +1,137 @@
package breakglass
import (
"context"
"errors"
"testing"
bgdomain "github.com/certctl-io/certctl/internal/auth/breakglass/domain"
)
// Coverage fill — v2.1.0 release gate Phase 3.
//
// Targets:
//
// - Service.List — was 0% pre-fill (added at Phase 7.5 of Bundle 2
// for the admin "list break-glass actors" surface). Exercises the
// ErrDisabled fail-closed branch + the repo-error wrap + the
// happy path.
// - Service.RemoveCredential repo-error branch.
// - Service.Unlock repo-error branch.
//
// These are the smallest additions that lift the package back across
// the 90 % per-package floor for the v2.1.0 release gate.
func TestService_List_DisabledReturnsErrDisabled(t *testing.T) {
svc, _, _, _ := newSvc(t, false /* enabled */)
got, err := svc.List(context.Background())
if !errors.Is(err, ErrDisabled) {
t.Fatalf("expected ErrDisabled when disabled, got %v", err)
}
if got != nil {
t.Errorf("expected nil slice when disabled, got %v", got)
}
}
func TestService_List_Enabled_EmptyAndPopulated(t *testing.T) {
svc, repo, _, _ := newSvc(t, true /* enabled */)
// Empty case.
got, err := svc.List(context.Background())
if err != nil {
t.Fatalf("List (empty): %v", err)
}
if len(got) != 0 {
t.Errorf("expected 0 rows, got %d", len(got))
}
// Seed two rows via SetPassword (which exercises the repo Create
// path); List then returns both. Order is repo-defined.
if _, err := svc.SetPassword(context.Background(), "u-admin", "alice", "StrongPW123!"); err != nil {
t.Fatalf("SetPassword alice: %v", err)
}
if _, err := svc.SetPassword(context.Background(), "u-admin", "bob", "StrongPW123!"); err != nil {
t.Fatalf("SetPassword bob: %v", err)
}
got, err = svc.List(context.Background())
if err != nil {
t.Fatalf("List (populated): %v", err)
}
if len(got) != 2 {
t.Errorf("expected 2 rows, got %d", len(got))
}
// Sanity-check: rows must carry the persisted ActorIDs.
have := map[string]bool{}
for _, r := range got {
have[r.ActorID] = true
}
if !have["alice"] || !have["bob"] {
t.Errorf("expected both 'alice' and 'bob' in list; got actor IDs %v", have)
}
_ = repo
}
// TestService_List_RepoErrorWraps verifies the err-wrap branch by
// forcing a stub repo to return an error from List.
func TestService_List_RepoErrorWraps(t *testing.T) {
svc, repo, _, _ := newSvc(t, true /* enabled */)
// Inject a List-failing stub by replacing the repo's behavior;
// stubRepo's List doesn't have an injectable error, so use a
// minimal local wrapper.
wrapped := &listErrRepo{inner: repo, err: errors.New("boom")}
svc.repo = wrapped
got, err := svc.List(context.Background())
if err == nil {
t.Fatalf("expected wrap error, got nil")
}
if got != nil {
t.Errorf("expected nil rows on err, got %v", got)
}
}
// listErrRepo wraps stubRepo and returns a configured error from List.
type listErrRepo struct {
inner *stubRepo
err error
}
func (r *listErrRepo) Create(ctx context.Context, c *bgdomain.BreakglassCredential) error {
return r.inner.Create(ctx, c)
}
func (r *listErrRepo) GetByActor(ctx context.Context, actorID, tenantID string) (*bgdomain.BreakglassCredential, error) {
return r.inner.GetByActor(ctx, actorID, tenantID)
}
func (r *listErrRepo) UpdatePasswordHash(ctx context.Context, actorID, tenantID, newHash string) error {
return r.inner.UpdatePasswordHash(ctx, actorID, tenantID, newHash)
}
func (r *listErrRepo) IncrementFailure(ctx context.Context, actorID, tenantID string, threshold, durationSec int) (*bgdomain.BreakglassCredential, error) {
return r.inner.IncrementFailure(ctx, actorID, tenantID, threshold, durationSec)
}
func (r *listErrRepo) ResetFailureCount(ctx context.Context, actorID, tenantID string) error {
return r.inner.ResetFailureCount(ctx, actorID, tenantID)
}
func (r *listErrRepo) Delete(ctx context.Context, actorID, tenantID string) error {
return r.inner.Delete(ctx, actorID, tenantID)
}
func (r *listErrRepo) List(_ context.Context, _ string) ([]*bgdomain.BreakglassCredential, error) {
return nil, r.err
}
// TestService_RemoveCredential_DisabledReturnsErrDisabled exercises
// the fail-closed branch in RemoveCredential (previously uncovered).
func TestService_RemoveCredential_DisabledReturnsErrDisabled(t *testing.T) {
svc, _, _, _ := newSvc(t, false /* enabled */)
if err := svc.RemoveCredential(context.Background(), "u-admin", "alice"); !errors.Is(err, ErrDisabled) {
t.Errorf("expected ErrDisabled, got %v", err)
}
}
// TestService_Unlock_DisabledReturnsErrDisabled exercises the
// fail-closed branch in Unlock (previously uncovered).
func TestService_Unlock_DisabledReturnsErrDisabled(t *testing.T) {
svc, _, _, _ := newSvc(t, false /* enabled */)
if err := svc.Unlock(context.Background(), "u-admin", "alice"); !errors.Is(err, ErrDisabled) {
t.Errorf("expected ErrDisabled, got %v", err)
}
}
+244
View File
@@ -0,0 +1,244 @@
package oidc
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
)
// Coverage fill — v2.1.0 release gate Phase 3.
//
// Targets two service-level functions added by post-merge fixes that
// shipped without unit tests:
//
// - Service.JWKSStatus — added in audit 2026-05-10 MED-7 closure
// (per-provider JWKS counters + cache state).
// - Service.TestDiscovery — added in audit 2026-05-10 MED-5 closure
// (dry-run /api/v1/auth/oidc/test endpoint).
// TestJWKSStatus_ReturnsLoadError_WhenProviderUnknown asserts that
// JWKSStatus forwards the getOrLoad error verbatim when the requested
// providerID is not in the repo. This is the entry-point fail-closed
// branch.
func TestJWKSStatus_ReturnsLoadError_WhenProviderUnknown(t *testing.T) {
svc := newServiceForUnitTest(t)
snap, err := svc.JWKSStatus(context.Background(), "rp-does-not-exist")
if err == nil {
t.Fatalf("expected error for unknown provider, got nil")
}
if snap != nil {
t.Errorf("expected nil snapshot on error, got %+v", snap)
}
}
// TestJWKSStatus_ReturnsSnapshot_AfterAuthRequestPopulatesEntry pre-
// warms the provider cache via HandleAuthRequest (which calls
// getOrLoad → populates s.cache) and then asserts JWKSStatus returns
// a non-nil snapshot reflecting the entry's stats.
func TestJWKSStatus_ReturnsSnapshot_AfterAuthRequestPopulatesEntry(t *testing.T) {
idp := newMockIdP(t)
svc, _ := newServiceWithProviderAndPL(t, idp.URL(), "rp-jwks-status")
// Pre-warm the cache.
if _, _, _, err := svc.HandleAuthRequest(context.Background(), "rp-jwks-status", "10.0.0.1", "test/1.0"); err != nil {
t.Fatalf("HandleAuthRequest: %v", err)
}
snap, err := svc.JWKSStatus(context.Background(), "rp-jwks-status")
if err != nil {
t.Fatalf("JWKSStatus: %v", err)
}
if snap == nil {
t.Fatalf("expected non-nil snapshot")
}
// CurrentKIDs is intentionally empty (go-oidc doesn't expose its
// JWKS cache). Test the shape rather than the kids.
if snap.CurrentKIDs == nil {
t.Errorf("CurrentKIDs must be non-nil (empty slice OK)")
}
}
// TestTestDiscovery_DiscoveryFailure_ReturnsErrorsSlice points
// TestDiscovery at a URL that doesn't serve a discovery doc; the
// function MUST return res with DiscoverySucceeded=false and a
// non-empty Errors slice, and a nil err (per the documented "non-
// fatal at this layer; per-leg failure carried in res.Errors"
// contract).
func TestTestDiscovery_DiscoveryFailure_ReturnsErrorsSlice(t *testing.T) {
svc := newServiceForUnitTest(t)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer srv.Close()
res, err := svc.TestDiscovery(context.Background(), srv.URL)
if err != nil {
t.Fatalf("TestDiscovery (non-fatal): %v", err)
}
if res == nil {
t.Fatalf("expected non-nil result")
}
if res.DiscoverySucceeded {
t.Errorf("expected DiscoverySucceeded=false when discovery doc is missing")
}
if len(res.Errors) == 0 {
t.Errorf("expected non-empty Errors slice")
}
if !strings.Contains(strings.Join(res.Errors, "|"), "discovery fetch failed") {
t.Errorf("expected 'discovery fetch failed' in errors; got %v", res.Errors)
}
}
// TestTestDiscovery_HappyPath_AgainstMockIdP exercises the
// success path: discovery doc fetch, claims parse, alg-downgrade
// check (RS256 → not denied), JWKS reachability.
func TestTestDiscovery_HappyPath_AgainstMockIdP(t *testing.T) {
idp := newMockIdP(t)
svc := newServiceForUnitTest(t)
res, err := svc.TestDiscovery(context.Background(), idp.URL())
if err != nil {
t.Fatalf("TestDiscovery: %v", err)
}
if !res.DiscoverySucceeded {
t.Errorf("expected DiscoverySucceeded=true")
}
if res.IssuerEcho != idp.URL() {
t.Errorf("expected IssuerEcho=%q, got %q", idp.URL(), res.IssuerEcho)
}
if res.AuthorizationURL == "" || res.TokenURL == "" {
t.Errorf("expected non-empty AuthorizationURL+TokenURL; got %q / %q", res.AuthorizationURL, res.TokenURL)
}
if !res.JWKSReachable {
t.Errorf("expected JWKSReachable=true; got Errors=%v", res.Errors)
}
if len(res.SupportedAlgValues) == 0 {
t.Errorf("expected non-empty SupportedAlgValues")
}
// Mock IdP advertises RS256; no downgrade-defense trip.
for _, e := range res.Errors {
if strings.Contains(e, "alg-downgrade defense tripped") {
t.Errorf("unexpected alg-downgrade trip: %s", e)
}
}
}
// TestTestDiscovery_AlgDowngradeDetected runs against a stub IdP that
// advertises HS256 in id_token_signing_alg_values_supported. The
// function MUST flag the downgrade attack vector in res.Errors but
// MUST NOT short-circuit (per-leg observability is the contract).
func TestTestDiscovery_AlgDowngradeDetected(t *testing.T) {
svc := newServiceForUnitTest(t)
mux := http.NewServeMux()
srv := httptest.NewServer(mux)
defer srv.Close()
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"issuer": srv.URL,
"authorization_endpoint": srv.URL + "/authorize",
"token_endpoint": srv.URL + "/token",
"jwks_uri": srv.URL + "/jwks",
"id_token_signing_alg_values_supported": []string{"HS256", "RS256"},
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"keys":[]}`))
})
res, err := svc.TestDiscovery(context.Background(), srv.URL)
if err != nil {
t.Fatalf("TestDiscovery: %v", err)
}
if !res.DiscoverySucceeded {
t.Errorf("expected DiscoverySucceeded=true; got Errors=%v", res.Errors)
}
found := false
for _, e := range res.Errors {
if strings.Contains(e, "alg-downgrade defense tripped") && strings.Contains(e, "HS256") {
found = true
break
}
}
if !found {
t.Errorf("expected alg-downgrade-tripped:HS256 in errors; got %v", res.Errors)
}
}
// TestTestDiscovery_MissingJWKSURI surfaces the "discovery doc omits
// jwks_uri" branch.
func TestTestDiscovery_MissingJWKSURI(t *testing.T) {
svc := newServiceForUnitTest(t)
mux := http.NewServeMux()
srv := httptest.NewServer(mux)
defer srv.Close()
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"issuer": srv.URL,
"authorization_endpoint": srv.URL + "/authorize",
"token_endpoint": srv.URL + "/token",
"id_token_signing_alg_values_supported": []string{"RS256"},
// jwks_uri intentionally omitted
})
})
res, err := svc.TestDiscovery(context.Background(), srv.URL)
if err != nil {
t.Fatalf("TestDiscovery: %v", err)
}
if res.JWKSReachable {
t.Errorf("expected JWKSReachable=false when jwks_uri is missing")
}
found := false
for _, e := range res.Errors {
if strings.Contains(e, "omits jwks_uri") {
found = true
}
}
if !found {
t.Errorf("expected 'omits jwks_uri' in errors; got %v", res.Errors)
}
}
// TestTestDiscovery_JWKSFetchFails covers the jwksReachable error
// branch (non-2xx JWKS response).
func TestTestDiscovery_JWKSFetchFails(t *testing.T) {
svc := newServiceForUnitTest(t)
mux := http.NewServeMux()
srv := httptest.NewServer(mux)
defer srv.Close()
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]interface{}{
"issuer": srv.URL,
"authorization_endpoint": srv.URL + "/authorize",
"token_endpoint": srv.URL + "/token",
"jwks_uri": srv.URL + "/jwks",
"id_token_signing_alg_values_supported": []string{"RS256"},
})
})
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "internal", http.StatusInternalServerError)
})
res, err := svc.TestDiscovery(context.Background(), srv.URL)
if err != nil {
t.Fatalf("TestDiscovery: %v", err)
}
if res.JWKSReachable {
t.Errorf("expected JWKSReachable=false on 500")
}
found := false
for _, e := range res.Errors {
if strings.Contains(e, "JWKS endpoint returned non-200") {
found = true
}
}
if !found {
t.Errorf("expected 'JWKS endpoint returned non-200' in errors; got %v", res.Errors)
}
}
@@ -0,0 +1,89 @@
package session
import (
"crypto/hmac"
"crypto/sha256"
"testing"
)
// Coverage fill — v2.1.0 release gate Phase 3.
//
// Three previously-uncovered surfaces:
//
// - SetTrustedProxies (cmd/server config wire)
// - ComputeCookieHMAC (pre-login cookie verifier helper)
// - DecryptKeyMaterial (pre-login HMAC-key derive)
//
// Each is a thin wrapper called by main.go or the pre-login flow that
// never exits through a unit-test fixture. The tests below run them
// directly so the coverage gate stops flagging the package.
func TestSetTrustedProxies_RoundTrip(t *testing.T) {
t.Parallel() //nolint:paralleltest // shared package-level state
// Snapshot + restore so concurrent tests don't observe the override.
prev := trustedProxyCIDRs
defer func() { trustedProxyCIDRs = prev }()
want := []string{"10.0.0.0/8", "192.0.2.1"}
SetTrustedProxies(want)
if len(trustedProxyCIDRs) != len(want) {
t.Fatalf("expected %d entries, got %d", len(want), len(trustedProxyCIDRs))
}
for i, c := range want {
if trustedProxyCIDRs[i] != c {
t.Errorf("entry %d: got %q, want %q", i, trustedProxyCIDRs[i], c)
}
}
// Empty slice clears.
SetTrustedProxies(nil)
if len(trustedProxyCIDRs) != 0 {
t.Errorf("expected nil/empty after clear; got %v", trustedProxyCIDRs)
}
}
func TestComputeCookieHMAC_Deterministic(t *testing.T) {
t.Parallel()
key := []byte("a-32-byte-key-for-hmac-test-pad!")
mac1 := ComputeCookieHMAC("ses-1", "actor-1", key)
mac2 := ComputeCookieHMAC("ses-1", "actor-1", key)
if !hmac.Equal(mac1, mac2) {
t.Errorf("HMAC must be deterministic for the same inputs")
}
// Length is sha256.Size.
if len(mac1) != sha256.Size {
t.Errorf("expected len=%d (sha256), got %d", sha256.Size, len(mac1))
}
// Differing id2 changes the HMAC.
if hmac.Equal(mac1, ComputeCookieHMAC("ses-1", "actor-2", key)) {
t.Errorf("HMAC must differ when actor changes")
}
// Differing id1 changes the HMAC.
if hmac.Equal(mac1, ComputeCookieHMAC("ses-2", "actor-1", key)) {
t.Errorf("HMAC must differ when session changes")
}
}
func TestDecryptKeyMaterial_RoundTrip(t *testing.T) {
t.Parallel()
// encryptKeyMaterial + decryptKeyMaterial are the pair; round-trip
// asserts the public DecryptKeyMaterial wrapper does not bypass
// the decryption path.
plaintext := []byte("plain-32-byte-key-for-hmac-pad!!")
const passphrase = "test-passphrase-for-key-encrypt"
ct, err := encryptKeyMaterial(plaintext, passphrase)
if err != nil {
t.Fatalf("encryptKeyMaterial: %v", err)
}
got, err := DecryptKeyMaterial(ct, passphrase)
if err != nil {
t.Fatalf("DecryptKeyMaterial: %v", err)
}
if string(got) != string(plaintext) {
t.Errorf("decrypt mismatch: got %q, want %q", got, plaintext)
}
// Wrong passphrase → error (forwarded from decryptKeyMaterial).
if _, err := DecryptKeyMaterial(ct, "wrong-passphrase"); err == nil {
t.Errorf("expected error with wrong passphrase, got nil")
}
}