auth-bundle-1 follow-on: close coverage gaps to clear Phase 12 floors

CI run #486 (post-Bundle-1 merge + Go 1.25.10 bump) failed three
coverage-threshold gates:

  internal/api/handler   74.7% < floor 75 (-0.3pp)
  internal/auth          66.3% < floor 85 (-18.7pp)
  internal/service/auth  51.1% < floor 85 (-33.9pp)

The Phase 12 gate file's "85% with negative-test coverage" claim
turned out to be aspirational — the read-side and Update-path
methods on RoleService / PermissionService / ActorRoleService had
zero unit-test coverage, and internal/auth's keystore +
HasPermission helper had zero tests. This commit closes the gap
without lowering the gate.

Per-package CI-style averages after this commit (per
scripts/check-coverage-thresholds.sh's per-function-mean):

  internal/api/handler   76.1% (+1.4pp,  margin +1.1pp)
  internal/auth          90.5% (+24.2pp, margin +5.5pp)
  internal/service/auth  93.7% (+42.6pp, margin +8.7pp)

Tests added:

  internal/service/auth/service_test.go (+18 tests, +518 LOC):
    PermissionService.List, PermissionService.GetByName,
    RoleService.Get (4 paths), RoleService.List (system caller),
    RoleService.Update (4 paths), RoleService.ListPermissions
    (3 paths), RoleService.AddPermission/RemovePermission round-trip
    + gate paths, RoleService.Delete (success + nil-caller +
    no-perm + audit), RoleService.Create (nil-caller),
    ActorRoleService.ListForActor (self-bypass + cross-actor +
    nil-caller + system + with-perm), ActorRoleService.Effective-
    Permissions (same shape), ActorRoleService.ListKeys (3 paths +
    system bypass), ActorRoleService.Revoke (4 paths), Authorizer
    edge cases (empty actorID short-circuit, empty tenantID
    default, scoped-grant-without-scope-id no-match invariant,
    repo-error wrap-and-return, HoldsAnyOf early-exit), recordAudit
    nil-arm short-circuits.

  internal/auth/keystore_test.go (NEW, +175 LOC):
    StaticKeyStore.Len, StaticKeyStore.LookupByHash hit + miss,
    MutableKeyStore seeded lookup + Len, Add registers new key,
    AddHashed registers from precomputed hash, AddHashed replaces
    on duplicate hash (idempotent boot-loader contract),
    HasPermission no-actor / default-actor-type / checker-error /
    scoped-check threading.

  internal/auth/bootstrap/service_test.go (+36 LOC):
    Service.Available nil-receiver/nil-strategy short-circuit,
    Service.Available delegates to Strategy when configured.

  internal/api/handler/auth_test.go (+208 LOC):
    GetRole returns role + permissions, GetRole 404 + 401, UpdateRole
    200 + invalid-JSON-400 + 401, ListKeys returns actor list + 401,
    RemoveRolePermission 204 (global + scoped) + 401,
    rolePermToResponse scope encoding pin via GetRole.

Verified:
  gofmt -l . clean (touched files only).
  go vet ./internal/auth/... ./internal/service/auth/...
       ./internal/api/handler/ rc=0.
  go test -count=1 -short on the four packages green.
  CI-style per-function averages computed via the live
       scripts/check-coverage-thresholds.sh arithmetic — all three
       gated packages clear their floors with margin.

Per CLAUDE.md "complete path" + "do not lower the gate to make CI
green": gate file unchanged. The 85/85/75 floors stand.
This commit is contained in:
shankar0123
2026-05-10 02:04:36 +00:00
parent 3e91c7a1f0
commit 5d79e53ad0
4 changed files with 946 additions and 0 deletions
+36
View File
@@ -53,6 +53,42 @@ type fakeKeyStore struct {
added []addedEntry
}
// TestService_Available_NilServiceOrStrategyReturnsErrDisabled pins the
// no-strategy short-circuit on the Available probe. The HTTP handler
// uses Available to decide between 410 Gone (consumed/disabled) and
// proceeding to read the request body. A nil service or nil strategy
// means the bootstrap path was not configured at all; both arms return
// ErrDisabled so the operator-facing surface is identical.
func TestService_Available_NilServiceOrStrategyReturnsErrDisabled(t *testing.T) {
var svc *Service // nil receiver
if ok, err := svc.Available(context.Background()); ok || !errors.Is(err, ErrDisabled) {
t.Errorf("nil service: Available = (%v, %v); want (false, ErrDisabled)", ok, err)
}
// non-nil service but nil strategy
svc = &Service{}
if ok, err := svc.Available(context.Background()); ok || !errors.Is(err, ErrDisabled) {
t.Errorf("nil strategy: Available = (%v, %v); want (false, ErrDisabled)", ok, err)
}
}
// TestService_Available_DelegatesToStrategy pins the happy-path
// delegation: when an admin already exists, the strategy probe returns
// `exists=true` and Available reports false (one-shot path closes once
// any admin lands).
func TestService_Available_DelegatesToStrategy(t *testing.T) {
strategy := NewEnvTokenStrategy("the-token", func(_ context.Context) (bool, error) {
return true, nil // admin exists → bootstrap path closed
})
svc := NewService(strategy, &fakeMinter{}, &fakeGranter{}, &fakeAudit{}, &fakeKeyStore{}, sha)
ok, err := svc.Available(context.Background())
if err != nil {
t.Fatalf("Available err: %v", err)
}
if ok {
t.Errorf("admin-exists probe should yield Available=false; got true")
}
}
type addedEntry struct {
name string
hash string