launchdarkly-server-sdk 5.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,5 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::LDNewRelic do
4
+ subject { LaunchDarkly::LDNewRelic }
5
+ end
@@ -0,0 +1,120 @@
1
+ require "spec_helper"
2
+ require 'ostruct'
3
+
4
+ describe LaunchDarkly::PollingProcessor do
5
+ subject { LaunchDarkly::PollingProcessor }
6
+ let(:requestor) { double() }
7
+
8
+ def with_processor(store)
9
+ config = LaunchDarkly::Config.new(feature_store: store)
10
+ processor = subject.new(config, requestor)
11
+ begin
12
+ yield processor
13
+ ensure
14
+ processor.stop
15
+ end
16
+ end
17
+
18
+ describe 'successful request' do
19
+ flag = { key: 'flagkey', version: 1 }
20
+ segment = { key: 'segkey', version: 1 }
21
+ all_data = {
22
+ flags: {
23
+ flagkey: flag
24
+ },
25
+ segments: {
26
+ segkey: segment
27
+ }
28
+ }
29
+
30
+ it 'puts feature data in store' do
31
+ allow(requestor).to receive(:request_all_data).and_return(all_data)
32
+ store = LaunchDarkly::InMemoryFeatureStore.new
33
+ with_processor(store) do |processor|
34
+ ready = processor.start
35
+ ready.wait
36
+ expect(store.get(LaunchDarkly::FEATURES, "flagkey")).to eq(flag)
37
+ expect(store.get(LaunchDarkly::SEGMENTS, "segkey")).to eq(segment)
38
+ end
39
+ end
40
+
41
+ it 'sets initialized to true' do
42
+ allow(requestor).to receive(:request_all_data).and_return(all_data)
43
+ store = LaunchDarkly::InMemoryFeatureStore.new
44
+ with_processor(store) do |processor|
45
+ ready = processor.start
46
+ ready.wait
47
+ expect(processor.initialized?).to be true
48
+ expect(store.initialized?).to be true
49
+ end
50
+ end
51
+ end
52
+
53
+ describe 'connection error' do
54
+ it 'does not cause immediate failure, does not set initialized' do
55
+ allow(requestor).to receive(:request_all_data).and_raise(StandardError.new("test error"))
56
+ store = LaunchDarkly::InMemoryFeatureStore.new
57
+ with_processor(store) do |processor|
58
+ ready = processor.start
59
+ finished = ready.wait(0.2)
60
+ expect(finished).to be false
61
+ expect(processor.initialized?).to be false
62
+ expect(store.initialized?).to be false
63
+ end
64
+ end
65
+ end
66
+
67
+ describe 'HTTP errors' do
68
+ def verify_unrecoverable_http_error(status)
69
+ allow(requestor).to receive(:request_all_data).and_raise(LaunchDarkly::UnexpectedResponseError.new(status))
70
+ with_processor(LaunchDarkly::InMemoryFeatureStore.new) do |processor|
71
+ ready = processor.start
72
+ finished = ready.wait(0.2)
73
+ expect(finished).to be true
74
+ expect(processor.initialized?).to be false
75
+ end
76
+ end
77
+
78
+ def verify_recoverable_http_error(status)
79
+ allow(requestor).to receive(:request_all_data).and_raise(LaunchDarkly::UnexpectedResponseError.new(status))
80
+ with_processor(LaunchDarkly::InMemoryFeatureStore.new) do |processor|
81
+ ready = processor.start
82
+ finished = ready.wait(0.2)
83
+ expect(finished).to be false
84
+ expect(processor.initialized?).to be false
85
+ end
86
+ end
87
+
88
+ it 'stops immediately for error 401' do
89
+ verify_unrecoverable_http_error(401)
90
+ end
91
+
92
+ it 'stops immediately for error 403' do
93
+ verify_unrecoverable_http_error(403)
94
+ end
95
+
96
+ it 'does not stop immediately for error 408' do
97
+ verify_recoverable_http_error(408)
98
+ end
99
+
100
+ it 'does not stop immediately for error 429' do
101
+ verify_recoverable_http_error(429)
102
+ end
103
+
104
+ it 'does not stop immediately for error 503' do
105
+ verify_recoverable_http_error(503)
106
+ end
107
+ end
108
+
109
+ describe 'stop' do
110
+ it 'stops promptly rather than continuing to wait for poll interval' do
111
+ with_processor(LaunchDarkly::InMemoryFeatureStore.new) do |processor|
112
+ sleep(1) # somewhat arbitrary, but should ensure that it has started polling
113
+ start_time = Time.now
114
+ processor.stop
115
+ end_time = Time.now
116
+ expect(end_time - start_time).to be <(LaunchDarkly::Config.default_poll_interval - 5)
117
+ end
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,95 @@
1
+ require "feature_store_spec_base"
2
+ require "json"
3
+ require "redis"
4
+ require "spec_helper"
5
+
6
+
7
+
8
+ $my_prefix = 'testprefix'
9
+ $null_log = ::Logger.new($stdout)
10
+ $null_log.level = ::Logger::FATAL
11
+
12
+ $base_opts = {
13
+ prefix: $my_prefix,
14
+ logger: $null_log
15
+ }
16
+
17
+ def create_redis_store(opts = {})
18
+ LaunchDarkly::RedisFeatureStore.new($base_opts.merge(opts).merge({ expiration: 60 }))
19
+ end
20
+
21
+ def create_redis_store_uncached(opts = {})
22
+ LaunchDarkly::RedisFeatureStore.new($base_opts.merge(opts).merge({ expiration: 0 }))
23
+ end
24
+
25
+ def clear_all_data
26
+ client = Redis.new
27
+ client.flushdb
28
+ end
29
+
30
+
31
+ describe LaunchDarkly::RedisFeatureStore do
32
+ subject { LaunchDarkly::RedisFeatureStore }
33
+
34
+ return if ENV['LD_SKIP_DATABASE_TESTS'] == '1'
35
+
36
+ # These tests will all fail if there isn't a Redis instance running on the default port.
37
+
38
+ context "real Redis with local cache" do
39
+ include_examples "feature_store", method(:create_redis_store), method(:clear_all_data)
40
+ end
41
+
42
+ context "real Redis without local cache" do
43
+ include_examples "feature_store", method(:create_redis_store_uncached), method(:clear_all_data)
44
+ end
45
+
46
+ def make_concurrent_modifier_test_hook(other_client, flag, start_version, end_version)
47
+ test_hook = Object.new
48
+ version_counter = start_version
49
+ expect(test_hook).to receive(:before_update_transaction) { |base_key, key|
50
+ if version_counter <= end_version
51
+ new_flag = flag.clone
52
+ new_flag[:version] = version_counter
53
+ other_client.hset(base_key, key, new_flag.to_json)
54
+ version_counter = version_counter + 1
55
+ end
56
+ }.at_least(:once)
57
+ test_hook
58
+ end
59
+
60
+ it "handles upsert race condition against external client with lower version" do
61
+ other_client = Redis.new({ url: "redis://localhost:6379" })
62
+ flag = { key: "foo", version: 1 }
63
+ test_hook = make_concurrent_modifier_test_hook(other_client, flag, 2, 4)
64
+ store = create_redis_store({ test_hook: test_hook })
65
+
66
+ begin
67
+ store.init(LaunchDarkly::FEATURES => { flag[:key] => flag })
68
+
69
+ my_ver = { key: "foo", version: 10 }
70
+ store.upsert(LaunchDarkly::FEATURES, my_ver)
71
+ result = store.get(LaunchDarkly::FEATURES, flag[:key])
72
+ expect(result[:version]).to eq 10
73
+ ensure
74
+ other_client.close
75
+ end
76
+ end
77
+
78
+ it "handles upsert race condition against external client with higher version" do
79
+ other_client = Redis.new({ url: "redis://localhost:6379" })
80
+ flag = { key: "foo", version: 1 }
81
+ test_hook = make_concurrent_modifier_test_hook(other_client, flag, 3, 3)
82
+ store = create_redis_store({ test_hook: test_hook })
83
+
84
+ begin
85
+ store.init(LaunchDarkly::FEATURES => { flag[:key] => flag })
86
+
87
+ my_ver = { key: "foo", version: 2 }
88
+ store.upsert(LaunchDarkly::FEATURES, my_ver)
89
+ result = store.get(LaunchDarkly::FEATURES, flag[:key])
90
+ expect(result[:version]).to eq 3
91
+ ensure
92
+ other_client.close
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,214 @@
1
+ require "http_util"
2
+ require "spec_helper"
3
+
4
+ $sdk_key = "secret"
5
+
6
+ describe LaunchDarkly::Requestor do
7
+ def with_requestor(base_uri)
8
+ r = LaunchDarkly::Requestor.new($sdk_key, LaunchDarkly::Config.new(base_uri: base_uri))
9
+ yield r
10
+ r.stop
11
+ end
12
+
13
+ describe "request_all_flags" do
14
+ it "uses expected URI and headers" do
15
+ with_server do |server|
16
+ with_requestor(server.base_uri.to_s) do |requestor|
17
+ server.setup_ok_response("/", "{}")
18
+ requestor.request_all_data()
19
+ expect(server.requests.count).to eq 1
20
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-all"
21
+ expect(server.requests[0].header).to include({
22
+ "authorization" => [ $sdk_key ],
23
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
24
+ })
25
+ end
26
+ end
27
+ end
28
+
29
+ it "parses response" do
30
+ expected_data = { flags: { x: { key: "x" } } }
31
+ with_server do |server|
32
+ with_requestor(server.base_uri.to_s) do |requestor|
33
+ server.setup_ok_response("/", expected_data.to_json)
34
+ data = requestor.request_all_data()
35
+ expect(data).to eq expected_data
36
+ end
37
+ end
38
+ end
39
+
40
+ it "sends etag from previous response" do
41
+ etag = "xyz"
42
+ with_server do |server|
43
+ with_requestor(server.base_uri.to_s) do |requestor|
44
+ server.setup_response("/") do |req, res|
45
+ res.status = 200
46
+ res.body = "{}"
47
+ res["ETag"] = etag
48
+ end
49
+ requestor.request_all_data()
50
+ expect(server.requests.count).to eq 1
51
+
52
+ requestor.request_all_data()
53
+ expect(server.requests.count).to eq 2
54
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag ] })
55
+ end
56
+ end
57
+ end
58
+
59
+ it "can reuse cached data" do
60
+ etag = "xyz"
61
+ expected_data = { flags: { x: { key: "x" } } }
62
+ with_server do |server|
63
+ with_requestor(server.base_uri.to_s) do |requestor|
64
+ server.setup_response("/") do |req, res|
65
+ res.status = 200
66
+ res.body = expected_data.to_json
67
+ res["ETag"] = etag
68
+ end
69
+ requestor.request_all_data()
70
+ expect(server.requests.count).to eq 1
71
+
72
+ server.setup_response("/") do |req, res|
73
+ res.status = 304
74
+ end
75
+ data = requestor.request_all_data()
76
+ expect(server.requests.count).to eq 2
77
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag ] })
78
+ expect(data).to eq expected_data
79
+ end
80
+ end
81
+ end
82
+
83
+ it "replaces cached data with new data" do
84
+ etag1 = "abc"
85
+ etag2 = "xyz"
86
+ expected_data1 = { flags: { x: { key: "x" } } }
87
+ expected_data2 = { flags: { y: { key: "y" } } }
88
+ with_server do |server|
89
+ with_requestor(server.base_uri.to_s) do |requestor|
90
+ server.setup_response("/") do |req, res|
91
+ res.status = 200
92
+ res.body = expected_data1.to_json
93
+ res["ETag"] = etag1
94
+ end
95
+ data = requestor.request_all_data()
96
+ expect(data).to eq expected_data1
97
+ expect(server.requests.count).to eq 1
98
+
99
+ server.setup_response("/") do |req, res|
100
+ res.status = 304
101
+ end
102
+ data = requestor.request_all_data()
103
+ expect(data).to eq expected_data1
104
+ expect(server.requests.count).to eq 2
105
+ expect(server.requests[1].header).to include({ "if-none-match" => [ etag1 ] })
106
+
107
+ server.setup_response("/") do |req, res|
108
+ res.status = 200
109
+ res.body = expected_data2.to_json
110
+ res["ETag"] = etag2
111
+ end
112
+ data = requestor.request_all_data()
113
+ expect(data).to eq expected_data2
114
+ expect(server.requests.count).to eq 3
115
+ expect(server.requests[2].header).to include({ "if-none-match" => [ etag1 ] })
116
+
117
+ server.setup_response("/") do |req, res|
118
+ res.status = 304
119
+ end
120
+ data = requestor.request_all_data()
121
+ expect(data).to eq expected_data2
122
+ expect(server.requests.count).to eq 4
123
+ expect(server.requests[3].header).to include({ "if-none-match" => [ etag2 ] })
124
+ end
125
+ end
126
+ end
127
+
128
+ it "uses UTF-8 encoding by default" do
129
+ content = '{"flags": {"flagkey": {"key": "flagkey", "variations": ["blue", "grėeń"]}}}'
130
+ with_server do |server|
131
+ server.setup_ok_response("/sdk/latest-all", content, "application/json")
132
+ with_requestor(server.base_uri.to_s) do |requestor|
133
+ data = requestor.request_all_data
134
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
135
+ end
136
+ end
137
+ end
138
+
139
+ it "detects other encodings from Content-Type" do
140
+ content = '{"flags": {"flagkey": {"key": "flagkey", "variations": ["proszę", "dziękuję"]}}}'
141
+ with_server do |server|
142
+ server.setup_ok_response("/sdk/latest-all", content.encode(Encoding::ISO_8859_2),
143
+ "text/plain; charset=ISO-8859-2")
144
+ with_requestor(server.base_uri.to_s) do |requestor|
145
+ data = requestor.request_all_data
146
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
147
+ end
148
+ end
149
+ end
150
+
151
+ it "throws exception for error status" do
152
+ with_server do |server|
153
+ with_requestor(server.base_uri.to_s) do |requestor|
154
+ server.setup_response("/") do |req, res|
155
+ res.status = 400
156
+ end
157
+ expect { requestor.request_all_data() }.to raise_error(LaunchDarkly::UnexpectedResponseError)
158
+ end
159
+ end
160
+ end
161
+
162
+ it "can use a proxy server" do
163
+ content = '{"flags": {"flagkey": {"key": "flagkey"}}}'
164
+ with_server do |server|
165
+ server.setup_ok_response("/sdk/latest-all", content, "application/json", { "etag" => "x" })
166
+ with_server(StubProxyServer.new) do |proxy|
167
+ begin
168
+ ENV["http_proxy"] = proxy.base_uri.to_s
169
+ with_requestor(server.base_uri.to_s) do |requestor|
170
+ data = requestor.request_all_data
171
+ expect(data).to eq(JSON.parse(content, symbolize_names: true))
172
+ end
173
+ ensure
174
+ ENV["http_proxy"] = nil
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ describe "request_flag" do
182
+ it "uses expected URI and headers" do
183
+ with_server do |server|
184
+ with_requestor(server.base_uri.to_s) do |requestor|
185
+ server.setup_ok_response("/", "{}")
186
+ requestor.request_flag("key")
187
+ expect(server.requests.count).to eq 1
188
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-flags/key"
189
+ expect(server.requests[0].header).to include({
190
+ "authorization" => [ $sdk_key ],
191
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
192
+ })
193
+ end
194
+ end
195
+ end
196
+ end
197
+
198
+ describe "request_segment" do
199
+ it "uses expected URI and headers" do
200
+ with_server do |server|
201
+ with_requestor(server.base_uri.to_s) do |requestor|
202
+ server.setup_ok_response("/", "{}")
203
+ requestor.request_segment("key")
204
+ expect(server.requests.count).to eq 1
205
+ expect(server.requests[0].unparsed_uri).to eq "/sdk/latest-segments/key"
206
+ expect(server.requests[0].header).to include({
207
+ "authorization" => [ $sdk_key ],
208
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ]
209
+ })
210
+ end
211
+ end
212
+ end
213
+ end
214
+ end
@@ -0,0 +1,95 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.shared_examples "segment_store" do |create_store_method|
4
+
5
+ let(:segment0) {
6
+ {
7
+ key: "test-segment",
8
+ version: 11,
9
+ salt: "718ea30a918a4eba8734b57ab1a93227",
10
+ rules: []
11
+ }
12
+ }
13
+ let(:key0) { segment0[:key].to_sym }
14
+
15
+ let!(:store) do
16
+ s = create_store_method.call()
17
+ s.init({ key0 => segment0 })
18
+ s
19
+ end
20
+
21
+ def new_version_plus(f, deltaVersion, attrs = {})
22
+ f1 = f.clone
23
+ f1[:version] = f[:version] + deltaVersion
24
+ f1.update(attrs)
25
+ f1
26
+ end
27
+
28
+
29
+ it "is initialized" do
30
+ expect(store.initialized?).to eq true
31
+ end
32
+
33
+ it "can get existing feature with symbol key" do
34
+ expect(store.get(key0)).to eq segment0
35
+ end
36
+
37
+ it "can get existing feature with string key" do
38
+ expect(store.get(key0.to_s)).to eq segment0
39
+ end
40
+
41
+ it "gets nil for nonexisting feature" do
42
+ expect(store.get('nope')).to be_nil
43
+ end
44
+
45
+ it "can get all features" do
46
+ feature1 = segment0.clone
47
+ feature1[:key] = "test-feature-flag1"
48
+ feature1[:version] = 5
49
+ feature1[:on] = false
50
+ store.upsert(:"test-feature-flag1", feature1)
51
+ expect(store.all).to eq ({ key0 => segment0, :"test-feature-flag1" => feature1 })
52
+ end
53
+
54
+ it "can add new feature" do
55
+ feature1 = segment0.clone
56
+ feature1[:key] = "test-feature-flag1"
57
+ feature1[:version] = 5
58
+ feature1[:on] = false
59
+ store.upsert(:"test-feature-flag1", feature1)
60
+ expect(store.get(:"test-feature-flag1")).to eq feature1
61
+ end
62
+
63
+ it "can update feature with newer version" do
64
+ f1 = new_version_plus(segment0, 1, { on: !segment0[:on] })
65
+ store.upsert(key0, f1)
66
+ expect(store.get(key0)).to eq f1
67
+ end
68
+
69
+ it "cannot update feature with same version" do
70
+ f1 = new_version_plus(segment0, 0, { on: !segment0[:on] })
71
+ store.upsert(key0, f1)
72
+ expect(store.get(key0)).to eq segment0
73
+ end
74
+
75
+ it "cannot update feature with older version" do
76
+ f1 = new_version_plus(segment0, -1, { on: !segment0[:on] })
77
+ store.upsert(key0, f1)
78
+ expect(store.get(key0)).to eq segment0
79
+ end
80
+
81
+ it "can delete feature with newer version" do
82
+ store.delete(key0, segment0[:version] + 1)
83
+ expect(store.get(key0)).to be_nil
84
+ end
85
+
86
+ it "cannot delete feature with same version" do
87
+ store.delete(key0, segment0[:version])
88
+ expect(store.get(key0)).to eq segment0
89
+ end
90
+
91
+ it "cannot delete feature with older version" do
92
+ store.delete(key0, segment0[:version] - 1)
93
+ expect(store.get(key0)).to eq segment0
94
+ end
95
+ end