ldclient-rb 0.8.0 → 2.0.1
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/CHANGELOG.md +25 -0
- data/README.md +3 -3
- data/ext/mkrf_conf.rb +11 -0
- data/ldclient-rb.gemspec +3 -1
- data/lib/ldclient-rb.rb +6 -2
- data/lib/ldclient-rb/{store.rb → cache_store.rb} +0 -0
- data/lib/ldclient-rb/config.rb +27 -29
- data/lib/ldclient-rb/evaluation.rb +265 -0
- data/lib/ldclient-rb/events.rb +75 -0
- data/lib/ldclient-rb/feature_store.rb +60 -0
- data/lib/ldclient-rb/ldclient.rb +92 -303
- data/lib/ldclient-rb/polling.rb +56 -0
- data/lib/ldclient-rb/requestor.rb +56 -0
- data/lib/ldclient-rb/stream.rb +40 -140
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/config_spec.rb +3 -3
- data/spec/fixtures/feature.json +27 -58
- data/spec/ldclient_spec.rb +20 -226
- data/spec/stream_spec.rb +7 -63
- metadata +28 -7
- data/lib/ldclient-rb/settings.rb +0 -40
@@ -0,0 +1,56 @@
|
|
1
|
+
require "concurrent/atomics"
|
2
|
+
require "thread"
|
3
|
+
|
4
|
+
module LaunchDarkly
|
5
|
+
class PollingProcessor
|
6
|
+
|
7
|
+
def initialize(config, requestor)
|
8
|
+
@config = config
|
9
|
+
@requestor = requestor
|
10
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
11
|
+
@started = Concurrent::AtomicBoolean.new(false)
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialized?
|
15
|
+
@initialized.value
|
16
|
+
end
|
17
|
+
|
18
|
+
def start
|
19
|
+
return unless @started.make_true
|
20
|
+
@config.logger.info("[LDClient] Initializing polling connection")
|
21
|
+
create_worker
|
22
|
+
end
|
23
|
+
|
24
|
+
def poll
|
25
|
+
flags = @requestor.request_all_flags
|
26
|
+
if flags
|
27
|
+
@config.feature_store.init(flags)
|
28
|
+
if @initialized.make_true
|
29
|
+
@config.logger.info("[LDClient] Polling connection initialized")
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def create_worker
|
35
|
+
Thread.new do
|
36
|
+
@config.logger.debug("[LDClient] Starting polling worker")
|
37
|
+
loop do
|
38
|
+
begin
|
39
|
+
started_at = Time.now
|
40
|
+
poll
|
41
|
+
delta = @config.poll_interval - (Time.now - started_at)
|
42
|
+
if delta > 0
|
43
|
+
sleep(delta)
|
44
|
+
end
|
45
|
+
rescue StandardError => exn
|
46
|
+
@config.logger.error("[LDClient] Exception while polling: #{exn.inspect}")
|
47
|
+
# TODO: log_exception(__method__.to_s, exn)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
private :poll, :create_worker
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require "json"
|
2
|
+
require "net/http/persistent"
|
3
|
+
require "faraday/http_cache"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
|
7
|
+
class Requestor
|
8
|
+
def initialize(sdk_key, config)
|
9
|
+
@sdk_key = sdk_key
|
10
|
+
@config = config
|
11
|
+
@client = Faraday.new do |builder|
|
12
|
+
builder.use :http_cache, store: @config.cache_store
|
13
|
+
|
14
|
+
builder.adapter :net_http_persistent
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def request_all_flags()
|
19
|
+
make_request("/sdk/latest-flags")
|
20
|
+
end
|
21
|
+
|
22
|
+
def request_flag(key)
|
23
|
+
make_request("/sdk/latest-flags/" + key)
|
24
|
+
end
|
25
|
+
|
26
|
+
def make_request(path)
|
27
|
+
res = @client.get (@config.base_uri + path) do |req|
|
28
|
+
req.headers["Authorization"] = @sdk_key
|
29
|
+
req.headers["User-Agent"] = "RubyClient/" + LaunchDarkly::VERSION
|
30
|
+
req.options.timeout = @config.read_timeout
|
31
|
+
req.options.open_timeout = @config.connect_timeout
|
32
|
+
end
|
33
|
+
|
34
|
+
if res.status == 401
|
35
|
+
@config.logger.error("[LDClient] Invalid SDK key")
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
if res.status == 404
|
40
|
+
@config.logger.error("[LDClient] Resource not found")
|
41
|
+
return nil
|
42
|
+
end
|
43
|
+
|
44
|
+
if res.status / 100 != 2
|
45
|
+
@config.logger.error("[LDClient] Unexpected status code #{res.status}")
|
46
|
+
return nil
|
47
|
+
end
|
48
|
+
|
49
|
+
JSON.parse(res.body, symbolize_names: true)
|
50
|
+
end
|
51
|
+
|
52
|
+
private :make_request
|
53
|
+
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
data/lib/ldclient-rb/stream.rb
CHANGED
@@ -1,173 +1,73 @@
|
|
1
1
|
require "concurrent/atomics"
|
2
2
|
require "json"
|
3
|
-
require "
|
3
|
+
require "celluloid/eventsource"
|
4
4
|
|
5
5
|
module LaunchDarkly
|
6
|
-
PUT =
|
7
|
-
PATCH =
|
8
|
-
DELETE =
|
9
|
-
|
10
|
-
|
11
|
-
def initialize
|
12
|
-
@features = Hash.new
|
13
|
-
@lock = Concurrent::ReadWriteLock.new
|
14
|
-
@initialized = Concurrent::AtomicBoolean.new(false)
|
15
|
-
end
|
16
|
-
|
17
|
-
def get(key)
|
18
|
-
@lock.with_read_lock do
|
19
|
-
f = @features[key.to_sym]
|
20
|
-
(f.nil? || f[:deleted]) ? nil : f
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def all
|
25
|
-
@lock.with_read_lock do
|
26
|
-
@features.select { |_k, f| not f[:deleted] }
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
def delete(key, version)
|
31
|
-
@lock.with_write_lock do
|
32
|
-
old = @features[key.to_sym]
|
33
|
-
|
34
|
-
if !old.nil? && old[:version] < version
|
35
|
-
old[:deleted] = true
|
36
|
-
old[:version] = version
|
37
|
-
@features[key.to_sym] = old
|
38
|
-
elsif old.nil?
|
39
|
-
@features[key.to_sym] = { deleted: true, version: version }
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
def init(fs)
|
45
|
-
@lock.with_write_lock do
|
46
|
-
@features.replace(fs)
|
47
|
-
@initialized.make_true
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def upsert(key, feature)
|
52
|
-
@lock.with_write_lock do
|
53
|
-
old = @features[key.to_sym]
|
54
|
-
|
55
|
-
if old.nil? || old[:version] < feature[:version]
|
56
|
-
@features[key.to_sym] = feature
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
def initialized?
|
62
|
-
@initialized.value
|
63
|
-
end
|
64
|
-
end
|
6
|
+
PUT = :put
|
7
|
+
PATCH = :patch
|
8
|
+
DELETE = :delete
|
9
|
+
INDIRECT_PUT = :'indirect/put'
|
10
|
+
INDIRECT_PATCH = :'indirect/patch'
|
65
11
|
|
66
12
|
class StreamProcessor
|
67
|
-
def initialize(
|
68
|
-
@
|
13
|
+
def initialize(sdk_key, config, requestor)
|
14
|
+
@sdk_key = sdk_key
|
69
15
|
@config = config
|
70
|
-
@store = config.feature_store
|
71
|
-
@
|
16
|
+
@store = config.feature_store
|
17
|
+
@requestor = requestor
|
18
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
72
19
|
@started = Concurrent::AtomicBoolean.new(false)
|
73
20
|
end
|
74
21
|
|
75
22
|
def initialized?
|
76
|
-
@
|
77
|
-
end
|
78
|
-
|
79
|
-
def started?
|
80
|
-
@started.value
|
81
|
-
end
|
82
|
-
|
83
|
-
def get_all_features
|
84
|
-
if not initialized?
|
85
|
-
throw :uninitialized
|
86
|
-
end
|
87
|
-
@store.all
|
88
|
-
end
|
89
|
-
|
90
|
-
def get_feature(key)
|
91
|
-
if not initialized?
|
92
|
-
throw :uninitialized
|
93
|
-
end
|
94
|
-
@store.get(key)
|
95
|
-
end
|
96
|
-
|
97
|
-
def start_reactor
|
98
|
-
if defined?(Thin)
|
99
|
-
@config.logger.debug("Running in a Thin environment-- not starting EventMachine")
|
100
|
-
elsif EM.reactor_running?
|
101
|
-
@config.logger.debug("EventMachine already running")
|
102
|
-
else
|
103
|
-
@config.logger.debug("Starting EventMachine")
|
104
|
-
Thread.new { EM.run {} }
|
105
|
-
Thread.pass until EM.reactor_running?
|
106
|
-
end
|
107
|
-
EM.reactor_running?
|
23
|
+
@initialized.value
|
108
24
|
end
|
109
25
|
|
110
26
|
def start
|
111
|
-
# Try to start the reactor. If it's not started, we shouldn't start
|
112
|
-
# the stream processor
|
113
|
-
return if not start_reactor
|
114
|
-
|
115
|
-
# If someone else booted the stream processor connection, just return
|
116
27
|
return unless @started.make_true
|
117
28
|
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
@config.logger.info("[LDClient] Stream connection: #{error}")
|
136
|
-
set_disconnected
|
29
|
+
@config.logger.info("[LDClient] Initializing stream connection")
|
30
|
+
|
31
|
+
headers =
|
32
|
+
{
|
33
|
+
'Authorization' => @sdk_key,
|
34
|
+
'User-Agent' => 'RubyClient/' + LaunchDarkly::VERSION
|
35
|
+
}
|
36
|
+
opts = {:headers => headers, :with_credentials => true}
|
37
|
+
@es = Celluloid::EventSource.new(@config.stream_uri + "/flags", opts) do |conn|
|
38
|
+
conn.on(PUT) { |message| process_message(message, PUT) }
|
39
|
+
conn.on(PATCH) { |message| process_message(message, PATCH) }
|
40
|
+
conn.on(DELETE) { |message| process_message(message, DELETE) }
|
41
|
+
conn.on(INDIRECT_PUT) { |message| process_message(message, INDIRECT_PUT) }
|
42
|
+
conn.on(INDIRECT_PATCH) { |message| process_message(message, INDIRECT_PATCH) }
|
43
|
+
conn.on_error do |message|
|
44
|
+
@config.logger.error("[LDClient] Error connecting to stream. Status code: #{message[:status_code]}")
|
45
|
+
end
|
137
46
|
end
|
138
|
-
source.inactivity_timeout = 0
|
139
|
-
source.start
|
140
|
-
source
|
141
47
|
end
|
142
48
|
|
143
49
|
def process_message(message, method)
|
144
|
-
message = JSON.parse(message, symbolize_names: true)
|
50
|
+
message = JSON.parse(message.data, symbolize_names: true)
|
51
|
+
@config.logger.debug("[LDClient] Stream received #{method} message")
|
145
52
|
if method == PUT
|
146
53
|
@store.init(message)
|
54
|
+
@initialized.make_true
|
55
|
+
@config.logger.info("[LDClient] Stream initialized")
|
147
56
|
elsif method == PATCH
|
148
57
|
@store.upsert(message[:path][1..-1], message[:data])
|
149
58
|
elsif method == DELETE
|
150
59
|
@store.delete(message[:path][1..-1], message[:version])
|
60
|
+
elsif method == INDIRECT_PUT
|
61
|
+
@store.init(@requestor.request_all_flags)
|
62
|
+
@initialized.make_true
|
63
|
+
@config.logger.info("[LDClient] Stream initialized (via indirect message)")
|
64
|
+
elsif method == INDIRECT_PATCH
|
65
|
+
@store.upsert(@requestor.request_flag(message[:data]))
|
151
66
|
else
|
152
67
|
@config.logger.error("[LDClient] Unknown message received: #{method}")
|
153
68
|
end
|
154
|
-
set_connected
|
155
|
-
end
|
156
|
-
|
157
|
-
def set_disconnected
|
158
|
-
@disconnected.set(Time.now)
|
159
|
-
end
|
160
|
-
|
161
|
-
def set_connected
|
162
|
-
@disconnected.set(nil)
|
163
|
-
end
|
164
|
-
|
165
|
-
def should_fallback_update
|
166
|
-
disc = @disconnected.get
|
167
|
-
!disc.nil? && disc < (Time.now - 120)
|
168
69
|
end
|
169
70
|
|
170
|
-
|
171
|
-
private :boot_event_manager, :process_message, :set_connected, :set_disconnected, :start_reactor
|
71
|
+
private :process_message
|
172
72
|
end
|
173
73
|
end
|
data/lib/ldclient-rb/version.rb
CHANGED
data/spec/config_spec.rb
CHANGED
@@ -32,14 +32,14 @@ describe LaunchDarkly::Config do
|
|
32
32
|
expect(subject.new.stream_uri).to eq subject.default_stream_uri
|
33
33
|
end
|
34
34
|
end
|
35
|
-
describe ".
|
35
|
+
describe ".default_cache_store" do
|
36
36
|
it "uses Rails cache if it is available" do
|
37
37
|
rails = instance_double("Rails", cache: :cache)
|
38
38
|
stub_const("Rails", rails)
|
39
|
-
expect(subject.
|
39
|
+
expect(subject.default_cache_store).to eq :cache
|
40
40
|
end
|
41
41
|
it "uses memory store if Rails is not available" do
|
42
|
-
expect(subject.
|
42
|
+
expect(subject.default_cache_store).to be_an_instance_of LaunchDarkly::ThreadSafeMemoryStore
|
43
43
|
end
|
44
44
|
end
|
45
45
|
describe ".default_logger" do
|
data/spec/fixtures/feature.json
CHANGED
@@ -1,67 +1,36 @@
|
|
1
1
|
{
|
2
|
-
"
|
3
|
-
"
|
4
|
-
"kind":"flag",
|
5
|
-
"salt":"ZW5naW5lLmVuYWJsZQ==",
|
2
|
+
"key":"test-feature-flag",
|
3
|
+
"version":11,
|
6
4
|
"on":true,
|
7
|
-
"
|
5
|
+
"prerequisites":[
|
6
|
+
|
7
|
+
],
|
8
|
+
"salt":"718ea30a918a4eba8734b57ab1a93227",
|
9
|
+
"sel":"fe1244e5378c4f99976c9634e33667c6",
|
10
|
+
"targets":[
|
8
11
|
{
|
9
|
-
"
|
10
|
-
|
11
|
-
"targets":[
|
12
|
-
{
|
13
|
-
"attribute":"groups",
|
14
|
-
"op":"in",
|
15
|
-
"values":[
|
16
|
-
"Microsoft"
|
17
|
-
]
|
18
|
-
}
|
12
|
+
"values":[
|
13
|
+
"alice"
|
19
14
|
],
|
20
|
-
"
|
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
|
-
}
|
15
|
+
"variation":0
|
35
16
|
},
|
36
17
|
{
|
37
|
-
"
|
38
|
-
|
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
|
-
}
|
18
|
+
"values":[
|
19
|
+
"bob"
|
54
20
|
],
|
55
|
-
"
|
56
|
-
"attribute":"key",
|
57
|
-
"op":"in",
|
58
|
-
"values":[
|
59
|
-
"Alida.Caples@example.com"
|
60
|
-
]
|
61
|
-
}
|
21
|
+
"variation":1
|
62
22
|
}
|
63
23
|
],
|
64
|
-
"
|
65
|
-
|
66
|
-
|
67
|
-
|
24
|
+
"rules":[
|
25
|
+
|
26
|
+
],
|
27
|
+
"fallthrough":{
|
28
|
+
"variation":0
|
29
|
+
},
|
30
|
+
"offVariation":1,
|
31
|
+
"variations":[
|
32
|
+
true,
|
33
|
+
false
|
34
|
+
],
|
35
|
+
"deleted":false
|
36
|
+
}
|
data/spec/ldclient_spec.rb
CHANGED
@@ -1,10 +1,11 @@
|
|
1
1
|
require "spec_helper"
|
2
2
|
|
3
|
+
|
3
4
|
describe LaunchDarkly::LDClient do
|
4
5
|
subject { LaunchDarkly::LDClient }
|
6
|
+
let(:config) { LaunchDarkly::Config.new({:offline => true}) }
|
5
7
|
let(:client) do
|
6
|
-
|
7
|
-
subject.new("api_key")
|
8
|
+
subject.new("secret", config)
|
8
9
|
end
|
9
10
|
let(:feature) do
|
10
11
|
data = File.read(File.join("spec", "fixtures", "feature.json"))
|
@@ -23,246 +24,39 @@ describe LaunchDarkly::LDClient do
|
|
23
24
|
JSON.parse(data, symbolize_names: true)
|
24
25
|
end
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
30
|
-
client.update_user_flag_setting(nil, feature[:key], true)
|
31
|
-
end
|
32
|
-
|
33
|
-
it 'puts the new setting' do
|
34
|
-
result = double('result', success?: true, status: 204)
|
35
|
-
expect(client.instance_variable_get(:@client)).to receive(:put).and_return(result)
|
36
|
-
client.update_user_flag_setting(user[:key], feature[:key], true)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
describe '#flush' do
|
42
|
-
it "will flush and post all events" do
|
43
|
-
client.instance_variable_get(:@queue).push "asdf"
|
44
|
-
client.instance_variable_get(:@queue).push "asdf"
|
45
|
-
expect(client).to receive(:post_flushed_events)
|
46
|
-
client.flush
|
47
|
-
expect(client.instance_variable_get(:@queue).length).to eq 0
|
48
|
-
end
|
49
|
-
it "will not do anything if there are no events" do
|
50
|
-
expect(client).to_not receive(:post_flushed_events)
|
51
|
-
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
52
|
-
client.flush
|
53
|
-
end
|
54
|
-
end
|
55
|
-
|
56
|
-
describe '#post_flushed_events' do
|
57
|
-
let(:events) { ["event"] }
|
58
|
-
it "will flush and post all events" do
|
59
|
-
result = double("result", status: 200)
|
60
|
-
expect(client.instance_variable_get(:@client)).to receive(:post).with(LaunchDarkly::Config.default_events_uri + "/bulk").and_return result
|
61
|
-
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
62
|
-
client.send(:post_flushed_events, events)
|
63
|
-
expect(client.instance_variable_get(:@queue).length).to eq 0
|
64
|
-
end
|
65
|
-
it "will allow any 2XX response" do
|
66
|
-
result = double("result", status: 202)
|
67
|
-
expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
|
68
|
-
expect(client.instance_variable_get(:@config).logger).to_not receive :error
|
69
|
-
client.send(:post_flushed_events, events)
|
70
|
-
end
|
71
|
-
it "will work with unexpected post results" do
|
72
|
-
result = double("result", status: 418)
|
73
|
-
expect(client.instance_variable_get(:@client)).to receive(:post).and_return result
|
74
|
-
expect(client.instance_variable_get(:@config).logger).to receive :error
|
75
|
-
client.send(:post_flushed_events, events)
|
76
|
-
expect(client.instance_variable_get(:@queue).length).to eq 0
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
describe '#toggle?' do
|
81
|
-
it "will not fail" do
|
82
|
-
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_raise RuntimeError
|
83
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
84
|
-
result = client.toggle?(feature[:key], user, "default")
|
85
|
-
expect(result).to eq "default"
|
86
|
-
end
|
87
|
-
it "will specify the default value in the feature request event" do
|
88
|
-
expect(client).to receive(:add_event).with(hash_including(default: "default"))
|
89
|
-
result = client.toggle?(feature[:key], user, "default")
|
90
|
-
end
|
91
|
-
it "requires user" do
|
92
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
93
|
-
result = client.toggle?(feature[:key], nil, "default")
|
27
|
+
describe '#variation' do
|
28
|
+
it "will return the default value if the client is offline" do
|
29
|
+
result = client.variation(feature[:key], user, "default")
|
94
30
|
expect(result).to eq "default"
|
95
31
|
end
|
96
|
-
it "sanitizes the user in the event" do
|
97
|
-
expect(client).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
98
|
-
client.toggle?(feature[:key], numeric_key_user, "default")
|
99
|
-
end
|
100
|
-
it "returns value from streamed flag if available" do
|
101
|
-
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(true).twice
|
102
|
-
expect(client.instance_variable_get(:@stream_processor)).to receive(:started?).and_return true
|
103
|
-
expect(client.instance_variable_get(:@stream_processor)).to receive(:initialized?).and_return true
|
104
|
-
expect(client).to receive(:add_event)
|
105
|
-
expect(client).to receive(:get_streamed_flag).and_return feature
|
106
|
-
result = client.toggle?(feature[:key], user, "default")
|
107
|
-
expect(result).to eq false
|
108
|
-
end
|
109
|
-
it "returns value from normal request if streamed flag is not available" do
|
110
|
-
expect(client.instance_variable_get(:@config)).to receive(:stream?).and_return(false).twice
|
111
|
-
expect(client).to receive(:add_event)
|
112
|
-
expect(client).to receive(:get_flag_int).and_return feature
|
113
|
-
result = client.toggle?(feature[:key], user, "default")
|
114
|
-
expect(result).to eq false
|
115
|
-
end
|
116
32
|
end
|
117
33
|
|
118
|
-
describe '#
|
119
|
-
it "
|
120
|
-
|
121
|
-
|
122
|
-
end
|
123
|
-
it "sanitizes the user in the event" do
|
124
|
-
expect(client).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
125
|
-
client.identify(numeric_key_user)
|
34
|
+
describe '#secure_mode_hash' do
|
35
|
+
it "will return the expected value for a known message and secret" do
|
36
|
+
result = client.secure_mode_hash({:key => :Message})
|
37
|
+
expect(result).to eq "aa747c502a898200f9e4fa21bac68136f886a0e27aec70ba06daf2e2a5cb5597"
|
126
38
|
end
|
127
39
|
end
|
128
40
|
|
129
41
|
describe '#track' do
|
130
42
|
it "queues up an custom event" do
|
131
|
-
expect(client).to receive(:add_event).with(hash_including(kind: "custom", key: "custom_event_name", user: user, data: 42))
|
43
|
+
expect(client.instance_variable_get(:@event_processor)).to receive(:add_event).with(hash_including(kind: "custom", key: "custom_event_name", user: user, data: 42))
|
132
44
|
client.track("custom_event_name", user, 42)
|
133
45
|
end
|
134
46
|
it "sanitizes the user in the event" do
|
135
|
-
expect(client).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
47
|
+
expect(client.instance_variable_get(:@event_processor)).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
136
48
|
client.track("custom_event_name", numeric_key_user, nil)
|
137
49
|
end
|
138
50
|
end
|
139
51
|
|
140
|
-
describe '#
|
141
|
-
it "
|
142
|
-
expect(client).to receive(:
|
143
|
-
|
144
|
-
expect(client.send(:get_streamed_flag, "key")).to eq true
|
145
|
-
end
|
146
|
-
context "debug stream" do
|
147
|
-
it "will log an error if the streamed and polled flag do not match" do
|
148
|
-
expect(client.instance_variable_get(:@config)).to receive(:debug_stream?).and_return true
|
149
|
-
expect(client).to receive(:get_flag_stream).and_return true
|
150
|
-
expect(client).to receive(:get_flag_int).and_return false
|
151
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
152
|
-
expect(client.send(:get_streamed_flag, "key")).to eq true
|
153
|
-
end
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
describe '#all_flags' do
|
158
|
-
it "will parse and return the features list" do
|
159
|
-
result = double("Faraday::Response", status: 200, body: '{"asdf":"qwer"}')
|
160
|
-
expect(client).to receive(:make_request).with("/api/eval/features").and_return(result)
|
161
|
-
data = client.send(:all_flags)
|
162
|
-
expect(data).to eq(asdf: "qwer")
|
163
|
-
end
|
164
|
-
it "will log errors" do
|
165
|
-
result = double("Faraday::Response", status: 418)
|
166
|
-
expect(client).to receive(:make_request).with("/api/eval/features").and_return(result)
|
167
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
168
|
-
client.send(:all_flags)
|
169
|
-
end
|
170
|
-
end
|
171
|
-
|
172
|
-
describe '#get_flag_int' do
|
173
|
-
it "will return the parsed flag" do
|
174
|
-
result = double("Faraday::Response", status: 200, body: '{"asdf":"qwer"}')
|
175
|
-
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
176
|
-
data = client.send(:get_flag_int, "key")
|
177
|
-
expect(data).to eq(asdf: "qwer")
|
178
|
-
end
|
179
|
-
it "will accept 401 statuses" do
|
180
|
-
result = double("Faraday::Response", status: 401)
|
181
|
-
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
182
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
183
|
-
data = client.send(:get_flag_int, "key")
|
184
|
-
expect(data).to be_nil
|
185
|
-
end
|
186
|
-
it "will accept 404 statuses" do
|
187
|
-
result = double("Faraday::Response", status: 404)
|
188
|
-
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
189
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
190
|
-
data = client.send(:get_flag_int, "key")
|
191
|
-
expect(data).to be_nil
|
192
|
-
end
|
193
|
-
it "will accept non-standard statuses" do
|
194
|
-
result = double("Faraday::Response", status: 418)
|
195
|
-
expect(client).to receive(:make_request).with("/api/eval/features/key").and_return(result)
|
196
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:error)
|
197
|
-
data = client.send(:get_flag_int, "key")
|
198
|
-
expect(data).to be_nil
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
describe '#make_request' do
|
203
|
-
it "will make a proper request" do
|
204
|
-
expect(client.instance_variable_get :@client).to receive(:get)
|
205
|
-
client.send(:make_request, "/asdf")
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
describe '#param_for_user' do
|
210
|
-
it "will return a consistent hash of a user key, feature key, and feature salt" do
|
211
|
-
param = client.send(:param_for_user, feature, user)
|
212
|
-
expect(param).to be_between(0.0, 1.0).inclusive
|
213
|
-
end
|
214
|
-
end
|
215
|
-
|
216
|
-
describe '#evaluate' do
|
217
|
-
it "will return nil if there is no feature" do
|
218
|
-
expect(client.send(:evaluate, nil, user)).to eq nil
|
219
|
-
end
|
220
|
-
it "will return nil unless the feature is on" do
|
221
|
-
feature[:on] = false
|
222
|
-
expect(client.send(:evaluate, feature, user)).to eq nil
|
223
|
-
end
|
224
|
-
it "will return value if it matches the user" do
|
225
|
-
user = { key: "Alida.Caples@example.com" }
|
226
|
-
expect(client.send(:evaluate, feature, user)).to eq false
|
227
|
-
user = { key: "foo@bar.com" }
|
228
|
-
expect(client.send(:evaluate, feature, user)).to eq true
|
229
|
-
end
|
230
|
-
it "will return value if the target matches" do
|
231
|
-
user = { key: "asdf@asdf.com", custom: { groups: "Microsoft" } }
|
232
|
-
expect(client.send(:evaluate, feature, user)).to eq true
|
233
|
-
end
|
234
|
-
it "will return value if the weight matches" do
|
235
|
-
expect(client).to receive(:param_for_user).and_return 0.1
|
236
|
-
expect(client.send(:evaluate, feature, user)).to eq true
|
237
|
-
expect(client).to receive(:param_for_user).and_return 0.9
|
238
|
-
expect(client.send(:evaluate, feature, user)).to eq false
|
239
|
-
end
|
240
|
-
end
|
241
|
-
|
242
|
-
describe '#log_timings' do
|
243
|
-
let(:block) { lambda { "result" } }
|
244
|
-
let(:label) { "label" }
|
245
|
-
it "will not measure if not configured to do so" do
|
246
|
-
expect(Benchmark).to_not receive(:measure)
|
247
|
-
client.send(:log_timings, label, &block)
|
52
|
+
describe '#identify' do
|
53
|
+
it "queues up an identify event" do
|
54
|
+
expect(client.instance_variable_get(:@event_processor)).to receive(:add_event).with(hash_including(kind: "identify", key: user[:key], user: user))
|
55
|
+
client.identify(user)
|
248
56
|
end
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:debug?).and_return true
|
253
|
-
end
|
254
|
-
it "will benchmark timings and return result" do
|
255
|
-
expect(Benchmark).to receive(:measure).and_call_original
|
256
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:debug)
|
257
|
-
result = client.send(:log_timings, label, &block)
|
258
|
-
expect(result).to eq "result"
|
259
|
-
end
|
260
|
-
it "will raise exceptions if the block has them" do
|
261
|
-
block = lambda { raise RuntimeError }
|
262
|
-
expect(Benchmark).to receive(:measure).and_call_original
|
263
|
-
expect(client.instance_variable_get(:@config).logger).to receive(:debug)
|
264
|
-
expect { client.send(:log_timings, label, &block) }.to raise_error RuntimeError
|
265
|
-
end
|
57
|
+
it "sanitizes the user in the event" do
|
58
|
+
expect(client.instance_variable_get(:@event_processor)).to receive(:add_event).with(hash_including(user: sanitized_numeric_key_user))
|
59
|
+
client.identify(numeric_key_user)
|
266
60
|
end
|
267
61
|
end
|
268
62
|
|
@@ -276,4 +70,4 @@ describe LaunchDarkly::LDClient do
|
|
276
70
|
end
|
277
71
|
end
|
278
72
|
end
|
279
|
-
end
|
73
|
+
end
|