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") } }) } } // Additional comprehensive test cases func TestResolveVariables_EdgeCases(t *testing.T) { testCases := []struct { name string value string ctx *models.AuthorizationContext expected string }{ { "Empty string", "", &models.AuthorizationContext{}, "", }, { "No variables", "plain text", &models.AuthorizationContext{}, "plain text", }, { "Missing attribute", "${user.missing}", &models.AuthorizationContext{UserAttributes: map[string]string{}}, "", }, { "Nil context", "${user.name}", nil, "", }, { "Nested braces", "${{user.name}}", &models.AuthorizationContext{UserAttributes: map[string]string{"name": "John"}}, "${John}", }, { "Multiple same variable", "${user.name} and ${user.name}", &models.AuthorizationContext{UserAttributes: map[string]string{"name": "John"}}, "John and John", }, { "Special characters in value", "${user.special}", &models.AuthorizationContext{UserAttributes: map[string]string{"special": "<>&\"'"}}, "<>&\"'", }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := resolveVariables(tc.value, tc.ctx) if result != tc.expected { t.Errorf("resolveVariables(%q) = %q, want %q", tc.value, result, tc.expected) } }) } } func TestCompare_CaseSensitivity(t *testing.T) { testCases := []struct { name string operator string left string right string expected bool }{ {"Equals case sensitive", "equals", "Test", "test", false}, {"Equals same case", "equals", "Test", "Test", true}, {"Not equals case", "not_equals", "Test", "test", true}, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { result := compare(tc.operator, tc.left, tc.right) if result != tc.expected { t.Errorf("compare(%q, %q, %q) = %v, want %v", tc.operator, tc.left, tc.right, result, tc.expected) } }) } } func TestCompare_EmptyStrings(t *testing.T) { testCases := []struct { operator string left string right string expected bool }{ {"equals", "", "", true}, {"equals", "", "value", false}, {"not_equals", "", "", false}, {"not_equals", "", "value", true}, {"contains", "", "test", false}, {"contains", "test", "", true}, } for _, tc := range testCases { t.Run(tc.operator, func(t *testing.T) { result := compare(tc.operator, tc.left, tc.right) if result != tc.expected { t.Errorf("compare(%q, %q, %q) = %v, want %v", tc.operator, tc.left, tc.right, result, tc.expected) } }) } } // 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 TestEvaluatePolicies_NilContext(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 TestEvaluatePolicies_EmptyPoliciesList(t *testing.T) { ctx := &models.AuthorizationContext{ UserAttributes: map[string]string{"department": "IT"}, } satisfied, reason := EvaluatePolicies([]models.PolicyAttribute{}, ctx) if !satisfied { t.Error("EvaluatePolicies should return true for empty policies list") } if reason != "" { t.Errorf("Expected empty reason, got %q", reason) } } func TestEvaluatePolicies_ComplexConditions(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", Comparison: "equals", AttributeValue: "IT"}, {AttributeName: "level", Comparison: "gte", AttributeValue: "3"}, {AttributeName: "location", 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 TestResolveVariables_AllAttributeTypes(t *testing.T) { ctx := &models.AuthorizationContext{ UserID: "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) } } }