mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-07 13:41:30 +00:00
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.
This commit is contained in:
@@ -0,0 +1,316 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/certctl-io/certctl/internal/auth/breakglass"
|
||||
bgdomain "github.com/certctl-io/certctl/internal/auth/breakglass/domain"
|
||||
)
|
||||
|
||||
// Coverage fill — v2.1.0 release gate Phase 3.
|
||||
//
|
||||
// Handler-level tests for the Phase 7.5 break-glass HTTP surface.
|
||||
// Bundle 2 originally shipped these endpoints with service-level
|
||||
// tests only; the 6 0%-handler functions dragged the internal/api/
|
||||
// handler average below its 75 floor. This file backfills the
|
||||
// canonical positive + negative cases at the handler layer.
|
||||
|
||||
// =============================================================================
|
||||
// Fake BreakglassService.
|
||||
// =============================================================================
|
||||
|
||||
type fakeBreakglassSvc struct {
|
||||
enabled bool
|
||||
|
||||
// Per-method return shapes. Tests set the field they care about.
|
||||
setPasswordRes *breakglass.SetPasswordResult
|
||||
setPasswordErr error
|
||||
authRes *breakglass.AuthenticateResult
|
||||
authErr error
|
||||
unlockErr error
|
||||
removeErr error
|
||||
listOut []*bgdomain.BreakglassCredential
|
||||
listErr error
|
||||
|
||||
// Captured args (for assertions).
|
||||
gotSetCaller, gotSetTarget, gotSetPass string
|
||||
gotAuthActor, gotAuthPass, gotAuthIP, gotAuthUA string
|
||||
gotUnlockCaller, gotUnlockTarget string
|
||||
gotRemoveCaller, gotRemoveTarget string
|
||||
}
|
||||
|
||||
func (f *fakeBreakglassSvc) Enabled() bool { return f.enabled }
|
||||
|
||||
func (f *fakeBreakglassSvc) SetPassword(ctx context.Context, caller, target, pw string) (*breakglass.SetPasswordResult, error) {
|
||||
f.gotSetCaller, f.gotSetTarget, f.gotSetPass = caller, target, pw
|
||||
return f.setPasswordRes, f.setPasswordErr
|
||||
}
|
||||
func (f *fakeBreakglassSvc) Authenticate(ctx context.Context, actor, pw, ip, ua string) (*breakglass.AuthenticateResult, error) {
|
||||
f.gotAuthActor, f.gotAuthPass, f.gotAuthIP, f.gotAuthUA = actor, pw, ip, ua
|
||||
return f.authRes, f.authErr
|
||||
}
|
||||
func (f *fakeBreakglassSvc) Unlock(ctx context.Context, caller, target string) error {
|
||||
f.gotUnlockCaller, f.gotUnlockTarget = caller, target
|
||||
return f.unlockErr
|
||||
}
|
||||
func (f *fakeBreakglassSvc) RemoveCredential(ctx context.Context, caller, target string) error {
|
||||
f.gotRemoveCaller, f.gotRemoveTarget = caller, target
|
||||
return f.removeErr
|
||||
}
|
||||
func (f *fakeBreakglassSvc) List(ctx context.Context) ([]*bgdomain.BreakglassCredential, error) {
|
||||
return f.listOut, f.listErr
|
||||
}
|
||||
|
||||
func newBreakglassHandlerWithFake(t *testing.T, enabled bool) (*AuthBreakglassHandler, *fakeBreakglassSvc) {
|
||||
t.Helper()
|
||||
svc := &fakeBreakglassSvc{enabled: enabled}
|
||||
attrs := SessionCookieAttrs{Secure: true, SameSite: http.SameSiteLaxMode}
|
||||
return NewAuthBreakglassHandler(svc, attrs), svc
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 1. Public login endpoint.
|
||||
// =============================================================================
|
||||
|
||||
func TestBreakglassLogin_DisabledReturns404(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, false /* disabled */)
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"hunter2!!"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/breakglass/login", body)
|
||||
rec := httptest.NewRecorder()
|
||||
h.Login(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("disabled service must yield 404 (surface invisibility); got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassLogin_InvalidJSONReturns401(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/breakglass/login", bytes.NewBufferString("not-json"))
|
||||
rec := httptest.NewRecorder()
|
||||
h.Login(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("invalid JSON must map to 401 (NOT 400); got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassLogin_EmptyFieldsReturns401(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/breakglass/login", bytes.NewBufferString(`{"actor_id":"","password":""}`))
|
||||
rec := httptest.NewRecorder()
|
||||
h.Login(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("empty actor/password must map to 401; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassLogin_ServiceErrorReturns401(t *testing.T) {
|
||||
h, svc := newBreakglassHandlerWithFake(t, true)
|
||||
svc.authErr = errors.New("locked")
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"wrong"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/breakglass/login", body)
|
||||
rec := httptest.NewRecorder()
|
||||
h.Login(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("auth error must map to 401; got %d", rec.Code)
|
||||
}
|
||||
if svc.gotAuthActor != "alice" {
|
||||
t.Errorf("expected actor=alice; got %q", svc.gotAuthActor)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassLogin_SuccessSetsCookies(t *testing.T) {
|
||||
h, svc := newBreakglassHandlerWithFake(t, true)
|
||||
svc.authRes = &breakglass.AuthenticateResult{CookieValue: "ses-1.abc", CSRFToken: "csrf-xyz"}
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"hunter2!!"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/auth/breakglass/login", body)
|
||||
rec := httptest.NewRecorder()
|
||||
h.Login(rec, req)
|
||||
if rec.Code != http.StatusNoContent {
|
||||
t.Errorf("expected 204; got %d (body=%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
res := rec.Result()
|
||||
defer res.Body.Close()
|
||||
gotSession, gotCSRF := false, false
|
||||
for _, c := range res.Cookies() {
|
||||
if strings.Contains(c.Name, "session") || strings.Contains(c.Name, "Session") {
|
||||
gotSession = true
|
||||
}
|
||||
if strings.Contains(c.Name, "csrf") || strings.Contains(c.Name, "CSRF") {
|
||||
gotCSRF = true
|
||||
}
|
||||
}
|
||||
if !gotSession {
|
||||
t.Errorf("expected session cookie")
|
||||
}
|
||||
if !gotCSRF {
|
||||
t.Errorf("expected CSRF cookie")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// 2. Admin endpoints — no caller context = 401.
|
||||
// =============================================================================
|
||||
|
||||
func TestBreakglassSetPassword_NoCallerReturns401(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"StrongPW123!"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials", body)
|
||||
rec := httptest.NewRecorder()
|
||||
h.SetPassword(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("missing actor ctx must yield 401; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassSetPassword_DisabledReturns404(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, false)
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"StrongPW123!"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials", body)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.SetPassword(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("disabled must yield 404; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassSetPassword_InvalidJSONReturns400(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials", bytes.NewBufferString("nope"))
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.SetPassword(rec, req)
|
||||
if rec.Code != http.StatusBadRequest {
|
||||
t.Errorf("invalid JSON must map to 400 on admin endpoint; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassSetPassword_HappyPath(t *testing.T) {
|
||||
h, svc := newBreakglassHandlerWithFake(t, true)
|
||||
svc.setPasswordRes = &breakglass.SetPasswordResult{}
|
||||
body := bytes.NewBufferString(`{"actor_id":"alice","password":"StrongPW123!"}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials", body)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.SetPassword(rec, req)
|
||||
if rec.Code != http.StatusCreated && rec.Code != http.StatusOK && rec.Code != http.StatusNoContent {
|
||||
t.Errorf("expected 2xx; got %d (body=%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
if svc.gotSetTarget != "alice" {
|
||||
t.Errorf("expected target=alice; got %q", svc.gotSetTarget)
|
||||
}
|
||||
if svc.gotSetCaller != "admin" {
|
||||
t.Errorf("expected caller=admin; got %q", svc.gotSetCaller)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassUnlock_DisabledReturns404(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, false)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials/alice/unlock", nil)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.Unlock(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("disabled must yield 404; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassUnlock_NoActorReturns401(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/breakglass/credentials/alice/unlock", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.Unlock(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("missing actor ctx must yield 401; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassRemove_DisabledReturns404(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, false)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/auth/breakglass/credentials/alice", nil)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.Remove(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("disabled must yield 404; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassRemove_NoActorReturns401(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodDelete, "/api/v1/auth/breakglass/credentials/alice", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.Remove(rec, req)
|
||||
if rec.Code != http.StatusUnauthorized {
|
||||
t.Errorf("missing actor ctx must yield 401; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ListCredentials surfaces the read side.
|
||||
|
||||
func TestBreakglassListCredentials_DisabledReturns404(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, false)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/breakglass/credentials", nil)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ListCredentials(rec, req)
|
||||
if rec.Code != http.StatusNotFound {
|
||||
t.Errorf("disabled must yield 404; got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ListCredentials does not re-check the actor context — the auth
|
||||
// gate sits at the router/middleware layer via rbacGate. So a missing
|
||||
// actor ctx here just means the test fixture wasn't authenticated;
|
||||
// the handler itself returns 200 with the body content. The test
|
||||
// pins this contract so a future refactor that adds a handler-level
|
||||
// actor check will trip this case.
|
||||
func TestBreakglassListCredentials_NoActorCtxStillReturns200(t *testing.T) {
|
||||
h, _ := newBreakglassHandlerWithFake(t, true)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/breakglass/credentials", nil)
|
||||
rec := httptest.NewRecorder()
|
||||
h.ListCredentials(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("handler-only path returns 200 (router rbacGate is the auth gate); got %d", rec.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBreakglassListCredentials_HappyPath(t *testing.T) {
|
||||
h, svc := newBreakglassHandlerWithFake(t, true)
|
||||
svc.listOut = []*bgdomain.BreakglassCredential{
|
||||
{ActorID: "alice", TenantID: "t-default"},
|
||||
{ActorID: "bob", TenantID: "t-default"},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/auth/breakglass/credentials", nil)
|
||||
req = withAuthCtx(req, "admin", "User")
|
||||
rec := httptest.NewRecorder()
|
||||
h.ListCredentials(rec, req)
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Errorf("expected 200; got %d (body=%s)", rec.Code, rec.Body.String())
|
||||
}
|
||||
// Body should be JSON with both actors. We don't assume the exact
|
||||
// envelope shape; just check the names appear and the password
|
||||
// hashes are NOT present in the wire response.
|
||||
body := rec.Body.String()
|
||||
if !strings.Contains(body, "alice") || !strings.Contains(body, "bob") {
|
||||
t.Errorf("expected both actors in body; got: %s", body)
|
||||
}
|
||||
// The PasswordHash field carries json:"-" so the encoded value
|
||||
// must NEVER contain the hash. The field name "password_hash" or
|
||||
// any Argon2id PHC prefix is the signal.
|
||||
if strings.Contains(body, "password_hash") || strings.Contains(body, "$argon2") {
|
||||
t.Errorf("password hashes must NOT appear in wire response; got: %s", body)
|
||||
}
|
||||
// Defensive — confirm it's valid JSON.
|
||||
var anyResp interface{}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &anyResp); err != nil {
|
||||
t.Errorf("response body must be valid JSON: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
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")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
package oidc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Coverage fill — v2.1.0 release gate Phase 3.
|
||||
//
|
||||
// Targets two service-level functions added by post-merge fixes that
|
||||
// shipped without unit tests:
|
||||
//
|
||||
// - Service.JWKSStatus — added in audit 2026-05-10 MED-7 closure
|
||||
// (per-provider JWKS counters + cache state).
|
||||
// - Service.TestDiscovery — added in audit 2026-05-10 MED-5 closure
|
||||
// (dry-run /api/v1/auth/oidc/test endpoint).
|
||||
|
||||
// TestJWKSStatus_ReturnsLoadError_WhenProviderUnknown asserts that
|
||||
// JWKSStatus forwards the getOrLoad error verbatim when the requested
|
||||
// providerID is not in the repo. This is the entry-point fail-closed
|
||||
// branch.
|
||||
func TestJWKSStatus_ReturnsLoadError_WhenProviderUnknown(t *testing.T) {
|
||||
svc := newServiceForUnitTest(t)
|
||||
snap, err := svc.JWKSStatus(context.Background(), "rp-does-not-exist")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for unknown provider, got nil")
|
||||
}
|
||||
if snap != nil {
|
||||
t.Errorf("expected nil snapshot on error, got %+v", snap)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJWKSStatus_ReturnsSnapshot_AfterAuthRequestPopulatesEntry pre-
|
||||
// warms the provider cache via HandleAuthRequest (which calls
|
||||
// getOrLoad → populates s.cache) and then asserts JWKSStatus returns
|
||||
// a non-nil snapshot reflecting the entry's stats.
|
||||
func TestJWKSStatus_ReturnsSnapshot_AfterAuthRequestPopulatesEntry(t *testing.T) {
|
||||
idp := newMockIdP(t)
|
||||
svc, _ := newServiceWithProviderAndPL(t, idp.URL(), "rp-jwks-status")
|
||||
// Pre-warm the cache.
|
||||
if _, _, _, err := svc.HandleAuthRequest(context.Background(), "rp-jwks-status", "10.0.0.1", "test/1.0"); err != nil {
|
||||
t.Fatalf("HandleAuthRequest: %v", err)
|
||||
}
|
||||
snap, err := svc.JWKSStatus(context.Background(), "rp-jwks-status")
|
||||
if err != nil {
|
||||
t.Fatalf("JWKSStatus: %v", err)
|
||||
}
|
||||
if snap == nil {
|
||||
t.Fatalf("expected non-nil snapshot")
|
||||
}
|
||||
// CurrentKIDs is intentionally empty (go-oidc doesn't expose its
|
||||
// JWKS cache). Test the shape rather than the kids.
|
||||
if snap.CurrentKIDs == nil {
|
||||
t.Errorf("CurrentKIDs must be non-nil (empty slice OK)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestDiscovery_DiscoveryFailure_ReturnsErrorsSlice points
|
||||
// TestDiscovery at a URL that doesn't serve a discovery doc; the
|
||||
// function MUST return res with DiscoverySucceeded=false and a
|
||||
// non-empty Errors slice, and a nil err (per the documented "non-
|
||||
// fatal at this layer; per-leg failure carried in res.Errors"
|
||||
// contract).
|
||||
func TestTestDiscovery_DiscoveryFailure_ReturnsErrorsSlice(t *testing.T) {
|
||||
svc := newServiceForUnitTest(t)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.NotFound(w, r)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
res, err := svc.TestDiscovery(context.Background(), srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("TestDiscovery (non-fatal): %v", err)
|
||||
}
|
||||
if res == nil {
|
||||
t.Fatalf("expected non-nil result")
|
||||
}
|
||||
if res.DiscoverySucceeded {
|
||||
t.Errorf("expected DiscoverySucceeded=false when discovery doc is missing")
|
||||
}
|
||||
if len(res.Errors) == 0 {
|
||||
t.Errorf("expected non-empty Errors slice")
|
||||
}
|
||||
if !strings.Contains(strings.Join(res.Errors, "|"), "discovery fetch failed") {
|
||||
t.Errorf("expected 'discovery fetch failed' in errors; got %v", res.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestDiscovery_HappyPath_AgainstMockIdP exercises the
|
||||
// success path: discovery doc fetch, claims parse, alg-downgrade
|
||||
// check (RS256 → not denied), JWKS reachability.
|
||||
func TestTestDiscovery_HappyPath_AgainstMockIdP(t *testing.T) {
|
||||
idp := newMockIdP(t)
|
||||
svc := newServiceForUnitTest(t)
|
||||
|
||||
res, err := svc.TestDiscovery(context.Background(), idp.URL())
|
||||
if err != nil {
|
||||
t.Fatalf("TestDiscovery: %v", err)
|
||||
}
|
||||
if !res.DiscoverySucceeded {
|
||||
t.Errorf("expected DiscoverySucceeded=true")
|
||||
}
|
||||
if res.IssuerEcho != idp.URL() {
|
||||
t.Errorf("expected IssuerEcho=%q, got %q", idp.URL(), res.IssuerEcho)
|
||||
}
|
||||
if res.AuthorizationURL == "" || res.TokenURL == "" {
|
||||
t.Errorf("expected non-empty AuthorizationURL+TokenURL; got %q / %q", res.AuthorizationURL, res.TokenURL)
|
||||
}
|
||||
if !res.JWKSReachable {
|
||||
t.Errorf("expected JWKSReachable=true; got Errors=%v", res.Errors)
|
||||
}
|
||||
if len(res.SupportedAlgValues) == 0 {
|
||||
t.Errorf("expected non-empty SupportedAlgValues")
|
||||
}
|
||||
// Mock IdP advertises RS256; no downgrade-defense trip.
|
||||
for _, e := range res.Errors {
|
||||
if strings.Contains(e, "alg-downgrade defense tripped") {
|
||||
t.Errorf("unexpected alg-downgrade trip: %s", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestDiscovery_AlgDowngradeDetected runs against a stub IdP that
|
||||
// advertises HS256 in id_token_signing_alg_values_supported. The
|
||||
// function MUST flag the downgrade attack vector in res.Errors but
|
||||
// MUST NOT short-circuit (per-leg observability is the contract).
|
||||
func TestTestDiscovery_AlgDowngradeDetected(t *testing.T) {
|
||||
svc := newServiceForUnitTest(t)
|
||||
mux := http.NewServeMux()
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
|
||||
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issuer": srv.URL,
|
||||
"authorization_endpoint": srv.URL + "/authorize",
|
||||
"token_endpoint": srv.URL + "/token",
|
||||
"jwks_uri": srv.URL + "/jwks",
|
||||
"id_token_signing_alg_values_supported": []string{"HS256", "RS256"},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"keys":[]}`))
|
||||
})
|
||||
|
||||
res, err := svc.TestDiscovery(context.Background(), srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("TestDiscovery: %v", err)
|
||||
}
|
||||
if !res.DiscoverySucceeded {
|
||||
t.Errorf("expected DiscoverySucceeded=true; got Errors=%v", res.Errors)
|
||||
}
|
||||
found := false
|
||||
for _, e := range res.Errors {
|
||||
if strings.Contains(e, "alg-downgrade defense tripped") && strings.Contains(e, "HS256") {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected alg-downgrade-tripped:HS256 in errors; got %v", res.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestDiscovery_MissingJWKSURI surfaces the "discovery doc omits
|
||||
// jwks_uri" branch.
|
||||
func TestTestDiscovery_MissingJWKSURI(t *testing.T) {
|
||||
svc := newServiceForUnitTest(t)
|
||||
mux := http.NewServeMux()
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issuer": srv.URL,
|
||||
"authorization_endpoint": srv.URL + "/authorize",
|
||||
"token_endpoint": srv.URL + "/token",
|
||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||
// jwks_uri intentionally omitted
|
||||
})
|
||||
})
|
||||
|
||||
res, err := svc.TestDiscovery(context.Background(), srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("TestDiscovery: %v", err)
|
||||
}
|
||||
if res.JWKSReachable {
|
||||
t.Errorf("expected JWKSReachable=false when jwks_uri is missing")
|
||||
}
|
||||
found := false
|
||||
for _, e := range res.Errors {
|
||||
if strings.Contains(e, "omits jwks_uri") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected 'omits jwks_uri' in errors; got %v", res.Errors)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTestDiscovery_JWKSFetchFails covers the jwksReachable error
|
||||
// branch (non-2xx JWKS response).
|
||||
func TestTestDiscovery_JWKSFetchFails(t *testing.T) {
|
||||
svc := newServiceForUnitTest(t)
|
||||
mux := http.NewServeMux()
|
||||
srv := httptest.NewServer(mux)
|
||||
defer srv.Close()
|
||||
mux.HandleFunc("/.well-known/openid-configuration", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"issuer": srv.URL,
|
||||
"authorization_endpoint": srv.URL + "/authorize",
|
||||
"token_endpoint": srv.URL + "/token",
|
||||
"jwks_uri": srv.URL + "/jwks",
|
||||
"id_token_signing_alg_values_supported": []string{"RS256"},
|
||||
})
|
||||
})
|
||||
mux.HandleFunc("/jwks", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "internal", http.StatusInternalServerError)
|
||||
})
|
||||
|
||||
res, err := svc.TestDiscovery(context.Background(), srv.URL)
|
||||
if err != nil {
|
||||
t.Fatalf("TestDiscovery: %v", err)
|
||||
}
|
||||
if res.JWKSReachable {
|
||||
t.Errorf("expected JWKSReachable=false on 500")
|
||||
}
|
||||
found := false
|
||||
for _, e := range res.Errors {
|
||||
if strings.Contains(e, "JWKS endpoint returned non-200") {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("expected 'JWKS endpoint returned non-200' in errors; got %v", res.Errors)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Coverage fill — v2.1.0 release gate Phase 3.
|
||||
//
|
||||
// Three previously-uncovered surfaces:
|
||||
//
|
||||
// - SetTrustedProxies (cmd/server config wire)
|
||||
// - ComputeCookieHMAC (pre-login cookie verifier helper)
|
||||
// - DecryptKeyMaterial (pre-login HMAC-key derive)
|
||||
//
|
||||
// Each is a thin wrapper called by main.go or the pre-login flow that
|
||||
// never exits through a unit-test fixture. The tests below run them
|
||||
// directly so the coverage gate stops flagging the package.
|
||||
|
||||
func TestSetTrustedProxies_RoundTrip(t *testing.T) {
|
||||
t.Parallel() //nolint:paralleltest // shared package-level state
|
||||
// Snapshot + restore so concurrent tests don't observe the override.
|
||||
prev := trustedProxyCIDRs
|
||||
defer func() { trustedProxyCIDRs = prev }()
|
||||
|
||||
want := []string{"10.0.0.0/8", "192.0.2.1"}
|
||||
SetTrustedProxies(want)
|
||||
if len(trustedProxyCIDRs) != len(want) {
|
||||
t.Fatalf("expected %d entries, got %d", len(want), len(trustedProxyCIDRs))
|
||||
}
|
||||
for i, c := range want {
|
||||
if trustedProxyCIDRs[i] != c {
|
||||
t.Errorf("entry %d: got %q, want %q", i, trustedProxyCIDRs[i], c)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty slice clears.
|
||||
SetTrustedProxies(nil)
|
||||
if len(trustedProxyCIDRs) != 0 {
|
||||
t.Errorf("expected nil/empty after clear; got %v", trustedProxyCIDRs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestComputeCookieHMAC_Deterministic(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := []byte("a-32-byte-key-for-hmac-test-pad!")
|
||||
mac1 := ComputeCookieHMAC("ses-1", "actor-1", key)
|
||||
mac2 := ComputeCookieHMAC("ses-1", "actor-1", key)
|
||||
if !hmac.Equal(mac1, mac2) {
|
||||
t.Errorf("HMAC must be deterministic for the same inputs")
|
||||
}
|
||||
// Length is sha256.Size.
|
||||
if len(mac1) != sha256.Size {
|
||||
t.Errorf("expected len=%d (sha256), got %d", sha256.Size, len(mac1))
|
||||
}
|
||||
// Differing id2 changes the HMAC.
|
||||
if hmac.Equal(mac1, ComputeCookieHMAC("ses-1", "actor-2", key)) {
|
||||
t.Errorf("HMAC must differ when actor changes")
|
||||
}
|
||||
// Differing id1 changes the HMAC.
|
||||
if hmac.Equal(mac1, ComputeCookieHMAC("ses-2", "actor-1", key)) {
|
||||
t.Errorf("HMAC must differ when session changes")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecryptKeyMaterial_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
// encryptKeyMaterial + decryptKeyMaterial are the pair; round-trip
|
||||
// asserts the public DecryptKeyMaterial wrapper does not bypass
|
||||
// the decryption path.
|
||||
plaintext := []byte("plain-32-byte-key-for-hmac-pad!!")
|
||||
const passphrase = "test-passphrase-for-key-encrypt"
|
||||
ct, err := encryptKeyMaterial(plaintext, passphrase)
|
||||
if err != nil {
|
||||
t.Fatalf("encryptKeyMaterial: %v", err)
|
||||
}
|
||||
got, err := DecryptKeyMaterial(ct, passphrase)
|
||||
if err != nil {
|
||||
t.Fatalf("DecryptKeyMaterial: %v", err)
|
||||
}
|
||||
if string(got) != string(plaintext) {
|
||||
t.Errorf("decrypt mismatch: got %q, want %q", got, plaintext)
|
||||
}
|
||||
// Wrong passphrase → error (forwarded from decryptKeyMaterial).
|
||||
if _, err := DecryptKeyMaterial(ct, "wrong-passphrase"); err == nil {
|
||||
t.Errorf("expected error with wrong passphrase, got nil")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user