added unit testing
This commit is contained in:
@@ -0,0 +1,282 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"authorization/db"
|
||||
"authorization/models"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
func setupMockDB(t *testing.T) (sqlmock.Sqlmock, func()) {
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create mock database: %v", err)
|
||||
}
|
||||
|
||||
originalDB := db.DB
|
||||
db.DB = mockDB
|
||||
|
||||
cleanup := func() {
|
||||
db.DB = originalDB
|
||||
mockDB.Close()
|
||||
}
|
||||
|
||||
return mock, cleanup
|
||||
}
|
||||
|
||||
func TestAuthorize_PermissionNotFound(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "nonexistent",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
Environment: make(map[string]string),
|
||||
}
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("nonexistent", "read").
|
||||
WillReturnError(errors.New("permission not found"))
|
||||
|
||||
result, err := Authorize(ctx)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if result.Allowed {
|
||||
t.Error("Expected access denied")
|
||||
}
|
||||
if result.Message == "" {
|
||||
t.Error("Expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_Success(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
Environment: make(map[string]string),
|
||||
}
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document permission", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
// Mock policy attributes query (empty for this test)
|
||||
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
|
||||
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes WHERE permission_id = \\?").
|
||||
WithArgs(1).
|
||||
WillReturnRows(policyRows)
|
||||
|
||||
result, err := Authorize(ctx)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if !result.Allowed {
|
||||
t.Error("Expected access granted")
|
||||
}
|
||||
if result.Message != "Access granted" {
|
||||
t.Errorf("Expected 'Access granted', got '%s'", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_UserAttributesError(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
Environment: make(map[string]string),
|
||||
}
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document permission", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query with error
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnError(errors.New("database error"))
|
||||
|
||||
result, err := Authorize(ctx)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for user attributes failure")
|
||||
}
|
||||
if result.Allowed {
|
||||
t.Error("Expected access denied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorize_PolicyAttributesError(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
Environment: make(map[string]string),
|
||||
}
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document permission", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
// Mock policy attributes query with error
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes WHERE permission_id = \\?").
|
||||
WithArgs(1).
|
||||
WillReturnError(errors.New("database error"))
|
||||
|
||||
result, err := Authorize(ctx)
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected error for policy attributes failure")
|
||||
}
|
||||
if result.Allowed {
|
||||
t.Error("Expected access denied")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPermission_Success(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document permission", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
// Mock policy attributes query
|
||||
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
|
||||
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes WHERE permission_id = \\?").
|
||||
WithArgs(1).
|
||||
WillReturnRows(policyRows)
|
||||
|
||||
resourceData := map[string]string{"document_id": "123"}
|
||||
allowed, message, err := CheckPermission("user123", "document", "read", resourceData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if !allowed {
|
||||
t.Error("Expected access allowed")
|
||||
}
|
||||
if message != "Access granted" {
|
||||
t.Errorf("Expected 'Access granted', got '%s'", message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPermission_Denied(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnError(errors.New("permission not found"))
|
||||
|
||||
resourceData := map[string]string{"document_id": "123"}
|
||||
allowed, message, err := CheckPermission("user123", "document", "read", resourceData)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if allowed {
|
||||
t.Error("Expected access denied")
|
||||
}
|
||||
if message == "" {
|
||||
t.Error("Expected error message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckPermission_NilResourceData(t *testing.T) {
|
||||
mock, cleanup := setupMockDB(t)
|
||||
defer cleanup()
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document permission", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions WHERE resource = \\? AND action = \\? LIMIT 1").
|
||||
WithArgs("document", "read").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock user attributes query
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"})
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
// Mock policy attributes query
|
||||
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
|
||||
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes WHERE permission_id = \\?").
|
||||
WithArgs(1).
|
||||
WillReturnRows(policyRows)
|
||||
|
||||
allowed, message, err := CheckPermission("user123", "document", "read", nil)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
// Should not panic with nil resourceData
|
||||
if !allowed {
|
||||
t.Logf("Access denied with message: %s", message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"authorization/db"
|
||||
"authorization/models"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
func setupMockDBForCached(t *testing.T) (sqlmock.Sqlmock, func()) {
|
||||
mockDB, mock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create mock database: %v", err)
|
||||
}
|
||||
|
||||
originalDB := db.DB
|
||||
db.DB = mockDB
|
||||
|
||||
cleanup := func() {
|
||||
db.DB = originalDB
|
||||
mockDB.Close()
|
||||
}
|
||||
|
||||
return mock, cleanup
|
||||
}
|
||||
|
||||
func TestNewCachedAuthorizationService(t *testing.T) {
|
||||
mock, cleanup := setupMockDBForCached(t)
|
||||
defer cleanup()
|
||||
|
||||
// Mock the initial cache load queries
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document", "document", "read")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions ORDER BY id").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"})
|
||||
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
|
||||
WillReturnRows(policyRows)
|
||||
|
||||
service := NewCachedAuthorizationService()
|
||||
|
||||
if service == nil {
|
||||
t.Fatal("Expected service, got nil")
|
||||
}
|
||||
if service.PermissionCache == nil {
|
||||
t.Error("Expected PermissionCache to be initialized")
|
||||
}
|
||||
if service.PolicyCache == nil {
|
||||
t.Error("Expected PolicyCache to be initialized")
|
||||
}
|
||||
if service.UserAttrCache == nil {
|
||||
t.Error("Expected UserAttrCache to be initialized")
|
||||
}
|
||||
if service.CacheExpiry != 30*time.Second {
|
||||
t.Errorf("Expected CacheExpiry 30s, got %v", service.CacheExpiry)
|
||||
}
|
||||
|
||||
// Give time for cache to load
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
|
||||
func TestGetCachedUserAttributes_CacheHit(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Pre-populate cache
|
||||
service.UserAttrCache["user123"] = map[string]string{
|
||||
"department": "engineering",
|
||||
"level": "5",
|
||||
}
|
||||
|
||||
attrs, err := getCachedUserAttributes(service, "user123")
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(attrs) != 2 {
|
||||
t.Errorf("Expected 2 attributes, got %d", len(attrs))
|
||||
}
|
||||
if attrs["department"] != "engineering" {
|
||||
t.Errorf("Expected department 'engineering', got '%s'", attrs["department"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCachedUserAttributes_CacheMiss(t *testing.T) {
|
||||
mock, cleanup := setupMockDBForCached(t)
|
||||
defer cleanup()
|
||||
|
||||
service := &models.CachedAuthorizationService{
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Mock database query for cache miss
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
attrs, err := getCachedUserAttributes(service, "user123")
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if len(attrs) != 1 {
|
||||
t.Errorf("Expected 1 attribute, got %d", len(attrs))
|
||||
}
|
||||
|
||||
// Verify it's now in cache
|
||||
if _, exists := service.UserAttrCache["user123"]; !exists {
|
||||
t.Error("Expected user attributes to be cached")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshCache(t *testing.T) {
|
||||
mock, cleanup := setupMockDBForCached(t)
|
||||
defer cleanup()
|
||||
|
||||
service := &models.CachedAuthorizationService{
|
||||
PermissionCache: make(map[string]*models.Permission),
|
||||
PolicyCache: make(map[int][]models.PolicyAttribute),
|
||||
CacheMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Mock permission query
|
||||
permRows := sqlmock.NewRows([]string{"id", "permission_name", "description", "resource", "action"}).
|
||||
AddRow(1, "read_document", "Read document", "document", "read").
|
||||
AddRow(2, "write_document", "Write document", "document", "write")
|
||||
|
||||
mock.ExpectQuery("SELECT id, permission_name, description, resource, action FROM permissions ORDER BY id").
|
||||
WillReturnRows(permRows)
|
||||
|
||||
// Mock policy attributes query
|
||||
policyRows := sqlmock.NewRows([]string{"id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"}).
|
||||
AddRow(1, "department", "user", "=", "engineering", 1)
|
||||
|
||||
mock.ExpectQuery("SELECT id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, id").
|
||||
WillReturnRows(policyRows)
|
||||
|
||||
refreshCache(service)
|
||||
|
||||
// Verify permissions are cached
|
||||
if len(service.PermissionCache) != 2 {
|
||||
t.Errorf("Expected 2 permissions in cache, got %d", len(service.PermissionCache))
|
||||
}
|
||||
|
||||
// Verify policies are cached
|
||||
if len(service.PolicyCache[1]) != 1 {
|
||||
t.Errorf("Expected 1 policy for permission 1, got %d", len(service.PolicyCache[1]))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanUserAttributeCache(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Add many entries to trigger cleanup
|
||||
for i := 0; i < 10001; i++ {
|
||||
service.UserAttrCache[string(rune(i))] = map[string]string{"test": "value"}
|
||||
}
|
||||
|
||||
cleanUserAttributeCache(service)
|
||||
|
||||
if len(service.UserAttrCache) != 0 {
|
||||
t.Error("Expected user attribute cache to be cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCleanUserAttributeCache_SmallCache(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Add few entries
|
||||
service.UserAttrCache["user1"] = map[string]string{"test": "value"}
|
||||
service.UserAttrCache["user2"] = map[string]string{"test": "value"}
|
||||
|
||||
cleanUserAttributeCache(service)
|
||||
|
||||
if len(service.UserAttrCache) != 2 {
|
||||
t.Error("Expected small cache to remain unchanged")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeWithCache_Success(t *testing.T) {
|
||||
mock, cleanup := setupMockDBForCached(t)
|
||||
defer cleanup()
|
||||
|
||||
service := &models.CachedAuthorizationService{
|
||||
PermissionCache: make(map[string]*models.Permission),
|
||||
PolicyCache: make(map[int][]models.PolicyAttribute),
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
CacheMutex: &sync.RWMutex{},
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
// Add permission to cache
|
||||
service.PermissionCache["document:read"] = &models.Permission{
|
||||
ID: 1,
|
||||
PermissionName: "read_document",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
}
|
||||
|
||||
// Add empty policies
|
||||
service.PolicyCache[1] = []models.PolicyAttribute{}
|
||||
|
||||
// Mock user attributes query
|
||||
attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}).
|
||||
AddRow("department", "engineering")
|
||||
|
||||
mock.ExpectQuery("SELECT attribute_name, attribute_value FROM user_attributes WHERE user_id = \\?").
|
||||
WithArgs("user123").
|
||||
WillReturnRows(attrRows)
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "document",
|
||||
Action: "read",
|
||||
ResourceData: make(map[string]string),
|
||||
Environment: make(map[string]string),
|
||||
}
|
||||
|
||||
result, err := AuthorizeWithCache(service, ctx)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if !result.Allowed {
|
||||
t.Error("Expected access granted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorizeWithCache_PermissionNotFound(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
PermissionCache: make(map[string]*models.Permission),
|
||||
PolicyCache: make(map[int][]models.PolicyAttribute),
|
||||
CacheMutex: &sync.RWMutex{},
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
ctx := &models.AuthorizationContext{
|
||||
UserID: "user123",
|
||||
Resource: "nonexistent",
|
||||
Action: "read",
|
||||
}
|
||||
|
||||
result, err := AuthorizeWithCache(service, ctx)
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if result.Allowed {
|
||||
t.Error("Expected access denied")
|
||||
}
|
||||
if result.Message != "Permission not found" {
|
||||
t.Errorf("Expected 'Permission not found', got '%s'", result.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidateUserCache(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
}
|
||||
|
||||
service.UserAttrCache["user123"] = map[string]string{"test": "value"}
|
||||
|
||||
InvalidateUserCache(service, "user123")
|
||||
|
||||
if _, exists := service.UserAttrCache["user123"]; exists {
|
||||
t.Error("Expected user cache to be invalidated")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCacheStats(t *testing.T) {
|
||||
service := &models.CachedAuthorizationService{
|
||||
PermissionCache: make(map[string]*models.Permission),
|
||||
PolicyCache: make(map[int][]models.PolicyAttribute),
|
||||
UserAttrCache: make(map[string]map[string]string),
|
||||
CacheMutex: &sync.RWMutex{},
|
||||
UserAttrMutex: &sync.RWMutex{},
|
||||
LastCacheRefresh: time.Now().Add(-10 * time.Second),
|
||||
}
|
||||
|
||||
service.PermissionCache["doc:read"] = &models.Permission{ID: 1}
|
||||
service.PermissionCache["doc:write"] = &models.Permission{ID: 2}
|
||||
service.PolicyCache[1] = []models.PolicyAttribute{{ID: 1}}
|
||||
service.UserAttrCache["user1"] = map[string]string{"dept": "eng"}
|
||||
|
||||
stats := GetCacheStats(service)
|
||||
|
||||
if stats["permissions_cached"] != 2 {
|
||||
t.Errorf("Expected 2 permissions cached, got %v", stats["permissions_cached"])
|
||||
}
|
||||
if stats["policies_cached"] != 1 {
|
||||
t.Errorf("Expected 1 policy cached, got %v", stats["policies_cached"])
|
||||
}
|
||||
if stats["user_attributes_cached"] != 1 {
|
||||
t.Errorf("Expected 1 user attribute cached, got %v", stats["user_attributes_cached"])
|
||||
}
|
||||
|
||||
cacheAge := stats["cache_age_seconds"].(float64)
|
||||
if cacheAge < 9 || cacheAge > 12 {
|
||||
t.Errorf("Expected cache age around 10 seconds, got %v", cacheAge)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"authorization/models"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResolveVariables(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
ctx *models.AuthorizationContext
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "resolves user attribute",
|
||||
value: "${user.department}",
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{"department": "Engineering"},
|
||||
},
|
||||
expected: "Engineering",
|
||||
},
|
||||
{
|
||||
name: "resolves resource attribute",
|
||||
value: "${resource.owner}",
|
||||
ctx: &models.AuthorizationContext{
|
||||
ResourceData: map[string]string{"owner": "user123"},
|
||||
},
|
||||
expected: "user123",
|
||||
},
|
||||
{
|
||||
name: "resolves environment attribute",
|
||||
value: "${environment.time}",
|
||||
ctx: &models.AuthorizationContext{
|
||||
Environment: map[string]string{"time": "12:00"},
|
||||
},
|
||||
expected: "12:00",
|
||||
},
|
||||
{
|
||||
name: "resolves multiple variables",
|
||||
value: "${user.name} from ${user.department}",
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{
|
||||
"name": "John",
|
||||
"department": "IT",
|
||||
},
|
||||
},
|
||||
expected: "John from IT",
|
||||
},
|
||||
{
|
||||
name: "leaves unresolved variables unchanged",
|
||||
value: "${user.nonexistent}",
|
||||
ctx: &models.AuthorizationContext{UserAttributes: map[string]string{}},
|
||||
expected: "${user.nonexistent}",
|
||||
},
|
||||
{
|
||||
name: "handles no variables",
|
||||
value: "plain text",
|
||||
ctx: &models.AuthorizationContext{},
|
||||
expected: "plain text",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolveVariables(tt.value, tt.ctx)
|
||||
if got != tt.expected {
|
||||
t.Errorf("resolveVariables() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare_Equality(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
operator string
|
||||
want bool
|
||||
}{
|
||||
{"equal strings", "admin", "admin", "=", true},
|
||||
{"not equal strings", "admin", "user", "=", false},
|
||||
{"not equal operator true", "admin", "user", "!=", true},
|
||||
{"not equal operator false", "admin", "admin", "!=", false},
|
||||
{"equal with whitespace", " admin ", "admin", "=", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compare(tt.actual, tt.expected, tt.operator)
|
||||
if got != tt.want {
|
||||
t.Errorf("compare(%q, %q, %q) = %v, want %v", tt.actual, tt.expected, tt.operator, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare_Numeric(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
operator string
|
||||
want bool
|
||||
}{
|
||||
{"greater than true", "10", "5", ">", true},
|
||||
{"greater than false", "5", "10", ">", false},
|
||||
{"less than true", "5", "10", "<", true},
|
||||
{"less than false", "10", "5", "<", false},
|
||||
{"greater or equal true equal", "10", "10", ">=", true},
|
||||
{"greater or equal true greater", "11", "10", ">=", true},
|
||||
{"greater or equal false", "9", "10", ">=", false},
|
||||
{"less or equal true equal", "10", "10", "<=", true},
|
||||
{"less or equal true less", "9", "10", "<=", true},
|
||||
{"less or equal false", "11", "10", "<=", false},
|
||||
{"invalid number returns false", "abc", "10", ">", false},
|
||||
{"float comparison", "10.5", "10.2", ">", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compare(tt.actual, tt.expected, tt.operator)
|
||||
if got != tt.want {
|
||||
t.Errorf("compare(%q, %q, %q) = %v, want %v", tt.actual, tt.expected, tt.operator, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare_IN(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
want bool
|
||||
}{
|
||||
{"value in list", "admin", "admin,user,guest", true},
|
||||
{"value not in list", "superuser", "admin,user,guest", false},
|
||||
{"value in list with spaces", "admin", " admin , user , guest ", true},
|
||||
{"case insensitive match", "ADMIN", "admin,user,guest", true},
|
||||
{"single value match", "admin", "admin", true},
|
||||
{"empty list", "admin", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compare(tt.actual, tt.expected, "IN")
|
||||
if got != tt.want {
|
||||
t.Errorf("compare(%q, %q, IN) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare_StringOperations(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
operator string
|
||||
want bool
|
||||
}{
|
||||
{"contains true", "hello world", "world", "CONTAINS", true},
|
||||
{"contains false", "hello world", "xyz", "CONTAINS", false},
|
||||
{"contains case insensitive", "Hello World", "WORLD", "CONTAINS", true},
|
||||
{"starts with true", "hello world", "hello", "STARTS_WITH", true},
|
||||
{"starts with false", "hello world", "world", "STARTS_WITH", false},
|
||||
{"starts with case insensitive", "Hello World", "HELLO", "STARTS_WITH", true},
|
||||
{"ends with true", "hello world", "world", "ENDS_WITH", true},
|
||||
{"ends with false", "hello world", "hello", "ENDS_WITH", false},
|
||||
{"ends with case insensitive", "Hello World", "WORLD", "ENDS_WITH", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := compare(tt.actual, tt.expected, tt.operator)
|
||||
if got != tt.want {
|
||||
t.Errorf("compare(%q, %q, %q) = %v, want %v", tt.actual, tt.expected, tt.operator, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare_UnknownOperator(t *testing.T) {
|
||||
got := compare("value", "value", "UNKNOWN")
|
||||
if got != false {
|
||||
t.Errorf("compare with unknown operator should return false, got %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumericCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
compareFn func(float64, float64) bool
|
||||
want bool
|
||||
}{
|
||||
{
|
||||
name: "valid numbers greater",
|
||||
actual: "10",
|
||||
expected: "5",
|
||||
compareFn: func(a, e float64) bool { return a > e },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "valid numbers less",
|
||||
actual: "5",
|
||||
expected: "10",
|
||||
compareFn: func(a, e float64) bool { return a < e },
|
||||
want: true,
|
||||
},
|
||||
{
|
||||
name: "invalid actual number",
|
||||
actual: "abc",
|
||||
expected: "10",
|
||||
compareFn: func(a, e float64) bool { return a > e },
|
||||
want: false,
|
||||
},
|
||||
{
|
||||
name: "invalid expected number",
|
||||
actual: "10",
|
||||
expected: "xyz",
|
||||
compareFn: func(a, e float64) bool { return a > e },
|
||||
want: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := numericCompare(tt.actual, tt.expected, tt.compareFn)
|
||||
if got != tt.want {
|
||||
t.Errorf("numericCompare(%q, %q) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInComparison(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
actual string
|
||||
expected string
|
||||
want bool
|
||||
}{
|
||||
{"match in list", "admin", "admin,user,guest", true},
|
||||
{"no match in list", "superuser", "admin,user,guest", false},
|
||||
{"case insensitive", "ADMIN", "admin,user", true},
|
||||
{"with whitespace", " admin ", " admin , user ", true},
|
||||
{"single item match", "admin", "admin", true},
|
||||
{"empty expected", "admin", "", false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := inComparison(tt.actual, tt.expected)
|
||||
if got != tt.want {
|
||||
t.Errorf("inComparison(%q, %q) = %v, want %v", tt.actual, tt.expected, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePolicy(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policy models.PolicyAttribute
|
||||
ctx *models.AuthorizationContext
|
||||
wantSatisfied bool
|
||||
wantReasonEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "user attribute satisfied",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "user",
|
||||
AttributeName: "department",
|
||||
Comparison: "=",
|
||||
AttributeValue: "Engineering",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{"department": "Engineering"},
|
||||
},
|
||||
wantSatisfied: true,
|
||||
wantReasonEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "user attribute not satisfied",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "user",
|
||||
AttributeName: "department",
|
||||
Comparison: "=",
|
||||
AttributeValue: "HR",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{"department": "Engineering"},
|
||||
},
|
||||
wantSatisfied: false,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "user attribute not found",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "user",
|
||||
AttributeName: "nonexistent",
|
||||
Comparison: "=",
|
||||
AttributeValue: "value",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{},
|
||||
},
|
||||
wantSatisfied: false,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "resource attribute satisfied",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "resource",
|
||||
AttributeName: "owner",
|
||||
Comparison: "=",
|
||||
AttributeValue: "user123",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
ResourceData: map[string]string{"owner": "user123"},
|
||||
},
|
||||
wantSatisfied: true,
|
||||
wantReasonEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "environment attribute satisfied",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "environment",
|
||||
AttributeName: "location",
|
||||
Comparison: "=",
|
||||
AttributeValue: "US",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
Environment: map[string]string{"location": "US"},
|
||||
},
|
||||
wantSatisfied: true,
|
||||
wantReasonEmpty: true,
|
||||
},
|
||||
{
|
||||
name: "unknown attribute type",
|
||||
policy: models.PolicyAttribute{
|
||||
AttributeType: "unknown",
|
||||
AttributeName: "attr",
|
||||
Comparison: "=",
|
||||
AttributeValue: "value",
|
||||
},
|
||||
ctx: &models.AuthorizationContext{},
|
||||
wantSatisfied: false,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
satisfied, reason := evaluatePolicy(tt.policy, tt.ctx)
|
||||
|
||||
if satisfied != tt.wantSatisfied {
|
||||
t.Errorf("evaluatePolicy() satisfied = %v, want %v", satisfied, tt.wantSatisfied)
|
||||
}
|
||||
|
||||
if tt.wantReasonEmpty && reason != "" {
|
||||
t.Errorf("evaluatePolicy() reason = %q, want empty", reason)
|
||||
}
|
||||
|
||||
if !tt.wantReasonEmpty && reason == "" {
|
||||
t.Errorf("evaluatePolicy() reason is empty, want non-empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvaluatePolicies(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
policies []models.PolicyAttribute
|
||||
ctx *models.AuthorizationContext
|
||||
wantSatisfied bool
|
||||
wantReasonEmpty bool
|
||||
}{
|
||||
{
|
||||
name: "no policies returns true",
|
||||
policies: []models.PolicyAttribute{},
|
||||
ctx: &models.AuthorizationContext{},
|
||||
wantSatisfied: true,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "all policies satisfied",
|
||||
policies: []models.PolicyAttribute{
|
||||
{
|
||||
AttributeType: "user",
|
||||
AttributeName: "department",
|
||||
Comparison: "=",
|
||||
AttributeValue: "Engineering",
|
||||
},
|
||||
{
|
||||
AttributeType: "user",
|
||||
AttributeName: "level",
|
||||
Comparison: ">=",
|
||||
AttributeValue: "3",
|
||||
},
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{
|
||||
"department": "Engineering",
|
||||
"level": "5",
|
||||
},
|
||||
},
|
||||
wantSatisfied: true,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
{
|
||||
name: "one policy fails",
|
||||
policies: []models.PolicyAttribute{
|
||||
{
|
||||
AttributeType: "user",
|
||||
AttributeName: "department",
|
||||
Comparison: "=",
|
||||
AttributeValue: "Engineering",
|
||||
},
|
||||
{
|
||||
AttributeType: "user",
|
||||
AttributeName: "level",
|
||||
Comparison: ">=",
|
||||
AttributeValue: "5",
|
||||
},
|
||||
},
|
||||
ctx: &models.AuthorizationContext{
|
||||
UserAttributes: map[string]string{
|
||||
"department": "Engineering",
|
||||
"level": "3",
|
||||
},
|
||||
},
|
||||
wantSatisfied: false,
|
||||
wantReasonEmpty: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
satisfied, reason := EvaluatePolicies(tt.policies, tt.ctx)
|
||||
|
||||
if satisfied != tt.wantSatisfied {
|
||||
t.Errorf("EvaluatePolicies() satisfied = %v, want %v", satisfied, tt.wantSatisfied)
|
||||
}
|
||||
|
||||
if tt.wantReasonEmpty && reason != "" {
|
||||
t.Errorf("EvaluatePolicies() reason = %q, want empty", reason)
|
||||
}
|
||||
|
||||
if !tt.wantReasonEmpty && reason == "" {
|
||||
t.Errorf("EvaluatePolicies() reason is empty, want non-empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user