175 lines
4.7 KiB
Go
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")
|
|
}
|
|
}
|