ldclient-rb 2.5.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|