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.
- checksums.yaml +4 -4
- data/README.md +84 -233
- data/lib/decision_agent/ab_testing/ab_test.rb +197 -0
- data/lib/decision_agent/ab_testing/ab_test_assignment.rb +76 -0
- data/lib/decision_agent/ab_testing/ab_test_manager.rb +317 -0
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +188 -0
- data/lib/decision_agent/ab_testing/storage/activerecord_adapter.rb +155 -0
- data/lib/decision_agent/ab_testing/storage/adapter.rb +67 -0
- data/lib/decision_agent/ab_testing/storage/memory_adapter.rb +116 -0
- data/lib/decision_agent/agent.rb +5 -3
- data/lib/decision_agent/auth/access_audit_logger.rb +122 -0
- data/lib/decision_agent/auth/authenticator.rb +127 -0
- data/lib/decision_agent/auth/password_reset_manager.rb +57 -0
- data/lib/decision_agent/auth/password_reset_token.rb +33 -0
- data/lib/decision_agent/auth/permission.rb +29 -0
- data/lib/decision_agent/auth/permission_checker.rb +43 -0
- data/lib/decision_agent/auth/rbac_adapter.rb +278 -0
- data/lib/decision_agent/auth/rbac_config.rb +51 -0
- data/lib/decision_agent/auth/role.rb +56 -0
- data/lib/decision_agent/auth/session.rb +33 -0
- data/lib/decision_agent/auth/session_manager.rb +57 -0
- data/lib/decision_agent/auth/user.rb +70 -0
- data/lib/decision_agent/context.rb +24 -4
- data/lib/decision_agent/decision.rb +10 -3
- data/lib/decision_agent/dsl/condition_evaluator.rb +378 -1
- data/lib/decision_agent/dsl/schema_validator.rb +8 -1
- data/lib/decision_agent/errors.rb +38 -0
- data/lib/decision_agent/evaluation.rb +10 -3
- data/lib/decision_agent/evaluation_validator.rb +8 -13
- data/lib/decision_agent/monitoring/dashboard_server.rb +1 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +164 -7
- data/lib/decision_agent/monitoring/storage/activerecord_adapter.rb +253 -0
- data/lib/decision_agent/monitoring/storage/base_adapter.rb +90 -0
- data/lib/decision_agent/monitoring/storage/memory_adapter.rb +222 -0
- data/lib/decision_agent/testing/batch_test_importer.rb +373 -0
- data/lib/decision_agent/testing/batch_test_runner.rb +244 -0
- data/lib/decision_agent/testing/test_coverage_analyzer.rb +191 -0
- data/lib/decision_agent/testing/test_result_comparator.rb +235 -0
- data/lib/decision_agent/testing/test_scenario.rb +42 -0
- data/lib/decision_agent/version.rb +10 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +1 -1
- data/lib/decision_agent/versioning/file_storage_adapter.rb +96 -28
- data/lib/decision_agent/web/middleware/auth_middleware.rb +45 -0
- data/lib/decision_agent/web/middleware/permission_middleware.rb +94 -0
- data/lib/decision_agent/web/public/app.js +184 -29
- data/lib/decision_agent/web/public/batch_testing.html +640 -0
- data/lib/decision_agent/web/public/index.html +37 -9
- data/lib/decision_agent/web/public/login.html +298 -0
- data/lib/decision_agent/web/public/users.html +679 -0
- data/lib/decision_agent/web/server.rb +873 -7
- data/lib/decision_agent.rb +59 -0
- data/lib/generators/decision_agent/install/install_generator.rb +37 -0
- data/lib/generators/decision_agent/install/templates/ab_test_assignment_model.rb +45 -0
- data/lib/generators/decision_agent/install/templates/ab_test_model.rb +54 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_migration.rb +43 -0
- data/lib/generators/decision_agent/install/templates/ab_testing_tasks.rake +189 -0
- data/lib/generators/decision_agent/install/templates/decision_agent_tasks.rake +114 -0
- data/lib/generators/decision_agent/install/templates/decision_log.rb +57 -0
- data/lib/generators/decision_agent/install/templates/error_metric.rb +53 -0
- data/lib/generators/decision_agent/install/templates/evaluation_metric.rb +43 -0
- data/lib/generators/decision_agent/install/templates/monitoring_migration.rb +109 -0
- data/lib/generators/decision_agent/install/templates/performance_metric.rb +76 -0
- data/lib/generators/decision_agent/install/templates/rule_version.rb +1 -1
- data/spec/ab_testing/ab_test_assignment_spec.rb +253 -0
- data/spec/ab_testing/ab_test_manager_spec.rb +612 -0
- data/spec/ab_testing/ab_test_spec.rb +270 -0
- data/spec/ab_testing/ab_testing_agent_spec.rb +481 -0
- data/spec/ab_testing/storage/adapter_spec.rb +64 -0
- data/spec/ab_testing/storage/memory_adapter_spec.rb +485 -0
- data/spec/advanced_operators_spec.rb +1003 -0
- data/spec/agent_spec.rb +40 -0
- data/spec/audit_adapters_spec.rb +18 -0
- data/spec/auth/access_audit_logger_spec.rb +394 -0
- data/spec/auth/authenticator_spec.rb +112 -0
- data/spec/auth/password_reset_spec.rb +294 -0
- data/spec/auth/permission_checker_spec.rb +207 -0
- data/spec/auth/permission_spec.rb +73 -0
- data/spec/auth/rbac_adapter_spec.rb +550 -0
- data/spec/auth/rbac_config_spec.rb +82 -0
- data/spec/auth/role_spec.rb +51 -0
- data/spec/auth/session_manager_spec.rb +172 -0
- data/spec/auth/session_spec.rb +112 -0
- data/spec/auth/user_spec.rb +130 -0
- data/spec/context_spec.rb +43 -0
- data/spec/decision_agent_spec.rb +96 -0
- data/spec/decision_spec.rb +423 -0
- data/spec/dsl/condition_evaluator_spec.rb +774 -0
- data/spec/evaluation_spec.rb +364 -0
- data/spec/evaluation_validator_spec.rb +165 -0
- data/spec/examples.txt +1542 -548
- data/spec/issue_verification_spec.rb +95 -21
- data/spec/monitoring/metrics_collector_spec.rb +221 -3
- data/spec/monitoring/monitored_agent_spec.rb +1 -1
- data/spec/monitoring/prometheus_exporter_spec.rb +1 -1
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +498 -0
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -0
- data/spec/monitoring/storage/memory_adapter_spec.rb +247 -0
- data/spec/performance_optimizations_spec.rb +486 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/testing/batch_test_importer_spec.rb +693 -0
- data/spec/testing/batch_test_runner_spec.rb +307 -0
- data/spec/testing/test_coverage_analyzer_spec.rb +292 -0
- data/spec/testing/test_result_comparator_spec.rb +392 -0
- data/spec/testing/test_scenario_spec.rb +113 -0
- data/spec/versioning/adapter_spec.rb +156 -0
- data/spec/versioning_spec.rb +253 -0
- data/spec/web/middleware/auth_middleware_spec.rb +133 -0
- data/spec/web/middleware/permission_middleware_spec.rb +247 -0
- data/spec/web_ui_rack_spec.rb +1705 -0
- 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
|