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.
- checksums.yaml +4 -4
- data/.circleci/config.yml +11 -4
- data/CHANGELOG.md +9 -0
- data/README.md +7 -3
- data/ldclient-rb.gemspec +6 -24
- data/lib/ldclient-rb.rb +1 -0
- data/lib/ldclient-rb/events.rb +13 -7
- data/lib/ldclient-rb/ldclient.rb +29 -12
- data/lib/ldclient-rb/polling.rb +15 -8
- data/lib/ldclient-rb/requestor.rb +10 -14
- data/lib/ldclient-rb/stream.rb +26 -27
- data/lib/ldclient-rb/user_filter.rb +1 -0
- data/lib/ldclient-rb/util.rb +18 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/sse_client.rb +4 -0
- data/lib/sse_client/backoff.rb +38 -0
- data/lib/sse_client/sse_client.rb +162 -0
- data/lib/sse_client/sse_events.rb +67 -0
- data/lib/sse_client/streaming_http.rb +195 -0
- data/spec/events_spec.rb +30 -3
- data/spec/ldclient_spec.rb +1 -10
- data/spec/polling_spec.rb +89 -0
- data/spec/sse_client/sse_client_spec.rb +139 -0
- data/spec/sse_client/sse_events_spec.rb +100 -0
- data/spec/sse_client/sse_shared.rb +82 -0
- data/spec/sse_client/streaming_http_spec.rb +263 -0
- metadata +24 -36
@@ -0,0 +1,263 @@
|
|
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
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ldclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 5.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-
|
11
|
+
date: 2018-06-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -253,61 +253,33 @@ dependencies:
|
|
253
253
|
- !ruby/object:Gem::Version
|
254
254
|
version: '0.2'
|
255
255
|
- !ruby/object:Gem::Dependency
|
256
|
-
name:
|
256
|
+
name: http_tools
|
257
257
|
requirement: !ruby/object:Gem::Requirement
|
258
258
|
requirements:
|
259
259
|
- - "~>"
|
260
260
|
- !ruby/object:Gem::Version
|
261
|
-
version: 0.
|
261
|
+
version: 0.4.5
|
262
262
|
type: :runtime
|
263
263
|
prerelease: false
|
264
264
|
version_requirements: !ruby/object:Gem::Requirement
|
265
265
|
requirements:
|
266
266
|
- - "~>"
|
267
267
|
- !ruby/object:Gem::Version
|
268
|
-
version: 0.
|
268
|
+
version: 0.4.5
|
269
269
|
- !ruby/object:Gem::Dependency
|
270
|
-
name:
|
270
|
+
name: socketry
|
271
271
|
requirement: !ruby/object:Gem::Requirement
|
272
272
|
requirements:
|
273
273
|
- - "~>"
|
274
274
|
- !ruby/object:Gem::Version
|
275
|
-
version: 0.
|
275
|
+
version: 0.5.1
|
276
276
|
type: :runtime
|
277
277
|
prerelease: false
|
278
278
|
version_requirements: !ruby/object:Gem::Requirement
|
279
279
|
requirements:
|
280
280
|
- - "~>"
|
281
281
|
- !ruby/object:Gem::Version
|
282
|
-
version: 0.
|
283
|
-
- !ruby/object:Gem::Dependency
|
284
|
-
name: nio4r
|
285
|
-
requirement: !ruby/object:Gem::Requirement
|
286
|
-
requirements:
|
287
|
-
- - "<"
|
288
|
-
- !ruby/object:Gem::Version
|
289
|
-
version: '3'
|
290
|
-
type: :runtime
|
291
|
-
prerelease: false
|
292
|
-
version_requirements: !ruby/object:Gem::Requirement
|
293
|
-
requirements:
|
294
|
-
- - "<"
|
295
|
-
- !ruby/object:Gem::Version
|
296
|
-
version: '3'
|
297
|
-
- !ruby/object:Gem::Dependency
|
298
|
-
name: waitutil
|
299
|
-
requirement: !ruby/object:Gem::Requirement
|
300
|
-
requirements:
|
301
|
-
- - '='
|
302
|
-
- !ruby/object:Gem::Version
|
303
|
-
version: '0.2'
|
304
|
-
type: :runtime
|
305
|
-
prerelease: false
|
306
|
-
version_requirements: !ruby/object:Gem::Requirement
|
307
|
-
requirements:
|
308
|
-
- - '='
|
309
|
-
- !ruby/object:Gem::Version
|
310
|
-
version: '0.2'
|
282
|
+
version: 0.5.1
|
311
283
|
description: Official LaunchDarkly SDK for Ruby
|
312
284
|
email:
|
313
285
|
- team@launchdarkly.com
|
@@ -349,7 +321,13 @@ files:
|
|
349
321
|
- lib/ldclient-rb/simple_lru_cache.rb
|
350
322
|
- lib/ldclient-rb/stream.rb
|
351
323
|
- lib/ldclient-rb/user_filter.rb
|
324
|
+
- lib/ldclient-rb/util.rb
|
352
325
|
- lib/ldclient-rb/version.rb
|
326
|
+
- lib/sse_client.rb
|
327
|
+
- lib/sse_client/backoff.rb
|
328
|
+
- lib/sse_client/sse_client.rb
|
329
|
+
- lib/sse_client/sse_events.rb
|
330
|
+
- lib/sse_client/streaming_http.rb
|
353
331
|
- scripts/release.sh
|
354
332
|
- spec/config_spec.rb
|
355
333
|
- spec/evaluation_spec.rb
|
@@ -365,11 +343,16 @@ files:
|
|
365
343
|
- spec/in_memory_feature_store_spec.rb
|
366
344
|
- spec/ldclient_spec.rb
|
367
345
|
- spec/newrelic_spec.rb
|
346
|
+
- spec/polling_spec.rb
|
368
347
|
- spec/redis_feature_store_spec.rb
|
369
348
|
- spec/requestor_spec.rb
|
370
349
|
- spec/segment_store_spec_base.rb
|
371
350
|
- spec/simple_lru_cache_spec.rb
|
372
351
|
- spec/spec_helper.rb
|
352
|
+
- spec/sse_client/sse_client_spec.rb
|
353
|
+
- spec/sse_client/sse_events_spec.rb
|
354
|
+
- spec/sse_client/sse_shared.rb
|
355
|
+
- spec/sse_client/streaming_http_spec.rb
|
373
356
|
- spec/store_spec.rb
|
374
357
|
- spec/stream_spec.rb
|
375
358
|
- spec/user_filter_spec.rb
|
@@ -413,11 +396,16 @@ test_files:
|
|
413
396
|
- spec/in_memory_feature_store_spec.rb
|
414
397
|
- spec/ldclient_spec.rb
|
415
398
|
- spec/newrelic_spec.rb
|
399
|
+
- spec/polling_spec.rb
|
416
400
|
- spec/redis_feature_store_spec.rb
|
417
401
|
- spec/requestor_spec.rb
|
418
402
|
- spec/segment_store_spec_base.rb
|
419
403
|
- spec/simple_lru_cache_spec.rb
|
420
404
|
- spec/spec_helper.rb
|
405
|
+
- spec/sse_client/sse_client_spec.rb
|
406
|
+
- spec/sse_client/sse_events_spec.rb
|
407
|
+
- spec/sse_client/sse_shared.rb
|
408
|
+
- spec/sse_client/streaming_http_spec.rb
|
421
409
|
- spec/store_spec.rb
|
422
410
|
- spec/stream_spec.rb
|
423
411
|
- spec/user_filter_spec.rb
|