ldclient-rb 2.5.0 → 3.0.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -2
- data/circle.yml +11 -9
- data/ldclient-rb.gemspec +7 -2
- data/lib/ldclient-rb.rb +2 -2
- data/lib/ldclient-rb/config.rb +2 -1
- data/lib/ldclient-rb/evaluation.rb +77 -39
- data/lib/ldclient-rb/in_memory_store.rb +89 -0
- data/lib/ldclient-rb/ldclient.rb +2 -2
- data/lib/ldclient-rb/polling.rb +6 -3
- data/lib/ldclient-rb/{redis_feature_store.rb → redis_store.rb} +54 -42
- data/lib/ldclient-rb/requestor.rb +12 -0
- data/lib/ldclient-rb/stream.rb +44 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_spec.rb +204 -7
- data/spec/feature_store_spec_base.rb +20 -20
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/stream_spec.rb +32 -17
- metadata +6 -4
- data/lib/ldclient-rb/feature_store.rb +0 -63
@@ -26,6 +26,18 @@ module LaunchDarkly
|
|
26
26
|
make_request("/sdk/latest-flags/" + key)
|
27
27
|
end
|
28
28
|
|
29
|
+
def request_all_segments()
|
30
|
+
make_request("/sdk/latest-segments")
|
31
|
+
end
|
32
|
+
|
33
|
+
def request_segment(key)
|
34
|
+
make_request("/sdk/latest-segments/" + key)
|
35
|
+
end
|
36
|
+
|
37
|
+
def request_all_data()
|
38
|
+
make_request("/sdk/latest-all")
|
39
|
+
end
|
40
|
+
|
29
41
|
def make_request(path)
|
30
42
|
uri = @config.base_uri + path
|
31
43
|
res = @client.get (uri) do |req|
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -10,11 +10,16 @@ module LaunchDarkly
|
|
10
10
|
INDIRECT_PATCH = :'indirect/patch'
|
11
11
|
READ_TIMEOUT_SECONDS = 300 # 5 minutes; the stream should send a ping every 3 minutes
|
12
12
|
|
13
|
+
KEY_PATHS = {
|
14
|
+
FEATURES => "/flags/",
|
15
|
+
SEGMENTS => "/segments/"
|
16
|
+
}
|
17
|
+
|
13
18
|
class StreamProcessor
|
14
19
|
def initialize(sdk_key, config, requestor)
|
15
20
|
@sdk_key = sdk_key
|
16
21
|
@config = config
|
17
|
-
@
|
22
|
+
@feature_store = config.feature_store
|
18
23
|
@requestor = requestor
|
19
24
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
20
25
|
@started = Concurrent::AtomicBoolean.new(false)
|
@@ -36,7 +41,7 @@ module LaunchDarkly
|
|
36
41
|
'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
|
37
42
|
}
|
38
43
|
opts = {:headers => headers, :with_credentials => true, :proxy => @config.proxy, :read_timeout => READ_TIMEOUT_SECONDS}
|
39
|
-
@es = Celluloid::EventSource.new(@config.stream_uri + "/
|
44
|
+
@es = Celluloid::EventSource.new(@config.stream_uri + "/all", opts) do |conn|
|
40
45
|
conn.on(PUT) { |message| process_message(message, PUT) }
|
41
46
|
conn.on(PATCH) { |message| process_message(message, PATCH) }
|
42
47
|
conn.on(DELETE) { |message| process_message(message, DELETE) }
|
@@ -66,30 +71,61 @@ module LaunchDarkly
|
|
66
71
|
end
|
67
72
|
end
|
68
73
|
|
74
|
+
private
|
75
|
+
|
69
76
|
def process_message(message, method)
|
70
77
|
@config.logger.debug("[LDClient] Stream received #{method} message: #{message.data}")
|
71
78
|
if method == PUT
|
72
79
|
message = JSON.parse(message.data, symbolize_names: true)
|
73
|
-
@
|
80
|
+
@feature_store.init({
|
81
|
+
FEATURES => message[:data][:flags],
|
82
|
+
SEGMENTS => message[:data][:segments]
|
83
|
+
})
|
74
84
|
@initialized.make_true
|
75
85
|
@config.logger.info("[LDClient] Stream initialized")
|
76
86
|
elsif method == PATCH
|
77
87
|
message = JSON.parse(message.data, symbolize_names: true)
|
78
|
-
|
88
|
+
for kind in [FEATURES, SEGMENTS]
|
89
|
+
key = key_for_path(kind, message[:path])
|
90
|
+
if key
|
91
|
+
@feature_store.upsert(kind, message[:data])
|
92
|
+
break
|
93
|
+
end
|
94
|
+
end
|
79
95
|
elsif method == DELETE
|
80
96
|
message = JSON.parse(message.data, symbolize_names: true)
|
81
|
-
|
97
|
+
for kind in [FEATURES, SEGMENTS]
|
98
|
+
key = key_for_path(kind, message[:path])
|
99
|
+
if key
|
100
|
+
@feature_store.delete(kind, key, message[:version])
|
101
|
+
break
|
102
|
+
end
|
103
|
+
end
|
82
104
|
elsif method == INDIRECT_PUT
|
83
|
-
@
|
105
|
+
all_data = @requestor.request_all_data
|
106
|
+
@feature_store.init({
|
107
|
+
FEATURES => all_data[:flags],
|
108
|
+
SEGMENTS => all_data[:segments]
|
109
|
+
})
|
84
110
|
@initialized.make_true
|
85
111
|
@config.logger.info("[LDClient] Stream initialized (via indirect message)")
|
86
112
|
elsif method == INDIRECT_PATCH
|
87
|
-
|
113
|
+
key = feature_key_for_path(message.data)
|
114
|
+
if key
|
115
|
+
@feature_store.upsert(FEATURES, @requestor.request_flag(key))
|
116
|
+
else
|
117
|
+
key = segment_key_for_path(message.data)
|
118
|
+
if key
|
119
|
+
@feature_store.upsert(SEGMENTS, key, @requestor.request_segment(key))
|
120
|
+
end
|
121
|
+
end
|
88
122
|
else
|
89
123
|
@config.logger.warn("[LDClient] Unknown message received: #{method}")
|
90
124
|
end
|
91
125
|
end
|
92
126
|
|
93
|
-
|
127
|
+
def key_for_path(kind, path)
|
128
|
+
path.start_with?(KEY_PATHS[kind]) ? path[KEY_PATHS[kind].length..-1] : nil
|
129
|
+
end
|
94
130
|
end
|
95
131
|
end
|
data/lib/ldclient-rb/version.rb
CHANGED
data/spec/evaluation_spec.rb
CHANGED
@@ -4,6 +4,14 @@ describe LaunchDarkly::Evaluation do
|
|
4
4
|
subject { LaunchDarkly::Evaluation }
|
5
5
|
let(:features) { LaunchDarkly::InMemoryFeatureStore.new }
|
6
6
|
|
7
|
+
let(:user) {
|
8
|
+
{
|
9
|
+
key: "userkey",
|
10
|
+
email: "test@example.com",
|
11
|
+
name: "Bob"
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
7
15
|
include LaunchDarkly::Evaluation
|
8
16
|
|
9
17
|
describe "evaluate" do
|
@@ -60,7 +68,7 @@ describe LaunchDarkly::Evaluation do
|
|
60
68
|
variations: ['d', 'e'],
|
61
69
|
version: 2
|
62
70
|
}
|
63
|
-
features.upsert(
|
71
|
+
features.upsert(LaunchDarkly::FEATURES, flag1)
|
64
72
|
user = { key: 'x' }
|
65
73
|
events_should_be = [{kind: 'feature', key: 'feature1', value: 'd', version: 2, prereqOf: 'feature0'}]
|
66
74
|
expect(evaluate(flag, user, features)).to eq({value: 'b', events: events_should_be})
|
@@ -83,7 +91,7 @@ describe LaunchDarkly::Evaluation do
|
|
83
91
|
variations: ['d', 'e'],
|
84
92
|
version: 2
|
85
93
|
}
|
86
|
-
features.upsert(
|
94
|
+
features.upsert(LaunchDarkly::FEATURES, flag1)
|
87
95
|
user = { key: 'x' }
|
88
96
|
events_should_be = [{kind: 'feature', key: 'feature1', value: 'e', version: 2, prereqOf: 'feature0'}]
|
89
97
|
expect(evaluate(flag, user, features)).to eq({value: 'a', events: events_should_be})
|
@@ -133,19 +141,47 @@ describe LaunchDarkly::Evaluation do
|
|
133
141
|
it "can match built-in attribute" do
|
134
142
|
user = { key: 'x', name: 'Bob' }
|
135
143
|
clause = { attribute: 'name', op: 'in', values: ['Bob'] }
|
136
|
-
expect(clause_match_user(clause, user)).to be true
|
144
|
+
expect(clause_match_user(clause, user, features)).to be true
|
137
145
|
end
|
138
146
|
|
139
147
|
it "can match custom attribute" do
|
140
148
|
user = { key: 'x', name: 'Bob', custom: { legs: 4 } }
|
141
149
|
clause = { attribute: 'legs', op: 'in', values: [4] }
|
142
|
-
expect(clause_match_user(clause, user)).to be true
|
150
|
+
expect(clause_match_user(clause, user, features)).to be true
|
143
151
|
end
|
144
152
|
|
145
153
|
it "returns false for missing attribute" do
|
146
154
|
user = { key: 'x', name: 'Bob' }
|
147
155
|
clause = { attribute: 'legs', op: 'in', values: [4] }
|
148
|
-
expect(clause_match_user(clause, user)).to be false
|
156
|
+
expect(clause_match_user(clause, user, features)).to be false
|
157
|
+
end
|
158
|
+
|
159
|
+
it "can be negated" do
|
160
|
+
user = { key: 'x', name: 'Bob' }
|
161
|
+
clause = { attribute: 'name', op: 'in', values: ['Bob'], negate: true }
|
162
|
+
expect(clause_match_user(clause, user, features)).to be false
|
163
|
+
end
|
164
|
+
|
165
|
+
it "retrieves segment from segment store for segmentMatch operator" do
|
166
|
+
segment = {
|
167
|
+
key: 'segkey',
|
168
|
+
included: [ 'userkey' ],
|
169
|
+
version: 1,
|
170
|
+
deleted: false
|
171
|
+
}
|
172
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
173
|
+
|
174
|
+
user = { key: 'userkey' }
|
175
|
+
clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
|
176
|
+
|
177
|
+
expect(clause_match_user(clause, user, features)).to be true
|
178
|
+
end
|
179
|
+
|
180
|
+
it "falls through with no errors if referenced segment is not found" do
|
181
|
+
user = { key: 'userkey' }
|
182
|
+
clause = { attribute: '', op: 'segmentMatch', values: ['segkey'] }
|
183
|
+
|
184
|
+
expect(clause_match_user(clause, user, features)).to be false
|
149
185
|
end
|
150
186
|
|
151
187
|
it "can be negated" do
|
@@ -153,7 +189,7 @@ describe LaunchDarkly::Evaluation do
|
|
153
189
|
clause = { attribute: 'name', op: 'in', values: ['Bob'] }
|
154
190
|
expect {
|
155
191
|
clause[:negate] = true
|
156
|
-
}.to change {clause_match_user(clause, user)}.from(true).to(false)
|
192
|
+
}.to change {clause_match_user(clause, user, features)}.from(true).to(false)
|
157
193
|
end
|
158
194
|
end
|
159
195
|
|
@@ -255,7 +291,7 @@ describe LaunchDarkly::Evaluation do
|
|
255
291
|
it "should return #{shouldBe} for #{value1} #{op} #{value2}" do
|
256
292
|
user = { key: 'x', custom: { foo: value1 } }
|
257
293
|
clause = { attribute: 'foo', op: op, values: [value2] }
|
258
|
-
expect(clause_match_user(clause, user)).to be shouldBe
|
294
|
+
expect(clause_match_user(clause, user, features)).to be shouldBe
|
259
295
|
end
|
260
296
|
end
|
261
297
|
end
|
@@ -313,4 +349,165 @@ describe LaunchDarkly::Evaluation do
|
|
313
349
|
expect(result).to eq(0.0)
|
314
350
|
end
|
315
351
|
end
|
352
|
+
|
353
|
+
def make_flag(key)
|
354
|
+
{
|
355
|
+
key: key,
|
356
|
+
rules: [],
|
357
|
+
variations: [ false, true ],
|
358
|
+
on: true,
|
359
|
+
fallthrough: { variation: 0 },
|
360
|
+
version: 1
|
361
|
+
}
|
362
|
+
end
|
363
|
+
|
364
|
+
def make_segment(key)
|
365
|
+
{
|
366
|
+
key: key,
|
367
|
+
included: [],
|
368
|
+
excluded: [],
|
369
|
+
salt: 'abcdef',
|
370
|
+
version: 1
|
371
|
+
}
|
372
|
+
end
|
373
|
+
|
374
|
+
def make_segment_match_clause(segment)
|
375
|
+
{
|
376
|
+
op: :segmentMatch,
|
377
|
+
values: [ segment[:key] ],
|
378
|
+
negate: false
|
379
|
+
}
|
380
|
+
end
|
381
|
+
|
382
|
+
def make_user_matching_clause(user, attr)
|
383
|
+
{
|
384
|
+
attribute: attr.to_s,
|
385
|
+
op: :in,
|
386
|
+
values: [ user[attr.to_sym] ],
|
387
|
+
negate: false
|
388
|
+
}
|
389
|
+
end
|
390
|
+
|
391
|
+
describe 'segment matching' do
|
392
|
+
it 'explicitly includes user' do
|
393
|
+
segment = make_segment('segkey')
|
394
|
+
segment[:included] = [ user[:key] ]
|
395
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
396
|
+
clause = make_segment_match_clause(segment)
|
397
|
+
|
398
|
+
result = clause_match_user(clause, user, features)
|
399
|
+
expect(result).to be true
|
400
|
+
end
|
401
|
+
|
402
|
+
it 'explicitly excludes user' do
|
403
|
+
segment = make_segment('segkey')
|
404
|
+
segment[:excluded] = [ user[:key] ]
|
405
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
406
|
+
clause = make_segment_match_clause(segment)
|
407
|
+
|
408
|
+
result = clause_match_user(clause, user, features)
|
409
|
+
expect(result).to be false
|
410
|
+
end
|
411
|
+
|
412
|
+
it 'both includes and excludes user; include takes priority' do
|
413
|
+
segment = make_segment('segkey')
|
414
|
+
segment[:included] = [ user[:key] ]
|
415
|
+
segment[:excluded] = [ user[:key] ]
|
416
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
417
|
+
clause = make_segment_match_clause(segment)
|
418
|
+
|
419
|
+
result = clause_match_user(clause, user, features)
|
420
|
+
expect(result).to be true
|
421
|
+
end
|
422
|
+
|
423
|
+
it 'matches user by rule when weight is absent' do
|
424
|
+
segClause = make_user_matching_clause(user, :email)
|
425
|
+
segRule = {
|
426
|
+
clauses: [ segClause ]
|
427
|
+
}
|
428
|
+
segment = make_segment('segkey')
|
429
|
+
segment[:rules] = [ segRule ]
|
430
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
431
|
+
clause = make_segment_match_clause(segment)
|
432
|
+
|
433
|
+
result = clause_match_user(clause, user, features)
|
434
|
+
expect(result).to be true
|
435
|
+
end
|
436
|
+
|
437
|
+
it 'matches user by rule when weight is nil' do
|
438
|
+
segClause = make_user_matching_clause(user, :email)
|
439
|
+
segRule = {
|
440
|
+
clauses: [ segClause ],
|
441
|
+
weight: nil
|
442
|
+
}
|
443
|
+
segment = make_segment('segkey')
|
444
|
+
segment[:rules] = [ segRule ]
|
445
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
446
|
+
clause = make_segment_match_clause(segment)
|
447
|
+
|
448
|
+
result = clause_match_user(clause, user, features)
|
449
|
+
expect(result).to be true
|
450
|
+
end
|
451
|
+
|
452
|
+
it 'matches user with full rollout' do
|
453
|
+
segClause = make_user_matching_clause(user, :email)
|
454
|
+
segRule = {
|
455
|
+
clauses: [ segClause ],
|
456
|
+
weight: 100000
|
457
|
+
}
|
458
|
+
segment = make_segment('segkey')
|
459
|
+
segment[:rules] = [ segRule ]
|
460
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
461
|
+
clause = make_segment_match_clause(segment)
|
462
|
+
|
463
|
+
result = clause_match_user(clause, user, features)
|
464
|
+
expect(result).to be true
|
465
|
+
end
|
466
|
+
|
467
|
+
it "doesn't match user with zero rollout" do
|
468
|
+
segClause = make_user_matching_clause(user, :email)
|
469
|
+
segRule = {
|
470
|
+
clauses: [ segClause ],
|
471
|
+
weight: 0
|
472
|
+
}
|
473
|
+
segment = make_segment('segkey')
|
474
|
+
segment[:rules] = [ segRule ]
|
475
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
476
|
+
clause = make_segment_match_clause(segment)
|
477
|
+
|
478
|
+
result = clause_match_user(clause, user, features)
|
479
|
+
expect(result).to be false
|
480
|
+
end
|
481
|
+
|
482
|
+
it "matches user with multiple clauses" do
|
483
|
+
segClause1 = make_user_matching_clause(user, :email)
|
484
|
+
segClause2 = make_user_matching_clause(user, :name)
|
485
|
+
segRule = {
|
486
|
+
clauses: [ segClause1, segClause2 ]
|
487
|
+
}
|
488
|
+
segment = make_segment('segkey')
|
489
|
+
segment[:rules] = [ segRule ]
|
490
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
491
|
+
clause = make_segment_match_clause(segment)
|
492
|
+
|
493
|
+
result = clause_match_user(clause, user, features)
|
494
|
+
expect(result).to be true
|
495
|
+
end
|
496
|
+
|
497
|
+
it "doesn't match user with multiple clauses if a clause doesn't match" do
|
498
|
+
segClause1 = make_user_matching_clause(user, :email)
|
499
|
+
segClause2 = make_user_matching_clause(user, :name)
|
500
|
+
segClause2[:values] = [ 'wrong' ]
|
501
|
+
segRule = {
|
502
|
+
clauses: [ segClause1, segClause2 ]
|
503
|
+
}
|
504
|
+
segment = make_segment('segkey')
|
505
|
+
segment[:rules] = [ segRule ]
|
506
|
+
features.upsert(LaunchDarkly::SEGMENTS, segment)
|
507
|
+
clause = make_segment_match_clause(segment)
|
508
|
+
|
509
|
+
result = clause_match_user(clause, user, features)
|
510
|
+
expect(result).to be false
|
511
|
+
end
|
512
|
+
end
|
316
513
|
end
|
@@ -31,7 +31,7 @@ RSpec.shared_examples "feature_store" do |create_store_method|
|
|
31
31
|
|
32
32
|
let!(:store) do
|
33
33
|
s = create_store_method.call()
|
34
|
-
s.init({ key0 => feature0 })
|
34
|
+
s.init(LaunchDarkly::FEATURES => { key0 => feature0 })
|
35
35
|
s
|
36
36
|
end
|
37
37
|
|
@@ -48,15 +48,15 @@ RSpec.shared_examples "feature_store" do |create_store_method|
|
|
48
48
|
end
|
49
49
|
|
50
50
|
it "can get existing feature with symbol key" do
|
51
|
-
expect(store.get(key0)).to eq feature0
|
51
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
|
52
52
|
end
|
53
53
|
|
54
54
|
it "can get existing feature with string key" do
|
55
|
-
expect(store.get(key0.to_s)).to eq feature0
|
55
|
+
expect(store.get(LaunchDarkly::FEATURES, key0.to_s)).to eq feature0
|
56
56
|
end
|
57
57
|
|
58
58
|
it "gets nil for nonexisting feature" do
|
59
|
-
expect(store.get('nope')).to be_nil
|
59
|
+
expect(store.get(LaunchDarkly::FEATURES, 'nope')).to be_nil
|
60
60
|
end
|
61
61
|
|
62
62
|
it "can get all features" do
|
@@ -64,8 +64,8 @@ RSpec.shared_examples "feature_store" do |create_store_method|
|
|
64
64
|
feature1[:key] = "test-feature-flag1"
|
65
65
|
feature1[:version] = 5
|
66
66
|
feature1[:on] = false
|
67
|
-
store.upsert(
|
68
|
-
expect(store.all).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
|
67
|
+
store.upsert(LaunchDarkly::FEATURES, feature1)
|
68
|
+
expect(store.all(LaunchDarkly::FEATURES)).to eq ({ key0 => feature0, :"test-feature-flag1" => feature1 })
|
69
69
|
end
|
70
70
|
|
71
71
|
it "can add new feature" do
|
@@ -73,40 +73,40 @@ RSpec.shared_examples "feature_store" do |create_store_method|
|
|
73
73
|
feature1[:key] = "test-feature-flag1"
|
74
74
|
feature1[:version] = 5
|
75
75
|
feature1[:on] = false
|
76
|
-
store.upsert(
|
77
|
-
expect(store.get(:"test-feature-flag1")).to eq feature1
|
76
|
+
store.upsert(LaunchDarkly::FEATURES, feature1)
|
77
|
+
expect(store.get(LaunchDarkly::FEATURES, :"test-feature-flag1")).to eq feature1
|
78
78
|
end
|
79
79
|
|
80
80
|
it "can update feature with newer version" do
|
81
81
|
f1 = new_version_plus(feature0, 1, { on: !feature0[:on] })
|
82
|
-
store.upsert(
|
83
|
-
expect(store.get(key0)).to eq f1
|
82
|
+
store.upsert(LaunchDarkly::FEATURES, f1)
|
83
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq f1
|
84
84
|
end
|
85
85
|
|
86
86
|
it "cannot update feature with same version" do
|
87
87
|
f1 = new_version_plus(feature0, 0, { on: !feature0[:on] })
|
88
|
-
store.upsert(
|
89
|
-
expect(store.get(key0)).to eq feature0
|
88
|
+
store.upsert(LaunchDarkly::FEATURES, f1)
|
89
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
|
90
90
|
end
|
91
91
|
|
92
92
|
it "cannot update feature with older version" do
|
93
93
|
f1 = new_version_plus(feature0, -1, { on: !feature0[:on] })
|
94
|
-
store.upsert(
|
95
|
-
expect(store.get(key0)).to eq feature0
|
94
|
+
store.upsert(LaunchDarkly::FEATURES, f1)
|
95
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
|
96
96
|
end
|
97
97
|
|
98
98
|
it "can delete feature with newer version" do
|
99
|
-
store.delete(key0, feature0[:version] + 1)
|
100
|
-
expect(store.get(key0)).to be_nil
|
99
|
+
store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] + 1)
|
100
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to be_nil
|
101
101
|
end
|
102
102
|
|
103
103
|
it "cannot delete feature with same version" do
|
104
|
-
store.delete(key0, feature0[:version])
|
105
|
-
expect(store.get(key0)).to eq feature0
|
104
|
+
store.delete(LaunchDarkly::FEATURES, key0, feature0[:version])
|
105
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
|
106
106
|
end
|
107
107
|
|
108
108
|
it "cannot delete feature with older version" do
|
109
|
-
store.delete(key0, feature0[:version] - 1)
|
110
|
-
expect(store.get(key0)).to eq feature0
|
109
|
+
store.delete(LaunchDarkly::FEATURES, key0, feature0[:version] - 1)
|
110
|
+
expect(store.get(LaunchDarkly::FEATURES, key0)).to eq feature0
|
111
111
|
end
|
112
112
|
end
|