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,37 @@
1
+ {
2
+ "key":"test-feature-flag",
3
+ "version":11,
4
+ "on":true,
5
+ "prerequisites":[
6
+
7
+ ],
8
+ "salt":"718ea30a918a4eba8734b57ab1a93227",
9
+ "sel":"fe1244e5378c4f99976c9634e33667c6",
10
+ "targets":[
11
+ {
12
+ "values":[
13
+ "alice"
14
+ ],
15
+ "variation":0
16
+ },
17
+ {
18
+ "values":[
19
+ "bob"
20
+ ],
21
+ "variation":1
22
+ }
23
+ ],
24
+ "rules":[
25
+
26
+ ],
27
+ "fallthrough":{
28
+ "variation":0
29
+ },
30
+ "offVariation":1,
31
+ "variations":[
32
+ true,
33
+ false
34
+ ],
35
+ "trackEvents": true,
36
+ "deleted":false
37
+ }
@@ -0,0 +1,36 @@
1
+ {
2
+ "key":"test-feature-flag1",
3
+ "version":5,
4
+ "on":false,
5
+ "prerequisites":[
6
+
7
+ ],
8
+ "salt":"718ea30a918a4eba8734b57ab1a93227",
9
+ "sel":"fe1244e5378c4f99976c9634e33667c6",
10
+ "targets":[
11
+ {
12
+ "values":[
13
+ "alice"
14
+ ],
15
+ "variation":0
16
+ },
17
+ {
18
+ "values":[
19
+ "bob"
20
+ ],
21
+ "variation":1
22
+ }
23
+ ],
24
+ "rules":[
25
+
26
+ ],
27
+ "fallthrough":{
28
+ "variation":0
29
+ },
30
+ "offVariation":1,
31
+ "variations":[
32
+ true,
33
+ false
34
+ ],
35
+ "deleted":false
36
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "key":"user@test.com",
3
+ "custom":{
4
+ "groups":[
5
+ "microsoft",
6
+ "google"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,81 @@
1
+ require "spec_helper"
2
+ require "json"
3
+
4
+ describe LaunchDarkly::FeatureFlagsState do
5
+ subject { LaunchDarkly::FeatureFlagsState }
6
+
7
+ it "can get flag value" do
8
+ state = subject.new(true)
9
+ flag = { key: 'key' }
10
+ state.add_flag(flag, 'value', 1)
11
+
12
+ expect(state.flag_value('key')).to eq 'value'
13
+ end
14
+
15
+ it "returns nil for unknown flag" do
16
+ state = subject.new(true)
17
+
18
+ expect(state.flag_value('key')).to be nil
19
+ end
20
+
21
+ it "can be converted to values map" do
22
+ state = subject.new(true)
23
+ flag1 = { key: 'key1' }
24
+ flag2 = { key: 'key2' }
25
+ state.add_flag(flag1, 'value1', 0)
26
+ state.add_flag(flag2, 'value2', 1)
27
+
28
+ expect(state.values_map).to eq({ 'key1' => 'value1', 'key2' => 'value2' })
29
+ end
30
+
31
+ it "can be converted to JSON structure" do
32
+ state = subject.new(true)
33
+ flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
34
+ flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
35
+ state.add_flag(flag1, 'value1', 0)
36
+ state.add_flag(flag2, 'value2', 1)
37
+
38
+ result = state.as_json
39
+ expect(result).to eq({
40
+ 'key1' => 'value1',
41
+ 'key2' => 'value2',
42
+ '$flagsState' => {
43
+ 'key1' => {
44
+ :variation => 0,
45
+ :version => 100
46
+ },
47
+ 'key2' => {
48
+ :variation => 1,
49
+ :version => 200,
50
+ :trackEvents => true,
51
+ :debugEventsUntilDate => 1000
52
+ }
53
+ },
54
+ '$valid' => true
55
+ })
56
+ end
57
+
58
+ it "can be converted to JSON string" do
59
+ state = subject.new(true)
60
+ flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
61
+ flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
62
+ state.add_flag(flag1, 'value1', 0)
63
+ state.add_flag(flag2, 'value2', 1)
64
+
65
+ object = state.as_json
66
+ str = state.to_json
67
+ expect(object.to_json).to eq(str)
68
+ end
69
+
70
+ it "uses our custom serializer with JSON.generate" do
71
+ state = subject.new(true)
72
+ flag1 = { key: "key1", version: 100, offVariation: 0, variations: [ 'value1' ], trackEvents: false }
73
+ flag2 = { key: "key2", version: 200, offVariation: 1, variations: [ 'x', 'value2' ], trackEvents: true, debugEventsUntilDate: 1000 }
74
+ state.add_flag(flag1, 'value1', 0)
75
+ state.add_flag(flag2, 'value2', 1)
76
+
77
+ stringFromToJson = state.to_json
78
+ stringFromGenerate = JSON.generate(state)
79
+ expect(stringFromGenerate).to eq(stringFromToJson)
80
+ end
81
+ end
data/spec/http_util.rb ADDED
@@ -0,0 +1,109 @@
1
+ require "webrick"
2
+ require "webrick/httpproxy"
3
+ require "webrick/https"
4
+
5
+ class StubHTTPServer
6
+ attr_reader :requests
7
+
8
+ @@next_port = 50000
9
+
10
+ def initialize
11
+ @port = StubHTTPServer.next_port
12
+ begin
13
+ base_opts = {
14
+ BindAddress: '127.0.0.1',
15
+ Port: @port,
16
+ AccessLog: [],
17
+ Logger: NullLogger.new,
18
+ RequestCallback: method(:record_request)
19
+ }
20
+ @server = create_server(@port, base_opts)
21
+ rescue Errno::EADDRINUSE
22
+ @port = StubHTTPServer.next_port
23
+ retry
24
+ end
25
+ @requests = []
26
+ @requests_queue = Queue.new
27
+ end
28
+
29
+ def self.next_port
30
+ p = @@next_port
31
+ @@next_port = (p + 1 < 60000) ? p + 1 : 50000
32
+ p
33
+ end
34
+
35
+ def create_server(port, base_opts)
36
+ WEBrick::HTTPServer.new(base_opts)
37
+ end
38
+
39
+ def start
40
+ Thread.new { @server.start }
41
+ end
42
+
43
+ def stop
44
+ @server.shutdown
45
+ end
46
+
47
+ def base_uri
48
+ URI("http://127.0.0.1:#{@port}")
49
+ end
50
+
51
+ def setup_response(uri_path, &action)
52
+ @server.mount_proc(uri_path, action)
53
+ end
54
+
55
+ def setup_ok_response(uri_path, body, content_type=nil, headers={})
56
+ setup_response(uri_path) do |req, res|
57
+ res.status = 200
58
+ res.content_type = content_type if !content_type.nil?
59
+ res.body = body
60
+ headers.each { |n, v| res[n] = v }
61
+ end
62
+ end
63
+
64
+ def record_request(req, res)
65
+ @requests.push(req)
66
+ @requests_queue << req
67
+ end
68
+
69
+ def await_request
70
+ @requests_queue.pop
71
+ end
72
+ end
73
+
74
+ class StubProxyServer < StubHTTPServer
75
+ attr_reader :request_count
76
+ attr_accessor :connect_status
77
+
78
+ def initialize
79
+ super
80
+ @request_count = 0
81
+ end
82
+
83
+ def create_server(port, base_opts)
84
+ WEBrick::HTTPProxyServer.new(base_opts.merge({
85
+ ProxyContentHandler: proc do |req,res|
86
+ if !@connect_status.nil?
87
+ res.status = @connect_status
88
+ end
89
+ @request_count += 1
90
+ end
91
+ }))
92
+ end
93
+ end
94
+
95
+ class NullLogger
96
+ def method_missing(*)
97
+ self
98
+ end
99
+ end
100
+
101
+ def with_server(server = nil)
102
+ server = StubHTTPServer.new if server.nil?
103
+ begin
104
+ server.start
105
+ yield server
106
+ ensure
107
+ server.stop
108
+ end
109
+ end
@@ -0,0 +1,12 @@
1
+ require "feature_store_spec_base"
2
+ require "spec_helper"
3
+
4
+ def create_in_memory_store(opts = {})
5
+ LaunchDarkly::InMemoryFeatureStore.new
6
+ end
7
+
8
+ describe LaunchDarkly::InMemoryFeatureStore do
9
+ subject { LaunchDarkly::InMemoryFeatureStore }
10
+
11
+ include_examples "feature_store", method(:create_in_memory_store)
12
+ end
@@ -0,0 +1,42 @@
1
+ require "feature_store_spec_base"
2
+ require "diplomat"
3
+ require "spec_helper"
4
+
5
+
6
+ $my_prefix = 'testprefix'
7
+ $null_log = ::Logger.new($stdout)
8
+ $null_log.level = ::Logger::FATAL
9
+
10
+ $consul_base_opts = {
11
+ prefix: $my_prefix,
12
+ logger: $null_log
13
+ }
14
+
15
+ def create_consul_store(opts = {})
16
+ LaunchDarkly::Integrations::Consul::new_feature_store(
17
+ $consul_base_opts.merge(opts).merge({ expiration: 60 }))
18
+ end
19
+
20
+ def create_consul_store_uncached(opts = {})
21
+ LaunchDarkly::Integrations::Consul::new_feature_store(
22
+ $consul_base_opts.merge(opts).merge({ expiration: 0 }))
23
+ end
24
+
25
+ def clear_all_data
26
+ Diplomat::Kv.delete($my_prefix + '/', recurse: true)
27
+ end
28
+
29
+
30
+ describe "Consul feature store" do
31
+ return if ENV['LD_SKIP_DATABASE_TESTS'] == '1'
32
+
33
+ # These tests will all fail if there isn't a local Consul instance running.
34
+
35
+ context "with local cache" do
36
+ include_examples "feature_store", method(:create_consul_store), method(:clear_all_data)
37
+ end
38
+
39
+ context "without local cache" do
40
+ include_examples "feature_store", method(:create_consul_store_uncached), method(:clear_all_data)
41
+ end
42
+ end
@@ -0,0 +1,105 @@
1
+ require "feature_store_spec_base"
2
+ require "aws-sdk-dynamodb"
3
+ require "spec_helper"
4
+
5
+
6
+ $table_name = 'LD_DYNAMODB_TEST_TABLE'
7
+ $endpoint = 'http://localhost:8000'
8
+ $my_prefix = 'testprefix'
9
+ $null_log = ::Logger.new($stdout)
10
+ $null_log.level = ::Logger::FATAL
11
+
12
+ $dynamodb_opts = {
13
+ credentials: Aws::Credentials.new("key", "secret"),
14
+ region: "us-east-1",
15
+ endpoint: $endpoint
16
+ }
17
+
18
+ $ddb_base_opts = {
19
+ dynamodb_opts: $dynamodb_opts,
20
+ prefix: $my_prefix,
21
+ logger: $null_log
22
+ }
23
+
24
+ def create_dynamodb_store(opts = {})
25
+ LaunchDarkly::Integrations::DynamoDB::new_feature_store($table_name,
26
+ $ddb_base_opts.merge(opts).merge({ expiration: 60 }))
27
+ end
28
+
29
+ def create_dynamodb_store_uncached(opts = {})
30
+ LaunchDarkly::Integrations::DynamoDB::new_feature_store($table_name,
31
+ $ddb_base_opts.merge(opts).merge({ expiration: 0 }))
32
+ end
33
+
34
+ def clear_all_data
35
+ client = create_test_client
36
+ items_to_delete = []
37
+ req = {
38
+ table_name: $table_name,
39
+ projection_expression: '#namespace, #key',
40
+ expression_attribute_names: {
41
+ '#namespace' => 'namespace',
42
+ '#key' => 'key'
43
+ }
44
+ }
45
+ while true
46
+ resp = client.scan(req)
47
+ items_to_delete = items_to_delete + resp.items
48
+ break if resp.last_evaluated_key.nil? || resp.last_evaluated_key.length == 0
49
+ req.exclusive_start_key = resp.last_evaluated_key
50
+ end
51
+ requests = items_to_delete.map do |item|
52
+ { delete_request: { key: item } }
53
+ end
54
+ LaunchDarkly::Impl::Integrations::DynamoDB::DynamoDBUtil.batch_write_requests(client, $table_name, requests)
55
+ end
56
+
57
+ def create_table_if_necessary
58
+ client = create_test_client
59
+ begin
60
+ client.describe_table({ table_name: $table_name })
61
+ return # no error, table exists
62
+ rescue Aws::DynamoDB::Errors::ResourceNotFoundException
63
+ # fall through to code below - we'll create the table
64
+ end
65
+
66
+ req = {
67
+ table_name: $table_name,
68
+ key_schema: [
69
+ { attribute_name: "namespace", key_type: "HASH" },
70
+ { attribute_name: "key", key_type: "RANGE" }
71
+ ],
72
+ attribute_definitions: [
73
+ { attribute_name: "namespace", attribute_type: "S" },
74
+ { attribute_name: "key", attribute_type: "S" }
75
+ ],
76
+ provisioned_throughput: {
77
+ read_capacity_units: 1,
78
+ write_capacity_units: 1
79
+ }
80
+ }
81
+ client.create_table(req)
82
+
83
+ # When DynamoDB creates a table, it may not be ready to use immediately
84
+ end
85
+
86
+ def create_test_client
87
+ Aws::DynamoDB::Client.new($dynamodb_opts)
88
+ end
89
+
90
+
91
+ describe "DynamoDB feature store" do
92
+ return if ENV['LD_SKIP_DATABASE_TESTS'] == '1'
93
+
94
+ # These tests will all fail if there isn't a local DynamoDB instance running.
95
+
96
+ create_table_if_necessary
97
+
98
+ context "with local cache" do
99
+ include_examples "feature_store", method(:create_dynamodb_store), method(:clear_all_data)
100
+ end
101
+
102
+ context "without local cache" do
103
+ include_examples "feature_store", method(:create_dynamodb_store_uncached), method(:clear_all_data)
104
+ end
105
+ end
@@ -0,0 +1,276 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::Integrations::Util::CachingStoreWrapper do
4
+ subject { LaunchDarkly::Integrations::Util::CachingStoreWrapper }
5
+
6
+ THINGS = { namespace: "things" }
7
+
8
+ shared_examples "tests" do |cached|
9
+ opts = cached ? { expiration: 30 } : { expiration: 0 }
10
+
11
+ it "gets item" do
12
+ core = MockCore.new
13
+ wrapper = subject.new(core, opts)
14
+ key = "flag"
15
+ itemv1 = { key: key, version: 1 }
16
+ itemv2 = { key: key, version: 2 }
17
+
18
+ core.force_set(THINGS, itemv1)
19
+ expect(wrapper.get(THINGS, key)).to eq itemv1
20
+
21
+ core.force_set(THINGS, itemv2)
22
+ expect(wrapper.get(THINGS, key)).to eq (cached ? itemv1 : itemv2) # if cached, we will not see the new underlying value yet
23
+ end
24
+
25
+ it "gets deleted item" do
26
+ core = MockCore.new
27
+ wrapper = subject.new(core, opts)
28
+ key = "flag"
29
+ itemv1 = { key: key, version: 1, deleted: true }
30
+ itemv2 = { key: key, version: 2, deleted: false }
31
+
32
+ core.force_set(THINGS, itemv1)
33
+ expect(wrapper.get(THINGS, key)).to eq nil # item is filtered out because deleted is true
34
+
35
+ core.force_set(THINGS, itemv2)
36
+ expect(wrapper.get(THINGS, key)).to eq (cached ? nil : itemv2) # if cached, we will not see the new underlying value yet
37
+ end
38
+
39
+ it "gets missing item" do
40
+ core = MockCore.new
41
+ wrapper = subject.new(core, opts)
42
+ key = "flag"
43
+ item = { key: key, version: 1 }
44
+
45
+ expect(wrapper.get(THINGS, key)).to eq nil
46
+
47
+ core.force_set(THINGS, item)
48
+ expect(wrapper.get(THINGS, key)).to eq (cached ? nil : item) # the cache can retain a nil result
49
+ end
50
+
51
+ it "gets all items" do
52
+ core = MockCore.new
53
+ wrapper = subject.new(core, opts)
54
+ item1 = { key: "flag1", version: 1 }
55
+ item2 = { key: "flag2", version: 1 }
56
+
57
+ core.force_set(THINGS, item1)
58
+ core.force_set(THINGS, item2)
59
+ expect(wrapper.all(THINGS)).to eq({ item1[:key] => item1, item2[:key] => item2 })
60
+
61
+ core.force_remove(THINGS, item2[:key])
62
+ expect(wrapper.all(THINGS)).to eq (cached ?
63
+ { item1[:key] => item1, item2[:key] => item2 } :
64
+ { item1[:key] => item1 })
65
+ end
66
+
67
+ it "gets all items filtering out deleted items" do
68
+ core = MockCore.new
69
+ wrapper = subject.new(core, opts)
70
+ item1 = { key: "flag1", version: 1 }
71
+ item2 = { key: "flag2", version: 1, deleted: true }
72
+
73
+ core.force_set(THINGS, item1)
74
+ core.force_set(THINGS, item2)
75
+ expect(wrapper.all(THINGS)).to eq({ item1[:key] => item1 })
76
+ end
77
+
78
+ it "upserts item successfully" do
79
+ core = MockCore.new
80
+ wrapper = subject.new(core, opts)
81
+ key = "flag"
82
+ itemv1 = { key: key, version: 1 }
83
+ itemv2 = { key: key, version: 2 }
84
+
85
+ wrapper.upsert(THINGS, itemv1)
86
+ expect(core.data[THINGS][key]).to eq itemv1
87
+
88
+ wrapper.upsert(THINGS, itemv2)
89
+ expect(core.data[THINGS][key]).to eq itemv2
90
+
91
+ # if we have a cache, verify that the new item is now cached by writing a different value
92
+ # to the underlying data - Get should still return the cached item
93
+ if cached
94
+ itemv3 = { key: key, version: 3 }
95
+ core.force_set(THINGS, itemv3)
96
+ end
97
+
98
+ expect(wrapper.get(THINGS, key)).to eq itemv2
99
+ end
100
+
101
+ it "deletes item" do
102
+ core = MockCore.new
103
+ wrapper = subject.new(core, opts)
104
+ key = "flag"
105
+ itemv1 = { key: key, version: 1 }
106
+ itemv2 = { key: key, version: 2, deleted: true }
107
+ itemv3 = { key: key, version: 3 }
108
+
109
+ core.force_set(THINGS, itemv1)
110
+ expect(wrapper.get(THINGS, key)).to eq itemv1
111
+
112
+ wrapper.delete(THINGS, key, 2)
113
+ expect(core.data[THINGS][key]).to eq itemv2
114
+
115
+ core.force_set(THINGS, itemv3) # make a change that bypasses the cache
116
+
117
+ expect(wrapper.get(THINGS, key)).to eq (cached ? nil : itemv3)
118
+ end
119
+ end
120
+
121
+ context "cached" do
122
+ include_examples "tests", true
123
+
124
+ cached_opts = { expiration: 30 }
125
+
126
+ it "get uses values from init" do
127
+ core = MockCore.new
128
+ wrapper = subject.new(core, cached_opts)
129
+ item1 = { key: "flag1", version: 1 }
130
+ item2 = { key: "flag2", version: 1 }
131
+
132
+ wrapper.init({ THINGS => { item1[:key] => item1, item2[:key] => item2 } })
133
+ core.force_remove(THINGS, item1[:key])
134
+
135
+ expect(wrapper.get(THINGS, item1[:key])).to eq item1
136
+ end
137
+
138
+ it "get all uses values from init" do
139
+ core = MockCore.new
140
+ wrapper = subject.new(core, cached_opts)
141
+ item1 = { key: "flag1", version: 1 }
142
+ item2 = { key: "flag2", version: 1 }
143
+
144
+ wrapper.init({ THINGS => { item1[:key] => item1, item2[:key] => item2 } })
145
+ core.force_remove(THINGS, item1[:key])
146
+
147
+ expect(wrapper.all(THINGS)).to eq ({ item1[:key] => item1, item2[:key] => item2 })
148
+ end
149
+
150
+ it "upsert doesn't update cache if unsuccessful" do
151
+ # This is for an upsert where the data in the store has a higher version. In an uncached
152
+ # store, this is just a no-op as far as the wrapper is concerned so there's nothing to
153
+ # test here. In a cached store, we need to verify that the cache has been refreshed
154
+ # using the data that was found in the store.
155
+ core = MockCore.new
156
+ wrapper = subject.new(core, cached_opts)
157
+ key = "flag"
158
+ itemv1 = { key: key, version: 1 }
159
+ itemv2 = { key: key, version: 2 }
160
+
161
+ wrapper.upsert(THINGS, itemv2)
162
+ expect(core.data[THINGS][key]).to eq itemv2
163
+
164
+ wrapper.upsert(THINGS, itemv1)
165
+ expect(core.data[THINGS][key]).to eq itemv2 # value in store remains the same
166
+
167
+ itemv3 = { key: key, version: 3 }
168
+ core.force_set(THINGS, itemv3) # bypasses cache so we can verify that itemv2 is in the cache
169
+ expect(wrapper.get(THINGS, key)).to eq itemv2
170
+ end
171
+
172
+ it "initialized? can cache false result" do
173
+ core = MockCore.new
174
+ wrapper = subject.new(core, { expiration: 0.2 }) # use a shorter cache TTL for this test
175
+
176
+ expect(wrapper.initialized?).to eq false
177
+ expect(core.inited_query_count).to eq 1
178
+
179
+ core.inited = true
180
+ expect(wrapper.initialized?).to eq false
181
+ expect(core.inited_query_count).to eq 1
182
+
183
+ sleep(0.5)
184
+
185
+ expect(wrapper.initialized?).to eq true
186
+ expect(core.inited_query_count).to eq 2
187
+
188
+ # From this point on it should remain true and the method should not be called
189
+ expect(wrapper.initialized?).to eq true
190
+ expect(core.inited_query_count).to eq 2
191
+ end
192
+ end
193
+
194
+ context "uncached" do
195
+ include_examples "tests", false
196
+
197
+ uncached_opts = { expiration: 0 }
198
+
199
+ it "queries internal initialized state only if not already inited" do
200
+ core = MockCore.new
201
+ wrapper = subject.new(core, uncached_opts)
202
+
203
+ expect(wrapper.initialized?).to eq false
204
+ expect(core.inited_query_count).to eq 1
205
+
206
+ core.inited = true
207
+ expect(wrapper.initialized?).to eq true
208
+ expect(core.inited_query_count).to eq 2
209
+
210
+ core.inited = false
211
+ expect(wrapper.initialized?).to eq true
212
+ expect(core.inited_query_count).to eq 2
213
+ end
214
+
215
+ it "does not query internal initialized state if init has been called" do
216
+ core = MockCore.new
217
+ wrapper = subject.new(core, uncached_opts)
218
+
219
+ expect(wrapper.initialized?).to eq false
220
+ expect(core.inited_query_count).to eq 1
221
+
222
+ wrapper.init({})
223
+
224
+ expect(wrapper.initialized?).to eq true
225
+ expect(core.inited_query_count).to eq 1
226
+ end
227
+ end
228
+
229
+ class MockCore
230
+ def initialize
231
+ @data = {}
232
+ @inited = false
233
+ @inited_query_count = 0
234
+ end
235
+
236
+ attr_reader :data
237
+ attr_reader :inited_query_count
238
+ attr_accessor :inited
239
+
240
+ def force_set(kind, item)
241
+ @data[kind] = {} if !@data.has_key?(kind)
242
+ @data[kind][item[:key]] = item
243
+ end
244
+
245
+ def force_remove(kind, key)
246
+ @data[kind].delete(key) if @data.has_key?(kind)
247
+ end
248
+
249
+ def init_internal(all_data)
250
+ @data = all_data
251
+ @inited = true
252
+ end
253
+
254
+ def get_internal(kind, key)
255
+ items = @data[kind]
256
+ items.nil? ? nil : items[key]
257
+ end
258
+
259
+ def get_all_internal(kind)
260
+ @data[kind]
261
+ end
262
+
263
+ def upsert_internal(kind, item)
264
+ @data[kind] = {} if !@data.has_key?(kind)
265
+ old_item = @data[kind][item[:key]]
266
+ return old_item if !old_item.nil? && old_item[:version] >= item[:version]
267
+ @data[kind][item[:key]] = item
268
+ item
269
+ end
270
+
271
+ def initialized_internal?
272
+ @inited_query_count = @inited_query_count + 1
273
+ @inited
274
+ end
275
+ end
276
+ end