package services import ( "authorization/db" "authorization/models" "errors" "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 != 5*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 uess_user_management.user_attributes WHERE users_id = \\?"). WithArgs("user123", "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{}, } // Note: Permissions are no longer preloaded - they're cached on-demand // Only policies are preloaded during cache refresh // Mock policy attributes query policyRows := sqlmock.NewRows([]string{"policy_attributes_id", "attribute_name", "attribute_type", "comparison", "attribute_value", "permission_id"}). AddRow(1, "department", "user", "=", "engineering", 1). AddRow(2, "region", "user", "=", "01", 2) mock.ExpectQuery("SELECT policy_attributes_id, attribute_name, attribute_type, comparison, attribute_value, permission_id FROM policy_attributes ORDER BY permission_id, policy_attributes_id"). WillReturnRows(policyRows) refreshCache(service) // Verify permissions cache is empty (lazy loading) if len(service.PermissionCache) != 0 { t.Errorf("Expected 0 permissions in cache (lazy loading), 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])) } if len(service.PolicyCache[2]) != 1 { t.Errorf("Expected 1 policy for permission 2, got %d", len(service.PolicyCache[2])) } } 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 with role-aware key (roleID:resource:action) service.PermissionCache["1:document:read"] = &models.Permission{ ID: 1, PermissionName: "read_document", Resource: "document", Action: "read", } // Add empty policies service.PolicyCache[1] = []models.PolicyAttribute{} // Mock user query userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}). AddRow("user123", "john@example.com", 1, "0") mock.ExpectQuery("SELECT users_id, email_address"). WithArgs("user123"). WillReturnRows(userRows) // Mock user attributes query attrRows := sqlmock.NewRows([]string{"attribute_name", "attribute_value"}). AddRow("department", "engineering") mock.ExpectQuery("SELECT attribute_name, attribute_value FROM uess_user_management.user_attributes WHERE users_id = \\?"). WithArgs("user123", "user123"). WillReturnRows(attrRows) ctx := &models.AuthorizationContext{ UsersID: "user123", Resource: "document", Action: "read", RoleID: 1, 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) { mock, cleanup := setupMockDBForCached(t) defer cleanup() service := &models.CachedAuthorizationService{ PermissionCache: make(map[string]*models.Permission), PolicyCache: make(map[int][]models.PolicyAttribute), CacheMutex: &sync.RWMutex{}, UserAttrMutex: &sync.RWMutex{}, } ctx := &models.AuthorizationContext{ UsersID: "user123", Resource: "nonexistent", Action: "read", RoleID: 1, } // Mock user query userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}). AddRow("user123", "john@example.com", 1, "0") mock.ExpectQuery("SELECT users_id, email_address"). WithArgs("user123"). WillReturnRows(userRows) // Permission not in cache, so will query DB and fail mock.ExpectQuery("SELECT p.permissions_id, p.permission_name, p.description, p.resource, p.action FROM uess_user_management.permissions p INNER JOIN uess_user_management.role_permissions rp"). WithArgs("nonexistent", "read", 1). WillReturnError(errors.New("permission not found")) 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 granted to your role" { t.Errorf("Expected 'Permission not granted to your role', got '%s'", result.Message) } } func TestAuthorizeWithCache_DeletedUserRedirectsToLogin(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{}, } userRows := sqlmock.NewRows([]string{"users_id", "email_address", "role_id", "is_deleted"}). AddRow("deleted-user", "deleted@example.com", 1, "1") mock.ExpectQuery("SELECT users_id, email_address, role_id, is_deleted"). WithArgs("deleted-user"). WillReturnRows(userRows) ctx := &models.AuthorizationContext{ UsersID: "deleted-user", Resource: "document", Action: "read", RoleID: 1, } 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.RedirectRoute != "/login" { t.Errorf("Expected redirect to /login, got %q", result.RedirectRoute) } } 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) } }