ldclient-rb 4.0.0 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
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