launchdarkly-server-sdk 5.5.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.
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