package services import ( "authorization/models" "strings" "testing" ) const ( testCompareFormat = "compare(%q, %q, %q) = %v, want %v" testAdminUserGuest = "admin,user,guest" testHelloWorld = "hello world" testHelloWorldCased = "Hello World" ) 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 TestCompareEquality(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(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want) } }) } } func TestCompareNumeric(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(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want) } }) } } func TestCompareIN(t *testing.T) { tests := []struct { name string actual string expected string want bool }{ {"value in list", "admin", testAdminUserGuest, true}, {"value not in list", "superuser", testAdminUserGuest, false}, {"value in list with spaces", "admin", " admin , user , guest ", true}, {"case insensitive match", "ADMIN", testAdminUserGuest, 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 TestCompareStringOperations(t *testing.T) { tests := []struct { name string actual string expected string operator string want bool }{ {"contains true", testHelloWorld, "world", "CONTAINS", true}, {"contains false", testHelloWorld, "xyz", "CONTAINS", false}, {"contains case insensitive", testHelloWorldCased, "WORLD", "CONTAINS", true}, {"starts with true", testHelloWorld, "hello", "STARTS_WITH", true}, {"starts with false", testHelloWorld, "world", "STARTS_WITH", false}, {"starts with case insensitive", testHelloWorldCased, "HELLO", "STARTS_WITH", true}, {"ends with true", testHelloWorld, "world", "ENDS_WITH", true}, {"ends with false", testHelloWorld, "hello", "ENDS_WITH", false}, {"ends with case insensitive", testHelloWorldCased, "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(testCompareFormat, tt.actual, tt.expected, tt.operator, got, tt.want) } }) } } func TestCompareUnknownOperator(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", testAdminUserGuest, true}, {"no match in list", "superuser", testAdminUserGuest, 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") } }) } } // TestResolveVariablesEdgeCases tests variable resolution indirectly through EvaluatePolicies func TestResolveVariablesEdgeCases(t *testing.T) { // Instead of testing the private function directly, test it through EvaluatePolicies testCases := []struct { name string policy models.PolicyAttribute ctx *models.AuthorizationContext expectedResult bool }{ { name: "Empty string attribute", policy: models.PolicyAttribute{ AttributeName: "empty", AttributeType: "user", Comparison: "=", AttributeValue: "", }, ctx: &models.AuthorizationContext{ UserAttributes: map[string]string{"empty": ""}, }, expectedResult: true, }, { name: "Missing attribute", policy: models.PolicyAttribute{ AttributeName: "missing", AttributeType: "user", Comparison: "=", AttributeValue: "value", }, ctx: &models.AuthorizationContext{ UserAttributes: map[string]string{}, }, expectedResult: false, }, { name: "Special characters in value", policy: models.PolicyAttribute{ AttributeName: "special", AttributeType: "user", Comparison: "=", AttributeValue: "<>&\"'", }, ctx: &models.AuthorizationContext{ UserAttributes: map[string]string{"special": "<>&\"'"}, }, expectedResult: true, }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result, _ := EvaluatePolicies([]models.PolicyAttribute{tc.policy}, tc.ctx) if result != tc.expectedResult { t.Errorf("EvaluatePolicies() = %v, want %v", result, tc.expectedResult) } }) } } // TestCompareCaseSensitivity tests comparison through EvaluatePolicies func TestCompareCaseSensitivity(t *testing.T) { testCases := []struct { name string attributeValue string userAttrValue string operator string expected bool }{ {"Equals case sensitive", "Test", "test", "=", false}, {"Equals same case", "Test", "Test", "=", true}, {"Not equals case", "Test", "test", "!=", true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { policy := models.PolicyAttribute{ AttributeName: "value", AttributeType: "user", Comparison: tc.operator, AttributeValue: tc.attributeValue, } ctx := &models.AuthorizationContext{ UserAttributes: map[string]string{"value": tc.userAttrValue}, } result, _ := EvaluatePolicies([]models.PolicyAttribute{policy}, ctx) if result != tc.expected { t.Errorf("comparison(%q, %q, %q) = %v, want %v", tc.operator, tc.userAttrValue, tc.attributeValue, result, tc.expected) } }) } } // TestCompareEmptyStrings tests empty string comparisons through EvaluatePolicies func TestCompareEmptyStrings(t *testing.T) { testCases := []struct { name string operator string userValue string expectedValue string expectedResult bool }{ {"equals both empty", "=", "", "", true}, {"equals one empty", "=", "", "value", false}, {"not_equals both empty", "!=", "", "", false}, {"not_equals one empty", "!=", "", "value", true}, {"contains value in empty", "CONTAINS", "", "test", false}, {"contains empty in value", "CONTAINS", "test", "", true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { policy := models.PolicyAttribute{ AttributeName: "value", AttributeType: "user", Comparison: tc.operator, AttributeValue: tc.expectedValue, } ctx := &models.AuthorizationContext{ UserAttributes: map[string]string{"value": tc.userValue}, } result, _ := EvaluatePolicies([]models.PolicyAttribute{policy}, ctx) if result != tc.expectedResult { t.Errorf("comparison(%q, %q, %q) = %v, want %v", tc.operator, tc.userValue, tc.expectedValue, result, tc.expectedResult) } }) } } // Note: Tests for numericCompare removed as it's an internal function. // It's tested indirectly through public Compare and EvaluatePolicies functions. // Note: Tests for inComparison removed as it's an internal function. // It's tested indirectly through public Compare and Evaluate Policies functions. func TestEvaluatePoliciesNilContext(t *testing.T) { policies := []models.PolicyAttribute{ {AttributeName: "department", Comparison: "equals", AttributeValue: "IT"}, } satisfied, _ := EvaluatePolicies(policies, nil) if satisfied { t.Error("EvaluatePolicies should return false for nil context") } } func TestEvaluatePoliciesEmptyPoliciesList(t *testing.T) { ctx := &models.AuthorizationContext{ UserAttributes: map[string]string{"department": "IT"}, } satisfied, _ := EvaluatePolicies([]models.PolicyAttribute{}, ctx) if !satisfied { t.Error("EvaluatePolicies should return true for empty policies list") } // Note: The function returns "No policies to evaluate" as the reason even when successful } func TestEvaluatePoliciesComplexConditions(t *testing.T) { ctx := &models.AuthorizationContext{ UserAttributes: map[string]string{ "department": "IT", "level": "5", "location": "US", }, ResourceData: map[string]string{ "classification": "public", }, Environment: map[string]string{ "time": "14:00", }, } policies := []models.PolicyAttribute{ {AttributeName: "department", AttributeType: "user", Comparison: "=", AttributeValue: "IT"}, {AttributeName: "level", AttributeType: "user", Comparison: ">=", AttributeValue: "3"}, {AttributeName: "location", AttributeType: "user", Comparison: "IN", AttributeValue: "US,UK,CA"}, } satisfied, reason := EvaluatePolicies(policies, ctx) if !satisfied { t.Errorf("EvaluatePolicies should satisfy all conditions, reason: %s", reason) } } // Note: Tests for compare removed as it's an internal function. // It's tested indirectly through public EvaluatePolicies functions. func TestResolveVariablesAllAttributeTypes(t *testing.T) { ctx := &models.AuthorizationContext{ UsersID: "user123", Resource: "document", Action: "read", UserAttributes: map[string]string{ "dept": "IT", }, ResourceData: map[string]string{ "owner": "user456", }, Environment: map[string]string{ "ip": "192.168.1.1", }, } testCases := []struct { input string expected string }{ {"User: ${user.dept}", "User: IT"}, {"Resource: ${resource.owner}", "Resource: user456"}, {"Env: ${environment.ip}", "Env: 192.168.1.1"}, {"Mixed: ${user.dept} ${resource.owner}", "Mixed: IT user456"}, } for _, tc := range testCases { result := resolveVariables(tc.input, ctx) if result != tc.expected { t.Errorf("resolveVariables(%q) = %q, want %q", tc.input, result, tc.expected) } } } func TestEvaluatePolicies_UserRegionMatchesResourceRegion(t *testing.T) { // Test case from the logs: user.region = ${resource.region} // User region should match the resource region provided in ResourceData tests := []struct { name string userRegion string resourceRegion string shouldBeAllowed bool description string }{ { name: "same region", userRegion: "01", resourceRegion: "01", shouldBeAllowed: true, description: "user region matches resource region", }, { name: "different region", userRegion: "02", resourceRegion: "01", shouldBeAllowed: false, description: "user region does not match resource region", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := &models.AuthorizationContext{ UsersID: "U0000000001", Resource: "personnel", Action: "assign_role", UserAttributes: map[string]string{ "region": tt.userRegion, }, ResourceData: map[string]string{ "region": tt.resourceRegion, }, } policies := []models.PolicyAttribute{ { AttributeType: "user", AttributeName: "region", Comparison: "=", AttributeValue: "${resource.region}", }, } satisfied, reason := EvaluatePolicies(policies, ctx) if satisfied != tt.shouldBeAllowed { t.Errorf("%s: got satisfied=%v, want %v. Reason: %s", tt.description, satisfied, tt.shouldBeAllowed, reason) } }) } } func TestEvaluatePolicies_MissingResourceAttribute(t *testing.T) { // Test case: when ResourceData doesn't have the required attribute // The policy should fail because the placeholder cannot be resolved ctx := &models.AuthorizationContext{ UsersID: "U0000000001", Resource: "personnel", Action: "assign_role", UserAttributes: map[string]string{ "region": "01", }, ResourceData: map[string]string{ // Missing "region" key - this should cause policy to fail }, } policies := []models.PolicyAttribute{ { AttributeType: "user", AttributeName: "region", Comparison: "=", AttributeValue: "${resource.region}", }, } satisfied, reason := EvaluatePolicies(policies, ctx) // When resource attribute is missing, the placeholder stays unresolved // "01" != "${resource.region}", so policy fails if satisfied { t.Errorf("EvaluatePolicies should fail when resource attribute is missing. Reason: %s", reason) } if reason == "" { t.Error("EvaluatePolicies should provide a reason when policy fails") } // Check that the error message indicates missing attributes if !strings.Contains(reason, "Missing required attributes") || !strings.Contains(reason, "${resource.region}") { t.Errorf("Expected error message about missing resource.region, got: %s", reason) } } func TestHasUnresolvedPlaceholders(t *testing.T) { tests := []struct { name string value string expected bool }{ { name: "has unresolved placeholder", value: "${resource.region}", expected: true, }, { name: "multiple unresolved placeholders", value: "${resource.region} and ${user.department}", expected: true, }, { name: "no unresolved placeholders", value: "US", expected: false, }, { name: "mixed resolved and unresolved", value: "US and ${resource.owner}", expected: true, }, { name: "empty string", value: "", expected: false, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := hasUnresolvedPlaceholders(tt.value) if result != tt.expected { t.Errorf("hasUnresolvedPlaceholders(%q) = %v, want %v", tt.value, result, tt.expected) } }) } } func TestExtractUnresolvedPlaceholders(t *testing.T) { tests := []struct { name string value string expected []string }{ { name: "single placeholder", value: "${resource.region}", expected: []string{"${resource.region}"}, }, { name: "multiple placeholders", value: "${resource.region} and ${user.department}", expected: []string{"${resource.region}", "${user.department}"}, }, { name: "no placeholders", value: "US", expected: []string{}, }, { name: "empty list when no matches", value: "", expected: []string{}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := extractUnresolvedPlaceholders(tt.value) if len(result) != len(tt.expected) { t.Errorf("extractUnresolvedPlaceholders(%q) returned %d items, want %d", tt.value, len(result), len(tt.expected)) } for i, expected := range tt.expected { if i >= len(result) || result[i] != expected { t.Errorf("extractUnresolvedPlaceholders(%q)[%d] = %q, want %q", tt.value, i, result[i], expected) } } }) } } func TestEvaluatePolicies_RegionBypassForAdminRoles(t *testing.T) { // Test that region policies are skipped for roleID 1 (Super Admin) and 2 (Admin) tests := []struct { name string roleID int userRegion string resourceRegion string shouldBeAllowed bool description string }{ { name: "roleID 1 bypasses region check", roleID: 1, userRegion: "02", resourceRegion: "01", shouldBeAllowed: true, description: "Super Admin should bypass region check", }, { name: "roleID 2 bypasses region check", roleID: 2, userRegion: "03", resourceRegion: "01", shouldBeAllowed: true, description: "Admin should bypass region check", }, { name: "other roleID respects region check", roleID: 3, userRegion: "02", resourceRegion: "01", shouldBeAllowed: false, description: "Non-admin roles should not bypass region check", }, { name: "Super Admin role bypasses region check", roleID: 1, userRegion: "02", resourceRegion: "01", shouldBeAllowed: true, description: "Super Admin role string should bypass region check", }, { name: "Admin role bypasses region check", roleID: 2, userRegion: "03", resourceRegion: "01", shouldBeAllowed: true, description: "Admin role should bypass region check", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ctx := &models.AuthorizationContext{ UsersID: "U0000000001", Resource: "personnel", Action: "assign_role", RoleID: tt.roleID, UserAttributes: map[string]string{ "region": tt.userRegion, }, ResourceData: map[string]string{ "region": tt.resourceRegion, }, } policies := []models.PolicyAttribute{ { AttributeType: "user", AttributeName: "region", Comparison: "=", AttributeValue: "${resource.region}", }, } satisfied, reason := EvaluatePolicies(policies, ctx) if satisfied != tt.shouldBeAllowed { t.Errorf("%s: got satisfied=%v, want %v. Reason: %s", tt.description, satisfied, tt.shouldBeAllowed, reason) } }) } }