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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +134 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +15 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +600 -0
- data/.simplecov +4 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +261 -0
- data/CODEOWNERS +1 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +13 -0
- data/README.md +56 -0
- data/Rakefile +5 -0
- data/azure-pipelines.yml +51 -0
- data/ext/mkrf_conf.rb +11 -0
- data/launchdarkly-server-sdk.gemspec +40 -0
- data/lib/ldclient-rb.rb +29 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +411 -0
- data/lib/ldclient-rb/evaluation.rb +455 -0
- data/lib/ldclient-rb/event_summarizer.rb +55 -0
- data/lib/ldclient-rb/events.rb +468 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/file_data_source.rb +312 -0
- data/lib/ldclient-rb/flags_state.rb +76 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +424 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/newrelic.rb +17 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +78 -0
- data/lib/ldclient-rb/redis_store.rb +87 -0
- data/lib/ldclient-rb/requestor.rb +101 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +141 -0
- data/lib/ldclient-rb/user_filter.rb +51 -0
- data/lib/ldclient-rb/util.rb +50 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/scripts/gendocs.sh +11 -0
- data/scripts/release.sh +27 -0
- data/spec/config_spec.rb +63 -0
- data/spec/evaluation_spec.rb +739 -0
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +642 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/feature_store_spec_base.rb +213 -0
- data/spec/file_data_source_spec.rb +255 -0
- data/spec/fixtures/feature.json +37 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/flags_state_spec.rb +81 -0
- data/spec/http_util.rb +109 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/integrations/consul_feature_store_spec.rb +42 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +471 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/polling_spec.rb +120 -0
- data/spec/redis_feature_store_spec.rb +95 -0
- data/spec/requestor_spec.rb +214 -0
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +60 -0
- data/spec/user_filter_spec.rb +91 -0
- data/spec/util_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- metadata +375 -0
@@ -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
|