mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 19:51:33 +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.
171 lines
5.6 KiB
Go
171 lines
5.6 KiB
Go
package handler
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/certctl-io/certctl/internal/domain"
|
|
)
|
|
|
|
// Coverage fill — v2.1.0 release gate Phase 3.
|
|
//
|
|
// A handful of constructor + setter + small-method functions added in
|
|
// recent fix bundles shipped without tests. The package-average
|
|
// floor (75%) trips because each 0%-function drags the script's
|
|
// per-function average down. The tests below cover the easy ones to
|
|
// lift the average back across.
|
|
|
|
// =============================================================================
|
|
// auth_session_oidc.go — WithPermissionChecker setter (added in MED-2).
|
|
// =============================================================================
|
|
|
|
type fakeOIDCPermChecker struct{}
|
|
|
|
func (f *fakeOIDCPermChecker) CheckPermission(_ context.Context, _, _, _, _, _ string, _ *string) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func TestAuthSessionOIDCHandler_WithPermissionChecker_ReturnsSelfAndSetsField(t *testing.T) {
|
|
h := &AuthSessionOIDCHandler{}
|
|
got := h.WithPermissionChecker(&fakeOIDCPermChecker{})
|
|
if got != h {
|
|
t.Errorf("WithPermissionChecker must return receiver for chaining; got %p, want %p", got, h)
|
|
}
|
|
if h.checker == nil {
|
|
t.Errorf("WithPermissionChecker must install the checker; got nil")
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// admin_crl_cache.go — NewAdminCRLCacheServiceImpl + CacheRows (added by
|
|
// the CRL-cache admin panel; never had handler-layer tests).
|
|
// =============================================================================
|
|
|
|
type fakeCRLCacheRepo struct {
|
|
getErr error
|
|
}
|
|
|
|
func (f *fakeCRLCacheRepo) Get(_ context.Context, _ string) (*domain.CRLCacheEntry, error) {
|
|
return nil, f.getErr
|
|
}
|
|
func (f *fakeCRLCacheRepo) Put(_ context.Context, _ *domain.CRLCacheEntry) error {
|
|
return nil
|
|
}
|
|
func (f *fakeCRLCacheRepo) NextCRLNumber(_ context.Context, _ string) (int64, error) {
|
|
return 1, nil
|
|
}
|
|
func (f *fakeCRLCacheRepo) RecordGenerationEvent(_ context.Context, _ *domain.CRLGenerationEvent) error {
|
|
return nil
|
|
}
|
|
func (f *fakeCRLCacheRepo) ListGenerationEvents(_ context.Context, _ string, _ int) ([]*domain.CRLGenerationEvent, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func TestNewAdminCRLCacheServiceImpl_ConstructsWithDefaults(t *testing.T) {
|
|
repo := &fakeCRLCacheRepo{}
|
|
idsFn := func() []string { return []string{"iss-1", "iss-2"} }
|
|
svc := NewAdminCRLCacheServiceImpl(repo, idsFn)
|
|
if svc == nil {
|
|
t.Fatalf("NewAdminCRLCacheServiceImpl returned nil")
|
|
}
|
|
if svc.cacheRepo == nil || svc.issuerIDs == nil || svc.now == nil {
|
|
t.Errorf("constructor must wire all fields; got cacheRepo=%v issuerIDs!=nil=%v now!=nil=%v",
|
|
svc.cacheRepo, svc.issuerIDs != nil, svc.now != nil)
|
|
}
|
|
if svc.eventLimit != 5 {
|
|
t.Errorf("expected default eventLimit=5; got %d", svc.eventLimit)
|
|
}
|
|
}
|
|
|
|
func TestAdminCRLCacheServiceImpl_CacheRows_EmptyIssuerListYieldsEmptyResult(t *testing.T) {
|
|
svc := NewAdminCRLCacheServiceImpl(&fakeCRLCacheRepo{}, func() []string { return nil })
|
|
rows, err := svc.CacheRows(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("CacheRows on empty issuer list: %v", err)
|
|
}
|
|
if len(rows) != 0 {
|
|
t.Errorf("expected 0 rows for empty issuer list; got %d", len(rows))
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// acme.go small helpers — itoaForRetryAfter + challengeURLBuilder.
|
|
// These are pure-helper functions added to the ACME surface; tested
|
|
// here to lift the package-average over the 75 floor.
|
|
// =============================================================================
|
|
|
|
func TestItoaForRetryAfter(t *testing.T) {
|
|
cases := []struct {
|
|
in int
|
|
want string
|
|
}{
|
|
{0, "0"},
|
|
{1, "1"},
|
|
{42, "42"},
|
|
{-5, "-5"},
|
|
{12345, "12345"},
|
|
}
|
|
for _, c := range cases {
|
|
got := itoaForRetryAfter(c.in)
|
|
if got != c.want {
|
|
t.Errorf("itoaForRetryAfter(%d) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestChallengeURLBuilder_ProfilePrefixAndHTTPS(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "https://certctl.local/acme/profile/p1/order", nil)
|
|
req.TLS = nil // simulate HTTP
|
|
req.Host = "x" // override
|
|
h := ACMEHandler{}
|
|
build := h.challengeURLBuilder(req, "p1")
|
|
got := build("chal-abc")
|
|
if !strings.HasPrefix(got, "http://x/acme/profile/p1/challenge/") {
|
|
t.Errorf("unexpected URL: %q", got)
|
|
}
|
|
if !strings.HasSuffix(got, "/chal-abc") {
|
|
t.Errorf("unexpected URL suffix: %q", got)
|
|
}
|
|
}
|
|
|
|
func TestChallengeURLBuilder_NoProfileFallsBackToShortPath(t *testing.T) {
|
|
req := httptest.NewRequest("GET", "http://certctl.local/acme/order", nil)
|
|
req.Host = "y"
|
|
h := ACMEHandler{}
|
|
build := h.challengeURLBuilder(req, "")
|
|
got := build("chal-1")
|
|
if !strings.Contains(got, "/acme/challenge/chal-1") {
|
|
t.Errorf("expected /acme/challenge/chal-1 fallback; got %q", got)
|
|
}
|
|
if strings.Contains(got, "/profile/") {
|
|
t.Errorf("must NOT contain /profile/ when profileID is empty; got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestAdminCRLCacheServiceImpl_CacheRows_PerIssuerErrorSurfacesAsEvent(t *testing.T) {
|
|
svc := NewAdminCRLCacheServiceImpl(
|
|
&fakeCRLCacheRepo{getErr: errors.New("lookup failed")},
|
|
func() []string { return []string{"iss-broken"} },
|
|
)
|
|
rows, err := svc.CacheRows(context.Background())
|
|
if err != nil {
|
|
t.Fatalf("CacheRows must NOT short-circuit on per-issuer failure: %v", err)
|
|
}
|
|
if len(rows) != 1 {
|
|
t.Fatalf("expected 1 row; got %d", len(rows))
|
|
}
|
|
if rows[0].IssuerID != "iss-broken" {
|
|
t.Errorf("expected issuer-id passthrough; got %q", rows[0].IssuerID)
|
|
}
|
|
if len(rows[0].RecentEvents) == 0 {
|
|
t.Fatalf("expected at least 1 RecentEvent for the lookup failure")
|
|
}
|
|
ev := rows[0].RecentEvents[0]
|
|
if ev.Succeeded {
|
|
t.Errorf("expected Succeeded=false on lookup failure")
|
|
}
|
|
}
|