ld-eventsource 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +90 -0
- data/.gitignore +15 -0
- data/CHANGELOG.md +7 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +47 -0
- data/LICENSE +13 -0
- data/README.md +45 -0
- data/Rakefile +5 -0
- data/ld-eventsource.gemspec +31 -0
- data/lib/ld-eventsource.rb +14 -0
- data/lib/ld-eventsource/client.rb +296 -0
- data/lib/ld-eventsource/errors.rb +67 -0
- data/lib/ld-eventsource/events.rb +16 -0
- data/lib/ld-eventsource/impl/backoff.rb +60 -0
- data/lib/ld-eventsource/impl/event_parser.rb +81 -0
- data/lib/ld-eventsource/impl/streaming_http.rb +222 -0
- data/lib/ld-eventsource/version.rb +3 -0
- data/scripts/gendocs.sh +12 -0
- data/scripts/release.sh +30 -0
- data/spec/client_spec.rb +346 -0
- data/spec/event_parser_spec.rb +100 -0
- data/spec/http_stub.rb +81 -0
- data/spec/streaming_http_spec.rb +263 -0
- metadata +169 -0
data/spec/http_stub.rb
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
require "webrick"
|
2
|
+
require "webrick/httpproxy"
|
3
|
+
require "webrick/https"
|
4
|
+
|
5
|
+
class StubHTTPServer
|
6
|
+
def initialize
|
7
|
+
@port = 50000
|
8
|
+
begin
|
9
|
+
@server = create_server(@port)
|
10
|
+
rescue Errno::EADDRINUSE
|
11
|
+
@port += 1
|
12
|
+
retry
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def create_server(port)
|
17
|
+
WEBrick::HTTPServer.new(
|
18
|
+
BindAddress: '127.0.0.1',
|
19
|
+
Port: port,
|
20
|
+
AccessLog: [],
|
21
|
+
Logger: NullLogger.new
|
22
|
+
)
|
23
|
+
end
|
24
|
+
|
25
|
+
def start
|
26
|
+
Thread.new { @server.start }
|
27
|
+
end
|
28
|
+
|
29
|
+
def stop
|
30
|
+
@server.shutdown
|
31
|
+
end
|
32
|
+
|
33
|
+
def base_uri
|
34
|
+
URI("http://127.0.0.1:#{@port}")
|
35
|
+
end
|
36
|
+
|
37
|
+
def setup_response(uri_path, &action)
|
38
|
+
@server.mount_proc(uri_path, action)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class StubProxyServer < StubHTTPServer
|
43
|
+
attr_reader :request_count
|
44
|
+
attr_accessor :connect_status
|
45
|
+
|
46
|
+
def initialize
|
47
|
+
super
|
48
|
+
@request_count = 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def create_server(port)
|
52
|
+
WEBrick::HTTPProxyServer.new(
|
53
|
+
BindAddress: '127.0.0.1',
|
54
|
+
Port: port,
|
55
|
+
AccessLog: [],
|
56
|
+
Logger: NullLogger.new,
|
57
|
+
ProxyContentHandler: proc do |req,res|
|
58
|
+
if !@connect_status.nil?
|
59
|
+
res.status = @connect_status
|
60
|
+
end
|
61
|
+
@request_count += 1
|
62
|
+
end
|
63
|
+
)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
class NullLogger
|
68
|
+
def method_missing(*)
|
69
|
+
self
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def with_server(server = nil)
|
74
|
+
server = StubHTTPServer.new if server.nil?
|
75
|
+
begin
|
76
|
+
server.start
|
77
|
+
yield server
|
78
|
+
ensure
|
79
|
+
server.stop
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require "ld-eventsource/impl/streaming_http"
|
2
|
+
require "socketry"
|
3
|
+
require "http_stub"
|
4
|
+
|
5
|
+
#
|
6
|
+
# End-to-end tests of HTTP requests against a real server
|
7
|
+
#
|
8
|
+
describe SSE::Impl::StreamingHTTPConnection do
|
9
|
+
subject { SSE::Impl::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"), headers: headers)) 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"))) 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"))) 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"))) 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"))) 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, read_timeout: 0.25) }.to raise_error(SSE::Errors::ReadTimeoutError)
|
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: proxy.base_uri)) 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: proxy.base_uri) }.to raise_error(SSE::Errors::HTTPProxyError)
|
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"))) 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: proxy.base_uri)) 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::Impl::HTTPResponseReader do
|
158
|
+
subject { SSE::Impl::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(SSE::Errors::ReadTimeoutError)
|
262
|
+
end
|
263
|
+
end
|
metadata
ADDED
@@ -0,0 +1,169 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ld-eventsource
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- LaunchDarkly
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2019-01-05 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.7'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.7'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rspec
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '3.2'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '3.2'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rake
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '10.0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '10.0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rspec_junit_formatter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 0.3.0
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 0.3.0
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: concurrent-ruby
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: http_tools
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: 0.4.5
|
90
|
+
type: :runtime
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: 0.4.5
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: socketry
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.5.1
|
104
|
+
type: :runtime
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.5.1
|
111
|
+
description: LaunchDarkly SSE client for Ruby
|
112
|
+
email:
|
113
|
+
- team@launchdarkly.com
|
114
|
+
executables: []
|
115
|
+
extensions: []
|
116
|
+
extra_rdoc_files: []
|
117
|
+
files:
|
118
|
+
- ".circleci/config.yml"
|
119
|
+
- ".gitignore"
|
120
|
+
- CHANGELOG.md
|
121
|
+
- Gemfile
|
122
|
+
- Gemfile.lock
|
123
|
+
- LICENSE
|
124
|
+
- README.md
|
125
|
+
- Rakefile
|
126
|
+
- ld-eventsource.gemspec
|
127
|
+
- lib/ld-eventsource.rb
|
128
|
+
- lib/ld-eventsource/client.rb
|
129
|
+
- lib/ld-eventsource/errors.rb
|
130
|
+
- lib/ld-eventsource/events.rb
|
131
|
+
- lib/ld-eventsource/impl/backoff.rb
|
132
|
+
- lib/ld-eventsource/impl/event_parser.rb
|
133
|
+
- lib/ld-eventsource/impl/streaming_http.rb
|
134
|
+
- lib/ld-eventsource/version.rb
|
135
|
+
- scripts/gendocs.sh
|
136
|
+
- scripts/release.sh
|
137
|
+
- spec/client_spec.rb
|
138
|
+
- spec/event_parser_spec.rb
|
139
|
+
- spec/http_stub.rb
|
140
|
+
- spec/streaming_http_spec.rb
|
141
|
+
homepage: https://github.com/launchdarkly/ruby-eventsource
|
142
|
+
licenses:
|
143
|
+
- Apache-2.0
|
144
|
+
metadata: {}
|
145
|
+
post_install_message:
|
146
|
+
rdoc_options: []
|
147
|
+
require_paths:
|
148
|
+
- lib
|
149
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
150
|
+
requirements:
|
151
|
+
- - ">="
|
152
|
+
- !ruby/object:Gem::Version
|
153
|
+
version: '0'
|
154
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - ">="
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '0'
|
159
|
+
requirements: []
|
160
|
+
rubyforge_project:
|
161
|
+
rubygems_version: 2.7.6
|
162
|
+
signing_key:
|
163
|
+
specification_version: 4
|
164
|
+
summary: LaunchDarkly SSE client
|
165
|
+
test_files:
|
166
|
+
- spec/client_spec.rb
|
167
|
+
- spec/event_parser_spec.rb
|
168
|
+
- spec/http_stub.rb
|
169
|
+
- spec/streaming_http_spec.rb
|