Files
Cyrene_v2/internal/auth/rbac_test.go
T

175 lines
4.7 KiB
Go

package auth
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func bearerHeader(token string) string {
return "Bearer " + token
}
func claimsHandler(t *testing.T) http.Handler {
t.Helper()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := ClaimsFromContext(r.Context())
if !ok {
http.Error(w, "no claims in context", http.StatusInternalServerError)
return
}
w.Header().Set("X-Claims-Sub", claims.Sub)
w.Header().Set("X-Claims-Role", string(claims.Role))
w.WriteHeader(http.StatusOK)
})
}
func doAuth(h http.Handler, tok string) *httptest.ResponseRecorder {
req := httptest.NewRequest(http.MethodGet, "/", nil)
if tok != "" {
req.Header.Set("Authorization", bearerHeader(tok))
}
rr := httptest.NewRecorder()
h.ServeHTTP(rr, req)
return rr
}
func responseCode(t *testing.T, rr *httptest.ResponseRecorder, want int) {
t.Helper()
if rr.Code != want {
t.Fatalf("status = %d, want %d (body: %s)", rr.Code, want, rr.Body.String())
}
}
func assertErrorCode(t *testing.T, rr *httptest.ResponseRecorder, want string) {
t.Helper()
var body struct {
Error struct {
Code string `json:"code"`
Message string `json:"message"`
} `json:"error"`
}
if err := json.NewDecoder(strings.NewReader(rr.Body.String())).Decode(&body); err != nil {
t.Fatalf("decode error body: %v", err)
}
if body.Error.Code != want {
t.Errorf("error.code = %q, want %q", body.Error.Code, want)
}
}
func TestAuthorize_NoBearer(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), "")
responseCode(t, rr, http.StatusUnauthorized)
assertErrorCode(t, rr, "unauthorized")
}
func TestAuthorize_InvalidToken(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), "not-a-jwt")
responseCode(t, rr, http.StatusUnauthorized)
assertErrorCode(t, rr, "unauthorized")
}
func TestAuthorize_ExpiredToken(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleViewer, -time.Minute)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusUnauthorized)
assertErrorCode(t, rr, "unauthorized")
}
func TestAuthorize_OwnerSatisfiesAdminAndViewer(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleOwner, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize(RoleAdmin, RoleViewer)(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusOK)
if rr.Header().Get("X-Claims-Sub") != "u" {
t.Errorf("X-Claims-Sub = %q, want u", rr.Header().Get("X-Claims-Sub"))
}
if rr.Header().Get("X-Claims-Role") != "owner" {
t.Errorf("X-Claims-Role = %q, want owner", rr.Header().Get("X-Claims-Role"))
}
}
func TestAuthorize_AdminSatisfiesViewer(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleAdmin, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize(RoleViewer)(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusOK)
}
func TestAuthorize_AdminFailsOwner(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleAdmin, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize(RoleOwner)(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusForbidden)
assertErrorCode(t, rr, "forbidden")
}
func TestAuthorize_ViewerFailsAdmin(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleViewer, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize(RoleAdmin)(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusForbidden)
assertErrorCode(t, rr, "forbidden")
}
func TestAuthorize_AnyAuthenticated(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleViewer, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
rr := doAuth(svc.Authorize()(claimsHandler(t)), tok)
responseCode(t, rr, http.StatusOK)
}
func TestAuthorize_BearerPrefixRequired(t *testing.T) {
t.Parallel()
svc := New(testConfig(""))
tok, err := svc.Sign("u", RoleViewer, time.Hour)
if err != nil {
t.Fatalf("Sign error: %v", err)
}
req := httptest.NewRequest(http.MethodGet, "/", nil)
req.Header.Set("Authorization", tok) // no "Bearer " prefix
rr := httptest.NewRecorder()
svc.Authorize(RoleViewer)(claimsHandler(t)).ServeHTTP(rr, req)
responseCode(t, rr, http.StatusUnauthorized)
}
func TestClaimsFromContext_Missing(t *testing.T) {
t.Parallel()
if _, ok := ClaimsFromContext(context.Background()); ok {
t.Fatal("ClaimsFromContext on empty ctx should return ok=false")
}
}