Files
certctl/internal/auth/breakglass/coverage_fill_test.go
T
shankar0123 80cbd2db59 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.
2026-05-11 14:12:11 +00:00

138 lines
4.8 KiB
Go

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