ResolveSpec/pkg/security/composite_test.go
2025-12-09 11:18:11 +02:00

435 lines
11 KiB
Go

package security
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"testing"
)
// Mock implementations for testing composite provider
type mockAuth struct {
loginResp *LoginResponse
loginErr error
logoutErr error
authUser *UserContext
authErr error
supportsRefresh bool
supportsValidate bool
}
func (m *mockAuth) Login(ctx context.Context, req LoginRequest) (*LoginResponse, error) {
return m.loginResp, m.loginErr
}
func (m *mockAuth) Logout(ctx context.Context, req LogoutRequest) error {
return m.logoutErr
}
func (m *mockAuth) Authenticate(r *http.Request) (*UserContext, error) {
return m.authUser, m.authErr
}
// Optional interface implementations
func (m *mockAuth) RefreshToken(ctx context.Context, refreshToken string) (*LoginResponse, error) {
if !m.supportsRefresh {
return nil, errors.New("not supported")
}
return m.loginResp, m.loginErr
}
func (m *mockAuth) ValidateToken(ctx context.Context, token string) (bool, error) {
if !m.supportsValidate {
return false, errors.New("not supported")
}
return true, nil
}
type mockColSec struct {
rules []ColumnSecurity
err error
supportsCache bool
}
func (m *mockColSec) GetColumnSecurity(ctx context.Context, userID int, schema, table string) ([]ColumnSecurity, error) {
return m.rules, m.err
}
func (m *mockColSec) ClearCache(ctx context.Context, userID int, schema, table string) error {
if !m.supportsCache {
return errors.New("not supported")
}
return nil
}
type mockRowSec struct {
rowSec RowSecurity
err error
supportsCache bool
}
func (m *mockRowSec) GetRowSecurity(ctx context.Context, userID int, schema, table string) (RowSecurity, error) {
return m.rowSec, m.err
}
func (m *mockRowSec) ClearCache(ctx context.Context, userID int, schema, table string) error {
if !m.supportsCache {
return errors.New("not supported")
}
return nil
}
// Test NewCompositeSecurityProvider
func TestNewCompositeSecurityProvider(t *testing.T) {
t.Run("with all valid providers", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, err := NewCompositeSecurityProvider(auth, colSec, rowSec)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if composite == nil {
t.Fatal("expected non-nil composite provider")
}
})
t.Run("with nil authenticator", func(t *testing.T) {
colSec := &mockColSec{}
rowSec := &mockRowSec{}
_, err := NewCompositeSecurityProvider(nil, colSec, rowSec)
if err == nil {
t.Fatal("expected error with nil authenticator")
}
})
t.Run("with nil column security provider", func(t *testing.T) {
auth := &mockAuth{}
rowSec := &mockRowSec{}
_, err := NewCompositeSecurityProvider(auth, nil, rowSec)
if err == nil {
t.Fatal("expected error with nil column security provider")
}
})
t.Run("with nil row security provider", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{}
_, err := NewCompositeSecurityProvider(auth, colSec, nil)
if err == nil {
t.Fatal("expected error with nil row security provider")
}
})
}
// Test CompositeSecurityProvider authentication delegation
func TestCompositeSecurityProviderAuth(t *testing.T) {
userCtx := &UserContext{
UserID: 1,
UserName: "testuser",
}
t.Run("login delegates to authenticator", func(t *testing.T) {
auth := &mockAuth{
loginResp: &LoginResponse{
Token: "abc123",
User: userCtx,
},
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
req := LoginRequest{Username: "test", Password: "pass"}
resp, err := composite.Login(ctx, req)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if resp.Token != "abc123" {
t.Errorf("expected token abc123, got %s", resp.Token)
}
})
t.Run("logout delegates to authenticator", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
req := LogoutRequest{Token: "abc123", UserID: 1}
err := composite.Logout(ctx, req)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
t.Run("authenticate delegates to authenticator", func(t *testing.T) {
auth := &mockAuth{
authUser: userCtx,
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
req := httptest.NewRequest("GET", "/test", nil)
user, err := composite.Authenticate(req)
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if user.UserID != 1 {
t.Errorf("expected UserID 1, got %d", user.UserID)
}
})
}
// Test CompositeSecurityProvider security provider delegation
func TestCompositeSecurityProviderSecurity(t *testing.T) {
t.Run("get column security delegates to column provider", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{
rules: []ColumnSecurity{
{Schema: "public", Tablename: "users", Path: []string{"email"}},
},
}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
rules, err := composite.GetColumnSecurity(ctx, 1, "public", "users")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if len(rules) != 1 {
t.Errorf("expected 1 rule, got %d", len(rules))
}
})
t.Run("get row security delegates to row provider", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{}
rowSec := &mockRowSec{
rowSec: RowSecurity{
Schema: "public",
Tablename: "orders",
Template: "user_id = {UserID}",
},
}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
rowSecResult, err := composite.GetRowSecurity(ctx, 1, "public", "orders")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if rowSecResult.Template != "user_id = {UserID}" {
t.Errorf("expected template 'user_id = {UserID}', got %s", rowSecResult.Template)
}
})
}
// Test CompositeSecurityProvider optional interfaces
func TestCompositeSecurityProviderOptionalInterfaces(t *testing.T) {
t.Run("refresh token with support", func(t *testing.T) {
auth := &mockAuth{
supportsRefresh: true,
loginResp: &LoginResponse{
Token: "new-token",
},
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
resp, err := composite.RefreshToken(ctx, "old-token")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if resp.Token != "new-token" {
t.Errorf("expected token new-token, got %s", resp.Token)
}
})
t.Run("refresh token without support", func(t *testing.T) {
auth := &mockAuth{
supportsRefresh: false,
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
_, err := composite.RefreshToken(ctx, "token")
if err == nil {
t.Fatal("expected error when refresh not supported")
}
})
t.Run("validate token with support", func(t *testing.T) {
auth := &mockAuth{
supportsValidate: true,
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
valid, err := composite.ValidateToken(ctx, "token")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
if !valid {
t.Error("expected token to be valid")
}
})
t.Run("validate token without support", func(t *testing.T) {
auth := &mockAuth{
supportsValidate: false,
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
_, err := composite.ValidateToken(ctx, "token")
if err == nil {
t.Fatal("expected error when validate not supported")
}
})
}
// Test CompositeSecurityProvider cache clearing
func TestCompositeSecurityProviderClearCache(t *testing.T) {
t.Run("clear cache with support", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{supportsCache: true}
rowSec := &mockRowSec{supportsCache: true}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
err := composite.ClearCache(ctx, 1, "public", "users")
if err != nil {
t.Fatalf("expected no error, got %v", err)
}
})
t.Run("clear cache without support", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{supportsCache: false}
rowSec := &mockRowSec{supportsCache: false}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
// Should not error even if providers don't support cache
// (they just won't implement the interface)
err := composite.ClearCache(ctx, 1, "public", "users")
if err != nil {
// It's ok if this errors, as the providers don't implement Cacheable
t.Logf("cache clear returned error as expected: %v", err)
}
})
t.Run("clear cache with partial support", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{supportsCache: true}
rowSec := &mockRowSec{supportsCache: false}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
err := composite.ClearCache(ctx, 1, "public", "users")
// Should succeed for column security even if row security fails
if err == nil {
t.Log("cache clear succeeded partially")
} else {
t.Logf("cache clear returned error: %v", err)
}
})
}
// Test error propagation
func TestCompositeSecurityProviderErrorPropagation(t *testing.T) {
t.Run("login error propagates", func(t *testing.T) {
auth := &mockAuth{
loginErr: errors.New("invalid credentials"),
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
_, err := composite.Login(ctx, LoginRequest{})
if err == nil {
t.Fatal("expected error to propagate")
}
})
t.Run("authenticate error propagates", func(t *testing.T) {
auth := &mockAuth{
authErr: errors.New("invalid token"),
}
colSec := &mockColSec{}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
req := httptest.NewRequest("GET", "/test", nil)
_, err := composite.Authenticate(req)
if err == nil {
t.Fatal("expected error to propagate")
}
})
t.Run("column security error propagates", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{
err: errors.New("failed to load column security"),
}
rowSec := &mockRowSec{}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
_, err := composite.GetColumnSecurity(ctx, 1, "public", "users")
if err == nil {
t.Fatal("expected error to propagate")
}
})
t.Run("row security error propagates", func(t *testing.T) {
auth := &mockAuth{}
colSec := &mockColSec{}
rowSec := &mockRowSec{
err: errors.New("failed to load row security"),
}
composite, _ := NewCompositeSecurityProvider(auth, colSec, rowSec)
ctx := context.Background()
_, err := composite.GetRowSecurity(ctx, 1, "public", "orders")
if err == nil {
t.Fatal("expected error to propagate")
}
})
}