decision_agent 0.1.3 → 0.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +84 -233
  3. data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
  4. data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
  5. data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
  6. data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
  7. data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
  8. data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
  9. data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
  10. data/lib/decision_agent/agent.rb +5 -3
  11. data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
  12. data/lib/decision_agent/auth/authenticator.rb +127 -0
  13. data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
  14. data/lib/decision_agent/auth/password_reset_token.rb +33 -0
  15. data/lib/decision_agent/auth/permission.rb +29 -0
  16. data/lib/decision_agent/auth/permission_checker.rb +43 -0
  17. data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
  18. data/lib/decision_agent/auth/rbac_config.rb +51 -0
  19. data/lib/decision_agent/auth/role.rb +56 -0
  20. data/lib/decision_agent/auth/session.rb +33 -0
  21. data/lib/decision_agent/auth/session_manager.rb +57 -0
  22. data/lib/decision_agent/auth/user.rb +70 -0
  23. data/lib/decision_agent/context.rb +24 -4
  24. data/lib/decision_agent/decision.rb +10 -3
  25. data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
  26. data/lib/decision_agent/dsl/schema_validator.rb +8 -1
  27. data/lib/decision_agent/errors.rb +38 -0
  28. data/lib/decision_agent/evaluation.rb +10 -3
  29. data/lib/decision_agent/evaluation_validator.rb +8 -13
  30. data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
  31. data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
  32. data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
  33. data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
  34. data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
  35. data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
  36. data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
  37. data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
  38. data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
  39. data/lib/decision_agent/testing/test_scenario.rb +42 -0
  40. data/lib/decision_agent/version.rb +10 -1
  41. data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
  42. data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
  43. data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
  44. data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
  45. data/lib/decision_agent/web/public/app.js +184 -29
  46. data/lib/decision_agent/web/public/batch_testing.html +640 -0
  47. data/lib/decision_agent/web/public/index.html +37 -9
  48. data/lib/decision_agent/web/public/login.html +298 -0
  49. data/lib/decision_agent/web/public/users.html +679 -0
  50. data/lib/decision_agent/web/server.rb +873 -7
  51. data/lib/decision_agent.rb +59 -0
  52. data/lib/generators/decision_agent/install/install_generator.rb +37 -0
  53. data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
  54. data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
  55. data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
  56. data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
  57. data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
  58. data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
  59. data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
  60. data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
  61. data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
  62. data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
  63. data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
  64. data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
  65. data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
  66. data/spec/ab_testing/ab_test_spec.rb +270 -0
  67. data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
  68. data/spec/ab_testing/storage/adapter_spec.rb +64 -0
  69. data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
  70. data/spec/advanced_operators_spec.rb +1003 -0
  71. data/spec/agent_spec.rb +40 -0
  72. data/spec/audit_adapters_spec.rb +18 -0
  73. data/spec/auth/access_audit_logger_spec.rb +394 -0
  74. data/spec/auth/authenticator_spec.rb +112 -0
  75. data/spec/auth/password_reset_spec.rb +294 -0
  76. data/spec/auth/permission_checker_spec.rb +207 -0
  77. data/spec/auth/permission_spec.rb +73 -0
  78. data/spec/auth/rbac_adapter_spec.rb +550 -0
  79. data/spec/auth/rbac_config_spec.rb +82 -0
  80. data/spec/auth/role_spec.rb +51 -0
  81. data/spec/auth/session_manager_spec.rb +172 -0
  82. data/spec/auth/session_spec.rb +112 -0
  83. data/spec/auth/user_spec.rb +130 -0
  84. data/spec/context_spec.rb +43 -0
  85. data/spec/decision_agent_spec.rb +96 -0
  86. data/spec/decision_spec.rb +423 -0
  87. data/spec/dsl/condition_evaluator_spec.rb +774 -0
  88. data/spec/evaluation_spec.rb +364 -0
  89. data/spec/evaluation_validator_spec.rb +165 -0
  90. data/spec/examples.txt +1542 -548
  91. data/spec/issue_verification_spec.rb +95 -21
  92. data/spec/monitoring/metrics_collector_spec.rb +221 -3
  93. data/spec/monitoring/monitored_agent_spec.rb +1 -1
  94. data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
  95. data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
  96. data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
  97. data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
  98. data/spec/performance_optimizations_spec.rb +486 -0
  99. data/spec/spec_helper.rb +23 -0
  100. data/spec/testing/batch_test_importer_spec.rb +693 -0
  101. data/spec/testing/batch_test_runner_spec.rb +307 -0
  102. data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
  103. data/spec/testing/test_result_comparator_spec.rb +392 -0
  104. data/spec/testing/test_scenario_spec.rb +113 -0
  105. data/spec/versioning/adapter_spec.rb +156 -0
  106. data/spec/versioning_spec.rb +253 -0
  107. data/spec/web/middleware/auth_middleware_spec.rb +133 -0
  108. data/spec/web/middleware/permission_middleware_spec.rb +247 -0
  109. data/spec/web_ui_rack_spec.rb +1705 -0
  110. metadata +123 -6
@@ -0,0 +1,172 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::Auth::SessionManager do
4
+ let(:manager) { DecisionAgent::Auth::SessionManager.new }
5
+
6
+ describe "#initialize" do
7
+ it "initializes with empty sessions" do
8
+ expect(manager.count).to eq(0)
9
+ end
10
+ end
11
+
12
+ describe "#create_session" do
13
+ it "creates a new session" do
14
+ session = manager.create_session("user123")
15
+ expect(session).to be_a(DecisionAgent::Auth::Session)
16
+ expect(session.user_id).to eq("user123")
17
+ end
18
+
19
+ it "stores the session" do
20
+ session = manager.create_session("user123")
21
+ retrieved = manager.get_session(session.token)
22
+ expect(retrieved).to eq(session)
23
+ end
24
+
25
+ it "accepts custom expiration time" do
26
+ session = manager.create_session("user123", expires_in: 7200)
27
+ expect(session.expires_at).to be > Time.now.utc + 7100
28
+ end
29
+ end
30
+
31
+ describe "#get_session" do
32
+ it "returns session for valid token" do
33
+ session = manager.create_session("user123")
34
+ retrieved = manager.get_session(session.token)
35
+ expect(retrieved).to eq(session)
36
+ end
37
+
38
+ it "returns nil for invalid token" do
39
+ expect(manager.get_session("invalid_token")).to be_nil
40
+ end
41
+
42
+ it "returns nil for expired session" do
43
+ session = manager.create_session("user123", expires_in: -1)
44
+ expect(manager.get_session(session.token)).to be_nil
45
+ end
46
+ end
47
+
48
+ describe "#delete_session" do
49
+ it "deletes a session" do
50
+ session = manager.create_session("user123")
51
+ manager.delete_session(session.token)
52
+ expect(manager.get_session(session.token)).to be_nil
53
+ end
54
+
55
+ it "does not raise error for non-existent session" do
56
+ expect { manager.delete_session("nonexistent") }.not_to raise_error
57
+ end
58
+ end
59
+
60
+ describe "#delete_user_sessions" do
61
+ it "deletes all sessions for a user" do
62
+ session1 = manager.create_session("user123")
63
+ session2 = manager.create_session("user123")
64
+ session3 = manager.create_session("user456")
65
+
66
+ manager.delete_user_sessions("user123")
67
+
68
+ expect(manager.get_session(session1.token)).to be_nil
69
+ expect(manager.get_session(session2.token)).to be_nil
70
+ expect(manager.get_session(session3.token)).to eq(session3) # Other user's session still exists
71
+ end
72
+
73
+ it "does not raise error for user with no sessions" do
74
+ expect { manager.delete_user_sessions("nonexistent") }.not_to raise_error
75
+ end
76
+ end
77
+
78
+ describe "#cleanup_expired_sessions" do
79
+ it "removes expired sessions" do
80
+ # Create expired session
81
+ expired_session = manager.create_session("user123", expires_in: -1)
82
+ # Create valid session
83
+ valid_session = manager.create_session("user456", expires_in: 3600)
84
+
85
+ expect(manager.count).to eq(2)
86
+
87
+ # Force cleanup by setting last_cleanup far in the past and creating another session
88
+ manager.instance_variable_set(:@last_cleanup, Time.now - 400) # More than 300 seconds
89
+ manager.create_session("user789", expires_in: 3600)
90
+
91
+ # Expired session should be cleaned up
92
+ expect(manager.get_session(expired_session.token)).to be_nil
93
+ expect(manager.get_session(valid_session.token)).to eq(valid_session)
94
+ end
95
+
96
+ it "only runs cleanup after cleanup_interval" do
97
+ manager = DecisionAgent::Auth::SessionManager.new
98
+
99
+ # Create an expired session
100
+ expired_session = manager.create_session("user123", expires_in: -1)
101
+ expect(manager.count).to eq(1)
102
+
103
+ # The cleanup_expired_sessions is called during create_session, but it checks the interval
104
+ # Let's test by manually setting last_cleanup to far in the past
105
+ manager.instance_variable_set(:@last_cleanup, Time.now - 400) # More than 300 seconds
106
+ manager.create_session("user456", expires_in: 3600)
107
+
108
+ # The expired session should be cleaned up now
109
+ expect(manager.get_session(expired_session.token)).to be_nil
110
+ end
111
+ end
112
+
113
+ describe "#count" do
114
+ it "returns zero initially" do
115
+ expect(manager.count).to eq(0)
116
+ end
117
+
118
+ it "returns correct count after creating sessions" do
119
+ manager.create_session("user123")
120
+ expect(manager.count).to eq(1)
121
+
122
+ manager.create_session("user123")
123
+ expect(manager.count).to eq(2)
124
+
125
+ manager.create_session("user456")
126
+ expect(manager.count).to eq(3)
127
+ end
128
+
129
+ it "reflects deletions" do
130
+ session1 = manager.create_session("user123")
131
+ manager.create_session("user123")
132
+ expect(manager.count).to eq(2)
133
+
134
+ manager.delete_session(session1.token)
135
+ expect(manager.count).to eq(1)
136
+
137
+ manager.delete_user_sessions("user123")
138
+ expect(manager.count).to eq(0)
139
+ end
140
+
141
+ it "does not count expired sessions" do
142
+ manager.create_session("user123", expires_in: -1)
143
+ # Expired sessions are not counted when retrieved, but are still in storage until cleanup
144
+ # So count will include them until cleanup runs
145
+ expect(manager.count).to eq(1)
146
+
147
+ # After cleanup
148
+ manager.instance_variable_set(:@last_cleanup, Time.now - 400)
149
+ manager.create_session("user456", expires_in: 3600)
150
+ # Now expired session should be cleaned up
151
+ expect(manager.count).to eq(1)
152
+ end
153
+ end
154
+
155
+ describe "thread safety" do
156
+ it "handles concurrent access safely" do
157
+ threads = []
158
+ 10.times do
159
+ threads << Thread.new do
160
+ 10.times do
161
+ session = manager.create_session("user#{rand(100)}")
162
+ manager.get_session(session.token)
163
+ manager.count
164
+ end
165
+ end
166
+ end
167
+
168
+ threads.each(&:join)
169
+ expect(manager.count).to eq(100)
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,112 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::Auth::Session do
4
+ describe "#initialize" do
5
+ it "creates a session with user_id" do
6
+ session = DecisionAgent::Auth::Session.new(user_id: "user123")
7
+ expect(session.user_id).to eq("user123")
8
+ expect(session.token).to be_a(String)
9
+ expect(session.token.length).to eq(64) # 32 bytes hex = 64 chars
10
+ end
11
+
12
+ it "sets expiration time" do
13
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 1800)
14
+ expect(session.expires_at).to be > Time.now.utc
15
+ expect(session.expires_at).to be <= Time.now.utc + 1801
16
+ end
17
+
18
+ it "uses default expiration of 3600 seconds" do
19
+ session = DecisionAgent::Auth::Session.new(user_id: "user123")
20
+ expected_expiry = session.created_at + 3600
21
+ expect(session.expires_at).to be_within(1).of(expected_expiry)
22
+ end
23
+ end
24
+
25
+ describe "#expired?" do
26
+ it "returns false for valid session" do
27
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 3600)
28
+ expect(session.expired?).to be false
29
+ end
30
+
31
+ it "returns true for expired session" do
32
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: -1)
33
+ expect(session.expired?).to be true
34
+ end
35
+ end
36
+
37
+ describe "#valid?" do
38
+ it "returns true for non-expired session" do
39
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 3600)
40
+ expect(session.valid?).to be true
41
+ end
42
+
43
+ it "returns false for expired session" do
44
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: -1)
45
+ expect(session.valid?).to be false
46
+ end
47
+
48
+ it "returns false when expired? returns true" do
49
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: -1)
50
+ expect(session.expired?).to be true
51
+ expect(session.valid?).to be false
52
+ end
53
+
54
+ it "returns true when expired? returns false" do
55
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 3600)
56
+ expect(session.expired?).to be false
57
+ expect(session.valid?).to be true
58
+ end
59
+ end
60
+
61
+ describe "#to_h" do
62
+ it "returns hash with all session attributes" do
63
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 3600)
64
+ hash = session.to_h
65
+
66
+ expect(hash).to be_a(Hash)
67
+ expect(hash[:token]).to eq(session.token)
68
+ expect(hash[:user_id]).to eq("user123")
69
+ expect(hash[:created_at]).to be_a(String)
70
+ expect(hash[:expires_at]).to be_a(String)
71
+ end
72
+
73
+ it "serializes timestamps as ISO8601 strings" do
74
+ session = DecisionAgent::Auth::Session.new(user_id: "user123")
75
+ hash = session.to_h
76
+
77
+ expect { Time.iso8601(hash[:created_at]) }.not_to raise_error
78
+ expect { Time.iso8601(hash[:expires_at]) }.not_to raise_error
79
+ end
80
+
81
+ it "includes correct user_id" do
82
+ session = DecisionAgent::Auth::Session.new(user_id: "user456")
83
+ hash = session.to_h
84
+ expect(hash[:user_id]).to eq("user456")
85
+ end
86
+
87
+ it "includes correct token" do
88
+ session = DecisionAgent::Auth::Session.new(user_id: "user123")
89
+ hash = session.to_h
90
+ expect(hash[:token]).to eq(session.token)
91
+ end
92
+ end
93
+
94
+ describe "#created_at" do
95
+ it "is set to current UTC time" do
96
+ before = Time.now.utc
97
+ session = DecisionAgent::Auth::Session.new(user_id: "user123")
98
+ after = Time.now.utc
99
+
100
+ expect(session.created_at).to be >= before
101
+ expect(session.created_at).to be <= after
102
+ end
103
+ end
104
+
105
+ describe "#expires_at" do
106
+ it "is set based on expires_in parameter" do
107
+ session = DecisionAgent::Auth::Session.new(user_id: "user123", expires_in: 7200)
108
+ expected = session.created_at + 7200
109
+ expect(session.expires_at).to be_within(1).of(expected)
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,130 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent::Auth::User do
4
+ describe "#initialize" do
5
+ it "creates a user with email and password" do
6
+ user = DecisionAgent::Auth::User.new(
7
+ email: "test@example.com",
8
+ password: "password123"
9
+ )
10
+
11
+ expect(user.email).to eq("test@example.com")
12
+ expect(user.id).to be_a(String)
13
+ expect(user.active).to be true
14
+ expect(user.roles).to eq([])
15
+ end
16
+
17
+ it "creates a user with roles" do
18
+ user = DecisionAgent::Auth::User.new(
19
+ email: "admin@example.com",
20
+ password: "password123",
21
+ roles: %i[admin editor]
22
+ )
23
+
24
+ expect(user.roles).to include(:admin, :editor)
25
+ end
26
+
27
+ it "raises error if neither password nor password_hash provided" do
28
+ expect do
29
+ DecisionAgent::Auth::User.new(email: "test@example.com")
30
+ end.to raise_error(ArgumentError, /password/)
31
+ end
32
+ end
33
+
34
+ describe "#authenticate" do
35
+ it "returns true for correct password" do
36
+ user = DecisionAgent::Auth::User.new(
37
+ email: "test@example.com",
38
+ password: "password123"
39
+ )
40
+
41
+ expect(user.authenticate("password123")).to be true
42
+ end
43
+
44
+ it "returns false for incorrect password" do
45
+ user = DecisionAgent::Auth::User.new(
46
+ email: "test@example.com",
47
+ password: "password123"
48
+ )
49
+
50
+ expect(user.authenticate("wrongpassword")).to be false
51
+ end
52
+
53
+ it "returns false for inactive user" do
54
+ user = DecisionAgent::Auth::User.new(
55
+ email: "test@example.com",
56
+ password: "password123",
57
+ active: false
58
+ )
59
+
60
+ expect(user.authenticate("password123")).to be false
61
+ end
62
+ end
63
+
64
+ describe "#assign_role" do
65
+ it "adds a role to the user" do
66
+ user = DecisionAgent::Auth::User.new(
67
+ email: "test@example.com",
68
+ password: "password123"
69
+ )
70
+
71
+ user.assign_role(:editor)
72
+ expect(user.roles).to include(:editor)
73
+ end
74
+
75
+ it "does not add duplicate roles" do
76
+ user = DecisionAgent::Auth::User.new(
77
+ email: "test@example.com",
78
+ password: "password123"
79
+ )
80
+
81
+ user.assign_role(:editor)
82
+ user.assign_role(:editor)
83
+
84
+ expect(user.roles.count(:editor)).to eq(1)
85
+ end
86
+ end
87
+
88
+ describe "#remove_role" do
89
+ it "removes a role from the user" do
90
+ user = DecisionAgent::Auth::User.new(
91
+ email: "test@example.com",
92
+ password: "password123",
93
+ roles: %i[editor viewer]
94
+ )
95
+
96
+ user.remove_role(:editor)
97
+ expect(user.roles).not_to include(:editor)
98
+ expect(user.roles).to include(:viewer)
99
+ end
100
+ end
101
+
102
+ describe "#has_role?" do
103
+ it "returns true if user has the role" do
104
+ user = DecisionAgent::Auth::User.new(
105
+ email: "test@example.com",
106
+ password: "password123",
107
+ roles: [:editor]
108
+ )
109
+
110
+ expect(user.has_role?(:editor)).to be true
111
+ expect(user.has_role?(:admin)).to be false
112
+ end
113
+ end
114
+
115
+ describe "#to_h" do
116
+ it "returns a hash representation of the user" do
117
+ user = DecisionAgent::Auth::User.new(
118
+ email: "test@example.com",
119
+ password: "password123",
120
+ roles: [:editor]
121
+ )
122
+
123
+ hash = user.to_h
124
+ expect(hash[:email]).to eq("test@example.com")
125
+ expect(hash[:roles]).to eq(["editor"])
126
+ expect(hash[:active]).to be true
127
+ expect(hash[:id]).to be_a(String)
128
+ end
129
+ end
130
+ end
data/spec/context_spec.rb CHANGED
@@ -22,6 +22,49 @@ RSpec.describe DecisionAgent::Context do
22
22
  expect(context.to_h[:user]).to be_frozen
23
23
  expect(context.to_h[:user][:roles]).to be_frozen
24
24
  end
25
+
26
+ it "creates a copy before freezing to avoid mutating original data" do
27
+ original_data = { user: { name: "alice", roles: ["admin"] } }
28
+ original_data_id = original_data.object_id
29
+
30
+ context = DecisionAgent::Context.new(original_data)
31
+
32
+ # Should create a copy (different object_id) to avoid mutating original
33
+ expect(context.to_h.object_id).not_to eq(original_data_id)
34
+ expect(context.to_h).to be_frozen
35
+ expect(context.to_h[:user]).to be_frozen
36
+ # Original data should not be frozen
37
+ expect(original_data).not_to be_frozen
38
+ end
39
+
40
+ it "skips already frozen objects in deep_freeze" do
41
+ frozen_data = { user: { name: "alice", roles: ["admin"] } }
42
+ frozen_data.freeze
43
+ frozen_data[:user].freeze
44
+
45
+ context = DecisionAgent::Context.new(frozen_data)
46
+
47
+ expect(context.to_h).to be_frozen
48
+ expect(context.to_h[:user]).to be_frozen
49
+ end
50
+
51
+ it "does not freeze hash keys unnecessarily" do
52
+ key_symbol = :test_key
53
+ key_string = "test_key"
54
+ data = {
55
+ key_symbol => "value1",
56
+ key_string => "value2"
57
+ }
58
+
59
+ context = DecisionAgent::Context.new(data)
60
+
61
+ # Keys should not be frozen (they're typically symbols/strings that don't need freezing)
62
+ expect(context.to_h.keys.first).to eq(key_symbol)
63
+ expect(context.to_h.keys.last).to eq(key_string)
64
+ # Values should be frozen
65
+ expect(context.to_h[key_symbol]).to be_frozen
66
+ expect(context.to_h[key_string]).to be_frozen
67
+ end
25
68
  end
26
69
 
27
70
  describe "#[]" do
@@ -0,0 +1,96 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe DecisionAgent do
4
+ before do
5
+ # Reset permission_checker between tests to avoid leakage
6
+ DecisionAgent.permission_checker = nil
7
+ end
8
+
9
+ describe ".rbac_config" do
10
+ it "returns the global RBAC configuration" do
11
+ expect(DecisionAgent.rbac_config).to be_a(DecisionAgent::Auth::RbacConfig)
12
+ end
13
+ end
14
+
15
+ describe ".configure_rbac" do
16
+ context "with adapter_type and options" do
17
+ it "configures RBAC with adapter type" do
18
+ result = DecisionAgent.configure_rbac(:default)
19
+ expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
20
+ expect(DecisionAgent.rbac_config.adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
21
+ end
22
+
23
+ it "configures RBAC with custom options" do
24
+ result = DecisionAgent.configure_rbac(:custom,
25
+ can_proc: ->(_user, _permission, _resource) { true },
26
+ has_role_proc: ->(_user, _role) { false },
27
+ active_proc: ->(_user) { true })
28
+ expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
29
+ end
30
+ end
31
+
32
+ context "with block" do
33
+ it "yields the config block" do
34
+ # Now test setting a custom adapter via block
35
+ custom_adapter = DecisionAgent::Auth::DefaultAdapter.new
36
+ result = DecisionAgent.configure_rbac do |config|
37
+ config.adapter = custom_adapter
38
+ end
39
+
40
+ expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
41
+ expect(DecisionAgent.rbac_config.adapter).to eq(custom_adapter)
42
+ end
43
+ end
44
+
45
+ context "with no arguments" do
46
+ it "returns the rbac_config" do
47
+ result = DecisionAgent.configure_rbac
48
+ expect(result).to be_a(DecisionAgent::Auth::RbacConfig)
49
+ end
50
+ end
51
+ end
52
+
53
+ describe ".permission_checker" do
54
+ it "returns a PermissionChecker instance" do
55
+ checker = DecisionAgent.permission_checker
56
+ expect(checker).to be_a(DecisionAgent::Auth::PermissionChecker)
57
+ end
58
+
59
+ it "creates a new PermissionChecker if not set" do
60
+ # Reset permission_checker
61
+ DecisionAgent.permission_checker = nil
62
+ checker = DecisionAgent.permission_checker
63
+ expect(checker).to be_a(DecisionAgent::Auth::PermissionChecker)
64
+ end
65
+
66
+ it "returns the same instance on subsequent calls" do
67
+ checker1 = DecisionAgent.permission_checker
68
+ checker2 = DecisionAgent.permission_checker
69
+ expect(checker1).to eq(checker2)
70
+ end
71
+
72
+ it "uses the rbac_config adapter" do
73
+ DecisionAgent.configure_rbac(:default)
74
+ checker = DecisionAgent.permission_checker
75
+ adapter = checker.instance_variable_get(:@adapter)
76
+ expect(adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
77
+ expect(DecisionAgent.rbac_config.adapter).to be_a(DecisionAgent::Auth::RbacAdapter)
78
+ end
79
+ end
80
+
81
+ describe ".permission_checker=" do
82
+ it "sets a custom permission checker" do
83
+ custom_checker = double("CustomChecker")
84
+ DecisionAgent.permission_checker = custom_checker
85
+ expect(DecisionAgent.permission_checker).to eq(custom_checker)
86
+ end
87
+
88
+ it "overrides the default permission checker" do
89
+ original_checker = DecisionAgent.permission_checker
90
+ custom_checker = double("CustomChecker")
91
+ DecisionAgent.permission_checker = custom_checker
92
+ expect(DecisionAgent.permission_checker).not_to eq(original_checker)
93
+ expect(DecisionAgent.permission_checker).to eq(custom_checker)
94
+ end
95
+ end
96
+ end