restify 2.0.2 → 3.0.0.rc2
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 +56 -0
- data/README.md +31 -5
- data/lib/restify/adapter/base.rb +11 -2
- data/lib/restify/adapter/ethon.rb +462 -0
- data/lib/restify/adapter/loop_lock.rb +124 -0
- data/lib/restify/adapter/telemetry.rb +6 -4
- data/lib/restify/adapter/typhoeus.rb +48 -23
- data/lib/restify/cache.rb +1 -1
- data/lib/restify/context.rb +5 -5
- data/lib/restify/error.rb +13 -0
- data/lib/restify/global.rb +9 -30
- data/lib/restify/logging.rb +10 -4
- data/lib/restify/processors/base/parsing.rb +13 -1
- data/lib/restify/promise.rb +20 -2
- data/lib/restify/registry.rb +2 -2
- data/lib/restify/relation.rb +9 -17
- data/lib/restify/request.rb +2 -2
- data/lib/restify/resource.rb +20 -2
- data/lib/restify/response.rb +41 -5
- data/lib/restify/timeout.rb +11 -5
- data/lib/restify/version.rb +3 -3
- data/lib/restify/webmock.rb +126 -0
- data/lib/restify.rb +5 -1
- data/spec/restify/adapter/base_spec.rb +33 -0
- data/spec/restify/adapter/ethon_spec.rb +263 -0
- data/spec/restify/adapter/loop_lock_spec.rb +144 -0
- data/spec/restify/adapter/telemetry_spec.rb +61 -0
- data/spec/restify/adapter/typhoeus_spec.rb +26 -0
- data/spec/restify/cache_spec.rb +1 -3
- data/spec/restify/context_spec.rb +57 -2
- data/spec/restify/features/opentelemetry_spec.rb +1 -1
- data/spec/restify/features/protocols_spec.rb +28 -0
- data/spec/restify/features/redirects_spec.rb +45 -0
- data/spec/restify/features/request_errors_spec.rb +1 -1
- data/spec/restify/features/response_bodies_spec.rb +45 -0
- data/spec/restify/features/response_encoding_spec.rb +82 -0
- data/spec/restify/features/response_errors_spec.rb +22 -1
- data/spec/restify/features/webmock_spec.rb +92 -1
- data/spec/restify/global_spec.rb +118 -1
- data/spec/restify/link_spec.rb +13 -3
- data/spec/restify/processors/json_spec.rb +36 -0
- data/spec/restify/processors/msgpack_spec.rb +36 -0
- data/spec/restify/promise_spec.rb +82 -0
- data/spec/restify/relation_spec.rb +22 -7
- data/spec/restify/request_spec.rb +26 -0
- data/spec/restify/resource_spec.rb +6 -0
- data/spec/restify/timeout_spec.rb +11 -0
- data/spec/restify_spec.rb +1 -1
- data/spec/spec_helper.rb +10 -7
- data/spec/support/stub_server.rb +4 -4
- metadata +27 -105
- data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/LICENSE +0 -21
- data/vendor/bundle/ruby/3.4.0/bundler/gems/my-rubocop-3bcd2110ca87/README.md +0 -23
- data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/CHANGELOG.md +0 -255
- data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/activesupport-8.0.2/README.rdoc +0 -40
- data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/CHANGELOG.md +0 -301
- data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/LICENSE.txt +0 -202
- data/vendor/bundle/ruby/3.4.0/gems/addressable-2.8.7/README.md +0 -121
- data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/base64-0.2.0/README.md +0 -48
- data/vendor/bundle/ruby/3.4.0/gems/benchmark-0.4.0/README.md +0 -138
- data/vendor/bundle/ruby/3.4.0/gems/bigdecimal-3.1.9/LICENSE +0 -56
- data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/CHANGELOG.md +0 -603
- data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/LICENSE.txt +0 -21
- data/vendor/bundle/ruby/3.4.0/gems/concurrent-ruby-1.3.5/README.md +0 -407
- data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/LICENSE +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/connection_pool-2.5.3/README.md +0 -167
- data/vendor/bundle/ruby/3.4.0/gems/drb-2.2.1/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/CHANGELOG.md +0 -375
- data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/LICENSE +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/ethon-0.16.0/README.md +0 -118
- data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/CHANGELOG.md +0 -473
- data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE +0 -24
- data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/LICENSE.SPECS +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/ffi-1.17.2-x86_64-linux-gnu/README.md +0 -137
- data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/LICENSE.txt +0 -16
- data/vendor/bundle/ruby/3.4.0/gems/hitimes-3.1.0/README.md +0 -187
- data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/i18n-1.14.7/README.md +0 -127
- data/vendor/bundle/ruby/3.4.0/gems/little-plugger-1.1.4/README.rdoc +0 -53
- data/vendor/bundle/ruby/3.4.0/gems/logger-1.7.0/README.md +0 -104
- data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/LICENSE +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/logging-2.4.0/README.md +0 -140
- data/vendor/bundle/ruby/3.4.0/gems/minitest-5.25.5/README.rdoc +0 -842
- data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/ChangeLog +0 -368
- data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/LICENSE +0 -177
- data/vendor/bundle/ruby/3.4.0/gems/msgpack-1.8.0/README.md +0 -302
- data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/CHANGELOG.md +0 -275
- data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/LICENSE.md +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/multi_json-1.15.0/README.md +0 -121
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/CHANGELOG.md +0 -193
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/LICENSE +0 -201
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-api-1.5.0/README.md +0 -68
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/CHANGELOG.md +0 -98
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/LICENSE +0 -201
- data/vendor/bundle/ruby/3.4.0/gems/opentelemetry-common-0.22.0/README.md +0 -62
- data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/CHANGELOG.md +0 -498
- data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/LICENSE.txt +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/public_suffix-6.0.2/README.md +0 -222
- data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/CHANGELOG.md +0 -1146
- data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/MIT-LICENSE +0 -20
- data/vendor/bundle/ruby/3.4.0/gems/rack-3.1.15/README.md +0 -355
- data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/MIT-LICENSE +0 -21
- data/vendor/bundle/ruby/3.4.0/gems/rake-13.2.1/README.rdoc +0 -155
- data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/LICENSE +0 -21
- data/vendor/bundle/ruby/3.4.0/gems/rake-release-1.3.0/README.md +0 -107
- data/vendor/bundle/ruby/3.4.0/gems/securerandom-0.4.1/README.md +0 -72
- data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/CHANGELOG.md +0 -410
- data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/LICENSE +0 -22
- data/vendor/bundle/ruby/3.4.0/gems/typhoeus-1.4.1/README.md +0 -588
- data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/LICENSE +0 -19
- data/vendor/bundle/ruby/3.4.0/gems/tzinfo-2.0.6/README.md +0 -406
- data/vendor/bundle/ruby/3.4.0/gems/uri-1.0.3/README.md +0 -55
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::LoopLock do
|
|
6
|
+
subject(:lock) { described_class.new { interrupts << Thread.current } }
|
|
7
|
+
|
|
8
|
+
let(:interrupts) { Queue.new }
|
|
9
|
+
let(:threads) { [] }
|
|
10
|
+
|
|
11
|
+
after { threads.each(&:kill).each(&:join) }
|
|
12
|
+
|
|
13
|
+
# Run the block in another thread, and wait until it has returned.
|
|
14
|
+
# The thread is kept alive until the example finished.
|
|
15
|
+
def other(&)
|
|
16
|
+
done = Concurrent::Event.new
|
|
17
|
+
threads << Thread.new do
|
|
18
|
+
yield
|
|
19
|
+
done.set
|
|
20
|
+
sleep
|
|
21
|
+
end
|
|
22
|
+
expect(done.wait(1)).to be true
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def elapsed
|
|
26
|
+
start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
27
|
+
yield
|
|
28
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) - start
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
describe '#acquire' do
|
|
32
|
+
it 'acquires a free loop' do
|
|
33
|
+
expect(lock.acquire(1)).to be true
|
|
34
|
+
expect(lock).to be_owned
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it 'does not acquire the loop when done' do
|
|
38
|
+
expect(lock.acquire(1) { true }).to be false
|
|
39
|
+
expect(lock).not_to be_owned
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it 'times out while another thread holds the loop' do
|
|
43
|
+
other { lock.acquire(1) }
|
|
44
|
+
|
|
45
|
+
expect(elapsed { expect(lock.acquire(0.1)).to be false }).to be >= 0.1
|
|
46
|
+
expect(lock).not_to be_owned
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'waits for another thread to release the loop' do
|
|
50
|
+
acquired = Concurrent::Event.new
|
|
51
|
+
released = false
|
|
52
|
+
threads << Thread.new do
|
|
53
|
+
lock.acquire(1)
|
|
54
|
+
acquired.set
|
|
55
|
+
sleep 0.1
|
|
56
|
+
released = true
|
|
57
|
+
lock.release
|
|
58
|
+
end
|
|
59
|
+
expect(acquired.wait(1)).to be true
|
|
60
|
+
|
|
61
|
+
expect(lock.acquire(1)).to be true
|
|
62
|
+
expect(released).to be true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it 'stops waiting when done after being notified' do
|
|
66
|
+
done = false
|
|
67
|
+
other { lock.acquire(1) }
|
|
68
|
+
threads << Thread.new do
|
|
69
|
+
sleep 0.1
|
|
70
|
+
done = true
|
|
71
|
+
lock.notify
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
expect(elapsed { expect(lock.acquire(1) { done }).to be false }).to be < 0.5
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
it 'treats a dead thread as not holding the loop' do
|
|
78
|
+
Thread.new { lock.acquire(1) }.join
|
|
79
|
+
|
|
80
|
+
expect(lock.acquire(0.1)).to be true
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
describe '#release' do
|
|
85
|
+
it 'lets a waiting thread acquire the loop' do
|
|
86
|
+
lock.acquire(1)
|
|
87
|
+
waiter = Thread.new { lock.acquire(1) }
|
|
88
|
+
sleep 0.05
|
|
89
|
+
|
|
90
|
+
lock.release
|
|
91
|
+
|
|
92
|
+
expect(waiter.join(1)&.value).to be true
|
|
93
|
+
expect(lock).not_to be_owned
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
describe 'with a background thread' do
|
|
98
|
+
let(:background) { Queue.new }
|
|
99
|
+
|
|
100
|
+
# A background thread running the loop until interrupted, like an
|
|
101
|
+
# event loop waiting for sockets. Blocks on `interrupts` on purpose,
|
|
102
|
+
# and is killed after each example.
|
|
103
|
+
def start_background
|
|
104
|
+
threads << Thread.new do
|
|
105
|
+
loop do
|
|
106
|
+
lock.acquire_background
|
|
107
|
+
background << :acquired
|
|
108
|
+
interrupts.pop
|
|
109
|
+
lock.release
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
expect(background.pop(timeout: 1)).to eq :acquired
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
it 'interrupts the background thread to release the loop' do
|
|
117
|
+
start_background
|
|
118
|
+
|
|
119
|
+
expect(lock.acquire(1)).to be true
|
|
120
|
+
expect(background).to be_empty
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
it 'blocks the background thread while a thread holds the loop' do
|
|
124
|
+
start_background
|
|
125
|
+
lock.acquire(1)
|
|
126
|
+
|
|
127
|
+
sleep 0.1
|
|
128
|
+
expect(background).to be_empty
|
|
129
|
+
|
|
130
|
+
lock.release
|
|
131
|
+
expect(background.pop(timeout: 1)).to eq :acquired
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
describe '#reset!' do
|
|
136
|
+
it 'forgets threads holding the loop' do
|
|
137
|
+
other { lock.acquire(1) }
|
|
138
|
+
|
|
139
|
+
lock.reset!
|
|
140
|
+
|
|
141
|
+
expect(lock.acquire(0.1)).to be true
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::Telemetry do
|
|
6
|
+
subject(:call) { adapter.call(request).value! }
|
|
7
|
+
|
|
8
|
+
let(:adapter) do
|
|
9
|
+
Class.new(Restify::Adapter::Base) do
|
|
10
|
+
def call_native(request, writer)
|
|
11
|
+
writer.fulfill(Restify::Response.new(request, request.uri, 200, {}, ''))
|
|
12
|
+
end
|
|
13
|
+
end.new
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
let(:request) { Restify::Request.new(uri:) }
|
|
17
|
+
let(:span) { OTLE_EXPORTER.finished_spans.last }
|
|
18
|
+
|
|
19
|
+
before { call }
|
|
20
|
+
|
|
21
|
+
context 'with an explicit port' do
|
|
22
|
+
let(:uri) { Addressable::URI.parse('https://example.org:8443/resources') }
|
|
23
|
+
|
|
24
|
+
it 'records the given port' do
|
|
25
|
+
expect(span.name).to eq 'GET https://example.org:8443'
|
|
26
|
+
expect(span.attributes).to eq({
|
|
27
|
+
'http.request.method' => 'GET',
|
|
28
|
+
'http.response.status_code' => 200,
|
|
29
|
+
'server.address' => 'example.org',
|
|
30
|
+
'server.port' => 8443,
|
|
31
|
+
'url.full' => 'https://example.org:8443/resources',
|
|
32
|
+
'url.scheme' => 'https',
|
|
33
|
+
})
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
context 'without a port' do
|
|
38
|
+
let(:uri) { Addressable::URI.parse('https://example.org/resources') }
|
|
39
|
+
|
|
40
|
+
it 'records the default port of the scheme' do
|
|
41
|
+
expect(span.name).to eq 'GET https://example.org:443'
|
|
42
|
+
expect(span.attributes).to include(
|
|
43
|
+
'server.port' => 443,
|
|
44
|
+
'url.full' => 'https://example.org/resources',
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context 'with a URI given as a string' do
|
|
50
|
+
let(:uri) { 'http://example.org/resources' }
|
|
51
|
+
|
|
52
|
+
it 'parses it' do
|
|
53
|
+
expect(span.name).to eq 'GET http://example.org:80'
|
|
54
|
+
expect(span.attributes).to include(
|
|
55
|
+
'server.address' => 'example.org',
|
|
56
|
+
'server.port' => 80,
|
|
57
|
+
'url.full' => 'http://example.org/resources',
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify::Adapter::Typhoeus do
|
|
6
|
+
let(:request) { Restify::Request.new(uri: 'http://localhost:9292/base') }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
stub_request(:get, 'http://stubserver/base').to_return(status: 200, body: '{}')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
describe 'sync' do
|
|
13
|
+
let(:adapter) { described_class.new(sync: true) }
|
|
14
|
+
|
|
15
|
+
it 'runs requests from many threads' do
|
|
16
|
+
threads = Array.new(4) do
|
|
17
|
+
Thread.new { Array.new(10) { adapter.call(request).value!.code } }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Do not hang the suite on a deadlock.
|
|
21
|
+
expect(threads.map {|thread| thread.join(5)&.value }).to all eq Array.new(10, 200)
|
|
22
|
+
ensure
|
|
23
|
+
threads&.each(&:kill)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
data/spec/restify/cache_spec.rb
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'spec_helper'
|
|
4
|
-
require 'active_support'
|
|
5
|
-
require 'active_support/cache'
|
|
6
4
|
|
|
7
5
|
describe Restify::Cache do
|
|
8
6
|
subject(:cache) { described_class.new(store) }
|
|
9
7
|
|
|
10
|
-
let(:store) {
|
|
8
|
+
let(:store) { Object.new }
|
|
11
9
|
|
|
12
10
|
describe '#call' do
|
|
13
11
|
let(:request) { instance_double(Restify::Request) }
|
|
@@ -59,7 +59,7 @@ describe Restify::Context do
|
|
|
59
59
|
let(:load) { YAML.load(dump) }
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
it_behaves_like 'serialization'
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
context 'Marshall' do
|
|
@@ -68,7 +68,62 @@ describe Restify::Context do
|
|
|
68
68
|
let(:dump) { Marshal.dump(context) }
|
|
69
69
|
let(:load) { Marshal.load(dump) } # rubocop:disable Security/MarshalLoad
|
|
70
70
|
|
|
71
|
-
|
|
71
|
+
it_behaves_like 'serialization'
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
describe '#request' do
|
|
76
|
+
subject(:request) { context.request(:get, '/').value! }
|
|
77
|
+
|
|
78
|
+
let(:adapter) do
|
|
79
|
+
response = Restify::Response.new(nil, Addressable::URI.parse(uri), 200, {}, 'body')
|
|
80
|
+
|
|
81
|
+
instance_double(Restify::Adapter::Base).tap do |adapter|
|
|
82
|
+
allow(adapter).to receive(:call).and_return(Restify::Promise.fulfilled(response))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
let(:cache) do
|
|
87
|
+
Object.new.tap do |cache|
|
|
88
|
+
def cache.call(request)
|
|
89
|
+
yield(request)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
let(:kwargs) { {adapter:} }
|
|
95
|
+
|
|
96
|
+
it 'calls the adapter without a cache' do
|
|
97
|
+
expect(adapter).to receive(:call).with(Restify::Request)
|
|
98
|
+
|
|
99
|
+
request
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
context 'with a cache' do
|
|
103
|
+
let(:kwargs) { {adapter:, cache:} }
|
|
104
|
+
|
|
105
|
+
it 'lets the cache call the adapter' do
|
|
106
|
+
expect(cache).to receive(:call).with(Restify::Request).and_call_original
|
|
107
|
+
expect(adapter).to receive(:call).with(Restify::Request)
|
|
108
|
+
|
|
109
|
+
request
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
context 'with a global cache' do
|
|
114
|
+
around do |example|
|
|
115
|
+
Restify.cache = cache
|
|
116
|
+
example.run
|
|
117
|
+
ensure
|
|
118
|
+
Restify.cache = nil
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
it 'lets the cache call the adapter' do
|
|
122
|
+
expect(cache).to receive(:call).with(Restify::Request).and_call_original
|
|
123
|
+
expect(adapter).to receive(:call).with(Restify::Request)
|
|
124
|
+
|
|
125
|
+
request
|
|
126
|
+
end
|
|
72
127
|
end
|
|
73
128
|
end
|
|
74
129
|
end
|
|
@@ -38,7 +38,7 @@ describe Restify do
|
|
|
38
38
|
# Latest span has to be from the Ethon instrumentation and must
|
|
39
39
|
# have the Restify span as a parent.
|
|
40
40
|
spans[0].tap do |span|
|
|
41
|
-
expect(span.instrumentation_scope.name).to
|
|
41
|
+
expect(span.instrumentation_scope.name).to include('Ethon')
|
|
42
42
|
expect(span.parent_span_id).to eq spans[1].span_id
|
|
43
43
|
end
|
|
44
44
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify do
|
|
6
|
+
# Restify follows relations from URLs in server responses, and libcurl
|
|
7
|
+
# supports far more than HTTP. A server must therefore never be able
|
|
8
|
+
# to make it access e.g. a `file://` or `gopher://` URL.
|
|
9
|
+
describe 'Protocol restrictions' do
|
|
10
|
+
subject(:request) { Restify.new(uri).get.value! }
|
|
11
|
+
|
|
12
|
+
context 'with a file URI' do
|
|
13
|
+
let(:uri) { "file://#{File.expand_path(__FILE__)}" }
|
|
14
|
+
|
|
15
|
+
it 'refuses the request' do
|
|
16
|
+
expect { request }.to raise_error Restify::NetworkError, /protocol/i
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
context 'with a gopher URI' do
|
|
21
|
+
let(:uri) { 'gopher://localhost:70/1' }
|
|
22
|
+
|
|
23
|
+
it 'refuses the request' do
|
|
24
|
+
expect { request }.to raise_error Restify::NetworkError, /protocol/i
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify do
|
|
6
|
+
subject(:resource) { Restify.new('http://localhost:9292/old/resource').get.value! }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
stub_request(:get, 'http://stubserver/old/resource')
|
|
10
|
+
.to_return do
|
|
11
|
+
<<~HTTP
|
|
12
|
+
HTTP/1.1 302 Found
|
|
13
|
+
Location: http://localhost:9292/new/resource
|
|
14
|
+
|
|
15
|
+
HTTP
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
stub_request(:get, 'http://stubserver/new/resource')
|
|
19
|
+
.to_return do
|
|
20
|
+
<<~HTTP
|
|
21
|
+
HTTP/1.1 200 OK
|
|
22
|
+
Content-Type: application/json
|
|
23
|
+
|
|
24
|
+
{"name":"John Smith","sibling_url":"./sibling"}
|
|
25
|
+
HTTP
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
describe 'following redirects' do
|
|
30
|
+
it 'returns the redirected resource' do
|
|
31
|
+
expect(resource['name']).to eq 'John Smith'
|
|
32
|
+
expect(resource.response.code).to eq 200
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
it 'exposes the effective URI on the response' do
|
|
36
|
+
expect(resource.response.uri.to_s)
|
|
37
|
+
.to eq 'http://localhost:9292/new/resource'
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
it 'resolves relative relations against the effective URI' do
|
|
41
|
+
expect(resource.rel(:sibling).expand({}).to_s)
|
|
42
|
+
.to eq 'http://localhost:9292/new/sibling'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify do
|
|
6
|
+
subject(:resource) { Restify.new('http://localhost:9292/base').get.value! }
|
|
7
|
+
|
|
8
|
+
before do
|
|
9
|
+
stub_request(:get, 'http://stubserver/base')
|
|
10
|
+
.to_return(
|
|
11
|
+
status: 200,
|
|
12
|
+
headers: {
|
|
13
|
+
'Content-Type' => 'application/json',
|
|
14
|
+
'Link' => '<http://localhost:9292/other>; rel="other"',
|
|
15
|
+
},
|
|
16
|
+
body:,
|
|
17
|
+
)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
describe 'Response bodies' do
|
|
21
|
+
# #31: A content type is set, but the body is empty.
|
|
22
|
+
context 'when empty' do
|
|
23
|
+
let(:body) { '' }
|
|
24
|
+
|
|
25
|
+
it 'returns a resource without data' do
|
|
26
|
+
expect(resource.data).to be_nil
|
|
27
|
+
expect(resource).to have_relation :other
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
context 'when invalid' do
|
|
32
|
+
let(:body) { '{"name": ' }
|
|
33
|
+
|
|
34
|
+
it 'returns a resource with relations from headers' do
|
|
35
|
+
expect(resource).to have_relation :other
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
it 'raises on accessing data' do
|
|
39
|
+
expect { resource.data }.to raise_error(Restify::ParseError, /Could not parse response body/) do |error|
|
|
40
|
+
expect(error.cause).to be_a JSON::ParserError
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'spec_helper'
|
|
4
|
+
|
|
5
|
+
describe Restify do
|
|
6
|
+
describe 'Response body encoding' do
|
|
7
|
+
subject(:response) { Restify.new('http://localhost:9292/base').get.value!.response }
|
|
8
|
+
|
|
9
|
+
let(:text) { 'Grüße, Ærø – 日本' }
|
|
10
|
+
|
|
11
|
+
before do
|
|
12
|
+
stub_request(:get, 'http://stubserver/base').to_return(
|
|
13
|
+
status: 200,
|
|
14
|
+
headers: {'Content-Type' => content_type},
|
|
15
|
+
body: body,
|
|
16
|
+
)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
context 'with an UTF-8 charset' do
|
|
20
|
+
let(:content_type) { 'text/plain; charset=UTF-8' }
|
|
21
|
+
let(:body) { text.encode(Encoding::UTF_8) }
|
|
22
|
+
|
|
23
|
+
it 'returns the body in the given encoding' do
|
|
24
|
+
expect(response.body.encoding).to eq Encoding::UTF_8
|
|
25
|
+
expect(response.body).to eq text
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
context 'with an ISO-8859-1 charset' do
|
|
30
|
+
let(:content_type) { 'text/plain; charset=ISO-8859-1' }
|
|
31
|
+
let(:text) { 'Grüße, Ærø' }
|
|
32
|
+
let(:body) { text.encode(Encoding::ISO_8859_1) }
|
|
33
|
+
|
|
34
|
+
it 'returns the body in the given encoding' do
|
|
35
|
+
expect(response.body.encoding).to eq Encoding::ISO_8859_1
|
|
36
|
+
expect(response.body).to eq text.encode(Encoding::ISO_8859_1)
|
|
37
|
+
expect(response.body.encode(Encoding::UTF_8)).to eq text
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
context 'with a quoted charset' do
|
|
42
|
+
let(:content_type) { 'text/plain; charset="utf-8"' }
|
|
43
|
+
let(:body) { text.encode(Encoding::UTF_8) }
|
|
44
|
+
|
|
45
|
+
it 'returns the body in the given encoding' do
|
|
46
|
+
expect(response.body.encoding).to eq Encoding::UTF_8
|
|
47
|
+
expect(response.body).to eq text
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
context 'with an unknown charset' do
|
|
52
|
+
let(:content_type) { 'text/plain; charset=nonsense' }
|
|
53
|
+
let(:body) { text.encode(Encoding::UTF_8) }
|
|
54
|
+
|
|
55
|
+
it 'returns the body as binary' do
|
|
56
|
+
expect(response.body.encoding).to eq Encoding::BINARY
|
|
57
|
+
expect(response.body.b).to eq text.b
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
context 'without a charset' do
|
|
62
|
+
let(:content_type) { 'text/plain' }
|
|
63
|
+
let(:body) { text.encode(Encoding::UTF_8) }
|
|
64
|
+
|
|
65
|
+
it 'returns the body as binary' do
|
|
66
|
+
expect(response.body.encoding).to eq Encoding::BINARY
|
|
67
|
+
expect(response.body.b).to eq text.b
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
context 'with a JSON body' do
|
|
72
|
+
let(:content_type) { 'application/json; charset=ISO-8859-1' }
|
|
73
|
+
let(:text) { 'Grüße, Ærø' }
|
|
74
|
+
let(:body) { %("#{text}").encode(Encoding::ISO_8859_1) }
|
|
75
|
+
|
|
76
|
+
it 'decodes the body using the given encoding' do
|
|
77
|
+
expect(response.body.encoding).to eq Encoding::ISO_8859_1
|
|
78
|
+
expect(response.decoded_body).to eq text
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
@@ -5,11 +5,12 @@ require 'spec_helper'
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
before do
|
|
7
7
|
stub_request(:get, 'http://stubserver/base')
|
|
8
|
-
.to_return(status: http_status, headers:)
|
|
8
|
+
.to_return(status: http_status, headers:, body:)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
let(:http_status) { '200 OK' }
|
|
12
12
|
let(:headers) { {} }
|
|
13
|
+
let(:body) { '' }
|
|
13
14
|
|
|
14
15
|
describe 'Error handling' do
|
|
15
16
|
subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
|
|
@@ -20,6 +21,26 @@ describe Restify do
|
|
|
20
21
|
it 'throws a BadRequest exception' do
|
|
21
22
|
expect { request }.to raise_error Restify::BadRequest
|
|
22
23
|
end
|
|
24
|
+
|
|
25
|
+
# #31: Rails sets a JSON content type even for empty bodies.
|
|
26
|
+
context 'with a JSON content type and an empty body' do
|
|
27
|
+
let(:headers) { {'Content-Type' => 'application/json'} }
|
|
28
|
+
|
|
29
|
+
it 'throws a BadRequest exception' do
|
|
30
|
+
expect { request }.to raise_error Restify::BadRequest
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
context 'with an invalid JSON body' do
|
|
35
|
+
let(:headers) { {'Content-Type' => 'application/json'} }
|
|
36
|
+
let(:body) { '{"errors": ' }
|
|
37
|
+
|
|
38
|
+
it 'throws a BadRequest exception with the raw body as errors' do
|
|
39
|
+
expect { request }.to raise_error(Restify::BadRequest) do |error|
|
|
40
|
+
expect(error.errors).to eq '{"errors": '
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
23
44
|
end
|
|
24
45
|
|
|
25
46
|
context 'for 401 status codes' do
|
|
@@ -4,11 +4,13 @@ require 'spec_helper'
|
|
|
4
4
|
|
|
5
5
|
describe Restify do
|
|
6
6
|
before do
|
|
7
|
-
WebMock.enable!
|
|
7
|
+
WebMock.enable!(except: %i[typhoeus])
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
after do
|
|
11
11
|
WebMock.disable!(except: %i[net_http])
|
|
12
|
+
WebMock.disable_net_connect!
|
|
13
|
+
WebMock::CallbackRegistry.reset
|
|
12
14
|
end
|
|
13
15
|
|
|
14
16
|
describe 'WebMock' do
|
|
@@ -23,5 +25,94 @@ describe Restify do
|
|
|
23
25
|
it 'raises error for not stubbed requests' do
|
|
24
26
|
expect { resource }.to raise_error WebMock::NetConnectNotAllowedError
|
|
25
27
|
end
|
|
28
|
+
|
|
29
|
+
it 'returns stubbed responses' do
|
|
30
|
+
stub_request(:get, 'http://www.example.com/base')
|
|
31
|
+
.to_return(
|
|
32
|
+
headers: {'Content-Type' => 'application/json', 'X-Custom' => 'yes'},
|
|
33
|
+
body: '{"name": "restify", "self_url": "http://www.example.com/self"}',
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(resource['name']).to eq 'restify'
|
|
37
|
+
expect(resource).to have_relation :self
|
|
38
|
+
expect(resource.response.headers).to include('X_CUSTOM' => 'yes')
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
it 'records requests' do
|
|
42
|
+
stub_request(:post, 'http://www.example.com/base')
|
|
43
|
+
|
|
44
|
+
Restify.new('http://www.example.com/base').post({name: 'restify'}).value!
|
|
45
|
+
|
|
46
|
+
expect(
|
|
47
|
+
a_request(:post, 'http://www.example.com/base')
|
|
48
|
+
.with(body: {name: 'restify'}.to_json, headers: {'Content-Type' => 'application/json'}),
|
|
49
|
+
).to have_been_made.once
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
it 'raises network errors on stubbed timeouts' do
|
|
53
|
+
stub_request(:get, 'http://www.example.com/base').to_timeout
|
|
54
|
+
|
|
55
|
+
expect { resource }.to raise_error Restify::NetworkError, /timeout/i
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
it 'raises stubbed errors' do
|
|
59
|
+
stub_request(:get, 'http://www.example.com/base').to_raise(Errno::ECONNREFUSED)
|
|
60
|
+
|
|
61
|
+
expect { resource }.to raise_error Errno::ECONNREFUSED
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'creates a span for stubbed requests' do
|
|
65
|
+
stub_request(:get, 'http://www.example.com/base')
|
|
66
|
+
|
|
67
|
+
resource
|
|
68
|
+
|
|
69
|
+
expect(OTLE_EXPORTER.finished_spans.map {|span| span.instrumentation_scope.name }).to eq ['restify']
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
context 'when disabled' do
|
|
73
|
+
it 'does not intercept requests' do
|
|
74
|
+
stub_request(:get, 'http://localhost:9292/echo').to_return(body: 'stubbed')
|
|
75
|
+
WebMock.disable!(except: %i[net_http])
|
|
76
|
+
|
|
77
|
+
expect(Restify.new('http://localhost:9292/echo').get.value!.response.body).not_to eq 'stubbed'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Like WebMock does for other libcurl-based libraries: Redirects are
|
|
82
|
+
# returned as they are.
|
|
83
|
+
it 'returns stubbed redirects as they are' do
|
|
84
|
+
stub_request(:get, 'http://www.example.com/base')
|
|
85
|
+
.to_return(status: 302, headers: {'Location' => '/other'})
|
|
86
|
+
|
|
87
|
+
expect(resource.response.code).to eq 302
|
|
88
|
+
expect(resource.response.headers).to include('LOCATION' => '/other')
|
|
89
|
+
expect(a_request(:get, 'http://www.example.com/other')).not_to have_been_made
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
context 'with net connect allowed' do
|
|
93
|
+
before { WebMock.allow_net_connect! }
|
|
94
|
+
|
|
95
|
+
it 'passes requests on to the adapter' do
|
|
96
|
+
expect(Restify.new('http://localhost:9292/echo').get.value!.data).to include('REQUEST_METHOD' => 'GET')
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it 'invokes callbacks for real requests once' do
|
|
100
|
+
responses = Queue.new
|
|
101
|
+
WebMock.after_request(real_requests_only: true) do |signature, response|
|
|
102
|
+
responses << [signature.uri.to_s, response.status[0]]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
Restify.new('http://localhost:9292/echo').get.value!
|
|
106
|
+
|
|
107
|
+
expect(responses.pop(timeout: 1)).to eq ['http://localhost:9292/echo', 200]
|
|
108
|
+
expect(responses).to be_empty
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
it 'records real requests once' do
|
|
112
|
+
Restify.new('http://localhost:9292/echo').get.value!
|
|
113
|
+
|
|
114
|
+
expect(a_request(:get, 'http://localhost:9292/echo')).to have_been_made.once
|
|
115
|
+
end
|
|
116
|
+
end
|
|
26
117
|
end
|
|
27
118
|
end
|