devcycle-ruby-server-sdk 3.6.1 → 3.7.0

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.
@@ -4,18 +4,109 @@ require 'spec_helper'
4
4
  require 'open_feature/sdk'
5
5
 
6
6
  context 'user_from_openfeature_context' do
7
- context 'user_id' do
7
+ context 'user_id validation' do
8
+ it 'raises error when no user ID fields are provided' do
9
+ context = OpenFeature::SDK::EvaluationContext.new(email: 'test@example.com')
10
+ expect {
11
+ DevCycle::Provider.user_from_openfeature_context(context)
12
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
13
+ end
14
+
15
+ it 'raises error when targeting_key is not a string' do
16
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 123)
17
+ expect {
18
+ DevCycle::Provider.user_from_openfeature_context(context)
19
+ }.to raise_error(ArgumentError, "User ID field 'targeting_key' must be a string, got Integer")
20
+ end
21
+
22
+ it 'raises error when user_id is not a string' do
23
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 123)
24
+ expect {
25
+ DevCycle::Provider.user_from_openfeature_context(context)
26
+ }.to raise_error(ArgumentError, "User ID field 'user_id' must be a string, got Integer")
27
+ end
28
+
29
+ it 'raises error when userId is not a string' do
30
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 123)
31
+ expect {
32
+ DevCycle::Provider.user_from_openfeature_context(context)
33
+ }.to raise_error(ArgumentError, "User ID field 'userId' must be a string, got Integer")
34
+ end
35
+
36
+ it 'raises error when targeting_key is nil' do
37
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: nil)
38
+ expect {
39
+ DevCycle::Provider.user_from_openfeature_context(context)
40
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
41
+ end
8
42
 
9
- it 'returns a user with the user_id from the context' do
10
- context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id')
43
+ it 'raises error when targeting_key is empty string' do
44
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: '')
45
+ expect {
46
+ DevCycle::Provider.user_from_openfeature_context(context)
47
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
48
+ end
49
+
50
+ it 'raises error when user_id is empty string' do
51
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: '')
52
+ expect {
53
+ DevCycle::Provider.user_from_openfeature_context(context)
54
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
55
+ end
56
+
57
+ it 'raises error when userId is empty string' do
58
+ context = OpenFeature::SDK::EvaluationContext.new(userId: '')
59
+ expect {
60
+ DevCycle::Provider.user_from_openfeature_context(context)
61
+ }.to raise_error(ArgumentError, "User ID is required. Must provide one of: targeting_key, user_id, or userId")
62
+ end
63
+ end
64
+
65
+ context 'user_id fields priority' do
66
+ it 'returns a user with the user_id from the context when only user_id is provided' do
67
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value')
11
68
  user = DevCycle::Provider.user_from_openfeature_context(context)
12
- expect(user.user_id).to eq('user_id')
69
+ expect(user.user_id).to eq('user_id_value')
13
70
  end
14
71
 
15
- it 'returns a user with the targeting_key from the context' do
16
- context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key')
72
+ it 'returns a user with the targeting_key from the context when only targeting_key is provided' do
73
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value')
17
74
  user = DevCycle::Provider.user_from_openfeature_context(context)
18
- expect(user.user_id).to eq('targeting_key')
75
+ expect(user.user_id).to eq('targeting_key_value')
76
+ end
77
+
78
+ it 'returns a user with the userId from the context when only userId is provided' do
79
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId_value')
80
+ user = DevCycle::Provider.user_from_openfeature_context(context)
81
+ expect(user.user_id).to eq('userId_value')
82
+ end
83
+
84
+ it 'prioritizes targeting_key over user_id' do
85
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value')
86
+ user = DevCycle::Provider.user_from_openfeature_context(context)
87
+ expect(user.user_id).to eq('targeting_key_value')
88
+ expect(user.customData).to eq({})
89
+ end
90
+
91
+ it 'prioritizes targeting_key over userId' do
92
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', userId: 'userId_value')
93
+ user = DevCycle::Provider.user_from_openfeature_context(context)
94
+ expect(user.user_id).to eq('targeting_key_value')
95
+ expect(user.customData).to eq({})
96
+ end
97
+
98
+ it 'prioritizes user_id over userId' do
99
+ context = OpenFeature::SDK::EvaluationContext.new(user_id: 'user_id_value', userId: 'userId_value')
100
+ user = DevCycle::Provider.user_from_openfeature_context(context)
101
+ expect(user.user_id).to eq('user_id_value')
102
+ expect(user.customData).to eq({})
103
+ end
104
+
105
+ it 'prioritizes targeting_key over both user_id and userId' do
106
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key_value', user_id: 'user_id_value', userId: 'userId_value')
107
+ user = DevCycle::Provider.user_from_openfeature_context(context)
108
+ expect(user.user_id).to eq('targeting_key_value')
109
+ expect(user.customData).to eq({})
19
110
  end
20
111
  end
21
112
  context 'email' do
@@ -31,6 +122,19 @@ context 'user_from_openfeature_context' do
31
122
  expect(user.user_id).to eq('targeting_key')
32
123
  expect(user.email).to eq('email')
33
124
  end
125
+ it 'returns a user with a valid userId and email' do
126
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', email: 'email')
127
+ user = DevCycle::Provider.user_from_openfeature_context(context)
128
+ expect(user.user_id).to eq('userId')
129
+ expect(user.email).to eq('email')
130
+ end
131
+ it 'prioritizes targeting_key over user_id with email' do
132
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', email: 'email')
133
+ user = DevCycle::Provider.user_from_openfeature_context(context)
134
+ expect(user.user_id).to eq('targeting_key')
135
+ expect(user.email).to eq('email')
136
+ expect(user.customData).to eq({})
137
+ end
34
138
  end
35
139
 
36
140
  context 'customData' do
@@ -40,6 +144,18 @@ context 'user_from_openfeature_context' do
40
144
  expect(user.user_id).to eq('user_id')
41
145
  expect(user.customData).to eq({ 'key' => 'value' })
42
146
  end
147
+ it 'returns a user with userId and customData' do
148
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', customData: { 'key' => 'value' })
149
+ user = DevCycle::Provider.user_from_openfeature_context(context)
150
+ expect(user.user_id).to eq('userId')
151
+ expect(user.customData).to eq({ 'key' => 'value' })
152
+ end
153
+ it 'excludes all user ID fields from customData' do
154
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', customData: { 'key' => 'value' })
155
+ user = DevCycle::Provider.user_from_openfeature_context(context)
156
+ expect(user.user_id).to eq('targeting_key')
157
+ expect(user.customData).to eq({ 'key' => 'value' })
158
+ end
43
159
  end
44
160
 
45
161
  context 'privateCustomData' do
@@ -49,6 +165,12 @@ context 'user_from_openfeature_context' do
49
165
  expect(user.user_id).to eq('user_id')
50
166
  expect(user.privateCustomData).to eq({ 'key' => 'value' })
51
167
  end
168
+ it 'returns a user with userId and privateCustomData' do
169
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', privateCustomData: { 'key' => 'value' })
170
+ user = DevCycle::Provider.user_from_openfeature_context(context)
171
+ expect(user.user_id).to eq('userId')
172
+ expect(user.privateCustomData).to eq({ 'key' => 'value' })
173
+ end
52
174
  end
53
175
 
54
176
  context 'appVersion' do
@@ -65,6 +187,20 @@ context 'user_from_openfeature_context' do
65
187
  expect(user.user_id).to eq('user_id')
66
188
  expect(user.appBuild).to eq(1)
67
189
  end
190
+
191
+ it 'returns a user with userId and appVersion' do
192
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appVersion: '1.0.0')
193
+ user = DevCycle::Provider.user_from_openfeature_context(context)
194
+ expect(user.user_id).to eq('userId')
195
+ expect(user.appVersion).to eq('1.0.0')
196
+ end
197
+
198
+ it 'returns a user with userId and appBuild' do
199
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', appBuild: 1)
200
+ user = DevCycle::Provider.user_from_openfeature_context(context)
201
+ expect(user.user_id).to eq('userId')
202
+ expect(user.appBuild).to eq(1)
203
+ end
68
204
  end
69
205
  context 'randomFields' do
70
206
  it 'returns a user with customData fields mapped to any non-standard fields' do
@@ -73,6 +209,20 @@ context 'user_from_openfeature_context' do
73
209
  expect(user.user_id).to eq('user_id')
74
210
  expect(user.customData).to eq({ 'randomField' => 'value' })
75
211
  end
212
+
213
+ it 'returns a user with userId and customData fields mapped to any non-standard fields' do
214
+ context = OpenFeature::SDK::EvaluationContext.new(userId: 'userId', randomField: 'value')
215
+ user = DevCycle::Provider.user_from_openfeature_context(context)
216
+ expect(user.user_id).to eq('userId')
217
+ expect(user.customData).to eq({ 'randomField' => 'value' })
218
+ end
219
+
220
+ it 'excludes all user ID fields from custom data with random fields' do
221
+ context = OpenFeature::SDK::EvaluationContext.new(targeting_key: 'targeting_key', user_id: 'user_id', userId: 'userId', randomField: 'value')
222
+ user = DevCycle::Provider.user_from_openfeature_context(context)
223
+ expect(user.user_id).to eq('targeting_key')
224
+ expect(user.customData).to eq({ 'randomField' => 'value' })
225
+ end
76
226
  end
77
227
 
78
228
  context 'provider' do
@@ -0,0 +1,410 @@
1
+ require 'spec_helper'
2
+ require 'devcycle-ruby-server-sdk/eval_hooks_runner'
3
+
4
+ describe DevCycle::EvalHooksRunner do
5
+ let(:test_context) { DevCycle::HookContext.new(key: 'test-key', user: 'test-user', default_value: 'test-default') }
6
+
7
+ describe 'initialization' do
8
+ it 'initializes with empty hooks array' do
9
+ runner = DevCycle::EvalHooksRunner.new
10
+ expect(runner.eval_hooks).to be_empty
11
+ end
12
+
13
+ it 'initializes with provided hooks' do
14
+ hook = DevCycle::EvalHook.new
15
+ runner = DevCycle::EvalHooksRunner.new([hook])
16
+ expect(runner.eval_hooks).to include(hook)
17
+ end
18
+ end
19
+
20
+ describe '#add_hook' do
21
+ it 'adds a hook to the runner' do
22
+ runner = DevCycle::EvalHooksRunner.new
23
+ hook = DevCycle::EvalHook.new
24
+
25
+ runner.add_hook(hook)
26
+ expect(runner.eval_hooks).to include(hook)
27
+ end
28
+
29
+ it 'can add multiple hooks' do
30
+ runner = DevCycle::EvalHooksRunner.new
31
+ hook1 = DevCycle::EvalHook.new
32
+ hook2 = DevCycle::EvalHook.new
33
+
34
+ runner.add_hook(hook1)
35
+ runner.add_hook(hook2)
36
+
37
+ expect(runner.eval_hooks).to include(hook1, hook2)
38
+ expect(runner.eval_hooks.length).to eq(2)
39
+ end
40
+ end
41
+
42
+ describe '#clear_hooks' do
43
+ it 'removes all hooks from the runner' do
44
+ runner = DevCycle::EvalHooksRunner.new
45
+ hook = DevCycle::EvalHook.new
46
+
47
+ runner.add_hook(hook)
48
+ expect(runner.eval_hooks).not_to be_empty
49
+
50
+ runner.clear_hooks
51
+ expect(runner.eval_hooks).to be_empty
52
+ end
53
+ end
54
+
55
+ describe '#run_before_hooks' do
56
+ it 'runs before hooks in order' do
57
+ execution_order = []
58
+ runner = DevCycle::EvalHooksRunner.new
59
+
60
+ hook1 = DevCycle::EvalHook.new(
61
+ before: ->(context) {
62
+ execution_order << 'hook1'
63
+ context
64
+ }
65
+ )
66
+
67
+ hook2 = DevCycle::EvalHook.new(
68
+ before: ->(context) {
69
+ execution_order << 'hook2'
70
+ context
71
+ }
72
+ )
73
+
74
+ runner.add_hook(hook1)
75
+ runner.add_hook(hook2)
76
+
77
+ result = runner.run_before_hooks(test_context)
78
+
79
+ expect(execution_order).to eq(['hook1', 'hook2'])
80
+ expect(result).to eq(test_context)
81
+ end
82
+
83
+ it 'returns modified context from before hook' do
84
+ runner = DevCycle::EvalHooksRunner.new
85
+ modified_context = DevCycle::HookContext.new(key: 'modified', user: 'modified-user', default_value: 'modified-default')
86
+
87
+ hook = DevCycle::EvalHook.new(
88
+ before: ->(context) {
89
+ modified_context
90
+ }
91
+ )
92
+
93
+ runner.add_hook(hook)
94
+ result = runner.run_before_hooks(test_context)
95
+
96
+ expect(result).to eq(modified_context)
97
+ end
98
+
99
+ it 'handles hooks without before callbacks' do
100
+ runner = DevCycle::EvalHooksRunner.new
101
+ hook = DevCycle::EvalHook.new # No before callback
102
+
103
+ runner.add_hook(hook)
104
+ result = runner.run_before_hooks(test_context)
105
+
106
+ expect(result).to eq(test_context)
107
+ end
108
+
109
+ it 'raises BeforeHookError when a before hook raises an error' do
110
+ runner = DevCycle::EvalHooksRunner.new
111
+ hook1_called = false
112
+
113
+ hook1 = DevCycle::EvalHook.new(
114
+ before: ->(context) {
115
+ hook1_called = true
116
+ raise StandardError, 'Hook 1 error'
117
+ }
118
+ )
119
+
120
+ hook2 = DevCycle::EvalHook.new(
121
+ before: ->(context) {
122
+ # This should not be called because hook1 raises an error
123
+ context
124
+ }
125
+ )
126
+
127
+ runner.add_hook(hook1)
128
+ runner.add_hook(hook2)
129
+
130
+ expect { runner.run_before_hooks(test_context) }.to raise_error(DevCycle::BeforeHookError, /Hook 1 error/)
131
+ expect(hook1_called).to be true
132
+ end
133
+ end
134
+
135
+ describe '#run_after_hooks' do
136
+ it 'runs after hooks in order' do
137
+ execution_order = []
138
+ runner = DevCycle::EvalHooksRunner.new
139
+
140
+ hook1 = DevCycle::EvalHook.new(
141
+ after: ->(context) {
142
+ execution_order << 'hook1'
143
+ }
144
+ )
145
+
146
+ hook2 = DevCycle::EvalHook.new(
147
+ after: ->(context) {
148
+ execution_order << 'hook2'
149
+ }
150
+ )
151
+
152
+ runner.add_hook(hook1)
153
+ runner.add_hook(hook2)
154
+
155
+ runner.run_after_hooks(test_context)
156
+
157
+ expect(execution_order).to eq(['hook1', 'hook2'])
158
+ end
159
+
160
+ it 'handles hooks without after callbacks' do
161
+ runner = DevCycle::EvalHooksRunner.new
162
+ hook = DevCycle::EvalHook.new # No after callback
163
+
164
+ expect { runner.run_after_hooks(test_context) }.not_to raise_error
165
+ end
166
+
167
+ it 'raises AfterHookError when an after hook raises an error' do
168
+ runner = DevCycle::EvalHooksRunner.new
169
+ hook1_called = false
170
+
171
+ hook1 = DevCycle::EvalHook.new(
172
+ after: ->(context) {
173
+ hook1_called = true
174
+ raise StandardError, 'Hook 1 error'
175
+ }
176
+ )
177
+
178
+ hook2 = DevCycle::EvalHook.new(
179
+ after: ->(context) {
180
+ # This should not be called because hook1 raises an error
181
+ }
182
+ )
183
+
184
+ runner.add_hook(hook1)
185
+ runner.add_hook(hook2)
186
+
187
+ expect { runner.run_after_hooks(test_context) }.to raise_error(DevCycle::AfterHookError, /Hook 1 error/)
188
+ expect(hook1_called).to be true
189
+ end
190
+ end
191
+
192
+ describe '#run_error_hooks' do
193
+ it 'runs error hooks with context and error' do
194
+ runner = DevCycle::EvalHooksRunner.new
195
+ error_hook_called = false
196
+ received_error = nil
197
+
198
+ hook = DevCycle::EvalHook.new(
199
+ error: ->(context, error) {
200
+ error_hook_called = true
201
+ received_error = error
202
+ }
203
+ )
204
+
205
+ runner.add_hook(hook)
206
+ test_error = StandardError.new('Test error')
207
+
208
+ runner.run_error_hooks(test_context, test_error)
209
+
210
+ expect(error_hook_called).to be true
211
+ expect(received_error).to eq(test_error)
212
+ end
213
+
214
+ it 'runs multiple error hooks in order' do
215
+ execution_order = []
216
+ runner = DevCycle::EvalHooksRunner.new
217
+
218
+ hook1 = DevCycle::EvalHook.new(
219
+ error: ->(context, error) {
220
+ execution_order << 'hook1'
221
+ }
222
+ )
223
+
224
+ hook2 = DevCycle::EvalHook.new(
225
+ error: ->(context, error) {
226
+ execution_order << 'hook2'
227
+ }
228
+ )
229
+
230
+ runner.add_hook(hook1)
231
+ runner.add_hook(hook2)
232
+
233
+ test_error = StandardError.new('Test error')
234
+ runner.run_error_hooks(test_context, test_error)
235
+
236
+ expect(execution_order).to eq(['hook1', 'hook2'])
237
+ end
238
+
239
+ it 'handles hooks without error callbacks' do
240
+ runner = DevCycle::EvalHooksRunner.new
241
+ hook = DevCycle::EvalHook.new # No error callback
242
+
243
+ test_error = StandardError.new('Test error')
244
+ expect { runner.run_error_hooks(test_context, test_error) }.not_to raise_error
245
+ end
246
+
247
+ it 'continues execution when an error hook raises an error' do
248
+ runner = DevCycle::EvalHooksRunner.new
249
+ hook1_called = false
250
+ hook2_called = false
251
+
252
+ hook1 = DevCycle::EvalHook.new(
253
+ error: ->(context, error) {
254
+ hook1_called = true
255
+ raise StandardError, 'Error hook error'
256
+ }
257
+ )
258
+
259
+ hook2 = DevCycle::EvalHook.new(
260
+ error: ->(context, error) {
261
+ hook2_called = true
262
+ }
263
+ )
264
+
265
+ runner.add_hook(hook1)
266
+ runner.add_hook(hook2)
267
+
268
+ test_error = StandardError.new('Test error')
269
+ runner.run_error_hooks(test_context, test_error)
270
+
271
+ expect(hook1_called).to be true
272
+ expect(hook2_called).to be true
273
+ end
274
+ end
275
+
276
+ describe '#run_finally_hooks' do
277
+ it 'runs finally hooks in order' do
278
+ execution_order = []
279
+ runner = DevCycle::EvalHooksRunner.new
280
+
281
+ hook1 = DevCycle::EvalHook.new(
282
+ on_finally: ->(context) {
283
+ execution_order << 'hook1'
284
+ }
285
+ )
286
+
287
+ hook2 = DevCycle::EvalHook.new(
288
+ on_finally: ->(context) {
289
+ execution_order << 'hook2'
290
+ }
291
+ )
292
+
293
+ runner.add_hook(hook1)
294
+ runner.add_hook(hook2)
295
+
296
+ runner.run_finally_hooks(test_context)
297
+
298
+ expect(execution_order).to eq(['hook1', 'hook2'])
299
+ end
300
+
301
+ it 'handles hooks without finally callbacks' do
302
+ runner = DevCycle::EvalHooksRunner.new
303
+ hook = DevCycle::EvalHook.new # No finally callback
304
+
305
+ expect { runner.run_finally_hooks(test_context) }.not_to raise_error
306
+ end
307
+
308
+ it 'continues execution when a finally hook raises an error' do
309
+ runner = DevCycle::EvalHooksRunner.new
310
+ hook1_called = false
311
+ hook2_called = false
312
+
313
+ hook1 = DevCycle::EvalHook.new(
314
+ on_finally: ->(context) {
315
+ hook1_called = true
316
+ raise StandardError, 'Finally hook error'
317
+ }
318
+ )
319
+
320
+ hook2 = DevCycle::EvalHook.new(
321
+ on_finally: ->(context) {
322
+ hook2_called = true
323
+ }
324
+ )
325
+
326
+ runner.add_hook(hook1)
327
+ runner.add_hook(hook2)
328
+
329
+ runner.run_finally_hooks(test_context)
330
+
331
+ expect(hook1_called).to be true
332
+ expect(hook2_called).to be true
333
+ end
334
+ end
335
+ end
336
+
337
+ describe DevCycle::EvalHook do
338
+ describe 'initialization' do
339
+ it 'initializes with no callbacks' do
340
+ hook = DevCycle::EvalHook.new
341
+ expect(hook.before).to be_nil
342
+ expect(hook.after).to be_nil
343
+ expect(hook.on_finally).to be_nil
344
+ expect(hook.error).to be_nil
345
+ end
346
+
347
+ it 'initializes with provided callbacks' do
348
+ before_callback = ->(context) { context }
349
+ after_callback = ->(context) { }
350
+ error_callback = ->(context, error) { }
351
+ finally_callback = ->(context) { }
352
+
353
+ hook = DevCycle::EvalHook.new(
354
+ before: before_callback,
355
+ after: after_callback,
356
+ error: error_callback,
357
+ on_finally: finally_callback
358
+ )
359
+
360
+ expect(hook.before).to eq(before_callback)
361
+ expect(hook.after).to eq(after_callback)
362
+ expect(hook.error).to eq(error_callback)
363
+ expect(hook.on_finally).to eq(finally_callback)
364
+ end
365
+
366
+ it 'initializes with partial callbacks' do
367
+ before_callback = ->(context) { context }
368
+
369
+ hook = DevCycle::EvalHook.new(before: before_callback)
370
+
371
+ expect(hook.before).to eq(before_callback)
372
+ expect(hook.after).to be_nil
373
+ expect(hook.on_finally).to be_nil
374
+ expect(hook.error).to be_nil
375
+ end
376
+ end
377
+ end
378
+
379
+ describe DevCycle::HookContext do
380
+ describe 'initialization' do
381
+ it 'initializes with required parameters' do
382
+ user = { user_id: 'test-user' }
383
+ context = DevCycle::HookContext.new(
384
+ key: 'test-key',
385
+ user: user,
386
+ default_value: 'test-default'
387
+ )
388
+
389
+ expect(context.key).to eq('test-key')
390
+ expect(context.user).to eq(user)
391
+ expect(context.default_value).to eq('test-default')
392
+ end
393
+
394
+ it 'allows modification of attributes' do
395
+ context = DevCycle::HookContext.new(
396
+ key: 'original-key',
397
+ user: 'original-user',
398
+ default_value: 'original-default'
399
+ )
400
+
401
+ context.key = 'modified-key'
402
+ context.user = 'modified-user'
403
+ context.default_value = 'modified-default'
404
+
405
+ expect(context.key).to eq('modified-key')
406
+ expect(context.user).to eq('modified-user')
407
+ expect(context.default_value).to eq('modified-default')
408
+ end
409
+ end
410
+ end