feat(backend): update auth middleware
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
//go:build test
|
||||
// +build test
|
||||
|
||||
package mw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"crussell/testutils/jwt"
|
||||
)
|
||||
|
||||
func testHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
userID, _ := GetUserID(r.Context())
|
||||
role, _ := GetUserRole(r.Context())
|
||||
jti, _ := GetJTI(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(userID + "|" + role + "|" + jti))
|
||||
})
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RequireAuth
|
||||
// =============================================================================
|
||||
|
||||
func TestRequireAuth_NoHeader(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_MalformedHeader(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "NotBearer token123")
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_EmptyToken(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer ")
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_InvalidToken(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer this.is.not.a.valid.jwt")
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_ValidToken(t *testing.T) {
|
||||
token := jwt.GenerateTestToken("testuser123", "verified_email")
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if body == "" || body == "|" {
|
||||
t.Errorf("expected user info in response, got %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_AdminToken(t *testing.T) {
|
||||
token := jwt.GenerateAdminToken()
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAuth_SetsAllContextFields(t *testing.T) {
|
||||
token := jwt.GenerateTestToken("testuser123", "admin")
|
||||
|
||||
var capturedID, capturedRole, capturedJTI string
|
||||
collector := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
capturedID, _ = GetUserID(r.Context())
|
||||
capturedRole, _ = GetUserRole(r.Context())
|
||||
capturedJTI, _ = GetJTI(r.Context())
|
||||
w.WriteHeader(http.StatusOK)
|
||||
})
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAuth(collector).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("expected 200, got %d", w.Code)
|
||||
}
|
||||
if capturedID != "testuser123" {
|
||||
t.Errorf("expected userID 'testuser123', got %q", capturedID)
|
||||
}
|
||||
if capturedRole != "admin" {
|
||||
t.Errorf("expected role 'admin', got %q", capturedRole)
|
||||
}
|
||||
if capturedJTI == "" {
|
||||
t.Error("expected non-empty JTI")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// OptionalAuth
|
||||
// =============================================================================
|
||||
|
||||
func TestOptionalAuth_NoHeader(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
w := httptest.NewRecorder()
|
||||
OptionalAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 (pass-through), got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if body != "||" {
|
||||
t.Errorf("expected empty context (||), got %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalAuth_ValidToken(t *testing.T) {
|
||||
token := jwt.GenerateTestToken("testuser123", "verified_email")
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
OptionalAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200, got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if body == "||" {
|
||||
t.Error("expected context to be set with valid token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalAuth_InvalidToken(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Bearer obviously.invalid.token")
|
||||
w := httptest.NewRecorder()
|
||||
OptionalAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
// Should pass through without setting context
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 (pass-through on invalid token), got %d", w.Code)
|
||||
}
|
||||
|
||||
body := w.Body.String()
|
||||
if body != "||" {
|
||||
t.Errorf("expected empty context on invalid token, got %q", body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOptionalAuth_NoBearer(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
req.Header.Set("Authorization", "Basic somecreds")
|
||||
w := httptest.NewRecorder()
|
||||
OptionalAuth(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 (pass-through without Bearer prefix), got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RequireRole
|
||||
// =============================================================================
|
||||
|
||||
func TestRequireRole_Allowed(t *testing.T) {
|
||||
handler := RequireRole("admin")(testHandler())
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for admin role, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_MultipleRoles(t *testing.T) {
|
||||
handler := RequireRole("admin", "verified_email")(testHandler())
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "verified_email")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for verified_email in allowed list, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_Forbidden(t *testing.T) {
|
||||
handler := RequireRole("admin")(testHandler())
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "verified_email")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 for non-admin role, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_NoRoleInContext(t *testing.T) {
|
||||
handler := RequireRole("admin")(testHandler())
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
// No UserRoleKey in context
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 when no role in context, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireRole_EmptyAllowedList(t *testing.T) {
|
||||
handler := RequireRole()(testHandler())
|
||||
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 with empty allowed list, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RequireVerified (wraps RequireRole("verified_email", "admin"))
|
||||
// =============================================================================
|
||||
|
||||
func TestRequireVerified_VerifiedEmail(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "verified_email")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireVerified(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for verified_email, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireVerified_Admin(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireVerified(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for admin, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireVerified_Unverified(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "unverified_email")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireVerified(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 for unverified, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// RequireAdmin (RequireRole("admin") + UserIDKey guard)
|
||||
// =============================================================================
|
||||
|
||||
func TestRequireAdmin_AdminRole(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
ctx = context.WithValue(ctx, UserIDKey, "adminuser001")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAdmin(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for admin, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NonAdmin(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "verified_email")
|
||||
ctx = context.WithValue(ctx, UserIDKey, "someuser001")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAdmin(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 for non-admin, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_NoUserIDInContext(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
// No UserIDKey set
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAdmin(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 when UserID missing from context, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireAdmin_EmptyUserID(t *testing.T) {
|
||||
req := httptest.NewRequest("GET", "/", nil)
|
||||
ctx := context.WithValue(req.Context(), UserRoleKey, "admin")
|
||||
ctx = context.WithValue(ctx, UserIDKey, "")
|
||||
req = req.WithContext(ctx)
|
||||
w := httptest.NewRecorder()
|
||||
RequireAdmin(testHandler()).ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 for empty UserID, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Context helpers — GetUserID, GetUserRole, GetJTI
|
||||
// =============================================================================
|
||||
|
||||
func TestGetUserID_Present(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), UserIDKey, "abc123")
|
||||
id, ok := GetUserID(ctx)
|
||||
if !ok {
|
||||
t.Error("expected ok=true")
|
||||
}
|
||||
if id != "abc123" {
|
||||
t.Errorf("expected 'abc123', got %q", id)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserID_Missing(t *testing.T) {
|
||||
_, ok := GetUserID(context.Background())
|
||||
if ok {
|
||||
t.Error("expected ok=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserRole_Present(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), UserRoleKey, "admin")
|
||||
role, ok := GetUserRole(ctx)
|
||||
if !ok {
|
||||
t.Error("expected ok=true")
|
||||
}
|
||||
if role != "admin" {
|
||||
t.Errorf("expected 'admin', got %q", role)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetUserRole_Missing(t *testing.T) {
|
||||
_, ok := GetUserRole(context.Background())
|
||||
if ok {
|
||||
t.Error("expected ok=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJTI_Present(t *testing.T) {
|
||||
ctx := context.WithValue(context.Background(), JTIKey, "jti_test_123")
|
||||
jti, ok := GetJTI(ctx)
|
||||
if !ok {
|
||||
t.Error("expected ok=true")
|
||||
}
|
||||
if jti != "jti_test_123" {
|
||||
t.Errorf("expected 'jti_test_123', got %q", jti)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetJTI_Missing(t *testing.T) {
|
||||
_, ok := GetJTI(context.Background())
|
||||
if ok {
|
||||
t.Error("expected ok=false")
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Full middleware chain — RequireAuth → RequireAdmin
|
||||
// =============================================================================
|
||||
|
||||
func TestAuthChain_AdminTokenFullFlow(t *testing.T) {
|
||||
token := jwt.GenerateAdminToken()
|
||||
|
||||
handler := RequireAuth(RequireAdmin(testHandler()))
|
||||
|
||||
req := httptest.NewRequest("GET", "/admin/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("expected 200 for admin token through full chain, got %d: %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthChain_VerifiedTokenBlockedByRequireAdmin(t *testing.T) {
|
||||
token := jwt.GenerateTestToken("user123abc", "verified_email")
|
||||
|
||||
handler := RequireAuth(RequireAdmin(testHandler()))
|
||||
|
||||
req := httptest.NewRequest("GET", "/admin/dashboard", nil)
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusForbidden {
|
||||
t.Errorf("expected 403 for verified_email through RequireAdmin, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthChain_NoTokenBlockedByRequireAuth(t *testing.T) {
|
||||
handler := RequireAuth(RequireAdmin(testHandler()))
|
||||
|
||||
req := httptest.NewRequest("GET", "/admin/dashboard", nil)
|
||||
w := httptest.NewRecorder()
|
||||
handler.ServeHTTP(w, req)
|
||||
|
||||
if w.Code != http.StatusUnauthorized {
|
||||
t.Errorf("expected 401 with no token, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user