ld-eventsource 1.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.
- 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
|