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") } }