restify 1.10.0 → 1.15.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 +4 -4
- data/CHANGELOG.md +135 -53
- data/README.md +10 -12
- data/lib/restify/adapter/em.rb +6 -8
- data/lib/restify/adapter/pooled_em.rb +36 -40
- data/lib/restify/adapter/typhoeus.rb +69 -47
- data/lib/restify/context.rb +4 -4
- data/lib/restify/error.rb +48 -0
- data/lib/restify/global.rb +2 -2
- data/lib/restify/link.rb +4 -4
- data/lib/restify/processors/base.rb +2 -6
- data/lib/restify/processors/base/parsing.rb +2 -6
- data/lib/restify/promise.rb +2 -3
- data/lib/restify/relation.rb +1 -0
- data/lib/restify/request.rb +13 -5
- data/lib/restify/resource.rb +1 -1
- data/lib/restify/timeout.rb +2 -3
- data/lib/restify/version.rb +1 -1
- data/spec/restify/context_spec.rb +2 -2
- data/spec/restify/error_spec.rb +70 -0
- data/spec/restify/features/head_requests_spec.rb +5 -6
- data/spec/restify/features/request_bodies_spec.rb +83 -0
- data/spec/restify/features/request_headers_spec.rb +6 -7
- data/spec/restify/features/response_errors_spec.rb +127 -0
- data/spec/restify/global_spec.rb +2 -2
- data/spec/restify_spec.rb +50 -57
- data/spec/spec_helper.rb +12 -12
- data/spec/support/stub_server.rb +102 -0
- metadata +24 -13
- data/spec/restify/features/response_errors.rb +0 -79
@@ -4,20 +4,19 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe Restify do
|
6
6
|
let!(:request_stub) do
|
7
|
-
stub_request(:get,
|
8
|
-
|
7
|
+
stub_request(:get, "http://stubserver/base").to_return do
|
8
|
+
<<~HTTP
|
9
9
|
HTTP/1.1 200 OK
|
10
10
|
Content-Type: application/json
|
11
|
-
|
12
|
-
Link: <http://localhost/base>; rel="self"
|
11
|
+
Link: <http://localhost:9292/base>; rel="self"
|
13
12
|
|
14
13
|
{ "response": "success" }
|
15
|
-
|
14
|
+
HTTP
|
16
15
|
end
|
17
16
|
end
|
18
17
|
|
19
18
|
context 'with request headers configured for a single request' do
|
20
|
-
let(:context) { Restify.new('http://localhost/base') }
|
19
|
+
let(:context) { Restify.new('http://localhost:9292/base') }
|
21
20
|
|
22
21
|
it 'sends the headers only for that request' do
|
23
22
|
root = context.get(
|
@@ -37,7 +36,7 @@ describe Restify do
|
|
37
36
|
context 'with request headers configured for context' do
|
38
37
|
let(:context) do
|
39
38
|
Restify.new(
|
40
|
-
'http://localhost/base',
|
39
|
+
'http://localhost:9292/base',
|
41
40
|
headers: {'Accept' => 'application/msgpack, application/json'}
|
42
41
|
)
|
43
42
|
end
|
@@ -0,0 +1,127 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
|
5
|
+
describe Restify do
|
6
|
+
let!(:request_stub) do
|
7
|
+
stub_request(:get, 'http://stubserver/base')
|
8
|
+
.to_return(status: http_status, headers: headers)
|
9
|
+
end
|
10
|
+
|
11
|
+
let(:http_status) { '200 OK' }
|
12
|
+
let(:headers) { {} }
|
13
|
+
|
14
|
+
describe 'Error handling' do
|
15
|
+
subject(:request) { Restify.new('http://localhost:9292/base').get.value! }
|
16
|
+
|
17
|
+
context 'for 400 status codes' do
|
18
|
+
let(:http_status) { '400 Bad Request' }
|
19
|
+
|
20
|
+
it 'throws a BadRequest exception' do
|
21
|
+
expect { request }.to raise_error Restify::BadRequest
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
context 'for 401 status codes' do
|
26
|
+
let(:http_status) { '401 Unauthorized' }
|
27
|
+
|
28
|
+
it 'throws an Unauthorized exception' do
|
29
|
+
expect { request }.to raise_error Restify::Unauthorized
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
context 'for 404 status codes' do
|
34
|
+
let(:http_status) { '404 Not Found' }
|
35
|
+
|
36
|
+
it 'throws a ClientError exception' do
|
37
|
+
expect { request }.to raise_error Restify::NotFound
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
context 'for 406 status codes' do
|
42
|
+
let(:http_status) { '406 Not Acceptable' }
|
43
|
+
|
44
|
+
it 'throws a NotAcceptable exception' do
|
45
|
+
expect { request }.to raise_error Restify::NotAcceptable
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context 'for 422 status codes' do
|
50
|
+
let(:http_status) { '422 Unprocessable Entity' }
|
51
|
+
|
52
|
+
it 'throws a UnprocessableEntity exception' do
|
53
|
+
expect { request }.to raise_error Restify::UnprocessableEntity
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context 'for 429 status codes' do
|
58
|
+
let(:http_status) { '429 Too Many Requests' }
|
59
|
+
|
60
|
+
it 'throws a TooManyRequests exception' do
|
61
|
+
expect { request }.to raise_error Restify::TooManyRequests
|
62
|
+
end
|
63
|
+
|
64
|
+
describe 'the exception' do
|
65
|
+
subject(:exception) do
|
66
|
+
exception = nil
|
67
|
+
begin
|
68
|
+
request
|
69
|
+
rescue Restify::TooManyRequests => e
|
70
|
+
exception = e
|
71
|
+
end
|
72
|
+
exception
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'by default' do
|
76
|
+
it 'does not know when to retry again' do
|
77
|
+
expect(exception.retry_after).to be_nil
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
context 'with Retry-After header containing seconds' do
|
82
|
+
let(:headers) { {'Retry-After' => '120'} }
|
83
|
+
|
84
|
+
it 'determines the date correctly' do
|
85
|
+
now = DateTime.now
|
86
|
+
lower = now + Rational(119, 86_400)
|
87
|
+
upper = now + Rational(121, 86_400)
|
88
|
+
|
89
|
+
expect(exception.retry_after).to be_between(lower, upper)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'with Retry-After header containing HTTP date' do
|
94
|
+
let(:headers) { {'Retry-After' => 'Sun, 13 Mar 2033 13:03:33 GMT'} }
|
95
|
+
|
96
|
+
it 'parses the date correctly' do
|
97
|
+
expect(exception.retry_after.to_s).to eq '2033-03-13T13:03:33+00:00'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
context 'with Retry-After header containing invalid date string' do
|
102
|
+
let(:headers) { {'Retry-After' => 'tomorrow 12:00:00'} }
|
103
|
+
|
104
|
+
it 'does not know when to retry again' do
|
105
|
+
expect(exception.retry_after).to be_nil
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'for any other 4xx status codes' do
|
112
|
+
let(:http_status) { '415 Unsupported Media Type' }
|
113
|
+
|
114
|
+
it 'throws a generic ClientError exception' do
|
115
|
+
expect { request }.to raise_error Restify::ClientError
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
context 'for any 5xx status codes' do
|
120
|
+
let(:http_status) { '500 Internal Server Error' }
|
121
|
+
|
122
|
+
it 'throws a generic ServerError exception' do
|
123
|
+
expect { request }.to raise_error Restify::ServerError
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
data/spec/restify/global_spec.rb
CHANGED
@@ -26,10 +26,10 @@ describe Restify::Global do
|
|
26
26
|
let(:options) { {accept: 'application.vnd.github.v3+json'} }
|
27
27
|
let(:context) { Restify::Context.new uri, **options }
|
28
28
|
|
29
|
-
subject { global.new
|
29
|
+
subject { global.new(name, **options) }
|
30
30
|
|
31
31
|
it 'returns relation for stored registry item' do
|
32
|
-
Restify::Registry.store
|
32
|
+
Restify::Registry.store(name, uri, **options)
|
33
33
|
|
34
34
|
expect(subject).to be_a Restify::Relation
|
35
35
|
expect(subject.pattern).to eq uri
|
data/spec/restify_spec.rb
CHANGED
@@ -5,99 +5,94 @@ require 'spec_helper'
|
|
5
5
|
describe Restify do
|
6
6
|
context 'as a dynamic HATEOAS client' do
|
7
7
|
before do
|
8
|
-
stub_request(:get, 'http://
|
9
|
-
|
8
|
+
stub_request(:get, 'http://stubserver/base').to_return do
|
9
|
+
<<~HTTP
|
10
10
|
HTTP/1.1 200 OK
|
11
11
|
Content-Type: application/json
|
12
|
-
|
13
|
-
Link: <http://localhost/base/
|
14
|
-
Link: <http://localhost/base/courses{/id}>; rel="courses"
|
12
|
+
Link: <http://localhost:9292/base/users{/id}>; rel="users"
|
13
|
+
Link: <http://localhost:9292/base/courses{/id}>; rel="courses"
|
15
14
|
|
16
15
|
{
|
17
|
-
"profile_url": "http://localhost/base/profile",
|
18
|
-
"search_url": "http://localhost/base/search?q={query}",
|
16
|
+
"profile_url": "http://localhost:9292/base/profile",
|
17
|
+
"search_url": "http://localhost:9292/base/search?q={query}",
|
19
18
|
"mirror_url": null
|
20
19
|
}
|
21
|
-
|
20
|
+
HTTP
|
22
21
|
end
|
23
22
|
|
24
|
-
stub_request(:get, 'http://
|
25
|
-
|
23
|
+
stub_request(:get, 'http://stubserver/base/users')
|
24
|
+
.to_return do
|
25
|
+
<<~HTTP
|
26
26
|
HTTP/1.1 200 OK
|
27
27
|
Content-Type: application/json
|
28
|
-
Transfer-Encoding: chunked
|
29
28
|
|
30
29
|
[{
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
30
|
+
"name": "John Smith",
|
31
|
+
"url": "http://localhost:9292/base/users/john.smith",
|
32
|
+
"blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
|
33
|
+
"languages": ["de", "en"]
|
34
|
+
},
|
35
|
+
{
|
36
|
+
"name": "Jane Smith",
|
37
|
+
"self_url": "http://localhost:9292/base/user/jane.smith"
|
38
|
+
}]
|
39
|
+
HTTP
|
41
40
|
end
|
42
41
|
|
43
|
-
stub_request(:post, 'http://
|
42
|
+
stub_request(:post, 'http://stubserver/base/users')
|
44
43
|
.with(body: {})
|
45
44
|
.to_return do
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
Transfer-Encoding: chunked
|
45
|
+
<<~HTTP
|
46
|
+
HTTP/1.1 422 Unprocessable Entity
|
47
|
+
Content-Type: application/json
|
50
48
|
|
51
|
-
|
52
|
-
|
49
|
+
{"errors":{"name":["can't be blank"]}}
|
50
|
+
HTTP
|
53
51
|
end
|
54
52
|
|
55
|
-
stub_request(:post, 'http://
|
53
|
+
stub_request(:post, 'http://stubserver/base/users')
|
56
54
|
.with(body: {name: 'John Smith'})
|
57
55
|
.to_return do
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
RESPONSE
|
56
|
+
<<~HTTP
|
57
|
+
HTTP/1.1 201 Created
|
58
|
+
Content-Type: application/json
|
59
|
+
Location: http://localhost:9292/base/users/john.smith
|
60
|
+
|
61
|
+
{
|
62
|
+
"name": "John Smith",
|
63
|
+
"url": "http://localhost:9292/base/users/john.smith",
|
64
|
+
"blurb_url": "http://localhost:9292/base/users/john.smith/blurb",
|
65
|
+
"languages": ["de", "en"]
|
66
|
+
}
|
67
|
+
HTTP
|
71
68
|
end
|
72
69
|
|
73
|
-
stub_request(:get, 'http://
|
70
|
+
stub_request(:get, 'http://stubserver/base/users/john.smith')
|
74
71
|
.to_return do
|
75
|
-
|
72
|
+
<<~HTTP
|
76
73
|
HTTP/1.1 200 OK
|
77
74
|
Content-Type: application/json
|
78
|
-
Link: <http://localhost/base/users/john.smith>; rel="self"
|
79
|
-
Transfer-Encoding: chunked
|
75
|
+
Link: <http://localhost:9292/base/users/john.smith>; rel="self"
|
80
76
|
|
81
77
|
{
|
82
78
|
"name": "John Smith",
|
83
|
-
"url": "http://localhost/base/users/john.smith"
|
79
|
+
"url": "http://localhost:9292/base/users/john.smith"
|
84
80
|
}
|
85
|
-
|
81
|
+
HTTP
|
86
82
|
end
|
87
83
|
|
88
|
-
stub_request(:get, 'http://
|
84
|
+
stub_request(:get, 'http://stubserver/base/users/john.smith/blurb')
|
89
85
|
.to_return do
|
90
|
-
|
86
|
+
<<~HTTP
|
91
87
|
HTTP/1.1 200 OK
|
92
88
|
Content-Type: application/json
|
93
|
-
Link: <http://localhost/base/users/john.smith>; rel="user"
|
94
|
-
Transfer-Encoding: chunked
|
89
|
+
Link: <http://localhost:9292/base/users/john.smith>; rel="user"
|
95
90
|
|
96
91
|
{
|
97
92
|
"title": "Prof. Dr. John Smith",
|
98
93
|
"image": "http://example.org/avatar.png"
|
99
94
|
}
|
100
|
-
|
95
|
+
HTTP
|
101
96
|
end
|
102
97
|
end
|
103
98
|
|
@@ -107,7 +102,7 @@ describe Restify do
|
|
107
102
|
|
108
103
|
# First request the entry resource usually the
|
109
104
|
# root using GET and wait for it.
|
110
|
-
root = Restify.new('http://localhost/base').get.value!
|
105
|
+
root = Restify.new('http://localhost:9292/base').get.value!
|
111
106
|
|
112
107
|
# Therefore we need the `users` relations of our root
|
113
108
|
# resource.
|
@@ -134,7 +129,6 @@ describe Restify do
|
|
134
129
|
# the result is here.
|
135
130
|
expect { create_user_promise.value! }.to \
|
136
131
|
raise_error(Restify::ClientError) do |e|
|
137
|
-
|
138
132
|
# Because we forgot to send a "name" the server complains
|
139
133
|
# with an error code that will lead to a raised error.
|
140
134
|
|
@@ -195,7 +189,7 @@ describe Restify do
|
|
195
189
|
skip 'Seems to be impossible to detect EM scheduled fibers from within'
|
196
190
|
|
197
191
|
EM.synchrony do
|
198
|
-
root = Restify.new('http://localhost/base').get.value!
|
192
|
+
root = Restify.new('http://localhost:9292/base').get.value!
|
199
193
|
|
200
194
|
users_relation = root.rel(:users)
|
201
195
|
|
@@ -206,7 +200,6 @@ describe Restify do
|
|
206
200
|
|
207
201
|
expect { create_user_promise.value! }.to \
|
208
202
|
raise_error(Restify::ClientError) do |e|
|
209
|
-
|
210
203
|
expect(e.status).to eq :unprocessable_entity
|
211
204
|
expect(e.code).to eq 422
|
212
205
|
expect(e.errors).to eq 'name' => ["can't be blank"]
|
data/spec/spec_helper.rb
CHANGED
@@ -1,13 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require 'rspec'
|
4
|
-
require '
|
4
|
+
require 'rspec/collection_matchers'
|
5
5
|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
6
|
+
require 'simplecov'
|
7
|
+
SimpleCov.start do
|
8
|
+
add_filter 'spec'
|
9
|
+
end
|
10
|
+
|
11
|
+
if ENV['CI']
|
12
|
+
require 'codecov'
|
13
|
+
SimpleCov.formatter = SimpleCov::Formatter::Codecov
|
11
14
|
end
|
12
15
|
|
13
16
|
require 'restify'
|
@@ -31,17 +34,13 @@ if ENV['ADAPTER']
|
|
31
34
|
end
|
32
35
|
end
|
33
36
|
|
34
|
-
|
35
|
-
require 'rspec/collection_matchers'
|
36
|
-
require 'em-synchrony'
|
37
|
-
|
38
|
-
Dir[File.expand_path('spec/support/**/*.rb')].each {|f| require f }
|
37
|
+
require_relative 'support/stub_server.rb'
|
39
38
|
|
40
39
|
RSpec.configure do |config|
|
41
40
|
config.order = 'random'
|
42
41
|
|
43
42
|
config.before(:suite) do
|
44
|
-
::Restify::Timeout.default_timeout =
|
43
|
+
::Restify::Timeout.default_timeout = 0.1
|
45
44
|
end
|
46
45
|
|
47
46
|
config.before(:each) do
|
@@ -51,6 +50,7 @@ RSpec.configure do |config|
|
|
51
50
|
::Logging.logger.root.add_appenders ::Logging.appenders.stdout
|
52
51
|
end
|
53
52
|
|
53
|
+
config.warnings = true
|
54
54
|
config.after(:suite) do
|
55
55
|
EventMachine.stop if defined?(EventMachine) && EventMachine.reactor_running?
|
56
56
|
end
|
@@ -0,0 +1,102 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'puma'
|
4
|
+
require 'rack'
|
5
|
+
require 'webmock'
|
6
|
+
require 'webmock/rspec/matchers'
|
7
|
+
|
8
|
+
module Stub
|
9
|
+
# This Rack application matches the request received from rack against the
|
10
|
+
# webmock stub database and returns the response.
|
11
|
+
#
|
12
|
+
# A custom server name is used to
|
13
|
+
# 1) has a stable name without a dynamic port for easier `#stub_request`
|
14
|
+
# calls, and
|
15
|
+
# 2) to ensure no actual request is intercepted (they are send to
|
16
|
+
# `localhost:<port>`).
|
17
|
+
#
|
18
|
+
# If no stub is found a special HTTP 599 error code will be returned.
|
19
|
+
class Handler
|
20
|
+
def call(env) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
21
|
+
signature = WebMock::RequestSignature.new(
|
22
|
+
env['REQUEST_METHOD'].downcase,
|
23
|
+
"http://stubserver#{env['REQUEST_URI']}"
|
24
|
+
)
|
25
|
+
|
26
|
+
# Extract request headers from rack env. Most header should start with
|
27
|
+
# `HTTP_` but at least content type is present as `CONTENT_TYPE`.
|
28
|
+
headers = {}
|
29
|
+
env.each_pair do |key, value|
|
30
|
+
case key
|
31
|
+
when /^HTTP_(.*)$/, /^(CONTENT_.*)$/
|
32
|
+
headers[Regexp.last_match(1)] = value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
# Read request body from socket into string
|
37
|
+
signature.body = env['rack.input'].read
|
38
|
+
signature.headers = headers
|
39
|
+
|
40
|
+
WebMock::RequestRegistry.instance.requested_signatures.put(signature)
|
41
|
+
response = ::WebMock::StubRegistry.instance.response_for_request(signature)
|
42
|
+
|
43
|
+
# If no stub matched `nil` is returned.
|
44
|
+
if response
|
45
|
+
status = response.status
|
46
|
+
status = status.to_s.split(' ', 2) unless status.is_a?(Array)
|
47
|
+
status = Integer(status[0])
|
48
|
+
|
49
|
+
[status, response.headers || {}, [response.body.to_s]]
|
50
|
+
else
|
51
|
+
# Return special HTTP 599 with the error message that would normally
|
52
|
+
# appear on missing stubs.
|
53
|
+
[599, {}, [WebMock::NetConnectNotAllowedError.new(signature).message]]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
class Exception < ::StandardError; end
|
59
|
+
|
60
|
+
# Inject into base adapter to have HTTP 599 (missing stub) error raised as an
|
61
|
+
# extra exception, not just a server error.
|
62
|
+
module Patch
|
63
|
+
def call(request)
|
64
|
+
super.then do |response|
|
65
|
+
next response unless response.code == 599
|
66
|
+
|
67
|
+
raise ::Stub::Exception.new(response.body)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
::Restify::Adapter::Base.prepend(self)
|
72
|
+
end
|
73
|
+
|
74
|
+
class << self
|
75
|
+
def start_server!
|
76
|
+
@server = ::Puma::Server.new(Handler.new)
|
77
|
+
@server.add_tcp_listener('localhost', 9292)
|
78
|
+
|
79
|
+
Thread.new do
|
80
|
+
@server.run
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
RSpec.configure do |config|
|
87
|
+
config.include WebMock::API
|
88
|
+
config.include WebMock::Matchers
|
89
|
+
|
90
|
+
config.before(:suite) do
|
91
|
+
Stub.start_server!
|
92
|
+
|
93
|
+
# Net::HTTP adapter must be enabled, otherwise webmock fails to create mock
|
94
|
+
# responses from raw strings.
|
95
|
+
WebMock.disable!(except: %i[net_http])
|
96
|
+
end
|
97
|
+
|
98
|
+
config.around(:each) do |example|
|
99
|
+
example.run
|
100
|
+
WebMock.reset!
|
101
|
+
end
|
102
|
+
end
|