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