mirror of
https://github.com/SamyRai/tercul-backend.git
synced 2025-12-27 04:01:34 +00:00
This commit refactors the GraphQL test suite to resolve persistent build failures and establish a stable, mock-based unit testing environment. The key changes include: - Consolidating all GraphQL test helper functions into a single, canonical file (`internal/adapters/graphql/graphql_test_utils_test.go`). - Removing duplicated test helper code from `integration_test.go` and other test files. - Creating a new, dedicated unit test file for the `like` and `unlike` mutations (`internal/adapters/graphql/like_resolvers_unit_test.go`) using a mock-based approach. - Introducing mock services (`MockLikeService`, `MockAnalyticsService`) and updating mock repositories (`MockLikeRepository`, `MockWorkRepository`) in the `internal/testutil` package to support `testify/mock`. - Adding a `ContextWithUserID` helper function to `internal/platform/auth/middleware.go` to facilitate testing of authenticated resolvers. These changes resolve the `redeclared in this block` and package collision errors, resulting in a clean and passing test suite. This provides a solid foundation for future Test-Driven Development.
190 lines
5.2 KiB
Go
190 lines
5.2 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"tercul/internal/platform/log"
|
|
)
|
|
|
|
// ContextKey is a type for context keys
|
|
type ContextKey string
|
|
|
|
const (
|
|
// UserContextKey is the key for user in context
|
|
UserContextKey ContextKey = "user"
|
|
// ClaimsContextKey is the key for claims in context
|
|
ClaimsContextKey ContextKey = "claims"
|
|
)
|
|
|
|
// AuthMiddleware creates middleware for JWT authentication
|
|
func AuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// Skip authentication for certain paths
|
|
if shouldSkipAuth(r.URL.Path) {
|
|
next.ServeHTTP(w, r)
|
|
return
|
|
}
|
|
|
|
// Extract token from Authorization header
|
|
authHeader := r.Header.Get("Authorization")
|
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
|
if err != nil {
|
|
log.LogWarn("Authentication failed - missing or invalid token",
|
|
log.F("path", r.URL.Path),
|
|
log.F("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Validate token
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err != nil {
|
|
log.LogWarn("Authentication failed - invalid token",
|
|
log.F("path", r.URL.Path),
|
|
log.F("error", err))
|
|
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
// Add claims to context
|
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
})
|
|
}
|
|
}
|
|
|
|
// RoleMiddleware creates middleware for role-based authorization
|
|
func RoleMiddleware(requiredRole string) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
claims, ok := r.Context().Value(ClaimsContextKey).(*Claims)
|
|
if !ok {
|
|
log.LogWarn("Authorization failed - no claims in context",
|
|
log.F("path", r.URL.Path),
|
|
log.F("required_role", requiredRole))
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
jwtManager := NewJWTManager()
|
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
|
log.LogWarn("Authorization failed - insufficient role",
|
|
log.F("path", r.URL.Path),
|
|
log.F("user_role", claims.Role),
|
|
log.F("required_role", requiredRole))
|
|
http.Error(w, "Forbidden", http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// GraphQLAuthMiddleware creates middleware specifically for GraphQL requests
|
|
func GraphQLAuthMiddleware(jwtManager *JWTManager) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
// For GraphQL, we want to authenticate but not block requests
|
|
// This allows for both authenticated and anonymous queries
|
|
authHeader := r.Header.Get("Authorization")
|
|
if authHeader != "" {
|
|
tokenString, err := jwtManager.ExtractTokenFromHeader(authHeader)
|
|
if err == nil {
|
|
claims, err := jwtManager.ValidateToken(tokenString)
|
|
if err == nil {
|
|
// Add claims to context for authenticated requests
|
|
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
return
|
|
}
|
|
}
|
|
// If token is invalid, log warning but continue
|
|
log.LogWarn("GraphQL authentication failed - continuing with anonymous access",
|
|
log.F("path", r.URL.Path))
|
|
}
|
|
|
|
// Continue without authentication
|
|
next.ServeHTTP(w, r)
|
|
})
|
|
}
|
|
}
|
|
|
|
// GetClaimsFromContext extracts claims from context
|
|
func GetClaimsFromContext(ctx context.Context) (*Claims, bool) {
|
|
claims, ok := ctx.Value(ClaimsContextKey).(*Claims)
|
|
return claims, ok
|
|
}
|
|
|
|
// GetUserIDFromContext extracts user ID from context
|
|
func GetUserIDFromContext(ctx context.Context) (uint, bool) {
|
|
claims, ok := GetClaimsFromContext(ctx)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return claims.UserID, true
|
|
}
|
|
|
|
// IsAuthenticated checks if the request is authenticated
|
|
func IsAuthenticated(ctx context.Context) bool {
|
|
_, ok := GetClaimsFromContext(ctx)
|
|
return ok
|
|
}
|
|
|
|
// RequireAuth ensures the request is authenticated
|
|
func RequireAuth(ctx context.Context) (*Claims, error) {
|
|
claims, ok := GetClaimsFromContext(ctx)
|
|
if !ok {
|
|
return nil, ErrMissingToken
|
|
}
|
|
return claims, nil
|
|
}
|
|
|
|
// RequireRole ensures the user has the required role
|
|
func RequireRole(ctx context.Context, requiredRole string) (*Claims, error) {
|
|
claims, err := RequireAuth(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
jwtManager := NewJWTManager()
|
|
if err := jwtManager.RequireRole(claims.Role, requiredRole); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return claims, nil
|
|
}
|
|
|
|
// shouldSkipAuth determines if authentication should be skipped for a path
|
|
func shouldSkipAuth(path string) bool {
|
|
skipPaths := []string{
|
|
"/",
|
|
"/query",
|
|
"/health",
|
|
"/metrics",
|
|
"/favicon.ico",
|
|
}
|
|
|
|
for _, skipPath := range skipPaths {
|
|
if path == skipPath {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Skip static files
|
|
if strings.HasPrefix(path, "/static/") {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
// ContextWithUserID adds a user ID to the context for testing purposes.
|
|
func ContextWithUserID(ctx context.Context, userID uint) context.Context {
|
|
claims := &Claims{UserID: userID}
|
|
return context.WithValue(ctx, ClaimsContextKey, claims)
|
|
}
|