mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 21:21:40 +00:00
80cbd2db59
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.
138 lines
4.8 KiB
Go
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)
|
|
}
|
|
}
|