ldclient-rb 4.0.0 → 5.0.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.
data/spec/events_spec.rb CHANGED
@@ -351,12 +351,12 @@ describe LaunchDarkly::EventProcessor do
351
351
  expect(hc.get_request.headers["Authorization"]).to eq "sdk_key"
352
352
  end
353
353
 
354
- it "stops posting events after getting a 401 error" do
354
+ def verify_unrecoverable_http_error(status)
355
355
  @ep = subject.new("sdk_key", default_config, hc)
356
356
  e = { kind: "identify", user: user }
357
357
  @ep.add_event(e)
358
358
 
359
- hc.set_response_status(401)
359
+ hc.set_response_status(status)
360
360
  @ep.flush
361
361
  @ep.wait_until_inactive
362
362
  expect(hc.get_request).not_to be_nil
@@ -368,7 +368,7 @@ describe LaunchDarkly::EventProcessor do
368
368
  expect(hc.get_request).to be_nil
369
369
  end
370
370
 
371
- it "retries flush once after 5xx error" do
371
+ def verify_recoverable_http_error(status)
372
372
  @ep = subject.new("sdk_key", default_config, hc)
373
373
  e = { kind: "identify", user: user }
374
374
  @ep.add_event(e)
@@ -380,6 +380,33 @@ describe LaunchDarkly::EventProcessor do
380
380
  expect(hc.get_request).not_to be_nil
381
381
  expect(hc.get_request).not_to be_nil
382
382
  expect(hc.get_request).to be_nil # no 3rd request
383
+
384
+ # now verify that a subsequent flush still generates a request
385
+ hc.reset
386
+ @ep.add_event(e)
387
+ @ep.flush
388
+ @ep.wait_until_inactive
389
+ expect(hc.get_request).not_to be_nil
390
+ end
391
+
392
+ it "stops posting events after getting a 401 error" do
393
+ verify_unrecoverable_http_error(401)
394
+ end
395
+
396
+ it "stops posting events after getting a 403 error" do
397
+ verify_unrecoverable_http_error(403)
398
+ end
399
+
400
+ it "retries after 408 error" do
401
+ verify_recoverable_http_error(408)
402
+ end
403
+
404
+ it "retries after 429 error" do
405
+ verify_recoverable_http_error(429)
406
+ end
407
+
408
+ it "retries after 503 error" do
409
+ verify_recoverable_http_error(503)
383
410
  end
384
411
 
385
412
  it "retries flush once after connection error" do
@@ -7,7 +7,7 @@ describe LaunchDarkly::LDClient do
7
7
  let(:offline_client) do
8
8
  subject.new("secret", offline_config)
9
9
  end
10
- let(:update_processor) { NullUpdateProcessor.new }
10
+ let(:update_processor) { LaunchDarkly::NullUpdateProcessor.new }
11
11
  let(:config) { LaunchDarkly::Config.new({send_events: false, update_processor: update_processor}) }
12
12
  let(:client) do
13
13
  subject.new("secret", config)
@@ -160,13 +160,4 @@ describe LaunchDarkly::LDClient do
160
160
  expect(ep).not_to be_a(LaunchDarkly::NullEventProcessor)
161
161
  end
162
162
  end
163
-
164
- class NullUpdateProcessor
165
- def start
166
- end
167
-
168
- def initialized?
169
- true
170
- end
171
- end
172
163
  end
@@ -0,0 +1,89 @@
1
+ require "spec_helper"
2
+ require 'ostruct'
3
+
4
+ describe LaunchDarkly::PollingProcessor do
5
+ subject { LaunchDarkly::PollingProcessor }
6
+ let(:store) { LaunchDarkly::InMemoryFeatureStore.new }
7
+ let(:config) { LaunchDarkly::Config.new(feature_store: store) }
8
+ let(:requestor) { double() }
9
+ let(:processor) { subject.new(config, requestor) }
10
+
11
+ describe 'successful request' do
12
+ flag = { key: 'flagkey', version: 1 }
13
+ segment = { key: 'segkey', version: 1 }
14
+ all_data = {
15
+ flags: {
16
+ flagkey: flag
17
+ },
18
+ segments: {
19
+ segkey: segment
20
+ }
21
+ }
22
+
23
+ it 'puts feature data in store' do
24
+ allow(requestor).to receive(:request_all_data).and_return(all_data)
25
+ ready = processor.start
26
+ ready.wait
27
+ expect(store.get(LaunchDarkly::FEATURES, "flagkey")).to eq(flag)
28
+ expect(store.get(LaunchDarkly::SEGMENTS, "segkey")).to eq(segment)
29
+ end
30
+
31
+ it 'sets initialized to true' do
32
+ allow(requestor).to receive(:request_all_data).and_return(all_data)
33
+ ready = processor.start
34
+ ready.wait
35
+ expect(processor.initialized?).to be true
36
+ expect(store.initialized?).to be true
37
+ end
38
+ end
39
+
40
+ describe 'connection error' do
41
+ it 'does not cause immediate failure, does not set initialized' do
42
+ allow(requestor).to receive(:request_all_data).and_raise(StandardError.new("test error"))
43
+ ready = processor.start
44
+ finished = ready.wait(0.2)
45
+ expect(finished).to be false
46
+ expect(processor.initialized?).to be false
47
+ expect(store.initialized?).to be false
48
+ end
49
+ end
50
+
51
+ describe 'HTTP errors' do
52
+ def verify_unrecoverable_http_error(status)
53
+ allow(requestor).to receive(:request_all_data).and_raise(LaunchDarkly::UnexpectedResponseError.new(status))
54
+ ready = processor.start
55
+ finished = ready.wait(0.2)
56
+ expect(finished).to be true
57
+ expect(processor.initialized?).to be false
58
+ end
59
+
60
+ def verify_recoverable_http_error(status)
61
+ allow(requestor).to receive(:request_all_data).and_raise(LaunchDarkly::UnexpectedResponseError.new(status))
62
+ ready = processor.start
63
+ finished = ready.wait(0.2)
64
+ expect(finished).to be false
65
+ expect(processor.initialized?).to be false
66
+ end
67
+
68
+ it 'stops immediately for error 401' do
69
+ verify_unrecoverable_http_error(401)
70
+ end
71
+
72
+ it 'stops immediately for error 403' do
73
+ verify_unrecoverable_http_error(403)
74
+ end
75
+
76
+ it 'does not stop immediately for error 408' do
77
+ verify_recoverable_http_error(408)
78
+ end
79
+
80
+ it 'does not stop immediately for error 429' do
81
+ verify_recoverable_http_error(429)
82
+ end
83
+
84
+ it 'does not stop immediately for error 503' do
85
+ verify_recoverable_http_error(503)
86
+ end
87
+ end
88
+ end
89
+
@@ -0,0 +1,139 @@
1
+ require "spec_helper"
2
+ require "socketry"
3
+ require "sse_client/sse_shared"
4
+
5
+ #
6
+ # End-to-end tests of SSEClient against a real server
7
+ #
8
+ describe SSE::SSEClient do
9
+ subject { SSE::SSEClient }
10
+
11
+ def with_client(client)
12
+ begin
13
+ yield client
14
+ ensure
15
+ client.close
16
+ end
17
+ end
18
+
19
+ it "sends expected headers" do
20
+ with_server do |server|
21
+ requests = Queue.new
22
+ server.setup_response("/") do |req,res|
23
+ requests << req
24
+ res.content_type = "text/event-stream"
25
+ res.status = 200
26
+ end
27
+
28
+ headers = {
29
+ "Authorization" => "secret"
30
+ }
31
+
32
+ with_client(subject.new(server.base_uri, headers: headers)) do |client|
33
+ received_req = requests.pop
34
+ expect(received_req.header).to eq({
35
+ "accept" => ["text/event-stream"],
36
+ "cache-control" => ["no-cache"],
37
+ "host" => ["127.0.0.1"],
38
+ "authorization" => ["secret"]
39
+ })
40
+ end
41
+ end
42
+ end
43
+
44
+ it "receives messages" do
45
+ events_body = <<-EOT
46
+ event: go
47
+ data: foo
48
+ id: 1
49
+
50
+ event: stop
51
+ data: bar
52
+
53
+ EOT
54
+ with_server do |server|
55
+ server.setup_response("/") do |req,res|
56
+ res.content_type = "text/event-stream"
57
+ res.status = 200
58
+ res.body = events_body
59
+ end
60
+
61
+ event_sink = Queue.new
62
+ client = subject.new(server.base_uri) do |c|
63
+ c.on_event { |event| event_sink << event }
64
+ end
65
+
66
+ with_client(client) do |client|
67
+ expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", "1"))
68
+ expect(event_sink.pop).to eq(SSE::SSEEvent.new(:stop, "bar", nil))
69
+ end
70
+ end
71
+ end
72
+
73
+ it "reconnects after error response" do
74
+ events_body = <<-EOT
75
+ event: go
76
+ data: foo
77
+
78
+ EOT
79
+ with_server do |server|
80
+ attempt = 0
81
+ server.setup_response("/") do |req,res|
82
+ attempt += 1
83
+ if attempt == 1
84
+ res.status = 500
85
+ res.body = "sorry"
86
+ res.keep_alive = false
87
+ else
88
+ res.content_type = "text/event-stream"
89
+ res.status = 200
90
+ res.body = events_body
91
+ end
92
+ end
93
+
94
+ event_sink = Queue.new
95
+ error_sink = Queue.new
96
+ client = subject.new(server.base_uri, reconnect_time: 0.25) do |c|
97
+ c.on_event { |event| event_sink << event }
98
+ c.on_error { |error| error_sink << error }
99
+ end
100
+
101
+ with_client(client) do |client|
102
+ expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
103
+ expect(error_sink.pop).to eq({ status_code: 500, body: "sorry" })
104
+ expect(attempt).to be >= 2
105
+ end
106
+ end
107
+ end
108
+
109
+ it "reconnects after read timeout" do
110
+ events_body = <<-EOT
111
+ event: go
112
+ data: foo
113
+
114
+ EOT
115
+ with_server do |server|
116
+ attempt = 0
117
+ server.setup_response("/") do |req,res|
118
+ attempt += 1
119
+ if attempt == 1
120
+ sleep(2)
121
+ end
122
+ res.content_type = "text/event-stream"
123
+ res.status = 200
124
+ res.body = events_body
125
+ end
126
+
127
+ event_sink = Queue.new
128
+ client = subject.new(server.base_uri,
129
+ reconnect_time: 0.25, read_timeout: 0.25) do |c|
130
+ c.on_event { |event| event_sink << event }
131
+ end
132
+
133
+ with_client(client) do |client|
134
+ expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
135
+ expect(attempt).to be >= 2
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,100 @@
1
+ require "spec_helper"
2
+
3
+ describe SSE::EventParser do
4
+ subject { SSE::EventParser }
5
+
6
+ it "parses an event with all fields" do
7
+ lines = [
8
+ "event: abc\r\n",
9
+ "data: def\r\n",
10
+ "id: 1\r\n",
11
+ "\r\n"
12
+ ]
13
+ ep = subject.new(lines)
14
+
15
+ expected_event = SSE::SSEEvent.new(:abc, "def", "1")
16
+ output = ep.items.to_a
17
+ expect(output).to eq([ expected_event ])
18
+ end
19
+
20
+ it "parses an event with only data" do
21
+ lines = [
22
+ "data: def\r\n",
23
+ "\r\n"
24
+ ]
25
+ ep = subject.new(lines)
26
+
27
+ expected_event = SSE::SSEEvent.new(:message, "def", nil)
28
+ output = ep.items.to_a
29
+ expect(output).to eq([ expected_event ])
30
+ end
31
+
32
+ it "parses an event with multi-line data" do
33
+ lines = [
34
+ "data: def\r\n",
35
+ "data: ghi\r\n",
36
+ "\r\n"
37
+ ]
38
+ ep = subject.new(lines)
39
+
40
+ expected_event = SSE::SSEEvent.new(:message, "def\nghi", nil)
41
+ output = ep.items.to_a
42
+ expect(output).to eq([ expected_event ])
43
+ end
44
+
45
+ it "ignores comments" do
46
+ lines = [
47
+ ":",
48
+ "data: def\r\n",
49
+ ":",
50
+ "\r\n"
51
+ ]
52
+ ep = subject.new(lines)
53
+
54
+ expected_event = SSE::SSEEvent.new(:message, "def", nil)
55
+ output = ep.items.to_a
56
+ expect(output).to eq([ expected_event ])
57
+ end
58
+
59
+ it "parses reconnect interval" do
60
+ lines = [
61
+ "retry: 2500\r\n",
62
+ "\r\n"
63
+ ]
64
+ ep = subject.new(lines)
65
+
66
+ expected_item = SSE::SSESetRetryInterval.new(2500)
67
+ output = ep.items.to_a
68
+ expect(output).to eq([ expected_item ])
69
+ end
70
+
71
+ it "parses multiple events" do
72
+ lines = [
73
+ "event: abc\r\n",
74
+ "data: def\r\n",
75
+ "id: 1\r\n",
76
+ "\r\n",
77
+ "data: ghi\r\n",
78
+ "\r\n"
79
+ ]
80
+ ep = subject.new(lines)
81
+
82
+ expected_event_1 = SSE::SSEEvent.new(:abc, "def", "1")
83
+ expected_event_2 = SSE::SSEEvent.new(:message, "ghi", nil)
84
+ output = ep.items.to_a
85
+ expect(output).to eq([ expected_event_1, expected_event_2 ])
86
+ end
87
+
88
+ it "ignores events with no data" do
89
+ lines = [
90
+ "event: nothing\r\n",
91
+ "\r\n",
92
+ "event: nada\r\n",
93
+ "\r\n"
94
+ ]
95
+ ep = subject.new(lines)
96
+
97
+ output = ep.items.to_a
98
+ expect(output).to eq([])
99
+ end
100
+ end
@@ -0,0 +1,82 @@
1
+ require "spec_helper"
2
+ require "webrick"
3
+ require "webrick/httpproxy"
4
+ require "webrick/https"
5
+
6
+ class StubHTTPServer
7
+ def initialize
8
+ @port = 50000
9
+ begin
10
+ @server = create_server(@port)
11
+ rescue Errno::EADDRINUSE
12
+ @port += 1
13
+ retry
14
+ end
15
+ end
16
+
17
+ def create_server(port)
18
+ WEBrick::HTTPServer.new(
19
+ BindAddress: '127.0.0.1',
20
+ Port: port,
21
+ AccessLog: [],
22
+ Logger: NullLogger.new
23
+ )
24
+ end
25
+
26
+ def start
27
+ Thread.new { @server.start }
28
+ end
29
+
30
+ def stop
31
+ @server.shutdown
32
+ end
33
+
34
+ def base_uri
35
+ URI("http://127.0.0.1:#{@port}")
36
+ end
37
+
38
+ def setup_response(uri_path, &action)
39
+ @server.mount_proc(uri_path, action)
40
+ end
41
+ end
42
+
43
+ class StubProxyServer < StubHTTPServer
44
+ attr_reader :request_count
45
+ attr_accessor :connect_status
46
+
47
+ def initialize
48
+ super
49
+ @request_count = 0
50
+ end
51
+
52
+ def create_server(port)
53
+ WEBrick::HTTPProxyServer.new(
54
+ BindAddress: '127.0.0.1',
55
+ Port: port,
56
+ AccessLog: [],
57
+ Logger: NullLogger.new,
58
+ ProxyContentHandler: proc do |req,res|
59
+ if !@connect_status.nil?
60
+ res.status = @connect_status
61
+ end
62
+ @request_count += 1
63
+ end
64
+ )
65
+ end
66
+ end
67
+
68
+ class NullLogger
69
+ def method_missing(*)
70
+ self
71
+ end
72
+ end
73
+
74
+ def with_server(server = nil)
75
+ server = StubHTTPServer.new if server.nil?
76
+ begin
77
+ server.start
78
+ yield server
79
+ ensure
80
+ server.stop
81
+ end
82
+ end