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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +27 -0
- data/README.md +0 -1
- data/lib/restify/adapter/ethon/easy.rb +147 -0
- data/lib/restify/adapter/ethon/event_loop.rb +255 -0
- data/lib/restify/adapter/ethon/options.rb +81 -0
- data/lib/restify/adapter/ethon/pool.rb +43 -0
- data/lib/restify/adapter/ethon.rb +56 -307
- data/lib/restify/adapter/telemetry.rb +1 -1
- data/lib/restify/adapter/typhoeus.rb +5 -1
- data/lib/restify/context.rb +11 -1
- data/lib/restify/processors/base/parsing.rb +30 -25
- data/lib/restify/promise.rb +181 -47
- data/lib/restify/relation.rb +19 -5
- data/lib/restify/timeout.rb +19 -36
- data/lib/restify/version.rb +1 -1
- data/lib/restify/webmock.rb +1 -1
- data/lib/restify.rb +0 -1
- data/spec/restify/adapter/ethon/easy_spec.rb +111 -0
- data/spec/restify/adapter/ethon/event_loop_spec.rb +96 -0
- data/spec/restify/adapter/ethon/options_spec.rb +104 -0
- data/spec/restify/adapter/ethon/pool_spec.rb +81 -0
- data/spec/restify/adapter/ethon_spec.rb +111 -67
- data/spec/restify/adapter/loop_lock_spec.rb +6 -6
- data/spec/restify/context_spec.rb +14 -0
- data/spec/restify/features/opentelemetry_spec.rb +2 -0
- data/spec/restify/processors/json_spec.rb +67 -0
- data/spec/restify/promise_spec.rb +218 -21
- data/spec/restify/relation_spec.rb +23 -0
- data/spec/restify/timeout_spec.rb +38 -39
- metadata +9 -15
|
@@ -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(:
|
|
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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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(:@
|
|
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 =
|
|
16
|
+
done = Queue.new
|
|
17
17
|
threads << Thread.new do
|
|
18
18
|
yield
|
|
19
|
-
done
|
|
19
|
+
done << true
|
|
20
20
|
sleep
|
|
21
21
|
end
|
|
22
|
-
expect(done.
|
|
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 =
|
|
50
|
+
acquired = Queue.new
|
|
51
51
|
released = false
|
|
52
52
|
threads << Thread.new do
|
|
53
53
|
lock.acquire(1)
|
|
54
|
-
acquired
|
|
54
|
+
acquired << true
|
|
55
55
|
sleep 0.1
|
|
56
56
|
released = true
|
|
57
57
|
lock.release
|
|
58
58
|
end
|
|
59
|
-
expect(acquired.
|
|
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
|