test(bouncer,cabana): cover admin refresh subject checks without a database
Quick 260927-q23 (CR-01), unit coverage that runs under -short. - bouncer: TestRefreshAudienceForSubject covers active, pre/post cutoff, missing, nil provider, non-numeric sub, provider error, and proves token-only refusals never reach the provider - bouncer: TestJWTGuardTokensValidAfter pins the unchanged "User not found" message and errors.Is(err, ErrSubjectRejected) - cabana: TestPhase10Coverage subtest pins cookie expiry on subject refusals, no cookies over Bearer or on a provider error, and the post-cutoff success path
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
@@ -379,4 +380,104 @@ func TestPhase10Coverage(t *testing.T) {
|
||||
t.Fatalf("cookie refresh body=%s", rec.Body.String())
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("refresh enforces the guard's subject checks", func(t *testing.T) {
|
||||
const secret = "phase10-subject-secret"
|
||||
now := time.Now()
|
||||
cutoff := now.Add(-10 * time.Minute)
|
||||
subjects := refreshSubjects{byID: map[uint]*bouncer.Principal{
|
||||
5: {ID: 5, Backend: true, TokensValidAfter: cutoff},
|
||||
}}
|
||||
newService := func(users bouncer.UserProvider) *service {
|
||||
return &service{
|
||||
secret: secret,
|
||||
ttl: 15 * time.Minute,
|
||||
refreshTTL: 2 * time.Hour,
|
||||
issuer: "https://app.test" + DefaultAdminPrefix,
|
||||
bl: bouncer.NewMemoryBlacklist(),
|
||||
users: users,
|
||||
}
|
||||
}
|
||||
sign := func(sub string, iat time.Time, jti string) string {
|
||||
t.Helper()
|
||||
tok, err := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
"sub": sub, "aud": bouncer.AudienceBackend, "jti": jti,
|
||||
"iat": iat.Unix(), "nbf": iat.Unix(), "exp": iat.Add(15 * time.Minute).Unix(),
|
||||
}).SignedString([]byte(secret))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return tok
|
||||
}
|
||||
call := func(svc *service, token string, cookie bool) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, adminAPI("/auth/refresh"), nil)
|
||||
req.Header.Set("X-Requested-With", "XMLHttpRequest")
|
||||
if cookie {
|
||||
req.AddCookie(&http.Cookie{Name: AdminCookieName, Value: token})
|
||||
} else {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
}
|
||||
rec := httptest.NewRecorder()
|
||||
requireAjax(svc.refresh)(rec, req)
|
||||
return rec
|
||||
}
|
||||
errorCode := func(rec *httptest.ResponseRecorder) string {
|
||||
t.Helper()
|
||||
var body struct {
|
||||
Error struct {
|
||||
Code string `json:"code"`
|
||||
} `json:"error"`
|
||||
}
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
|
||||
t.Fatalf("error json: %v body=%s", err, rec.Body.String())
|
||||
}
|
||||
return body.Error.Code
|
||||
}
|
||||
assertExpired := func(name string, rec *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
if rec.Code != http.StatusUnauthorized || errorCode(rec) != "unauthenticated" {
|
||||
t.Fatalf("%s: status=%d body=%s, want 401 unauthenticated", name, rec.Code, rec.Body.String())
|
||||
}
|
||||
var expired *http.Cookie
|
||||
for _, c := range rec.Result().Cookies() {
|
||||
if c.Name == AdminCookieName {
|
||||
expired = c
|
||||
}
|
||||
}
|
||||
if expired == nil || expired.Value != "" || expired.MaxAge >= 0 || expired.Path != DefaultAdminPrefix {
|
||||
t.Fatalf("%s: cookie = %+v, want an expiring %s with Path %s", name, expired, AdminCookieName, DefaultAdminPrefix)
|
||||
}
|
||||
}
|
||||
assertNoCookies := func(name string, rec *httptest.ResponseRecorder) {
|
||||
t.Helper()
|
||||
if rec.Code != http.StatusUnauthorized || errorCode(rec) != "unauthenticated" {
|
||||
t.Fatalf("%s: status=%d body=%s, want 401 unauthenticated", name, rec.Code, rec.Body.String())
|
||||
}
|
||||
if got := rec.Header().Values("Set-Cookie"); len(got) != 0 {
|
||||
t.Fatalf("%s: set cookies %q", name, got)
|
||||
}
|
||||
}
|
||||
|
||||
svc := newService(subjects)
|
||||
assertExpired("pre-cutoff cookie", call(svc, sign("5", cutoff.Add(-time.Minute), "pre-cutoff-cookie"), true))
|
||||
assertExpired("unknown subject cookie", call(svc, sign("6", now.Add(-time.Minute), "unknown-cookie"), true))
|
||||
assertNoCookies("pre-cutoff bearer", call(svc, sign("5", cutoff.Add(-time.Minute), "pre-cutoff-bearer"), false))
|
||||
|
||||
failing := newService(refreshSubjects{err: errors.New("lookup failed")})
|
||||
assertNoCookies("provider error cookie", call(failing, sign("5", now.Add(-time.Minute), "provider-error"), true))
|
||||
|
||||
ok := call(svc, sign("5", cutoff.Add(time.Minute), "post-cutoff-cookie"), true)
|
||||
if ok.Code != http.StatusOK {
|
||||
t.Fatalf("post-cutoff cookie refresh status=%d body=%s", ok.Code, ok.Body.String())
|
||||
}
|
||||
var rotated *http.Cookie
|
||||
for _, c := range ok.Result().Cookies() {
|
||||
if c.Name == AdminCookieName {
|
||||
rotated = c
|
||||
}
|
||||
}
|
||||
if rotated == nil || rotated.Value == "" || rotated.MaxAge <= 0 || rotated.Path != DefaultAdminPrefix {
|
||||
t.Fatalf("post-cutoff rotated cookie = %+v", rotated)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user