gohorsejobs/backend/internal/api/middleware/auth_middleware_test.go
Tiago Yamamoto 052f5169c5 test(auth): add comprehensive auth tests with 98.6% coverage
Backend Tests Added:
- auth_middleware_test.go: 25+ tests for HeaderAuthGuard, OptionalHeaderAuthGuard, RequireRoles, TenantGuard, ExtractRoles, hasRole (100% coverage)
- cors_middleware_test.go: 7 tests for CORS origin validation (100% coverage)
- jwt_service_test.go: expanded with expiration parsing, wrong signing method tests (94.4% coverage)

Features:
- Maximum console.log/fmt.Printf output for debugging
- Tests for JWT from header and cookie fallback
- Tests for role-based access (case-insensitive)
- Tests for tenant enforcement
- Tests for token expiration parsing (7d, 2h, invalid formats)

Total backend auth coverage: 98.6%
2025-12-24 16:20:56 -03:00

601 lines
20 KiB
Go

package middleware
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
// MockAuthService implements ports.AuthService for testing
type MockAuthService struct {
mock.Mock
}
func (m *MockAuthService) ValidateToken(token string) (map[string]interface{}, error) {
fmt.Printf("[TEST LOG] ValidateToken called with token: '%s...'\n", token[:min(20, len(token))])
args := m.Called(token)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(map[string]interface{}), args.Error(1)
}
func (m *MockAuthService) GenerateToken(userID, tenantID string, roles []string) (string, error) {
fmt.Printf("[TEST LOG] GenerateToken called: userID=%s, tenantID=%s, roles=%v\n", userID, tenantID, roles)
args := m.Called(userID, tenantID, roles)
return args.String(0), args.Error(1)
}
func (m *MockAuthService) HashPassword(password string) (string, error) {
fmt.Printf("[TEST LOG] HashPassword called\n")
args := m.Called(password)
return args.String(0), args.Error(1)
}
func (m *MockAuthService) VerifyPassword(hash, password string) bool {
fmt.Printf("[TEST LOG] VerifyPassword called\n")
args := m.Called(hash, password)
return args.Bool(0)
}
// ============================================================================
// TestHeaderAuthGuard - Tests for the main auth middleware
// ============================================================================
func TestHeaderAuthGuard_ValidTokenFromHeader(t *testing.T) {
fmt.Println("\n[TEST] === TestHeaderAuthGuard_ValidTokenFromHeader ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
claims := map[string]interface{}{
"sub": "user-123",
"tenant": "tenant-456",
"roles": []interface{}{"admin", "user"},
}
mockAuth.On("ValidateToken", "valid-jwt-token").Return(claims, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - checking context values")
userID := r.Context().Value(ContextUserID)
tenantID := r.Context().Value(ContextTenantID)
roles := r.Context().Value(ContextRoles)
fmt.Printf("[TEST LOG] Context: userID=%v, tenantID=%v, roles=%v\n", userID, tenantID, roles)
assert.Equal(t, "user-123", userID)
assert.Equal(t, "tenant-456", tenantID)
assert.NotNil(t, roles)
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer valid-jwt-token")
rr := httptest.NewRecorder()
mw.HeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
mockAuth.AssertExpectations(t)
}
func TestHeaderAuthGuard_ValidTokenFromCookie(t *testing.T) {
fmt.Println("\n[TEST] === TestHeaderAuthGuard_ValidTokenFromCookie ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
claims := map[string]interface{}{
"sub": "user-cookie-123",
"tenant": "tenant-cookie-456",
"roles": []string{"candidate"},
}
mockAuth.On("ValidateToken", "cookie-jwt-token").Return(claims, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached via cookie auth")
userID := r.Context().Value(ContextUserID)
fmt.Printf("[TEST LOG] Context userID: %v\n", userID)
assert.Equal(t, "user-cookie-123", userID)
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/protected", nil)
req.AddCookie(&http.Cookie{Name: "jwt", Value: "cookie-jwt-token"})
rr := httptest.NewRecorder()
mw.HeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
mockAuth.AssertExpectations(t)
}
func TestHeaderAuthGuard_MissingToken(t *testing.T) {
fmt.Println("\n[TEST] === TestHeaderAuthGuard_MissingToken ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called when token is missing")
})
req := httptest.NewRequest("GET", "/protected", nil)
rr := httptest.NewRecorder()
mw.HeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusUnauthorized)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Contains(t, rr.Body.String(), "Missing Authorization Header or Cookie")
}
func TestHeaderAuthGuard_InvalidTokenFormat(t *testing.T) {
fmt.Println("\n[TEST] === TestHeaderAuthGuard_InvalidTokenFormat ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called with invalid token format")
})
// Test with "Basic" instead of "Bearer"
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Basic some-token")
rr := httptest.NewRecorder()
mw.HeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusUnauthorized)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
}
func TestHeaderAuthGuard_InvalidToken(t *testing.T) {
fmt.Println("\n[TEST] === TestHeaderAuthGuard_InvalidToken ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
mockAuth.On("ValidateToken", "invalid-token").Return(nil, fmt.Errorf("token expired"))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called with invalid token")
})
req := httptest.NewRequest("GET", "/protected", nil)
req.Header.Set("Authorization", "Bearer invalid-token")
rr := httptest.NewRecorder()
mw.HeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusUnauthorized)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
assert.Contains(t, rr.Body.String(), "Invalid Token")
mockAuth.AssertExpectations(t)
}
// ============================================================================
// TestOptionalHeaderAuthGuard - Tests for optional auth middleware
// ============================================================================
func TestOptionalHeaderAuthGuard_NoToken(t *testing.T) {
fmt.Println("\n[TEST] === TestOptionalHeaderAuthGuard_NoToken ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handlerCalled := false
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached without token - context should be empty")
handlerCalled = true
// Context values should be nil/empty
userID := r.Context().Value(ContextUserID)
fmt.Printf("[TEST LOG] Context userID (should be nil): %v\n", userID)
assert.Nil(t, userID)
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/public", nil)
rr := httptest.NewRecorder()
mw.OptionalHeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Handler was called: %v (expected: true)\n", handlerCalled)
assert.True(t, handlerCalled)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestOptionalHeaderAuthGuard_ValidToken(t *testing.T) {
fmt.Println("\n[TEST] === TestOptionalHeaderAuthGuard_ValidToken ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
claims := map[string]interface{}{
"sub": "optional-user",
"tenant": "optional-tenant",
"roles": []string{"viewer"},
}
mockAuth.On("ValidateToken", "optional-token").Return(claims, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached with optional token")
userID := r.Context().Value(ContextUserID)
fmt.Printf("[TEST LOG] Context userID: %v\n", userID)
assert.Equal(t, "optional-user", userID)
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/public", nil)
req.Header.Set("Authorization", "Bearer optional-token")
rr := httptest.NewRecorder()
mw.OptionalHeaderAuthGuard(handler).ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockAuth.AssertExpectations(t)
}
func TestOptionalHeaderAuthGuard_InvalidToken(t *testing.T) {
fmt.Println("\n[TEST] === TestOptionalHeaderAuthGuard_InvalidToken ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
mockAuth.On("ValidateToken", "bad-optional-token").Return(nil, fmt.Errorf("invalid"))
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called with invalid optional token")
})
req := httptest.NewRequest("GET", "/public", nil)
req.Header.Set("Authorization", "Bearer bad-optional-token")
rr := httptest.NewRecorder()
mw.OptionalHeaderAuthGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusUnauthorized)
assert.Equal(t, http.StatusUnauthorized, rr.Code)
mockAuth.AssertExpectations(t)
}
func TestOptionalHeaderAuthGuard_TokenFromCookie(t *testing.T) {
fmt.Println("\n[TEST] === TestOptionalHeaderAuthGuard_TokenFromCookie ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
claims := map[string]interface{}{
"sub": "cookie-user",
"tenant": "cookie-tenant",
"roles": []string{"user"},
}
mockAuth.On("ValidateToken", "cookie-optional-token").Return(claims, nil)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID := r.Context().Value(ContextUserID)
assert.Equal(t, "cookie-user", userID)
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/public", nil)
req.AddCookie(&http.Cookie{Name: "jwt", Value: "cookie-optional-token"})
rr := httptest.NewRecorder()
mw.OptionalHeaderAuthGuard(handler).ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
mockAuth.AssertExpectations(t)
}
// ============================================================================
// TestRequireRoles - Tests for role-based access control
// ============================================================================
func TestRequireRoles_UserHasRequiredRole(t *testing.T) {
fmt.Println("\n[TEST] === TestRequireRoles_UserHasRequiredRole ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - user has required role")
w.WriteHeader(http.StatusOK)
})
// Create request with roles in context
req := httptest.NewRequest("GET", "/admin", nil)
ctx := context.WithValue(req.Context(), ContextRoles, []string{"admin", "user"})
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
mw.RequireRoles("admin")(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestRequireRoles_UserLacksRequiredRole(t *testing.T) {
fmt.Println("\n[TEST] === TestRequireRoles_UserLacksRequiredRole ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called when user lacks role")
})
req := httptest.NewRequest("GET", "/admin", nil)
ctx := context.WithValue(req.Context(), ContextRoles, []string{"user", "viewer"})
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
mw.RequireRoles("admin", "superadmin")(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusForbidden)
assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Forbidden")
}
func TestRequireRoles_CaseInsensitive(t *testing.T) {
fmt.Println("\n[TEST] === TestRequireRoles_CaseInsensitive ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - case insensitive match worked")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/admin", nil)
ctx := context.WithValue(req.Context(), ContextRoles, []string{"ADMIN", "USER"})
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
mw.RequireRoles("admin")(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestRequireRoles_NoRolesInContext(t *testing.T) {
fmt.Println("\n[TEST] === TestRequireRoles_NoRolesInContext ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called when no roles in context")
})
req := httptest.NewRequest("GET", "/admin", nil)
// No roles in context
rr := httptest.NewRecorder()
mw.RequireRoles("admin")(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusForbidden)
assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Roles not found")
}
func TestRequireRoles_MultipleAllowedRoles(t *testing.T) {
fmt.Println("\n[TEST] === TestRequireRoles_MultipleAllowedRoles ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - matched one of multiple allowed roles")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/manage", nil)
ctx := context.WithValue(req.Context(), ContextRoles, []string{"moderator"})
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
// Allow admin, moderator, or superadmin
mw.RequireRoles("admin", "moderator", "superadmin")(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
}
// ============================================================================
// TestTenantGuard - Tests for tenant enforcement
// ============================================================================
func TestTenantGuard_ValidTenant(t *testing.T) {
fmt.Println("\n[TEST] === TestTenantGuard_ValidTenant ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - tenant is valid")
w.WriteHeader(http.StatusOK)
})
req := httptest.NewRequest("GET", "/tenant-resource", nil)
ctx := context.WithValue(req.Context(), ContextTenantID, "tenant-123")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
mw.TenantGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusOK)
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestTenantGuard_MissingTenant(t *testing.T) {
fmt.Println("\n[TEST] === TestTenantGuard_MissingTenant ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called when tenant is missing")
})
req := httptest.NewRequest("GET", "/tenant-resource", nil)
// No tenant in context
rr := httptest.NewRecorder()
mw.TenantGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusForbidden)
assert.Equal(t, http.StatusForbidden, rr.Code)
assert.Contains(t, rr.Body.String(), "Tenant Context Missing")
}
func TestTenantGuard_EmptyTenant(t *testing.T) {
fmt.Println("\n[TEST] === TestTenantGuard_EmptyTenant ===")
mockAuth := new(MockAuthService)
mw := NewMiddleware(mockAuth)
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t.Error("Handler should not be called when tenant is empty")
})
req := httptest.NewRequest("GET", "/tenant-resource", nil)
ctx := context.WithValue(req.Context(), ContextTenantID, "")
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
mw.TenantGuard(handler).ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: %d)\n", rr.Code, http.StatusForbidden)
assert.Equal(t, http.StatusForbidden, rr.Code)
}
// ============================================================================
// TestExtractRoles - Tests for the helper function
// ============================================================================
func TestExtractRoles_FromStringSlice(t *testing.T) {
fmt.Println("\n[TEST] === TestExtractRoles_FromStringSlice ===")
input := []string{"admin", "user", "viewer"}
result := ExtractRoles(input)
fmt.Printf("[TEST LOG] Input: %v, Result: %v\n", input, result)
assert.Equal(t, []string{"admin", "user", "viewer"}, result)
}
func TestExtractRoles_FromInterfaceSlice(t *testing.T) {
fmt.Println("\n[TEST] === TestExtractRoles_FromInterfaceSlice ===")
input := []interface{}{"admin", "moderator"}
result := ExtractRoles(input)
fmt.Printf("[TEST LOG] Input: %v, Result: %v\n", input, result)
assert.Equal(t, []string{"admin", "moderator"}, result)
}
func TestExtractRoles_FromNil(t *testing.T) {
fmt.Println("\n[TEST] === TestExtractRoles_FromNil ===")
result := ExtractRoles(nil)
fmt.Printf("[TEST LOG] Input: nil, Result: %v\n", result)
assert.Equal(t, []string{}, result)
}
func TestExtractRoles_FromUnknownType(t *testing.T) {
fmt.Println("\n[TEST] === TestExtractRoles_FromUnknownType ===")
input := "not-a-slice"
result := ExtractRoles(input)
fmt.Printf("[TEST LOG] Input: %v (type: string), Result: %v\n", input, result)
assert.Equal(t, []string{}, result)
}
func TestExtractRoles_FromMixedInterfaceSlice(t *testing.T) {
fmt.Println("\n[TEST] === TestExtractRoles_FromMixedInterfaceSlice ===")
input := []interface{}{"admin", 123, "user", nil}
result := ExtractRoles(input)
fmt.Printf("[TEST LOG] Input: %v, Result: %v\n", input, result)
// Should only extract strings
assert.Equal(t, []string{"admin", "user"}, result)
}
// ============================================================================
// TestHasRole - Tests for the role matching helper
// ============================================================================
func TestHasRole_SingleMatch(t *testing.T) {
fmt.Println("\n[TEST] === TestHasRole_SingleMatch ===")
userRoles := []string{"admin", "user"}
allowedRoles := []string{"admin"}
result := hasRole(userRoles, allowedRoles)
fmt.Printf("[TEST LOG] User roles: %v, Allowed: %v, Result: %v\n", userRoles, allowedRoles, result)
assert.True(t, result)
}
func TestHasRole_NoMatch(t *testing.T) {
fmt.Println("\n[TEST] === TestHasRole_NoMatch ===")
userRoles := []string{"user", "viewer"}
allowedRoles := []string{"admin", "superadmin"}
result := hasRole(userRoles, allowedRoles)
fmt.Printf("[TEST LOG] User roles: %v, Allowed: %v, Result: %v\n", userRoles, allowedRoles, result)
assert.False(t, result)
}
func TestHasRole_CaseInsensitive(t *testing.T) {
fmt.Println("\n[TEST] === TestHasRole_CaseInsensitive ===")
userRoles := []string{"ADMIN", "USER"}
allowedRoles := []string{"admin"}
result := hasRole(userRoles, allowedRoles)
fmt.Printf("[TEST LOG] User roles: %v, Allowed: %v, Result: %v\n", userRoles, allowedRoles, result)
assert.True(t, result)
}
func TestHasRole_EmptyUserRoles(t *testing.T) {
fmt.Println("\n[TEST] === TestHasRole_EmptyUserRoles ===")
userRoles := []string{}
allowedRoles := []string{"admin"}
result := hasRole(userRoles, allowedRoles)
fmt.Printf("[TEST LOG] User roles: %v, Allowed: %v, Result: %v\n", userRoles, allowedRoles, result)
assert.False(t, result)
}
func TestHasRole_EmptyAllowedRoles(t *testing.T) {
fmt.Println("\n[TEST] === TestHasRole_EmptyAllowedRoles ===")
userRoles := []string{"admin"}
allowedRoles := []string{}
result := hasRole(userRoles, allowedRoles)
fmt.Printf("[TEST LOG] User roles: %v, Allowed: %v, Result: %v\n", userRoles, allowedRoles, result)
assert.False(t, result)
}