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.
@@ -4,20 +4,19 @@ require 'spec_helper'
4
4
 
5
5
  describe Restify do
6
6
  let!(:request_stub) do
7
- stub_request(:get, 'http://localhost/base').to_return do
8
- <<-RESPONSE.gsub(/^ {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
- Transfer-Encoding: chunked
12
- Link: <http://localhost/base>; rel="self"
11
+ Link: <http://localhost:9292/base>; rel="self"
13
12
 
14
13
  { "response": "success" }
15
- RESPONSE
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
@@ -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 name, options }
29
+ subject { global.new(name, **options) }
30
30
 
31
31
  it 'returns relation for stored registry item' do
32
- Restify::Registry.store name, uri, options
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://localhost/base').to_return do
9
- <<-RESPONSE.gsub(/^ {10}/, '')
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
- Transfer-Encoding: chunked
13
- Link: <http://localhost/base/users{/id}>; rel="users"
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
- RESPONSE
20
+ HTTP
22
21
  end
23
22
 
24
- stub_request(:get, 'http://localhost/base/users').to_return do
25
- <<-RESPONSE.gsub(/^ {10}/, '')
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
- "name": "John Smith",
32
- "url": "http://localhost/base/users/john.smith",
33
- "blurb_url": "http://localhost/base/users/john.smith/blurb",
34
- "languages": ["de", "en"]
35
- },
36
- {
37
- "name": "Jane Smith",
38
- "self_url": "http://localhost/base/user/jane.smith"
39
- }]
40
- RESPONSE
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://localhost/base/users')
42
+ stub_request(:post, 'http://stubserver/base/users')
44
43
  .with(body: {})
45
44
  .to_return do
46
- <<-RESPONSE.gsub(/^ {12}/, '')
47
- HTTP/1.1 422 Unprocessable Entity
48
- Content-Type: application/json
49
- Transfer-Encoding: chunked
45
+ <<~HTTP
46
+ HTTP/1.1 422 Unprocessable Entity
47
+ Content-Type: application/json
50
48
 
51
- {"errors":{"name":["can't be blank"]}}
52
- RESPONSE
49
+ {"errors":{"name":["can't be blank"]}}
50
+ HTTP
53
51
  end
54
52
 
55
- stub_request(:post, 'http://localhost/base/users')
53
+ stub_request(:post, 'http://stubserver/base/users')
56
54
  .with(body: {name: 'John Smith'})
57
55
  .to_return do
58
- <<-RESPONSE.gsub(/^ {12}/, '')
59
- HTTP/1.1 201 Created
60
- Content-Type: application/json
61
- Location: http://localhost/base/users/john.smith
62
- Transfer-Encoding: chunked
63
-
64
- {
65
- "name": "John Smith",
66
- "url": "http://localhost/base/users/john.smith",
67
- "blurb_url": "http://localhost/base/users/john.smith/blurb",
68
- "languages": ["de", "en"]
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://localhost/base/users/john.smith')
70
+ stub_request(:get, 'http://stubserver/base/users/john.smith')
74
71
  .to_return do
75
- <<-RESPONSE.gsub(/^ {10}/, '')
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
- RESPONSE
81
+ HTTP
86
82
  end
87
83
 
88
- stub_request(:get, 'http://localhost/base/users/john.smith/blurb')
84
+ stub_request(:get, 'http://stubserver/base/users/john.smith/blurb')
89
85
  .to_return do
90
- <<-RESPONSE.gsub(/^ {10}/, '')
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
- RESPONSE
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 'webmock/rspec'
4
+ require 'rspec/collection_matchers'
5
5
 
6
- if ENV['CI'] || (defined?(:RUBY_ENGINE) && RUBY_ENGINE != 'rbx')
7
- require 'coveralls'
8
- Coveralls.wear! do
9
- add_filter 'spec'
10
- end
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
- require 'webmock/rspec'
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 = 2
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