ldclient-rb 5.4.3 → 5.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +33 -6
- data/CHANGELOG.md +19 -0
- data/CONTRIBUTING.md +0 -12
- data/Gemfile.lock +22 -3
- data/README.md +41 -35
- data/ldclient-rb.gemspec +4 -3
- data/lib/ldclient-rb.rb +9 -1
- data/lib/ldclient-rb/cache_store.rb +1 -0
- data/lib/ldclient-rb/config.rb +201 -90
- data/lib/ldclient-rb/evaluation.rb +56 -8
- data/lib/ldclient-rb/event_summarizer.rb +3 -0
- data/lib/ldclient-rb/events.rb +16 -0
- data/lib/ldclient-rb/expiring_cache.rb +1 -0
- data/lib/ldclient-rb/file_data_source.rb +18 -13
- data/lib/ldclient-rb/flags_state.rb +3 -2
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +15 -4
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +135 -77
- data/lib/ldclient-rb/memoized_value.rb +2 -0
- data/lib/ldclient-rb/newrelic.rb +1 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +3 -3
- data/lib/ldclient-rb/polling.rb +1 -0
- data/lib/ldclient-rb/redis_store.rb +24 -190
- data/lib/ldclient-rb/requestor.rb +3 -2
- data/lib/ldclient-rb/simple_lru_cache.rb +1 -0
- data/lib/ldclient-rb/stream.rb +22 -10
- data/lib/ldclient-rb/user_filter.rb +1 -0
- data/lib/ldclient-rb/util.rb +1 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/scripts/gendocs.sh +12 -0
- data/spec/feature_store_spec_base.rb +173 -72
- data/spec/file_data_source_spec.rb +2 -2
- data/spec/http_util.rb +103 -0
- data/spec/in_memory_feature_store_spec.rb +1 -1
- data/spec/integrations/consul_feature_store_spec.rb +41 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +104 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +83 -4
- data/spec/redis_feature_store_spec.rb +25 -16
- data/spec/requestor_spec.rb +44 -38
- data/spec/stream_spec.rb +18 -18
- metadata +55 -33
- data/lib/sse_client.rb +0 -4
- data/lib/sse_client/backoff.rb +0 -38
- data/lib/sse_client/sse_client.rb +0 -171
- data/lib/sse_client/sse_events.rb +0 -67
- data/lib/sse_client/streaming_http.rb +0 -199
- data/spec/sse_client/sse_client_spec.rb +0 -177
- data/spec/sse_client/sse_events_spec.rb +0 -100
- data/spec/sse_client/sse_shared.rb +0 -82
- data/spec/sse_client/streaming_http_spec.rb +0 -263
@@ -1,177 +0,0 @@
|
|
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
|
-
|
140
|
-
it "reconnects if stream returns EOF" do
|
141
|
-
events_body_1 = <<-EOT
|
142
|
-
event: go
|
143
|
-
data: foo
|
144
|
-
|
145
|
-
EOT
|
146
|
-
events_body_2 = <<-EOT
|
147
|
-
event: go
|
148
|
-
data: bar
|
149
|
-
|
150
|
-
EOT
|
151
|
-
with_server do |server|
|
152
|
-
attempt = 0
|
153
|
-
server.setup_response("/") do |req,res|
|
154
|
-
attempt += 1
|
155
|
-
if attempt == 1
|
156
|
-
res.body = events_body_1
|
157
|
-
else
|
158
|
-
res.body = events_body_2
|
159
|
-
end
|
160
|
-
res.content_type = "text/event-stream"
|
161
|
-
res.status = 200
|
162
|
-
end
|
163
|
-
|
164
|
-
event_sink = Queue.new
|
165
|
-
client = subject.new(server.base_uri,
|
166
|
-
reconnect_time: 0.25, read_timeout: 0.25) do |c|
|
167
|
-
c.on_event { |event| event_sink << event }
|
168
|
-
end
|
169
|
-
|
170
|
-
with_client(client) do |client|
|
171
|
-
expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "foo", nil))
|
172
|
-
expect(event_sink.pop).to eq(SSE::SSEEvent.new(:go, "bar", nil))
|
173
|
-
expect(attempt).to be >= 2
|
174
|
-
end
|
175
|
-
end
|
176
|
-
end
|
177
|
-
end
|
@@ -1,100 +0,0 @@
|
|
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
|
@@ -1,82 +0,0 @@
|
|
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
|
@@ -1,263 +0,0 @@
|
|
1
|
-
require "spec_helper"
|
2
|
-
require "socketry"
|
3
|
-
require "sse_client/sse_shared"
|
4
|
-
|
5
|
-
#
|
6
|
-
# End-to-end tests of HTTP requests against a real server
|
7
|
-
#
|
8
|
-
describe SSE::StreamingHTTPConnection do
|
9
|
-
subject { SSE::StreamingHTTPConnection }
|
10
|
-
|
11
|
-
def with_connection(cxn)
|
12
|
-
begin
|
13
|
-
yield cxn
|
14
|
-
ensure
|
15
|
-
cxn.close
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
it "makes HTTP connection and sends request" do
|
20
|
-
with_server do |server|
|
21
|
-
requests = Queue.new
|
22
|
-
server.setup_response("/foo") do |req,res|
|
23
|
-
requests << req
|
24
|
-
res.status = 200
|
25
|
-
end
|
26
|
-
headers = {
|
27
|
-
"Accept" => "text/plain"
|
28
|
-
}
|
29
|
-
with_connection(subject.new(server.base_uri.merge("/foo?bar"), nil, headers, 30, 30)) do
|
30
|
-
received_req = requests.pop
|
31
|
-
expect(received_req.unparsed_uri).to eq("/foo?bar")
|
32
|
-
expect(received_req.header).to eq({
|
33
|
-
"accept" => ["text/plain"],
|
34
|
-
"host" => [server.base_uri.host]
|
35
|
-
})
|
36
|
-
end
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
it "receives response status" do
|
41
|
-
with_server do |server|
|
42
|
-
server.setup_response("/foo") do |req,res|
|
43
|
-
res.status = 204
|
44
|
-
end
|
45
|
-
with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
|
46
|
-
expect(cxn.status).to eq(204)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
it "receives response headers" do
|
52
|
-
with_server do |server|
|
53
|
-
server.setup_response("/foo") do |req,res|
|
54
|
-
res["Content-Type"] = "application/json"
|
55
|
-
end
|
56
|
-
with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
|
57
|
-
expect(cxn.headers["content-type"]).to eq("application/json")
|
58
|
-
end
|
59
|
-
end
|
60
|
-
end
|
61
|
-
|
62
|
-
it "can read response as lines" do
|
63
|
-
body = <<-EOT
|
64
|
-
This is
|
65
|
-
a response
|
66
|
-
EOT
|
67
|
-
with_server do |server|
|
68
|
-
server.setup_response("/foo") do |req,res|
|
69
|
-
res.body = body
|
70
|
-
end
|
71
|
-
with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
|
72
|
-
lines = cxn.read_lines
|
73
|
-
expect(lines.next).to eq("This is\n")
|
74
|
-
expect(lines.next).to eq("a response\n")
|
75
|
-
end
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
it "can read entire response body" do
|
80
|
-
body = <<-EOT
|
81
|
-
This is
|
82
|
-
a response
|
83
|
-
EOT
|
84
|
-
with_server do |server|
|
85
|
-
server.setup_response("/foo") do |req,res|
|
86
|
-
res.body = body
|
87
|
-
end
|
88
|
-
with_connection(subject.new(server.base_uri.merge("/foo"), nil, {}, 30, 30)) do |cxn|
|
89
|
-
read_body = cxn.read_all
|
90
|
-
expect(read_body).to eq("This is\na response\n")
|
91
|
-
end
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
it "enforces read timeout" do
|
96
|
-
with_server do |server|
|
97
|
-
server.setup_response("/") do |req,res|
|
98
|
-
sleep(2)
|
99
|
-
res.status = 200
|
100
|
-
end
|
101
|
-
expect { subject.new(server.base_uri, nil, {}, 30, 0.25) }.to raise_error(Socketry::TimeoutError)
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
it "connects to HTTP server through proxy" do
|
106
|
-
body = "hi"
|
107
|
-
with_server do |server|
|
108
|
-
server.setup_response("/") do |req,res|
|
109
|
-
res.body = body
|
110
|
-
end
|
111
|
-
with_server(StubProxyServer.new) do |proxy|
|
112
|
-
with_connection(subject.new(server.base_uri, proxy.base_uri, {}, 30, 30)) do |cxn|
|
113
|
-
read_body = cxn.read_all
|
114
|
-
expect(read_body).to eq("hi")
|
115
|
-
expect(proxy.request_count).to eq(1)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
120
|
-
|
121
|
-
it "throws error if proxy responds with error status" do
|
122
|
-
with_server do |server|
|
123
|
-
server.setup_response("/") do |req,res|
|
124
|
-
res.body = body
|
125
|
-
end
|
126
|
-
with_server(StubProxyServer.new) do |proxy|
|
127
|
-
proxy.connect_status = 403
|
128
|
-
expect { subject.new(server.base_uri, proxy.base_uri, {}, 30, 30) }.to raise_error(SSE::ProxyError)
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
# The following 2 tests were originally written to connect to an embedded HTTPS server made with
|
134
|
-
# WEBrick. Unfortunately, some unknown problem prevents WEBrick's self-signed certificate feature
|
135
|
-
# from working in JRuby 9.1 (but not in any other Ruby version). Therefore these tests currently
|
136
|
-
# hit an external URL.
|
137
|
-
|
138
|
-
it "connects to HTTPS server" do
|
139
|
-
with_connection(subject.new(URI("https://app.launchdarkly.com"), nil, {}, 30, 30)) do |cxn|
|
140
|
-
expect(cxn.status).to eq 200
|
141
|
-
end
|
142
|
-
end
|
143
|
-
|
144
|
-
it "connects to HTTPS server through proxy" do
|
145
|
-
with_server(StubProxyServer.new) do |proxy|
|
146
|
-
with_connection(subject.new(URI("https://app.launchdarkly.com"), proxy.base_uri, {}, 30, 30)) do |cxn|
|
147
|
-
expect(cxn.status).to eq 200
|
148
|
-
expect(proxy.request_count).to eq(1)
|
149
|
-
end
|
150
|
-
end
|
151
|
-
end
|
152
|
-
end
|
153
|
-
|
154
|
-
#
|
155
|
-
# Tests of response parsing functionality without a real HTTP request
|
156
|
-
#
|
157
|
-
describe SSE::HTTPResponseReader do
|
158
|
-
subject { SSE::HTTPResponseReader }
|
159
|
-
|
160
|
-
let(:simple_response) { <<-EOT
|
161
|
-
HTTP/1.1 200 OK
|
162
|
-
Cache-Control: no-cache
|
163
|
-
Content-Type: text/event-stream
|
164
|
-
|
165
|
-
line1\r
|
166
|
-
line2
|
167
|
-
\r
|
168
|
-
EOT
|
169
|
-
}
|
170
|
-
|
171
|
-
def make_chunks(str)
|
172
|
-
# arbitrarily split content into 5-character blocks
|
173
|
-
str.scan(/.{1,5}/m).to_enum
|
174
|
-
end
|
175
|
-
|
176
|
-
def mock_socket_without_timeout(chunks)
|
177
|
-
mock_socket(chunks) { :eof }
|
178
|
-
end
|
179
|
-
|
180
|
-
def mock_socket_with_timeout(chunks)
|
181
|
-
mock_socket(chunks) { raise Socketry::TimeoutError }
|
182
|
-
end
|
183
|
-
|
184
|
-
def mock_socket(chunks)
|
185
|
-
sock = double
|
186
|
-
allow(sock).to receive(:readpartial) do
|
187
|
-
begin
|
188
|
-
chunks.next
|
189
|
-
rescue StopIteration
|
190
|
-
yield
|
191
|
-
end
|
192
|
-
end
|
193
|
-
sock
|
194
|
-
end
|
195
|
-
|
196
|
-
it "parses status code" do
|
197
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
198
|
-
reader = subject.new(socket, 0)
|
199
|
-
expect(reader.status).to eq(200)
|
200
|
-
end
|
201
|
-
|
202
|
-
it "parses headers" do
|
203
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
204
|
-
reader = subject.new(socket, 0)
|
205
|
-
expect(reader.headers).to eq({
|
206
|
-
'cache-control' => 'no-cache',
|
207
|
-
'content-type' => 'text/event-stream'
|
208
|
-
})
|
209
|
-
end
|
210
|
-
|
211
|
-
it "can read entire response body" do
|
212
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
213
|
-
reader = subject.new(socket, 0)
|
214
|
-
expect(reader.read_all).to eq("line1\r\nline2\n\r\n")
|
215
|
-
end
|
216
|
-
|
217
|
-
it "can read response body as lines" do
|
218
|
-
socket = mock_socket_without_timeout(make_chunks(simple_response))
|
219
|
-
reader = subject.new(socket, 0)
|
220
|
-
expect(reader.read_lines.to_a).to eq([
|
221
|
-
"line1\r\n",
|
222
|
-
"line2\n",
|
223
|
-
"\r\n"
|
224
|
-
])
|
225
|
-
end
|
226
|
-
|
227
|
-
it "handles chunked encoding" do
|
228
|
-
chunked_response = <<-EOT
|
229
|
-
HTTP/1.1 200 OK
|
230
|
-
Content-Type: text/plain
|
231
|
-
Transfer-Encoding: chunked
|
232
|
-
|
233
|
-
6\r
|
234
|
-
things\r
|
235
|
-
A\r
|
236
|
-
and stuff\r
|
237
|
-
0\r
|
238
|
-
\r
|
239
|
-
EOT
|
240
|
-
socket = mock_socket_without_timeout(make_chunks(chunked_response))
|
241
|
-
reader = subject.new(socket, 0)
|
242
|
-
expect(reader.read_all).to eq("things and stuff")
|
243
|
-
end
|
244
|
-
|
245
|
-
it "raises error if response ends without complete headers" do
|
246
|
-
malformed_response = <<-EOT
|
247
|
-
HTTP/1.1 200 OK
|
248
|
-
Cache-Control: no-cache
|
249
|
-
EOT
|
250
|
-
socket = mock_socket_without_timeout(make_chunks(malformed_response))
|
251
|
-
expect { subject.new(socket, 0) }.to raise_error(EOFError)
|
252
|
-
end
|
253
|
-
|
254
|
-
it "throws timeout if thrown by socket read" do
|
255
|
-
socket = mock_socket_with_timeout(make_chunks(simple_response))
|
256
|
-
reader = subject.new(socket, 0)
|
257
|
-
lines = reader.read_lines
|
258
|
-
lines.next
|
259
|
-
lines.next
|
260
|
-
lines.next
|
261
|
-
expect { lines.next }.to raise_error(Socketry::TimeoutError)
|
262
|
-
end
|
263
|
-
end
|