Files
Authorization/services/policy_evaluator_test.go
T

667 lines
17 KiB
Go

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)
}
}
}