ldclient-rb 0.4.0 → 0.5.0
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 +4 -4
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +601 -0
- data/.simplecov +4 -0
- data/CONTRIBUTING.md +10 -0
- data/Gemfile +1 -1
- data/LICENSE.txt +1 -1
- data/README.md +72 -13
- data/Rakefile +3 -0
- data/circle.yml +29 -0
- data/ldclient-rb.gemspec +8 -6
- data/lib/ldclient-rb/config.rb +73 -76
- data/lib/ldclient-rb/ldclient.rb +201 -199
- data/lib/ldclient-rb/newrelic.rb +4 -7
- data/lib/ldclient-rb/store.rb +10 -11
- data/lib/ldclient-rb/stream.rb +67 -67
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/config_spec.rb +45 -0
- data/spec/fixtures/feature.json +67 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/ldclient_spec.rb +226 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +118 -0
- data/spec/version_spec.rb +7 -0
- metadata +82 -31
data/lib/ldclient-rb/newrelic.rb
CHANGED
@@ -1,19 +1,16 @@
|
|
1
1
|
module LaunchDarkly
|
2
|
-
|
3
2
|
class LDNewRelic
|
4
3
|
begin
|
5
|
-
require
|
4
|
+
require "newrelic_rpm"
|
6
5
|
NR_ENABLED = defined?(::NewRelic::Agent.add_custom_parameters)
|
7
|
-
rescue
|
6
|
+
rescue ScriptError, StandardError
|
8
7
|
NR_ENABLED = false
|
9
8
|
end
|
10
9
|
|
11
10
|
def self.annotate_transaction(key, value)
|
12
11
|
if NR_ENABLED
|
13
|
-
::NewRelic::Agent.add_custom_parameters(
|
12
|
+
::NewRelic::Agent.add_custom_parameters(key.to_s => value.to_s)
|
14
13
|
end
|
15
14
|
end
|
16
15
|
end
|
17
|
-
|
18
|
-
|
19
|
-
end
|
16
|
+
end
|
data/lib/ldclient-rb/store.rb
CHANGED
@@ -1,37 +1,36 @@
|
|
1
|
-
require
|
1
|
+
require "thread_safe"
|
2
2
|
|
3
3
|
module LaunchDarkly
|
4
|
-
|
5
4
|
# A thread-safe in-memory store suitable for use
|
6
5
|
# with the Faraday caching HTTP client. Uses the
|
7
|
-
# Threadsafe gem as the underlying cache.
|
8
|
-
#
|
6
|
+
# Threadsafe gem as the underlying cache.
|
7
|
+
#
|
9
8
|
# @see https://github.com/plataformatec/faraday-http-cache
|
10
9
|
# @see https://github.com/ruby-concurrency/thread_safe
|
11
|
-
#
|
10
|
+
#
|
12
11
|
class ThreadSafeMemoryStore
|
13
|
-
#
|
12
|
+
#
|
14
13
|
# Default constructor
|
15
|
-
#
|
14
|
+
#
|
16
15
|
# @return [ThreadSafeMemoryStore] a new store
|
17
16
|
def initialize
|
18
17
|
@cache = ThreadSafe::Cache.new
|
19
18
|
end
|
20
19
|
|
21
|
-
#
|
20
|
+
#
|
22
21
|
# Read a value from the cache
|
23
22
|
# @param key [Object] the cache key
|
24
|
-
#
|
23
|
+
#
|
25
24
|
# @return [Object] the cache value
|
26
25
|
def read(key)
|
27
26
|
@cache[key]
|
28
27
|
end
|
29
28
|
|
30
|
-
#
|
29
|
+
#
|
31
30
|
# Store a value in the cache
|
32
31
|
# @param key [Object] the cache key
|
33
32
|
# @param value [Object] the value to associate with the key
|
34
|
-
#
|
33
|
+
#
|
35
34
|
# @return [Object] the value
|
36
35
|
def write(key, value)
|
37
36
|
@cache[key] = value
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -1,67 +1,66 @@
|
|
1
|
-
require
|
2
|
-
require
|
3
|
-
require
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "json"
|
3
|
+
require "ld-em-eventsource"
|
4
4
|
|
5
5
|
module LaunchDarkly
|
6
|
-
|
7
6
|
PUT = "put"
|
8
7
|
PATCH = "patch"
|
9
8
|
DELETE = "delete"
|
10
9
|
|
11
10
|
class InMemoryFeatureStore
|
12
|
-
def initialize
|
11
|
+
def initialize
|
13
12
|
@features = Hash.new
|
14
13
|
@lock = Concurrent::ReadWriteLock.new
|
15
14
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
16
15
|
end
|
17
16
|
|
18
17
|
def get(key)
|
19
|
-
@lock.with_read_lock
|
18
|
+
@lock.with_read_lock do
|
20
19
|
f = @features[key.to_sym]
|
21
20
|
(f.nil? || f[:deleted]) ? nil : f
|
22
|
-
|
21
|
+
end
|
23
22
|
end
|
24
23
|
|
25
|
-
def all
|
26
|
-
@lock.with_read_lock
|
27
|
-
@features.select {|
|
28
|
-
|
24
|
+
def all
|
25
|
+
@lock.with_read_lock do
|
26
|
+
@features.select { |_k, f| not f[:deleted] }
|
27
|
+
end
|
29
28
|
end
|
30
29
|
|
31
30
|
def delete(key, version)
|
32
|
-
@lock.with_write_lock
|
31
|
+
@lock.with_write_lock do
|
33
32
|
old = @features[key.to_sym]
|
34
33
|
|
35
|
-
if old
|
34
|
+
if !old.nil? && old[:version] < version
|
36
35
|
old[:deleted] = true
|
37
36
|
old[:version] = version
|
38
37
|
@features[key.to_sym] = old
|
39
38
|
elsif old.nil?
|
40
|
-
@features[key.to_sym] = {:
|
39
|
+
@features[key.to_sym] = { deleted: true, version: version }
|
41
40
|
end
|
42
|
-
|
41
|
+
end
|
43
42
|
end
|
44
43
|
|
45
44
|
def init(fs)
|
46
|
-
@lock.with_write_lock
|
45
|
+
@lock.with_write_lock do
|
47
46
|
@features.replace(fs)
|
48
47
|
@initialized.make_true
|
49
|
-
|
48
|
+
end
|
50
49
|
end
|
51
50
|
|
52
51
|
def upsert(key, feature)
|
53
|
-
@lock.with_write_lock
|
52
|
+
@lock.with_write_lock do
|
54
53
|
old = @features[key.to_sym]
|
55
54
|
|
56
|
-
if old.nil?
|
55
|
+
if old.nil? || old[:version] < feature[:version]
|
57
56
|
@features[key.to_sym] = feature
|
58
57
|
end
|
59
|
-
|
58
|
+
end
|
60
59
|
end
|
61
60
|
|
62
|
-
def initialized?
|
61
|
+
def initialized?
|
63
62
|
@initialized.value
|
64
|
-
end
|
63
|
+
end
|
65
64
|
end
|
66
65
|
|
67
66
|
class StreamProcessor
|
@@ -73,11 +72,11 @@ module LaunchDarkly
|
|
73
72
|
@started = Concurrent::AtomicBoolean.new(false)
|
74
73
|
end
|
75
74
|
|
76
|
-
def initialized?
|
75
|
+
def initialized?
|
77
76
|
@store.initialized?
|
78
77
|
end
|
79
78
|
|
80
|
-
def started?
|
79
|
+
def started?
|
81
80
|
@started.value
|
82
81
|
end
|
83
82
|
|
@@ -88,9 +87,9 @@ module LaunchDarkly
|
|
88
87
|
@store.get(key)
|
89
88
|
end
|
90
89
|
|
91
|
-
def start_reactor
|
90
|
+
def start_reactor
|
92
91
|
if defined?(Thin)
|
93
|
-
@config.logger.debug("Running in a Thin environment-- not starting EventMachine")
|
92
|
+
@config.logger.debug("Running in a Thin environment-- not starting EventMachine")
|
94
93
|
elsif EM.reactor_running?
|
95
94
|
@config.logger.debug("EventMachine already running")
|
96
95
|
else
|
@@ -101,66 +100,67 @@ module LaunchDarkly
|
|
101
100
|
EM.reactor_running?
|
102
101
|
end
|
103
102
|
|
104
|
-
def start
|
103
|
+
def start
|
105
104
|
# Try to start the reactor. If it's not started, we shouldn't start
|
106
105
|
# the stream processor
|
107
|
-
if not start_reactor
|
108
|
-
return
|
109
|
-
end
|
106
|
+
return if not start_reactor
|
110
107
|
|
111
108
|
# If someone else booted the stream processor connection, just return
|
112
|
-
|
113
|
-
return
|
114
|
-
end
|
109
|
+
return unless @started.make_true
|
115
110
|
|
116
111
|
# If we're the first and only thread to set started, boot
|
117
112
|
# the stream processor connection
|
118
113
|
EM.defer do
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
json = JSON.parse(message, :symbolize_names => true)
|
136
|
-
@store.delete(json[:path][1..-1], json[:version])
|
137
|
-
set_connected
|
138
|
-
end
|
139
|
-
source.error do |error|
|
140
|
-
@config.logger.error("[LDClient] Error subscribing to stream API: #{error}")
|
141
|
-
set_disconnected
|
142
|
-
end
|
143
|
-
source.inactivity_timeout = 0
|
144
|
-
source.start
|
114
|
+
boot_event_manager
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def boot_event_manager
|
119
|
+
source = EM::EventSource.new(@config.stream_uri + "/features",
|
120
|
+
{},
|
121
|
+
"Accept" => "text/event-stream",
|
122
|
+
"Authorization" => "api_key " + @api_key,
|
123
|
+
"User-Agent" => "RubyClient/" + LaunchDarkly::VERSION)
|
124
|
+
source.on(PUT) { |message| process_message(message, PUT) }
|
125
|
+
source.on(PATCH) { |message| process_message(message, PATCH) }
|
126
|
+
source.on(DELETE) { |message| process_message(message, DELETE) }
|
127
|
+
source.error do |error|
|
128
|
+
@config.logger.info("[LDClient] Error subscribing to stream API: #{error}")
|
129
|
+
set_disconnected
|
145
130
|
end
|
131
|
+
source.inactivity_timeout = 0
|
132
|
+
source.start
|
133
|
+
source
|
146
134
|
end
|
147
135
|
|
148
|
-
def
|
136
|
+
def process_message(message, method)
|
137
|
+
message = JSON.parse(message, symbolize_names: true)
|
138
|
+
if method == PUT
|
139
|
+
@store.init(message)
|
140
|
+
elsif method == PATCH
|
141
|
+
@store.upsert(message[:path][1..-1], message[:data])
|
142
|
+
elsif method == DELETE
|
143
|
+
@store.delete(message[:path][1..-1], message[:version])
|
144
|
+
else
|
145
|
+
@config.logger.error("[LDClient] Unknown message received: #{method}")
|
146
|
+
end
|
147
|
+
set_connected
|
148
|
+
end
|
149
|
+
|
150
|
+
def set_disconnected
|
149
151
|
@disconnected.set(Time.now)
|
150
152
|
end
|
151
153
|
|
152
|
-
def set_connected
|
154
|
+
def set_connected
|
153
155
|
@disconnected.set(nil)
|
154
156
|
end
|
155
157
|
|
156
|
-
def should_fallback_update
|
158
|
+
def should_fallback_update
|
157
159
|
disc = @disconnected.get
|
158
|
-
disc
|
160
|
+
!disc.nil? && disc < (Time.now - 120)
|
159
161
|
end
|
160
162
|
|
161
163
|
# TODO mark private methods
|
162
|
-
private :set_connected, :set_disconnected, :start_reactor
|
163
|
-
|
164
|
+
private :boot_event_manager, :process_message, :set_connected, :set_disconnected, :start_reactor
|
164
165
|
end
|
165
|
-
|
166
|
-
end
|
166
|
+
end
|
data/lib/ldclient-rb/version.rb
CHANGED
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::Config do
|
4
|
+
subject { LaunchDarkly::Config }
|
5
|
+
describe ".initialize" do
|
6
|
+
it "can be initialized with default settings" do
|
7
|
+
expect(subject).to receive(:default_capacity).and_return 1234
|
8
|
+
expect(subject.new.capacity).to eq 1234
|
9
|
+
end
|
10
|
+
it "accepts custom arguments" do
|
11
|
+
expect(subject).to_not receive(:default_capacity)
|
12
|
+
expect(subject.new(capacity: 50).capacity).to eq 50
|
13
|
+
end
|
14
|
+
it "will chomp base_url and stream_uri" do
|
15
|
+
uri = "https://test.launchdarkly.com"
|
16
|
+
config = subject.new(base_uri: uri + "/")
|
17
|
+
expect(config.base_uri).to eq uri
|
18
|
+
end
|
19
|
+
end
|
20
|
+
describe "@base_uri" do
|
21
|
+
it "can be read" do
|
22
|
+
expect(subject.new.base_uri).to eq subject.default_base_uri
|
23
|
+
end
|
24
|
+
end
|
25
|
+
describe ".default_store" do
|
26
|
+
it "uses Rails cache if it is available" do
|
27
|
+
rails = instance_double("Rails", cache: :cache)
|
28
|
+
stub_const("Rails", rails)
|
29
|
+
expect(subject.default_store).to eq :cache
|
30
|
+
end
|
31
|
+
it "uses memory store if Rails is not available" do
|
32
|
+
expect(subject.default_store).to be_an_instance_of LaunchDarkly::ThreadSafeMemoryStore
|
33
|
+
end
|
34
|
+
end
|
35
|
+
describe ".default_logger" do
|
36
|
+
it "uses Rails logger if it is available" do
|
37
|
+
rails = instance_double("Rails", logger: :logger)
|
38
|
+
stub_const("Rails", rails)
|
39
|
+
expect(subject.default_logger).to eq :logger
|
40
|
+
end
|
41
|
+
it "Uses logger if Rails is not available" do
|
42
|
+
expect(subject.default_logger).to be_an_instance_of Logger
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
{
|
2
|
+
"name":"New recommendations engine",
|
3
|
+
"key":"engine.enable",
|
4
|
+
"kind":"flag",
|
5
|
+
"salt":"ZW5naW5lLmVuYWJsZQ==",
|
6
|
+
"on":true,
|
7
|
+
"variations":[
|
8
|
+
{
|
9
|
+
"value":true,
|
10
|
+
"weight":31,
|
11
|
+
"targets":[
|
12
|
+
{
|
13
|
+
"attribute":"groups",
|
14
|
+
"op":"in",
|
15
|
+
"values":[
|
16
|
+
"Microsoft"
|
17
|
+
]
|
18
|
+
}
|
19
|
+
],
|
20
|
+
"userTarget":{
|
21
|
+
"attribute":"key",
|
22
|
+
"op":"in",
|
23
|
+
"values":[
|
24
|
+
"foo@bar.com",
|
25
|
+
"Abbey.Orkwis@example.com",
|
26
|
+
"Abbie.Stolte@example.com",
|
27
|
+
"44d2781c-5985-4d89-b07a-0dffbd24126f",
|
28
|
+
"0ffe4f0c-7aa9-4621-a87c-abe1c051abd8",
|
29
|
+
"f52d99be-6a40-4cdd-a7b4-0548834fcbe5",
|
30
|
+
"Vernetta.Belden@example.com",
|
31
|
+
"c9d591bd-ea1f-465f-86d2-239ea41d0f0f",
|
32
|
+
"870745092"
|
33
|
+
]
|
34
|
+
}
|
35
|
+
},
|
36
|
+
{
|
37
|
+
"value":false,
|
38
|
+
"weight":69,
|
39
|
+
"targets":[
|
40
|
+
{
|
41
|
+
"attribute":"key",
|
42
|
+
"op":"in",
|
43
|
+
"values":[
|
44
|
+
"Alida.Caples@example.com"
|
45
|
+
]
|
46
|
+
},
|
47
|
+
{
|
48
|
+
"attribute":"groups",
|
49
|
+
"op":"in",
|
50
|
+
"values":[
|
51
|
+
|
52
|
+
]
|
53
|
+
}
|
54
|
+
],
|
55
|
+
"userTarget":{
|
56
|
+
"attribute":"key",
|
57
|
+
"op":"in",
|
58
|
+
"values":[
|
59
|
+
"Alida.Caples@example.com"
|
60
|
+
]
|
61
|
+
}
|
62
|
+
}
|
63
|
+
],
|
64
|
+
"ttl":0,
|
65
|
+
"commitDate":"2015-05-10T06:06:45.381Z",
|
66
|
+
"creationDate":"2014-09-02T20:39:18.61Z"
|
67
|
+
}
|
@@ -0,0 +1,226 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe LaunchDarkly::LDClient do
|
4
|
+
subject { LaunchDarkly::LDClient }
|
5
|
+
let(:client) do
|
6
|
+
expect_any_instance_of(LaunchDarkly::LDClient).to receive :create_worker
|
7
|
+
subject.new("api_key")
|
8
|
+
end
|
9
|
+
let(:feature) do
|
10
|
+
data = File.read(File.join("spec", "fixtures", "feature.json"))
|
11
|
+
JSON.parse(data, symbolize_names: true)
|
12
|
+
end
|
13
|
+
let(:user) do
|
14
|
+
data = File.read(File.join("spec", "fixtures", "user.json"))
|
15
|
+
JSON.parse(data, symbolize_names: true)
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '#flush' do
|
19
|
+
it "will flush and post all events" do
|
20
|
+
client.instance_variable_get(:@queue).push "asdf"
|
21
|
+
client.instance_variable_get(:@queue).push "asdf"
|
22
|
+
expect(client).to receive(:post_flushed_events)
|
23
|
+
client.flush
|
24
|
+
expect(client.instance_variable_get(:@queue).length).to eq 0
|
25
|
+
end
|
26
|
+
it "will not do anything if there are no events" do
|
27
|
+
expect(client).to_not receive(:post_flushed_events)
|
28
|
+
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
29
|
+
client.flush
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#post_flushed_events' do
|
34
|
+
let(:events) { ["event"] }
|
35
|
+
it "will flush and post all events" do
|
36
|
+
result = double("result", status: 200)
|
37
|
+
expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
|
38
|
+
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
39
|
+
client.send(:post_flushed_events, events)
|
40
|
+
expect(client.instance_variable_get(:@queue).length).to eq 0
|
41
|
+
end
|
42
|
+
it "will allow any 2XX response" do
|
43
|
+
result = double("result", status: 202)
|
44
|
+
expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
|
45
|
+
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
46
|
+
client.send(:post_flushed_events, events)
|
47
|
+
end
|
48
|
+
it "will work with unexpected post results" do
|
49
|
+
result = double("result", status: 418)
|
50
|
+
expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
|
51
|
+
expect(client.instance_variable_get(:@config).logger).to receive :error
|
52
|
+
client.send(:post_flushed_events, events)
|
53
|
+
expect(client.instance_variable_get(:@queue).length).to eq 0
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe '#toggle?' do
|
58
|
+
it "will not fail" do
|
59
|
+
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_raise RuntimeError
|
60
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
61
|
+
result = client.toggle?(feature[:key], user, "default")
|
62
|
+
expect(result).to eq "default"
|
63
|
+
end
|
64
|
+
it "requires user" do
|
65
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
66
|
+
result = client.toggle?(feature[:key], nil, "default")
|
67
|
+
expect(result).to eq "default"
|
68
|
+
end
|
69
|
+
it "returns value from streamed flag if available" do
|
70
|
+
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(true).twice
|
71
|
+
expect(client.instance_variable_get(:@stream_processor)).to receive(:started?).and_return true
|
72
|
+
expect(client.instance_variable_get(:@stream_processor)).to receive(:initialized?).and_return true
|
73
|
+
expect(client).to receive(:add_event)
|
74
|
+
expect(client).to receive(:get_streamed_flag).and_return feature
|
75
|
+
result = client.toggle?(feature[:key], user, "default")
|
76
|
+
expect(result).to eq false
|
77
|
+
end
|
78
|
+
it "returns value from normal request if streamed flag is not available" do
|
79
|
+
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(false).twice
|
80
|
+
expect(client).to receive(:add_event)
|
81
|
+
expect(client).to receive(:get_flag_int).and_return feature
|
82
|
+
result = client.toggle?(feature[:key], user, "default")
|
83
|
+
expect(result).to eq false
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe '#get_streamed_flag' do
|
88
|
+
it "will not check the polled flag normally" do
|
89
|
+
expect(client).to receive(:get_flag_stream).and_return true
|
90
|
+
expect(client).to_not receive(:get_flag_int)
|
91
|
+
expect(client.send(:get_streamed_flag, "key")).to eq true
|
92
|
+
end
|
93
|
+
context "debug stream" do
|
94
|
+
it "will log an error if the streamed and polled flag do not match" do
|
95
|
+
expect(client.instance_variable_get(:@config)).to receive(:debug_stream?).and_return true
|
96
|
+
expect(client).to receive(:get_flag_stream).and_return true
|
97
|
+
expect(client).to receive(:get_flag_int).and_return false
|
98
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
99
|
+
expect(client.send(:get_streamed_flag, "key")).to eq true
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#get_features' do
|
105
|
+
it "will parse and return the features list" do
|
106
|
+
result = double("Faraday::Response", status: 200, body: '{"items": ["asdf"]}')
|
107
|
+
expect(client).to receive(:make_request).with("/api/features").and_return(result)
|
108
|
+
data = client.send(:get_features)
|
109
|
+
expect(data).to eq ["asdf"]
|
110
|
+
end
|
111
|
+
it "will log errors" do
|
112
|
+
result = double("Faraday::Response", status: 418)
|
113
|
+
expect(client).to receive(:make_request).with("/api/features").and_return(result)
|
114
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
115
|
+
client.send(:get_features)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#get_flag_int' do
|
120
|
+
it "will return the parsed flag" do
|
121
|
+
result = double("Faraday::Response", status: 200, body: '{"asdf":"qwer"}')
|
122
|
+
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
123
|
+
data = client.send(:get_flag_int, "key")
|
124
|
+
expect(data).to eq(asdf: "qwer")
|
125
|
+
end
|
126
|
+
it "will accept 401 statuses" do
|
127
|
+
result = double("Faraday::Response", status: 401)
|
128
|
+
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
129
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
130
|
+
data = client.send(:get_flag_int, "key")
|
131
|
+
expect(data).to be_nil
|
132
|
+
end
|
133
|
+
it "will accept 404 statuses" do
|
134
|
+
result = double("Faraday::Response", status: 404)
|
135
|
+
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
136
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
137
|
+
data = client.send(:get_flag_int, "key")
|
138
|
+
expect(data).to be_nil
|
139
|
+
end
|
140
|
+
it "will accept non-standard statuses" do
|
141
|
+
result = double("Faraday::Response", status: 418)
|
142
|
+
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
143
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
144
|
+
data = client.send(:get_flag_int, "key")
|
145
|
+
expect(data).to be_nil
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe '#make_request' do
|
150
|
+
it "will make a proper request" do
|
151
|
+
expect(client.instance_variable_get :@client).to receive(:get)
|
152
|
+
client.send(:make_request, "/asdf")
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
describe '#param_for_user' do
|
157
|
+
it "will return a consistent hash of a user key, feature key, and feature salt" do
|
158
|
+
param = client.send(:param_for_user, feature, user)
|
159
|
+
expect(param).to be_between(0.0, 1.0).inclusive
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
describe '#evaluate' do
|
164
|
+
it "will return nil if there is no feature" do
|
165
|
+
expect(client.send(:evaluate, nil, user)).to eq nil
|
166
|
+
end
|
167
|
+
it "will return nil unless the feature is on" do
|
168
|
+
feature[:on] = false
|
169
|
+
expect(client.send(:evaluate, feature, user)).to eq nil
|
170
|
+
end
|
171
|
+
it "will return value if it matches the user" do
|
172
|
+
user = { key: "Alida.Caples@example.com" }
|
173
|
+
expect(client.send(:evaluate, feature, user)).to eq false
|
174
|
+
user = { key: "foo@bar.com" }
|
175
|
+
expect(client.send(:evaluate, feature, user)).to eq true
|
176
|
+
end
|
177
|
+
it "will return value if the target matches" do
|
178
|
+
user = { key: "asdf@asdf.com", custom: { groups: "Microsoft" } }
|
179
|
+
expect(client.send(:evaluate, feature, user)).to eq true
|
180
|
+
end
|
181
|
+
it "will return value if the weight matches" do
|
182
|
+
expect(client).to receive(:param_for_user).and_return 0.1
|
183
|
+
expect(client.send(:evaluate, feature, user)).to eq true
|
184
|
+
expect(client).to receive(:param_for_user).and_return 0.9
|
185
|
+
expect(client.send(:evaluate, feature, user)).to eq false
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
describe '#log_timings' do
|
190
|
+
let(:block) { lambda { "result" } }
|
191
|
+
let(:label) { "label" }
|
192
|
+
it "will not measure if not configured to do so" do
|
193
|
+
expect(Benchmark).to_not receive(:measure)
|
194
|
+
client.send(:log_timings, label, &block)
|
195
|
+
end
|
196
|
+
context "logging enabled" do
|
197
|
+
before do
|
198
|
+
expect(client.instance_variable_get(:@config)).to receive(:log_timings?).and_return true
|
199
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:debug?).and_return true
|
200
|
+
end
|
201
|
+
it "will benchmark timings and return result" do
|
202
|
+
expect(Benchmark).to receive(:measure).and_call_original
|
203
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:debug)
|
204
|
+
result = client.send(:log_timings, label, &block)
|
205
|
+
expect(result).to eq "result"
|
206
|
+
end
|
207
|
+
it "will raise exceptions if the block has them" do
|
208
|
+
block = lambda { raise RuntimeError }
|
209
|
+
expect(Benchmark).to receive(:measure).and_call_original
|
210
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:debug)
|
211
|
+
expect { client.send(:log_timings, label, &block) }.to raise_error RuntimeError
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
describe '#log_exception' do
|
217
|
+
it "log error data" do
|
218
|
+
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
219
|
+
begin
|
220
|
+
raise StandardError.new 'asdf'
|
221
|
+
rescue StandardError => exn
|
222
|
+
client.send(:log_exception, 'caller', exn)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|