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%
This commit is contained in:
Tiago Yamamoto 2025-12-24 16:20:56 -03:00
parent 7720f2e35e
commit 052f5169c5
5 changed files with 944 additions and 2 deletions

View file

@ -46,6 +46,7 @@ require (
github.com/go-openapi/swag/typeutils v0.25.4 // indirect
github.com/go-openapi/swag/yamlutils v0.25.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.1.0 // indirect
github.com/swaggo/files/v2 v2.0.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/mod v0.30.0 // indirect

View file

@ -81,6 +81,7 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=

View file

@ -0,0 +1,601 @@
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)
}

View file

@ -0,0 +1,205 @@
package middleware
import (
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/stretchr/testify/assert"
)
// ============================================================================
// TestCORSMiddleware - Tests for CORS middleware
// ============================================================================
func TestCORSMiddleware_AllowedOrigin(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_AllowedOrigin ===")
// Set allowed origins
os.Setenv("CORS_ORIGINS", "http://localhost:3000,http://example.com")
defer os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - CORS headers should be set")
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("Origin", "http://localhost:3000")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s'\n", rr.Header().Get("Access-Control-Allow-Origin"))
fmt.Printf("[TEST LOG] Access-Control-Allow-Credentials: '%s'\n", rr.Header().Get("Access-Control-Allow-Credentials"))
assert.Equal(t, "http://localhost:3000", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "true", rr.Header().Get("Access-Control-Allow-Credentials"))
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestCORSMiddleware_DeniedOrigin(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_DeniedOrigin ===")
os.Setenv("CORS_ORIGINS", "http://localhost:3000")
defer os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - CORS origin should be empty")
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("Origin", "http://evil.com")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s' (expected empty)\n", rr.Header().Get("Access-Control-Allow-Origin"))
// Origin not in allowed list - should not set Access-Control-Allow-Origin
assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Origin"))
// But credentials header should still be set
assert.Equal(t, "true", rr.Header().Get("Access-Control-Allow-Credentials"))
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestCORSMiddleware_WildcardOrigin(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_WildcardOrigin ===")
os.Setenv("CORS_ORIGINS", "*")
defer os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - wildcard CORS")
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("Origin", "http://any-origin.com")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s' (expected *)\n", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "*", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestCORSMiddleware_PreflightOptions(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_PreflightOptions ===")
os.Setenv("CORS_ORIGINS", "http://localhost:3000")
defer os.Unsetenv("CORS_ORIGINS")
handlerCalled := false
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
handlerCalled = true
t.Error("Handler should not be called for OPTIONS preflight")
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("OPTIONS", "/api/test", nil)
req.Header.Set("Origin", "http://localhost:3000")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Response status: %d (expected: 200 for preflight)\n", rr.Code)
fmt.Printf("[TEST LOG] Handler was called: %v (expected: false)\n", handlerCalled)
fmt.Printf("[TEST LOG] Access-Control-Allow-Methods: '%s'\n", rr.Header().Get("Access-Control-Allow-Methods"))
assert.False(t, handlerCalled)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "POST")
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "GET")
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "PUT")
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Methods"), "DELETE")
assert.Contains(t, rr.Header().Get("Access-Control-Allow-Headers"), "Authorization")
}
func TestCORSMiddleware_DefaultOrigin(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_DefaultOrigin ===")
// Clear CORS_ORIGINS to test default
os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - default origin")
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("Origin", "http://localhost:3000")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s'\n", rr.Header().Get("Access-Control-Allow-Origin"))
// Default should allow localhost:3000
assert.Equal(t, "http://localhost:3000", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, http.StatusOK, rr.Code)
}
func TestCORSMiddleware_MultipleOrigins(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_MultipleOrigins ===")
os.Setenv("CORS_ORIGINS", "http://app.example.com, http://admin.example.com")
defer os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
// Test second origin in list
req := httptest.NewRequest("GET", "/api/test", nil)
req.Header.Set("Origin", "http://admin.example.com")
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s'\n", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, "http://admin.example.com", rr.Header().Get("Access-Control-Allow-Origin"))
}
func TestCORSMiddleware_NoOriginHeader(t *testing.T) {
fmt.Println("\n[TEST] === TestCORSMiddleware_NoOriginHeader ===")
os.Setenv("CORS_ORIGINS", "http://localhost:3000")
defer os.Unsetenv("CORS_ORIGINS")
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("[TEST LOG] Handler reached - no origin header in request")
w.WriteHeader(http.StatusOK)
})
middleware := CORSMiddleware(handler)
req := httptest.NewRequest("GET", "/api/test", nil)
// No Origin header set
rr := httptest.NewRecorder()
middleware.ServeHTTP(rr, req)
fmt.Printf("[TEST LOG] Access-Control-Allow-Origin: '%s' (expected empty)\n", rr.Header().Get("Access-Control-Allow-Origin"))
// No origin means no matching, so header should be empty
assert.Equal(t, "", rr.Header().Get("Access-Control-Allow-Origin"))
assert.Equal(t, http.StatusOK, rr.Code)
}

View file

@ -1,6 +1,7 @@
package auth_test
import (
"fmt"
"os"
"testing"
@ -9,6 +10,8 @@ import (
)
func TestJWTService_HashAndVerifyPassword(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_HashAndVerifyPassword ===")
// Setup
os.Setenv("PASSWORD_PEPPER", "test-pepper")
defer os.Unsetenv("PASSWORD_PEPPER")
@ -16,30 +19,37 @@ func TestJWTService_HashAndVerifyPassword(t *testing.T) {
service := auth.NewJWTService("secret", "issuer")
t.Run("Should hash and verify password correctly", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing password hash and verify")
password := "mysecurepassword"
hash, err := service.HashPassword(password)
fmt.Printf("[TEST LOG] Hash generated: %s...\n", hash[:20])
assert.NoError(t, err)
assert.NotEmpty(t, hash)
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Password verification result: %v\n", valid)
assert.True(t, valid)
})
t.Run("Should fail verification with wrong password", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing wrong password rejection")
password := "password"
hash, _ := service.HashPassword(password)
valid := service.VerifyPassword(hash, "wrong-password")
fmt.Printf("[TEST LOG] Wrong password verification result: %v (expected false)\n", valid)
assert.False(t, valid)
})
t.Run("Should fail verification with wrong pepper", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing wrong pepper rejection")
password := "password"
hash, _ := service.HashPassword(password)
// Change pepper
os.Setenv("PASSWORD_PEPPER", "wrong-pepper")
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Wrong pepper verification result: %v (expected false)\n", valid)
assert.False(t, valid)
// Reset pepper
@ -47,29 +57,153 @@ func TestJWTService_HashAndVerifyPassword(t *testing.T) {
})
}
func TestJWTService_HashPassword_NoPepper(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_HashPassword_NoPepper ===")
os.Unsetenv("PASSWORD_PEPPER")
defer os.Setenv("PASSWORD_PEPPER", "test-pepper")
service := auth.NewJWTService("secret", "issuer")
password := "password-no-pepper"
hash, err := service.HashPassword(password)
fmt.Printf("[TEST LOG] Hash without pepper: %s...\n", hash[:20])
assert.NoError(t, err)
assert.NotEmpty(t, hash)
// Should still verify (empty pepper is still valid)
valid := service.VerifyPassword(hash, password)
fmt.Printf("[TEST LOG] Verification without pepper: %v\n", valid)
assert.True(t, valid)
}
func TestJWTService_TokenOperations(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_TokenOperations ===")
service := auth.NewJWTService("secret", "issuer")
t.Run("Should generate and validate token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing token generation and validation")
userID := "user-123"
tenantID := "tenant-456"
roles := []string{"admin"}
token, err := service.GenerateToken(userID, tenantID, roles)
fmt.Printf("[TEST LOG] Token generated: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
claims, err := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Claims: sub=%v, tenant=%v\n", claims["sub"], claims["tenant"])
assert.NoError(t, err)
assert.Equal(t, userID, claims["sub"])
assert.Equal(t, tenantID, claims["tenant"])
// JSON numbers are float64, so careful with types if we check deep structure,
// but roles might come back as []interface{}
})
t.Run("Should fail invalid token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid token rejection")
claims, err := service.ValidateToken("invalid-token")
fmt.Printf("[TEST LOG] Invalid token error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
}
func TestJWTService_GenerateToken_ExpirationParsing(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_GenerateToken_ExpirationParsing ===")
service := auth.NewJWTService("secret", "issuer")
t.Run("Default expiration (no env)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing default expiration (24h)")
os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with default expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
claims, _ := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Token claims: exp=%v\n", claims["exp"])
assert.NotNil(t, claims["exp"])
})
t.Run("Days format (7d)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing days format expiration (7d)")
os.Setenv("JWT_EXPIRATION", "7d")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with 7d expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Duration format (2h)", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing duration format expiration (2h)")
os.Setenv("JWT_EXPIRATION", "2h")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with 2h expiration: %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Invalid days format fallback", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid days format (abcd)")
os.Setenv("JWT_EXPIRATION", "abcd")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with invalid format (fallback): %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
t.Run("Invalid day number fallback", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing invalid day number (xxd)")
os.Setenv("JWT_EXPIRATION", "xxd")
defer os.Unsetenv("JWT_EXPIRATION")
token, err := service.GenerateToken("user", "tenant", []string{"role"})
fmt.Printf("[TEST LOG] Token with xxd format (fallback): %s...\n", token[:50])
assert.NoError(t, err)
assert.NotEmpty(t, token)
})
}
func TestJWTService_ValidateToken_WrongSigningMethod(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_ValidateToken_WrongSigningMethod ===")
service := auth.NewJWTService("secret", "issuer")
// A token signed with a different algorithm would fail validation
// This is hard to test directly, but we can test with a malformed token
t.Run("Malformed token", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing malformed token")
claims, err := service.ValidateToken("eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIn0.")
fmt.Printf("[TEST LOG] Malformed token error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
t.Run("Token with different secret", func(t *testing.T) {
fmt.Println("[TEST LOG] Testing token from different secret")
otherService := auth.NewJWTService("different-secret", "issuer")
token, _ := otherService.GenerateToken("user", "tenant", []string{"role"})
claims, err := service.ValidateToken(token)
fmt.Printf("[TEST LOG] Wrong secret error: %v\n", err)
assert.Error(t, err)
assert.Nil(t, claims)
})
}
func TestJWTService_NewJWTService(t *testing.T) {
fmt.Println("\n[TEST] === TestJWTService_NewJWTService ===")
service := auth.NewJWTService("my-secret", "my-issuer")
fmt.Printf("[TEST LOG] Service created: %v\n", service)
assert.NotNil(t, service)
}