decision_agent 0.1.4 → 0.1.7
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 +83 -232
- data/bin/decision_agent +1 -1
- data/lib/decision_agent/ab_testing/ab_testing_agent.rb +46 -10
- 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 +17 -5
- 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 +38 -10
- 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 +52 -0
- data/lib/generators/decision_agent/install/templates/README +1 -1
- 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 +282 -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/monitoring/metrics_collector_spec.rb +220 -2
- data/spec/monitoring/storage/activerecord_adapter_spec.rb +153 -1
- data/spec/monitoring/storage/base_adapter_spec.rb +61 -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 +103 -11
- data/spec/examples.txt +0 -612
data/spec/agent_spec.rb
CHANGED
|
@@ -48,6 +48,46 @@ RSpec.describe DecisionAgent::Agent do
|
|
|
48
48
|
expect(agent.scoring_strategy).to be_a(DecisionAgent::Scoring::WeightedAverage)
|
|
49
49
|
expect(agent.audit_adapter).to be_a(DecisionAgent::Audit::NullAdapter)
|
|
50
50
|
end
|
|
51
|
+
|
|
52
|
+
it "enables validation by default in non-production environments" do
|
|
53
|
+
evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
|
|
54
|
+
original_env = ENV.fetch("RAILS_ENV", nil)
|
|
55
|
+
ENV["RAILS_ENV"] = "development"
|
|
56
|
+
|
|
57
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
58
|
+
# Validation should be enabled (we can't directly test this, but we can test behavior)
|
|
59
|
+
# If validation is enabled, invalid evaluations would raise errors
|
|
60
|
+
expect(agent).to be_a(DecisionAgent::Agent)
|
|
61
|
+
|
|
62
|
+
ENV["RAILS_ENV"] = original_env
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "disables validation in production by default" do
|
|
66
|
+
evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
|
|
67
|
+
original_env = ENV.fetch("RAILS_ENV", nil)
|
|
68
|
+
ENV["RAILS_ENV"] = "production"
|
|
69
|
+
|
|
70
|
+
agent = DecisionAgent::Agent.new(evaluators: [evaluator])
|
|
71
|
+
expect(agent).to be_a(DecisionAgent::Agent)
|
|
72
|
+
|
|
73
|
+
ENV["RAILS_ENV"] = original_env
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
it "allows explicit validation control" do
|
|
77
|
+
evaluator = DecisionAgent::Evaluators::StaticEvaluator.new(decision: "approve")
|
|
78
|
+
|
|
79
|
+
agent_with_validation = DecisionAgent::Agent.new(
|
|
80
|
+
evaluators: [evaluator],
|
|
81
|
+
validate_evaluations: true
|
|
82
|
+
)
|
|
83
|
+
expect(agent_with_validation).to be_a(DecisionAgent::Agent)
|
|
84
|
+
|
|
85
|
+
agent_without_validation = DecisionAgent::Agent.new(
|
|
86
|
+
evaluators: [evaluator],
|
|
87
|
+
validate_evaluations: false
|
|
88
|
+
)
|
|
89
|
+
expect(agent_without_validation).to be_a(DecisionAgent::Agent)
|
|
90
|
+
end
|
|
51
91
|
end
|
|
52
92
|
|
|
53
93
|
describe "#decide" do
|
data/spec/audit_adapters_spec.rb
CHANGED
|
@@ -1,6 +1,24 @@
|
|
|
1
1
|
require "spec_helper"
|
|
2
2
|
|
|
3
3
|
RSpec.describe "Audit Adapters" do
|
|
4
|
+
describe DecisionAgent::Audit::Adapter do
|
|
5
|
+
it "raises NotImplementedError when record is called" do
|
|
6
|
+
adapter = DecisionAgent::Audit::Adapter.new
|
|
7
|
+
decision = DecisionAgent::Decision.new(
|
|
8
|
+
decision: "approve",
|
|
9
|
+
confidence: 0.8,
|
|
10
|
+
explanations: [],
|
|
11
|
+
evaluations: [],
|
|
12
|
+
audit_payload: {}
|
|
13
|
+
)
|
|
14
|
+
context = DecisionAgent::Context.new({ user: "alice" })
|
|
15
|
+
|
|
16
|
+
expect do
|
|
17
|
+
adapter.record(decision, context)
|
|
18
|
+
end.to raise_error(NotImplementedError, /Subclasses must implement #record/)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
4
22
|
describe DecisionAgent::Audit::NullAdapter do
|
|
5
23
|
it "implements record method without side effects" do
|
|
6
24
|
adapter = DecisionAgent::Audit::NullAdapter.new
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe DecisionAgent::Auth::AccessAuditLogger do
|
|
4
|
+
let(:adapter) { DecisionAgent::Audit::InMemoryAccessAdapter.new }
|
|
5
|
+
let(:logger) { DecisionAgent::Auth::AccessAuditLogger.new(adapter: adapter) }
|
|
6
|
+
|
|
7
|
+
describe "#log_authentication" do
|
|
8
|
+
it "logs successful login" do
|
|
9
|
+
logger.log_authentication(
|
|
10
|
+
"login",
|
|
11
|
+
user_id: "user123",
|
|
12
|
+
email: "test@example.com",
|
|
13
|
+
success: true
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
logs = adapter.all_logs
|
|
17
|
+
expect(logs.size).to eq(1)
|
|
18
|
+
expect(logs.first[:event_type]).to eq("login")
|
|
19
|
+
expect(logs.first[:user_id]).to eq("user123")
|
|
20
|
+
expect(logs.first[:success]).to be true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it "logs failed login" do
|
|
24
|
+
logger.log_authentication(
|
|
25
|
+
"login",
|
|
26
|
+
user_id: nil,
|
|
27
|
+
email: "test@example.com",
|
|
28
|
+
success: false,
|
|
29
|
+
reason: "Invalid password"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
logs = adapter.all_logs
|
|
33
|
+
expect(logs.size).to eq(1)
|
|
34
|
+
expect(logs.first[:success]).to be false
|
|
35
|
+
expect(logs.first[:reason]).to eq("Invalid password")
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
describe "#log_permission_check" do
|
|
40
|
+
it "logs permission check" do
|
|
41
|
+
logger.log_permission_check(
|
|
42
|
+
user_id: "user123",
|
|
43
|
+
permission: :write,
|
|
44
|
+
resource_type: "rule",
|
|
45
|
+
resource_id: "rule456",
|
|
46
|
+
granted: true
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
logs = adapter.all_logs
|
|
50
|
+
expect(logs.size).to eq(1)
|
|
51
|
+
expect(logs.first[:event_type]).to eq("permission_check")
|
|
52
|
+
expect(logs.first[:permission]).to eq("write")
|
|
53
|
+
expect(logs.first[:granted]).to be true
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
describe "#log_access" do
|
|
58
|
+
it "logs access event" do
|
|
59
|
+
logger.log_access(
|
|
60
|
+
user_id: "user123",
|
|
61
|
+
action: "create",
|
|
62
|
+
resource_type: "rule",
|
|
63
|
+
resource_id: "rule456",
|
|
64
|
+
success: true
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
logs = adapter.all_logs
|
|
68
|
+
expect(logs.size).to eq(1)
|
|
69
|
+
expect(logs.first[:event_type]).to eq("access")
|
|
70
|
+
expect(logs.first[:action]).to eq("create")
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
describe "#query" do
|
|
75
|
+
before do
|
|
76
|
+
logger.log_authentication("login", user_id: "user1", email: "user1@example.com", success: true)
|
|
77
|
+
logger.log_authentication("login", user_id: "user2", email: "user2@example.com", success: true)
|
|
78
|
+
logger.log_permission_check(user_id: "user1", permission: :write, granted: true)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
it "filters by user_id" do
|
|
82
|
+
logs = logger.query(user_id: "user1")
|
|
83
|
+
expect(logs.size).to eq(2)
|
|
84
|
+
expect(logs.all? { |log| log[:user_id] == "user1" }).to be true
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "filters by event_type" do
|
|
88
|
+
logs = logger.query(event_type: "login")
|
|
89
|
+
expect(logs.size).to eq(2)
|
|
90
|
+
expect(logs.all? { |log| log[:event_type] == "login" }).to be true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "filters by start_time" do
|
|
94
|
+
start_time = Time.now.utc - 3600
|
|
95
|
+
logger.log_authentication("login", user_id: "user3", email: "user3@example.com", success: true)
|
|
96
|
+
|
|
97
|
+
logs = logger.query(start_time: start_time)
|
|
98
|
+
expect(logs.size).to be >= 1
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "limits results" do
|
|
102
|
+
logs = logger.query(limit: 2)
|
|
103
|
+
expect(logs.size).to eq(2)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
it "filters by end_time" do
|
|
107
|
+
end_time = Time.now.utc + 3600
|
|
108
|
+
logs = logger.query(end_time: end_time)
|
|
109
|
+
expect(logs.size).to eq(3) # All logs are before end_time
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
it "filters by start_time and end_time together" do
|
|
113
|
+
start_time = Time.now.utc - 1800
|
|
114
|
+
end_time = Time.now.utc + 1800
|
|
115
|
+
logs = logger.query(start_time: start_time, end_time: end_time)
|
|
116
|
+
expect(logs.size).to be >= 0
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "handles string timestamps" do
|
|
120
|
+
start_time = (Time.now.utc - 3600).iso8601
|
|
121
|
+
logs = logger.query(start_time: start_time)
|
|
122
|
+
expect(logs).to be_an(Array)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
it "returns logs in reverse order (most recent first)" do
|
|
126
|
+
# Clear any existing logs first
|
|
127
|
+
adapter.clear
|
|
128
|
+
|
|
129
|
+
logger.log_authentication("test1", user_id: "user1", email: "user1@example.com", success: true)
|
|
130
|
+
sleep(0.01) # Ensure different timestamps
|
|
131
|
+
logger.log_authentication("test2", user_id: "user1", email: "user1@example.com", success: true)
|
|
132
|
+
|
|
133
|
+
logs = logger.query(user_id: "user1")
|
|
134
|
+
expect(logs.size).to eq(2)
|
|
135
|
+
expect(logs.first[:event_type]).to eq("test2")
|
|
136
|
+
expect(logs.last[:event_type]).to eq("test1")
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
describe "#log_authentication" do
|
|
141
|
+
it "includes timestamp in log entry" do
|
|
142
|
+
logger.log_authentication("login", user_id: "user1", email: "user1@example.com", success: true)
|
|
143
|
+
logs = adapter.all_logs
|
|
144
|
+
expect(logs.first[:timestamp]).to be_a(String)
|
|
145
|
+
expect { Time.parse(logs.first[:timestamp]) }.not_to raise_error
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "includes ip_address field (nil by default)" do
|
|
149
|
+
logger.log_authentication("login", user_id: "user1", email: "user1@example.com", success: true)
|
|
150
|
+
logs = adapter.all_logs
|
|
151
|
+
expect(logs.first[:ip_address]).to be_nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
it "converts event_type to string" do
|
|
155
|
+
logger.log_authentication(:login, user_id: "user1", email: "user1@example.com", success: true)
|
|
156
|
+
logs = adapter.all_logs
|
|
157
|
+
expect(logs.first[:event_type]).to eq("login")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
describe "#log_permission_check" do
|
|
162
|
+
it "includes all fields in log entry" do
|
|
163
|
+
logger.log_permission_check(
|
|
164
|
+
user_id: "user123",
|
|
165
|
+
permission: :write,
|
|
166
|
+
resource_type: "rule",
|
|
167
|
+
resource_id: "rule456",
|
|
168
|
+
granted: false
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
logs = adapter.all_logs
|
|
172
|
+
log = logs.first
|
|
173
|
+
expect(log[:event_type]).to eq("permission_check")
|
|
174
|
+
expect(log[:user_id]).to eq("user123")
|
|
175
|
+
expect(log[:permission]).to eq("write")
|
|
176
|
+
expect(log[:resource_type]).to eq("rule")
|
|
177
|
+
expect(log[:resource_id]).to eq("rule456")
|
|
178
|
+
expect(log[:granted]).to be false
|
|
179
|
+
expect(log[:timestamp]).to be_a(String)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
it "handles nil resource_type and resource_id" do
|
|
183
|
+
logger.log_permission_check(
|
|
184
|
+
user_id: "user123",
|
|
185
|
+
permission: :read,
|
|
186
|
+
granted: true
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
logs = adapter.all_logs
|
|
190
|
+
log = logs.first
|
|
191
|
+
expect(log[:resource_type]).to be_nil
|
|
192
|
+
expect(log[:resource_id]).to be_nil
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
describe "#log_access" do
|
|
197
|
+
it "includes all fields in log entry" do
|
|
198
|
+
logger.log_access(
|
|
199
|
+
user_id: "user123",
|
|
200
|
+
action: "delete",
|
|
201
|
+
resource_type: "version",
|
|
202
|
+
resource_id: "version789",
|
|
203
|
+
success: false
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
logs = adapter.all_logs
|
|
207
|
+
log = logs.first
|
|
208
|
+
expect(log[:event_type]).to eq("access")
|
|
209
|
+
expect(log[:user_id]).to eq("user123")
|
|
210
|
+
expect(log[:action]).to eq("delete")
|
|
211
|
+
expect(log[:resource_type]).to eq("version")
|
|
212
|
+
expect(log[:resource_id]).to eq("version789")
|
|
213
|
+
expect(log[:success]).to be false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
it "converts action to string" do
|
|
217
|
+
logger.log_access(user_id: "user1", action: :create, success: true)
|
|
218
|
+
logs = adapter.all_logs
|
|
219
|
+
expect(logs.first[:action]).to eq("create")
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
describe "adapter attribute" do
|
|
224
|
+
it "returns the configured adapter" do
|
|
225
|
+
custom_adapter = double("CustomAdapter")
|
|
226
|
+
logger = DecisionAgent::Auth::AccessAuditLogger.new(adapter: custom_adapter)
|
|
227
|
+
expect(logger.adapter).to eq(custom_adapter)
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
# Test for InMemoryAccessAdapter - class is nested inside AccessAuditLogger
|
|
233
|
+
# but may not be directly accessible. Testing through AccessAuditLogger instead.
|
|
234
|
+
RSpec.describe DecisionAgent::Auth::AccessAuditLogger do
|
|
235
|
+
describe "InMemoryAccessAdapter integration" do
|
|
236
|
+
let(:logger) { DecisionAgent::Auth::AccessAuditLogger.new }
|
|
237
|
+
|
|
238
|
+
it "uses InMemoryAccessAdapter by default" do
|
|
239
|
+
expect(logger.adapter).to be_a(DecisionAgent::Audit::InMemoryAccessAdapter)
|
|
240
|
+
rescue NameError
|
|
241
|
+
# If class is not directly accessible, test through logger interface
|
|
242
|
+
logger.log_authentication("test", user_id: "user1")
|
|
243
|
+
logs = logger.adapter.all_logs
|
|
244
|
+
expect(logs.size).to eq(1)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
RSpec.describe DecisionAgent::Audit::InMemoryAccessAdapter do
|
|
250
|
+
let(:adapter) { described_class.new }
|
|
251
|
+
|
|
252
|
+
describe "#initialize" do
|
|
253
|
+
it "initializes with empty logs" do
|
|
254
|
+
expect(adapter.all_logs).to eq([])
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
describe "#record_access" do
|
|
259
|
+
it "stores log entry" do
|
|
260
|
+
log_entry = { event_type: "test", user_id: "user1", timestamp: Time.now.utc.iso8601 }
|
|
261
|
+
adapter.record_access(log_entry)
|
|
262
|
+
expect(adapter.all_logs.size).to eq(1)
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it "stores duplicate of log entry" do
|
|
266
|
+
log_entry = { event_type: "test", user_id: "user1", timestamp: Time.now.utc.iso8601 }
|
|
267
|
+
adapter.record_access(log_entry)
|
|
268
|
+
log_entry[:modified] = true
|
|
269
|
+
expect(adapter.all_logs.first[:modified]).to be_nil
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it "is thread-safe" do
|
|
273
|
+
threads = []
|
|
274
|
+
10.times do |i|
|
|
275
|
+
threads << Thread.new do
|
|
276
|
+
10.times do
|
|
277
|
+
adapter.record_access({ event_type: "test", user_id: "user#{i}", timestamp: Time.now.utc.iso8601 })
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
threads.each(&:join)
|
|
282
|
+
expect(adapter.all_logs.size).to eq(100)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
describe "#query_access_logs" do
|
|
287
|
+
before do
|
|
288
|
+
adapter.record_access({ event_type: "login", user_id: "user1", timestamp: (Time.now.utc - 7200).iso8601 })
|
|
289
|
+
adapter.record_access({ event_type: "login", user_id: "user2", timestamp: (Time.now.utc - 3600).iso8601 })
|
|
290
|
+
adapter.record_access({ event_type: "logout", user_id: "user1", timestamp: Time.now.utc.iso8601 })
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
it "filters by user_id" do
|
|
294
|
+
logs = adapter.query_access_logs(user_id: "user1")
|
|
295
|
+
expect(logs.size).to eq(2)
|
|
296
|
+
expect(logs.all? { |log| log[:user_id] == "user1" }).to be true
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
it "filters by event_type" do
|
|
300
|
+
logs = adapter.query_access_logs(event_type: "login")
|
|
301
|
+
expect(logs.size).to eq(2)
|
|
302
|
+
expect(logs.all? { |log| log[:event_type] == "login" }).to be true
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
it "filters by start_time" do
|
|
306
|
+
start_time = Time.now.utc - 1800
|
|
307
|
+
logs = adapter.query_access_logs(start_time: start_time)
|
|
308
|
+
expect(logs.size).to eq(1)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
it "filters by end_time" do
|
|
312
|
+
end_time = Time.now.utc - 1800
|
|
313
|
+
logs = adapter.query_access_logs(end_time: end_time)
|
|
314
|
+
expect(logs.size).to eq(2)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
it "limits results" do
|
|
318
|
+
logs = adapter.query_access_logs(limit: 1)
|
|
319
|
+
expect(logs.size).to eq(1)
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
it "handles string timestamps" do
|
|
323
|
+
start_time = (Time.now.utc - 1800).iso8601
|
|
324
|
+
logs = adapter.query_access_logs(start_time: start_time)
|
|
325
|
+
expect(logs).to be_an(Array)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
it "returns results in reverse order" do
|
|
329
|
+
logs = adapter.query_access_logs
|
|
330
|
+
expect(logs.first[:event_type]).to eq("logout")
|
|
331
|
+
expect(logs.last[:event_type]).to eq("login")
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
it "is thread-safe" do
|
|
335
|
+
threads = []
|
|
336
|
+
5.times do
|
|
337
|
+
threads << Thread.new do
|
|
338
|
+
adapter.query_access_logs(user_id: "user1")
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
threads.each(&:join)
|
|
342
|
+
# Should not raise errors
|
|
343
|
+
expect(adapter.query_access_logs.size).to eq(3)
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
describe "#all_logs" do
|
|
348
|
+
it "returns copy of logs" do
|
|
349
|
+
adapter.record_access({ event_type: "test", timestamp: Time.now.utc.iso8601 })
|
|
350
|
+
logs1 = adapter.all_logs
|
|
351
|
+
logs2 = adapter.all_logs
|
|
352
|
+
expect(logs1).not_to be(logs2)
|
|
353
|
+
logs1 << { modified: true }
|
|
354
|
+
expect(adapter.all_logs.size).to eq(1)
|
|
355
|
+
end
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
describe "#clear" do
|
|
359
|
+
it "clears all logs" do
|
|
360
|
+
adapter.record_access({ event_type: "test", timestamp: Time.now.utc.iso8601 })
|
|
361
|
+
expect(adapter.all_logs.size).to eq(1)
|
|
362
|
+
adapter.clear
|
|
363
|
+
expect(adapter.all_logs.size).to eq(0)
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
it "is thread-safe" do
|
|
367
|
+
adapter.record_access({ event_type: "test", timestamp: Time.now.utc.iso8601 })
|
|
368
|
+
threads = []
|
|
369
|
+
5.times do
|
|
370
|
+
threads << Thread.new do
|
|
371
|
+
adapter.clear
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
threads.each(&:join)
|
|
375
|
+
expect(adapter.all_logs.size).to eq(0)
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
RSpec.describe DecisionAgent::Audit::AccessAdapter do
|
|
381
|
+
let(:adapter) { described_class.new }
|
|
382
|
+
|
|
383
|
+
describe "#record_access" do
|
|
384
|
+
it "raises NotImplementedError" do
|
|
385
|
+
expect { adapter.record_access({}) }.to raise_error(NotImplementedError, /must implement #record_access/)
|
|
386
|
+
end
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
describe "#query_access_logs" do
|
|
390
|
+
it "raises NotImplementedError" do
|
|
391
|
+
expect { adapter.query_access_logs({}) }.to raise_error(NotImplementedError, /must implement #query_access_logs/)
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
require "spec_helper"
|
|
2
|
+
|
|
3
|
+
RSpec.describe DecisionAgent::Auth::Authenticator do
|
|
4
|
+
let(:authenticator) { DecisionAgent::Auth::Authenticator.new }
|
|
5
|
+
|
|
6
|
+
describe "#create_user" do
|
|
7
|
+
it "creates a new user" do
|
|
8
|
+
user = authenticator.create_user(
|
|
9
|
+
email: "test@example.com",
|
|
10
|
+
password: "password123"
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
expect(user.email).to eq("test@example.com")
|
|
14
|
+
expect(user.id).to be_a(String)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it "creates a user with roles" do
|
|
18
|
+
user = authenticator.create_user(
|
|
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
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#login" do
|
|
29
|
+
before do
|
|
30
|
+
authenticator.create_user(
|
|
31
|
+
email: "test@example.com",
|
|
32
|
+
password: "password123"
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
it "returns a session for valid credentials" do
|
|
37
|
+
session = authenticator.login("test@example.com", "password123")
|
|
38
|
+
|
|
39
|
+
expect(session).to be_a(DecisionAgent::Auth::Session)
|
|
40
|
+
expect(session.user_id).to be_a(String)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
it "returns nil for invalid email" do
|
|
44
|
+
session = authenticator.login("wrong@example.com", "password123")
|
|
45
|
+
expect(session).to be_nil
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
it "returns nil for invalid password" do
|
|
49
|
+
session = authenticator.login("test@example.com", "wrongpassword")
|
|
50
|
+
expect(session).to be_nil
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
it "returns nil for inactive user" do
|
|
54
|
+
user = authenticator.find_user_by_email("test@example.com")
|
|
55
|
+
user.active = false
|
|
56
|
+
|
|
57
|
+
session = authenticator.login("test@example.com", "password123")
|
|
58
|
+
expect(session).to be_nil
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
describe "#logout" do
|
|
63
|
+
it "deletes the session" do
|
|
64
|
+
authenticator.create_user(
|
|
65
|
+
email: "test@example.com",
|
|
66
|
+
password: "password123"
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
session = authenticator.login("test@example.com", "password123")
|
|
70
|
+
token = session.token
|
|
71
|
+
|
|
72
|
+
authenticator.logout(token)
|
|
73
|
+
|
|
74
|
+
expect(authenticator.authenticate(token)).to be_nil
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
describe "#authenticate" do
|
|
79
|
+
it "returns user and session for valid token" do
|
|
80
|
+
authenticator.create_user(
|
|
81
|
+
email: "test@example.com",
|
|
82
|
+
password: "password123"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
session = authenticator.login("test@example.com", "password123")
|
|
86
|
+
result = authenticator.authenticate(session.token)
|
|
87
|
+
|
|
88
|
+
expect(result).to be_a(Hash)
|
|
89
|
+
expect(result[:user]).to be_a(DecisionAgent::Auth::User)
|
|
90
|
+
expect(result[:session]).to be_a(DecisionAgent::Auth::Session)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
it "returns nil for invalid token" do
|
|
94
|
+
result = authenticator.authenticate("invalid_token")
|
|
95
|
+
expect(result).to be_nil
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
it "returns nil for expired session" do
|
|
99
|
+
authenticator.create_user(
|
|
100
|
+
email: "test@example.com",
|
|
101
|
+
password: "password123"
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
session = authenticator.login("test@example.com", "password123")
|
|
105
|
+
# Manually expire the session
|
|
106
|
+
session.instance_variable_set(:@expires_at, Time.now.utc - 1)
|
|
107
|
+
|
|
108
|
+
result = authenticator.authenticate(session.token)
|
|
109
|
+
expect(result).to be_nil
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|