ld-eventsource 1.0.0

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