restify 3.0.0.rc2 → 3.0.0.rc3

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.
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::Ethon::Options do
6
+ subject(:options) { described_class.new(values) }
7
+
8
+ def number(name)
9
+ described_class.number(name)
10
+ end
11
+
12
+ describe '#to_a' do
13
+ subject(:compiled) { options.to_a }
14
+
15
+ context 'with bool options' do
16
+ let(:values) { {followlocation: true, verbose: false, tcp_keepalive: 0} }
17
+
18
+ it 'converts to 1 or 0' do
19
+ expect(compiled).to eq [
20
+ [number(:followlocation), :long, 1],
21
+ [number(:verbose), :long, 0],
22
+ [number(:tcp_keepalive), :long, 0],
23
+ ]
24
+ end
25
+ end
26
+
27
+ context 'with int options' do
28
+ let(:values) { {timeout_ms: 1500, maxredirs: '5'} }
29
+
30
+ it { expect(compiled).to eq [[number(:timeout_ms), :long, 1500], [number(:maxredirs), :long, 5]] }
31
+ end
32
+
33
+ context 'with enum options' do
34
+ let(:values) { {sslversion: :tlsv1, proxytype: 'https', timecondition: 1} }
35
+
36
+ it 'converts symbols, strings, and numbers' do
37
+ expect(compiled).to eq [
38
+ [number(:sslversion), :long, 1],
39
+ [number(:proxytype), :long, 2],
40
+ [number(:timecondition), :long, 1],
41
+ ]
42
+ end
43
+ end
44
+
45
+ context 'with bitmask options' do
46
+ let(:values) { {protocols: %i[http https], redir_protocols: :https} }
47
+
48
+ it { expect(compiled).to eq [[number(:protocols), :long, 3], [number(:redir_protocols), :long, 2]] }
49
+ end
50
+
51
+ context 'with string options' do
52
+ let(:values) { {useragent: :restify} }
53
+
54
+ it { expect(compiled).to eq [[number(:useragent), :string, 'restify']] }
55
+ end
56
+
57
+ context 'with nil values' do
58
+ let(:values) { {useragent: nil, timeout_ms: 5} }
59
+
60
+ it { expect(compiled).to eq [[number(:timeout_ms), :long, 5]] }
61
+ end
62
+
63
+ context 'with other option types' do
64
+ let(:values) { {postquote: []} }
65
+
66
+ it 'uses the generic handling' do
67
+ expect(compiled).to eq [[:postquote, nil, []]]
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#initialize' do
73
+ it 'raises on unknown options' do
74
+ expect { described_class.new(unknown: 1) }.to raise_error ArgumentError, /Unknown libcurl option: unknown/
75
+ end
76
+
77
+ it 'raises on unknown values' do
78
+ expect { described_class.new(sslversion: :tlsv9) }.to raise_error ArgumentError, /sslversion: :tlsv9/
79
+ end
80
+ end
81
+
82
+ describe '#apply' do
83
+ let(:values) { {followlocation: true, useragent: 'restify', postquote: []} }
84
+ let(:easy) { instance_double(Restify::Adapter::Ethon::Easy, handle: FFI::Pointer.new(1)) }
85
+
86
+ it 'sets converted options directly, and others with the generic handling' do
87
+ expect(Ethon::Curl).to receive(:easy_setopt).with(easy.handle, number(:followlocation), :long, 1)
88
+ expect(Ethon::Curl).to receive(:easy_setopt).with(easy.handle, number(:useragent), :string, 'restify')
89
+ expect(Ethon::Curl).to receive(:set_option).with(:postquote, [], easy.handle)
90
+
91
+ options.apply(easy)
92
+ end
93
+ end
94
+
95
+ describe '.number' do
96
+ it 'returns the number of the libcurl option' do
97
+ expect(number(:timeout_ms)).to eq 155
98
+ end
99
+
100
+ it 'raises on unknown options' do
101
+ expect { number(:unknown) }.to raise_error ArgumentError, /Unknown libcurl option/
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Restify::Adapter::Ethon::Pool do
6
+ subject(:pool) do
7
+ described_class.new(size: 2) do
8
+ created << Handle.new(0, Pointer.new(true))
9
+ created.last
10
+ end
11
+ end
12
+
13
+ let(:created) { [] }
14
+
15
+ before do
16
+ stub_const('Pointer', Struct.new(:autorelease))
17
+ stub_const('Handle', Struct.new(:resets, :handle) do
18
+ def reset
19
+ self.resets += 1
20
+ end
21
+ end,)
22
+ end
23
+
24
+ def release(*handles)
25
+ handles.each {|handle| pool.release(handle) }
26
+ end
27
+
28
+ describe '#checkout' do
29
+ it 'creates a handle when none is idle' do
30
+ expect(pool.checkout).to be created.first
31
+ end
32
+
33
+ it 'reuses released handles' do
34
+ handle = pool.checkout
35
+ release(handle)
36
+
37
+ expect(pool.checkout).to be handle
38
+ expect(created.size).to eq 1
39
+ end
40
+
41
+ it 'returns distinct handles to concurrent threads' do
42
+ release(*Array.new(2) { pool.checkout })
43
+
44
+ handles = Array.new(4) { Thread.new { pool.checkout } }.map(&:value)
45
+
46
+ expect(handles.map(&:object_id).uniq.size).to eq 4
47
+ end
48
+ end
49
+
50
+ describe '#release' do
51
+ it 'resets handles' do
52
+ handle = pool.checkout
53
+ release(handle)
54
+
55
+ expect(handle.resets).to eq 1
56
+ end
57
+
58
+ it 'keeps a limited number of idle handles' do
59
+ released = Array.new(3) { pool.checkout }
60
+ release(*released)
61
+
62
+ # Two of them are reused, the third one is created anew.
63
+ reused = Array.new(3) { pool.checkout }
64
+
65
+ expect(reused.count {|handle| released.any? {|r| r.equal?(handle) } }).to eq 2
66
+ expect(created.size).to eq 4
67
+ end
68
+ end
69
+
70
+ describe '#forked!' do
71
+ it 'does not release idle handles' do
72
+ idle = pool.checkout
73
+ release(idle)
74
+
75
+ pool.forked!
76
+
77
+ expect(idle.handle.autorelease).to be false
78
+ expect(pool.checkout).not_to be idle
79
+ end
80
+ end
81
+ end
@@ -6,87 +6,32 @@ describe Restify::Adapter::Ethon do
6
6
  describe 'completed transfers' do
7
7
  subject(:value) do
8
8
  Restify::Promise.create do |writer|
9
- described_class.new.send(
10
- :complete,
11
- easy,
12
- request,
13
- writer,
14
- )
9
+ described_class.new.send(:complete, easy, request, writer)
15
10
  end.value!
16
11
  end
17
12
 
18
13
  let(:request) { Restify::Request.new(uri: 'http://example.org/base') }
19
- let(:return_code) { :ok }
20
- let(:response_code) { 200 }
21
- let(:effective_url) { 'http://example.org/base' }
22
-
23
- # A transfer is handed back from libcurl with its return code and,
24
- # for HTTP, the status code of the response.
25
- def easy
26
- instance_double(
27
- described_class::Easy,
28
- return_code:,
29
- response_code:,
30
- effective_url:,
31
- response_headers: "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n",
32
- response_body: '{}',
33
- )
34
- end
14
+ let(:easy) { instance_double(described_class::Easy, return_code: :ok, response_code: 200) }
35
15
 
36
16
  it 'fulfills the promise with the response' do
37
- expect(value.code).to eq 200
38
- expect(value.uri.to_s).to eq 'http://example.org/base'
39
- end
40
-
41
- context 'when redirected' do
42
- let(:effective_url) { 'http://example.org/other/base' }
17
+ response = instance_double(Restify::Response)
18
+ allow(easy).to receive(:response).with(request).and_return(response)
43
19
 
44
- it 'uses the effective URL as the response URI' do
45
- expect(value.uri.to_s).to eq 'http://example.org/other/base'
46
- end
20
+ expect(value).to be response
47
21
  end
48
22
 
49
- context 'when the transfer failed' do
50
- let(:return_code) { :couldnt_connect }
51
- let(:response_code) { 0 }
23
+ it 'rejects the promise on network errors' do
24
+ allow(easy).to receive(:response).and_raise(Restify::NetworkError.new(request, 'kaboom'))
52
25
 
53
- it 'rejects the promise' do
54
- expect { value }.to raise_error Restify::NetworkError, /connect/i
55
- end
56
- end
57
-
58
- # Non-HTTP protocols are refused before a transfer is started, but
59
- # libcurl still reports success with a zero status code whenever no
60
- # HTTP status was received, and no status code at all when it cannot
61
- # be read back. Neither has a response to process.
62
- context 'without an HTTP status' do
63
- let(:response_code) { 0 }
64
-
65
- it 'rejects the promise' do
66
- expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
67
- end
68
- end
69
-
70
- context 'with an unavailable HTTP status' do
71
- let(:response_code) { nil }
72
-
73
- it 'rejects the promise' do
74
- expect { value }.to raise_error Restify::NetworkError, /without HTTP status/
75
- end
26
+ expect { value }.to raise_error Restify::NetworkError, /kaboom/
76
27
  end
77
28
 
78
29
  # Unexpected errors must reject the promise too, as the caller would
79
30
  # otherwise keep waiting for it until it times out.
80
- context 'when building the response fails' do
81
- def easy
82
- super.tap do |double|
83
- allow(double).to receive(:response_headers).and_raise('kaboom')
84
- end
85
- end
31
+ it 'rejects the promise on unexpected errors' do
32
+ allow(easy).to receive(:response).and_raise('kaboom')
86
33
 
87
- it 'rejects the promise' do
88
- expect { value }.to raise_error 'kaboom'
89
- end
34
+ expect { value }.to raise_error 'kaboom'
90
35
  end
91
36
  end
92
37
 
@@ -135,6 +80,13 @@ describe Restify::Adapter::Ethon do
135
80
  expect(adapter.call(request('fast')).value!.code).to eq 200
136
81
  end
137
82
 
83
+ it 'can wait again after a timeout' do
84
+ promise = adapter.call(request('slow'))
85
+
86
+ expect { promise.value!(0.1) }.to raise_error Timeout::Error
87
+ expect(promise.value!(1).code).to eq 200
88
+ end
89
+
138
90
  it 'processes requests from many threads' do
139
91
  threads = Array.new(8) do
140
92
  Thread.new do
@@ -180,13 +132,105 @@ describe Restify::Adapter::Ethon do
180
132
 
181
133
  context 'when waiting from within the event loop' do
182
134
  it 'does not run the loop again' do
183
- adapter.instance_variable_get(:@loop).acquire(0.1)
135
+ adapter.instance_variable_get(:@events).instance_variable_get(:@lock).acquire(0.1)
184
136
 
185
137
  expect(adapter.drive(Restify::Promise.new, Restify::Timeout.new(0.1))).to be false
186
138
  end
187
139
  end
188
140
  end
189
141
 
142
+ describe 'reusing easy handles' do
143
+ let(:adapter) { described_class.new }
144
+ let(:root) { Restify.new('http://localhost:9292/echo', adapter:) }
145
+
146
+ # Easy handles of completed transfers, in order.
147
+ let(:handles) { [] }
148
+
149
+ before do
150
+ allow(adapter).to receive(:complete).and_wrap_original do |m, easy, *args|
151
+ handles << easy
152
+ m.call(easy, *args)
153
+ end
154
+ end
155
+
156
+ def echo(method, *, **)
157
+ JSON.parse(root.send(method, *, **).value!.response.body)
158
+ end
159
+
160
+ it 'reuses the handle of a completed request' do
161
+ 2.times { root.get.value! }
162
+
163
+ expect(handles[1]).to be handles[0]
164
+ end
165
+
166
+ it 'does not reuse handles of requests in progress' do
167
+ Restify::Promise.new(Array.new(2) { root.get }).value!
168
+
169
+ expect(handles[1]).not_to be handles[0]
170
+ end
171
+
172
+ # Nothing must leak from one request into the next one.
173
+ describe 'subsequent requests' do
174
+ it 'use the method of each request' do
175
+ %i[post get head put patch delete get].each do |method|
176
+ response = root.send(method).value!.response
177
+ next if method == :head
178
+
179
+ expect(JSON.parse(response.body)).to include('REQUEST_METHOD' => method.to_s.upcase)
180
+ end
181
+ end
182
+
183
+ it 'do not send a previous body' do
184
+ root.post('payload').value!
185
+
186
+ expect(echo(:get)).not_to have_key('CONTENT_LENGTH')
187
+ end
188
+
189
+ it 'do not send previous headers' do
190
+ root.get(headers: {'X-Custom' => 'yes'}).value!
191
+
192
+ expect(echo(:get)).not_to have_key('HTTP_X_CUSTOM')
193
+ end
194
+
195
+ it 'receive a body after a HEAD request' do
196
+ root.head.value!
197
+
198
+ expect(root.get.value!.response.body).not_to be_empty
199
+ end
200
+ end
201
+
202
+ describe 'request bodies' do
203
+ before { stub_request(:any, 'http://stubserver/body') }
204
+
205
+ let(:root) { Restify.new('http://localhost:9292/body', adapter:) }
206
+
207
+ %i[post put patch].each do |method|
208
+ it "sends #{method.upcase} bodies" do
209
+ root.send(method, 'payload').value!
210
+ root.send(method, 'other').value!
211
+
212
+ expect(a_request(method, 'http://stubserver/body').with(body: 'payload')).to have_been_made.once
213
+ expect(a_request(method, 'http://stubserver/body').with(body: 'other')).to have_been_made.once
214
+ end
215
+ end
216
+ end
217
+ end
218
+
219
+ describe 'options' do
220
+ it 'raises on unknown options' do
221
+ expect { described_class.new(options: {unknown: 1}) }.to raise_error ArgumentError, /Unknown libcurl option/
222
+ end
223
+
224
+ it 'sets options on each request' do
225
+ adapter = described_class.new(options: {useragent: 'restify-spec'})
226
+ root = Restify.new('http://localhost:9292/echo', adapter:)
227
+
228
+ 2.times do
229
+ expect(JSON.parse(root.get.value!.response.body)).to include('HTTP_USER_AGENT' => 'restify-spec')
230
+ end
231
+ end
232
+ end
233
+
190
234
  describe 'after fork' do
191
235
  let(:adapter) { described_class.new }
192
236
  let(:connects) { Queue.new }
@@ -13,13 +13,13 @@ describe Restify::Adapter::LoopLock do
13
13
  # Run the block in another thread, and wait until it has returned.
14
14
  # The thread is kept alive until the example finished.
15
15
  def other(&)
16
- done = Concurrent::Event.new
16
+ done = Queue.new
17
17
  threads << Thread.new do
18
18
  yield
19
- done.set
19
+ done << true
20
20
  sleep
21
21
  end
22
- expect(done.wait(1)).to be true
22
+ expect(done.pop(timeout: 1)).to be true
23
23
  end
24
24
 
25
25
  def elapsed
@@ -47,16 +47,16 @@ describe Restify::Adapter::LoopLock do
47
47
  end
48
48
 
49
49
  it 'waits for another thread to release the loop' do
50
- acquired = Concurrent::Event.new
50
+ acquired = Queue.new
51
51
  released = false
52
52
  threads << Thread.new do
53
53
  lock.acquire(1)
54
- acquired.set
54
+ acquired << true
55
55
  sleep 0.1
56
56
  released = true
57
57
  lock.release
58
58
  end
59
- expect(acquired.wait(1)).to be true
59
+ expect(acquired.pop(timeout: 1)).to be true
60
60
 
61
61
  expect(lock.acquire(1)).to be true
62
62
  expect(released).to be true
@@ -72,6 +72,20 @@ describe Restify::Context do
72
72
  end
73
73
  end
74
74
 
75
+ describe '#join' do
76
+ let(:uri) { 'http://localhost/base/' }
77
+
78
+ it 'resolves relative URIs' do
79
+ expect(context.join('../other').to_s).to eq 'http://localhost/other'
80
+ end
81
+
82
+ it 'returns absolute URIs as they are' do
83
+ absolute = Addressable::URI.parse('http://example.org/path')
84
+
85
+ expect(context.join(absolute)).to be absolute
86
+ end
87
+ end
88
+
75
89
  describe '#request' do
76
90
  subject(:request) { context.request(:get, '/').value! }
77
91
 
@@ -39,6 +39,8 @@ describe Restify do
39
39
  # have the Restify span as a parent.
40
40
  spans[0].tap do |span|
41
41
  expect(span.instrumentation_scope.name).to include('Ethon')
42
+ expect(span.name).to eq 'GET'
43
+ expect(span.attributes).to include('http.request.method' => 'GET')
42
44
  expect(span.parent_span_id).to eq spans[1].span_id
43
45
  end
44
46
  end
@@ -96,6 +96,21 @@ describe Restify::Processors::Json do
96
96
  it { expect(resource.relation(:search)).to eq 'https://google.com{?q}' }
97
97
  end
98
98
 
99
+ context 'relation fields with relative references' do
100
+ let(:context) { Restify::Context.new('http://test.host/users/42') }
101
+ let(:body) do
102
+ <<-JSON
103
+ {"items_url": "items", "status_url": "none"}
104
+ JSON
105
+ end
106
+
107
+ it 'resolves them against the context URI' do
108
+ expect(resource.relation(:items).expand({}).to_s).to eq 'http://test.host/users/items'
109
+ end
110
+
111
+ it { is_expected.to have_relation :status }
112
+ end
113
+
99
114
  context 'object with implicit self relation' do
100
115
  let(:body) do
101
116
  <<-JSON
@@ -106,6 +121,58 @@ describe Restify::Processors::Json do
106
121
  it { expect(resource.relation(:self)).to eq '/self' }
107
122
  end
108
123
 
124
+ context 'relation fields in any case' do
125
+ let(:body) do
126
+ <<-JSON
127
+ {"Search_URL": "/search", "URL": "/self"}
128
+ JSON
129
+ end
130
+
131
+ it { expect(resource.relation(:search)).to eq '/search' }
132
+ it { expect(resource.relation(:self)).to eq '/self' }
133
+ end
134
+
135
+ context 'several fields for the same relation' do
136
+ let(:body) do
137
+ <<-JSON
138
+ {"url": "/first", "self_url": "/second"}
139
+ JSON
140
+ end
141
+
142
+ it { expect(resource.relation(:self)).to eq '/first' }
143
+ end
144
+
145
+ context 'relation fields without URLs' do
146
+ let(:body) do
147
+ <<-JSON
148
+ {"a_url": null, "c_url": "", "d_url": 42, "e_url": 1.5,
149
+ "f_url": true, "g_url": {"href": "/g"}, "h_url": ["/h"]}
150
+ JSON
151
+ end
152
+
153
+ it { expect(resource._restify_relations).to be_empty }
154
+ end
155
+
156
+ context 'fields similar to relations' do
157
+ let(:body) do
158
+ <<-JSON
159
+ {"_url": "/a", "url_b": "/b"}
160
+ JSON
161
+ end
162
+
163
+ it { expect(resource._restify_relations).to be_empty }
164
+ end
165
+
166
+ context 'relation fields with non-ASCII characters' do
167
+ let(:body) do
168
+ <<-JSON
169
+ {"\u00FCber_url": "/a", "\u212A_url": "/kelvin", "\u212Aurl": "/b"}
170
+ JSON
171
+ end
172
+
173
+ it { expect(resource._restify_relations).to be_empty }
174
+ end
175
+
109
176
  context 'single array' do
110
177
  let(:body) do
111
178
  <<-JSON