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