gds-api-adapters 4.2.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -8,6 +8,9 @@ module GdsApi
8
8
  class TimedOutException < BaseError
9
9
  end
10
10
 
11
+ class TooManyRedirects < BaseError
12
+ end
13
+
11
14
  class HTTPErrorResponse < BaseError
12
15
  attr_accessor :code
13
16
 
@@ -1,8 +1,8 @@
1
1
  require_relative 'response'
2
2
  require_relative 'exceptions'
3
3
  require_relative 'version'
4
- require 'net/http'
5
4
  require 'lrucache'
5
+ require 'rest-client'
6
6
 
7
7
  module GdsApi
8
8
  class JsonClient
@@ -37,7 +37,9 @@ module GdsApi
37
37
  DEFAULT_CACHE_TTL = 15 * 60 # 15 minutes
38
38
 
39
39
  def get_raw(url)
40
- do_raw_request(Net::HTTP::Get, url)
40
+ ignoring GdsApi::HTTPNotFound do
41
+ do_raw_request(:get, url)
42
+ end
41
43
  end
42
44
 
43
45
  # Define "safe" methods for each supported HTTP method
@@ -55,108 +57,135 @@ module GdsApi
55
57
  end
56
58
 
57
59
  def get_json!(url, &create_response)
58
- @cache[url] ||= do_json_request(Net::HTTP::Get, url, nil, &create_response)
60
+ @cache[url] ||= do_json_request(:get, url, nil, &create_response)
59
61
  end
60
62
 
61
63
  def post_json!(url, params)
62
- do_json_request(Net::HTTP::Post, url, params)
64
+ do_json_request(:post, url, params)
63
65
  end
64
66
 
65
67
  def put_json!(url, params)
66
- do_json_request(Net::HTTP::Put, url, params)
68
+ do_json_request(:put, url, params)
67
69
  end
68
70
 
69
71
  def delete_json!(url, params = nil)
70
- do_request(Net::HTTP::Delete, url, params)
72
+ do_request(:delete, url, params)
71
73
  end
72
74
 
73
75
  private
74
- def do_raw_request(method_class, url, params = nil)
75
- response, loggable = do_request(method_class, url, params)
76
+ def do_raw_request(method, url, params = nil)
77
+ response = do_request(method, url, params)
76
78
  response.body
79
+
80
+ rescue RestClient::ResourceNotFound => e
81
+ raise GdsApi::HTTPNotFound.new(e.http_code)
82
+
83
+ rescue RestClient::Exception => e
84
+ raise GdsApi::HTTPErrorResponse.new(response.code.to_i), e.response.body
77
85
  end
78
86
 
79
- # method_class: the Net::HTTP class to use, e.g. Net::HTTP::Get
87
+ # method: the symbolic name of the method to use, e.g. :get, :post
80
88
  # url: the request URL
81
89
  # params: the data to send (JSON-serialised) in the request body
82
90
  # create_response: optional block to instantiate a custom response object
83
91
  # from the Net::HTTPResponse
84
- def do_json_request(method_class, url, params = nil, &create_response)
92
+ def do_json_request(method, url, params = nil, &create_response)
85
93
 
86
- response, loggable = do_request(method_class, url, params)
94
+ begin
95
+ response = do_request(method, url, params)
87
96
 
88
- # If no custom response is given, just instantiate Response
89
- create_response ||= Proc.new { |r| Response.new(r) }
97
+ rescue RestClient::ResourceNotFound => e
98
+ raise GdsApi::HTTPNotFound.new(e.http_code)
90
99
 
91
- if response.is_a?(Net::HTTPSuccess)
92
- logger.info loggable.merge(status: 'success', end_time: Time.now.to_f).to_json
93
- create_response.call(response)
94
- elsif response.is_a?(Net::HTTPNotFound)
95
- raise GdsApi::HTTPNotFound.new(response.code.to_i)
96
- else
100
+ rescue RestClient::Exception => e
101
+ # Attempt to parse the body as JSON if possible
97
102
  body = begin
98
- JSON.parse(response.body.to_s)
103
+ e.http_body ? JSON.parse(e.http_body) : nil
99
104
  rescue JSON::ParserError
100
- response.body
105
+ e.http_body
101
106
  end
102
- loggable.merge!(status: response.code, end_time: Time.now.to_f, body: body)
103
- logger.warn loggable.to_json
104
- raise GdsApi::HTTPErrorResponse.new(response.code.to_i), body
107
+ raise GdsApi::HTTPErrorResponse.new(e.http_code), body
105
108
  end
106
- end
107
109
 
108
- def extract_url_and_path(url)
109
- url = URI.parse(url)
110
- path = url.path
111
- path = path + "?" + url.query if url.query
112
- return url, path
110
+ # If no custom response is given, just instantiate Response
111
+ create_response ||= Proc.new { |r| Response.new(r) }
112
+ create_response.call(response.net_http_res)
113
113
  end
114
114
 
115
- def attach_auth_options(request)
115
+ # Take a hash of parameters for Request#execute; return a hash of
116
+ # parameters with authentication information included
117
+ def with_auth_options(method_params)
116
118
  if @options[:bearer_token]
117
- request.add_field('Authorization', "Bearer #{@options[:bearer_token]}")
119
+ headers = method_params[:headers] || {}
120
+ method_params.merge(headers: headers.merge(
121
+ {"Authorization" => "Bearer #{@options[:bearer_token]}"}
122
+ ))
118
123
  elsif @options[:basic_auth]
119
- request.basic_auth(@options[:basic_auth][:user], @options[:basic_auth][:password])
124
+ method_params.merge(
125
+ user: @options[:basic_auth][:user],
126
+ password: @options[:basic_auth][:password]
127
+ )
128
+ else
129
+ method_params
120
130
  end
121
131
  end
122
132
 
123
- def set_timeout(http)
124
- unless options[:disable_timeout]
125
- http.read_timeout = options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS
133
+ # Take a hash of parameters for Request#execute; return a hash of
134
+ # parameters with timeouts included
135
+ def with_timeout(method_params)
136
+ if options[:disable_timeout]
137
+ method_params.merge(timeout: -1)
138
+ else
139
+ method_params.merge(
140
+ timeout: options[:timeout] || DEFAULT_TIMEOUT_IN_SECONDS
141
+ )
126
142
  end
127
143
  end
128
144
 
129
- def ssl_options(port)
130
- if port == 443
131
- {use_ssl: true, verify_mode: OpenSSL::SSL::VERIFY_NONE}
132
- else
133
- {}
134
- end
145
+ def with_ssl_options(method_params)
146
+ method_params.merge(
147
+ # This is the default value anyway, but we should probably be explicit
148
+ verify_ssl: OpenSSL::SSL::VERIFY_NONE
149
+ )
135
150
  end
136
151
 
137
- def do_request(method_class, url, params = nil)
152
+ def do_request(method, url, params = nil)
138
153
  loggable = {request_uri: url, start_time: Time.now.to_f}
139
154
  start_logging = loggable.merge(action: 'start')
140
155
  logger.debug start_logging.to_json
141
156
 
142
- url, path = extract_url_and_path(url)
143
-
144
- response = Net::HTTP.start(url.host, url.port, nil, nil, nil, nil, ssl_options(url.port)) do |http|
145
- set_timeout(http)
146
- request = method_class.new(path, DEFAULT_REQUEST_HEADERS)
147
- attach_auth_options(request)
148
- request.body = params.to_json if params
149
- http.request(request)
157
+ method_params = {
158
+ method: method,
159
+ url: url,
160
+ headers: DEFAULT_REQUEST_HEADERS
161
+ }
162
+ method_params[:payload] = params.to_json if params
163
+ method_params = with_auth_options(method_params)
164
+ method_params = with_timeout(method_params)
165
+ if URI.parse(url).is_a? URI::HTTPS
166
+ method_params = with_ssl_options(method_params)
150
167
  end
151
168
 
152
- return response, loggable
169
+ return ::RestClient::Request.execute(method_params)
153
170
 
154
171
  rescue Errno::ECONNREFUSED => e
155
172
  logger.error loggable.merge(status: 'refused', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
156
173
  raise GdsApi::EndpointNotFound.new("Could not connect to #{url}")
157
- rescue Timeout::Error => e
174
+
175
+ rescue RestClient::RequestTimeout => e
158
176
  logger.error loggable.merge(status: 'timeout', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
159
177
  raise GdsApi::TimedOutException.new
178
+
179
+ rescue RestClient::MaxRedirectsReached => e
180
+ raise GdsApi::TooManyRedirects
181
+
182
+ rescue RestClient::Exception => e
183
+ # Log the error here, since we have access to loggable, but raise the
184
+ # exception up to the calling method to deal with
185
+ loggable.merge!(status: e.http_code, end_time: Time.now.to_f, body: e.http_body)
186
+ logger.warn loggable.to_json
187
+ raise
188
+
160
189
  rescue Errno::ECONNRESET => e
161
190
  logger.error loggable.merge(status: 'connection_reset', error_message: e.message, error_class: e.class.name, end_time: Time.now.to_f).to_json
162
191
  raise GdsApi::TimedOutException.new
@@ -1,3 +1,3 @@
1
1
  module GdsApi
2
- VERSION = '4.2.0'
2
+ VERSION = '4.3.0'
3
3
  end
@@ -51,6 +51,14 @@ class JsonClientTest < MiniTest::Spec
51
51
  end
52
52
  end
53
53
 
54
+ def test_get_should_raise_error_on_restclient_error
55
+ url = "http://some.endpoint/some.json"
56
+ stub_request(:get, url).to_raise(RestClient::ServerBrokeConnection)
57
+ assert_raises GdsApi::HTTPErrorResponse do
58
+ @client.get_json(url)
59
+ end
60
+ end
61
+
54
62
  def test_should_fetch_and_parse_json_into_response
55
63
  url = "http://some.endpoint/some.json"
56
64
  stub_request(:get, url).to_return(:body => "{}", :status => 200)
@@ -112,6 +120,7 @@ class JsonClientTest < MiniTest::Spec
112
120
  assert_equal response_b.to_hash, response_c.to_hash
113
121
  assert_equal response_a.to_hash, response_d.to_hash
114
122
  end
123
+
115
124
  def test_should_cache_requests_for_15_mins_by_default
116
125
  GdsApi::JsonClient.cache = nil # cause it to contruct a new cache instance.
117
126
 
@@ -188,6 +197,103 @@ class JsonClientTest < MiniTest::Spec
188
197
  end
189
198
  end
190
199
 
200
+ def test_get_should_follow_permanent_redirect
201
+ url = "http://some.endpoint/some.json"
202
+ new_url = "http://some.endpoint/other.json"
203
+ stub_request(:get, url).to_return(
204
+ :body => "",
205
+ :status => 301,
206
+ :headers => {"Location" => new_url}
207
+ )
208
+ stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
209
+ result = @client.get_json(url)
210
+ assert_equal 1, result.a
211
+ end
212
+
213
+ def test_get_should_follow_found_redirect
214
+ url = "http://some.endpoint/some.json"
215
+ new_url = "http://some.endpoint/other.json"
216
+ stub_request(:get, url).to_return(
217
+ :body => "",
218
+ :status => 302,
219
+ :headers => {"Location" => new_url}
220
+ )
221
+ stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
222
+ result = @client.get_json(url)
223
+ assert_equal 1, result.a
224
+ end
225
+
226
+ def test_get_should_follow_see_other
227
+ url = "http://some.endpoint/some.json"
228
+ new_url = "http://some.endpoint/other.json"
229
+ stub_request(:get, url).to_return(
230
+ :body => "",
231
+ :status => 302,
232
+ :headers => {"Location" => new_url}
233
+ )
234
+ stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
235
+ result = @client.get_json(url)
236
+ assert_equal 1, result.a
237
+ end
238
+
239
+ def test_get_should_follow_temporary_redirect
240
+ url = "http://some.endpoint/some.json"
241
+ new_url = "http://some.endpoint/other.json"
242
+ stub_request(:get, url).to_return(
243
+ :body => "",
244
+ :status => 307,
245
+ :headers => {"Location" => new_url}
246
+ )
247
+ stub_request(:get, new_url).to_return(:body => '{"a": 1}', :status => 200)
248
+ result = @client.get_json(url)
249
+ assert_equal 1, result.a
250
+ end
251
+
252
+ def test_should_handle_infinite_redirects
253
+ url = "http://some.endpoint/some.json"
254
+ redirect = {
255
+ :body => "",
256
+ :status => 302,
257
+ :headers => {"Location" => url}
258
+ }
259
+
260
+ # Theoretically, we could set this up to mock out any number of requests
261
+ # with a redirect to the same URL, but we'd risk getting the test code into
262
+ # an infinite loop if the code didn't do what it was supposed to. The
263
+ # failure response block aborts the test if we have too many requests.
264
+ failure = lambda { |request| flunk("Request called too many times") }
265
+ stub_request(:get, url).to_return(redirect).times(11).then.to_return(failure)
266
+
267
+ assert_raises GdsApi::TooManyRedirects do
268
+ @client.get_json(url)
269
+ end
270
+ end
271
+
272
+ def test_should_handle_mutual_redirects
273
+ first_url = "http://some.endpoint/some.json"
274
+ second_url = "http://some.endpoint/some-other.json"
275
+
276
+ first_redirect = {
277
+ :body => "",
278
+ :status => 302,
279
+ :headers => {"Location" => second_url}
280
+ }
281
+ second_redirect = {
282
+ :body => "",
283
+ :status => 302,
284
+ :headers => {"Location" => first_url}
285
+ }
286
+
287
+ # See the comment in the above test for an explanation of this
288
+ failure = lambda { |request| flunk("Request called too many times") }
289
+ stub_request(:get, first_url).to_return(first_redirect).times(6).then.to_return(failure)
290
+ stub_request(:get, second_url).to_return(second_redirect).times(6).then.to_return(failure)
291
+
292
+ assert_raises GdsApi::TooManyRedirects do
293
+ @client.get_json(first_url)
294
+ end
295
+ end
296
+
191
297
  def test_post_should_be_nil_if_404_returned_from_endpoint
192
298
  url = "http://some.endpoint/some.json"
193
299
  stub_request(:post, url).to_return(:body => "{}", :status => 404)
@@ -202,6 +308,19 @@ class JsonClientTest < MiniTest::Spec
202
308
  end
203
309
  end
204
310
 
311
+ def test_post_should_error_on_found_redirect
312
+ url = "http://some.endpoint/some.json"
313
+ new_url = "http://some.endpoint/other.json"
314
+ stub_request(:post, url).to_return(
315
+ :body => "",
316
+ :status => 302,
317
+ :headers => {"Location" => new_url}
318
+ )
319
+ assert_raises GdsApi::HTTPErrorResponse do
320
+ @client.post_json(url, {})
321
+ end
322
+ end
323
+
205
324
  def test_put_should_be_nil_if_404_returned_from_endpoint
206
325
  url = "http://some.endpoint/some.json"
207
326
  stub_request(:put, url).to_return(:body => "{}", :status => 404)
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: gds-api-adapters
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 4.2.0
5
+ version: 4.3.0
6
6
  platform: ruby
7
7
  authors:
8
8
  - James Stewart
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2013-01-16 00:00:00 Z
13
+ date: 2013-01-29 00:00:00 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: plek
@@ -57,8 +57,19 @@ dependencies:
57
57
  prerelease: false
58
58
  version_requirements: *id004
59
59
  - !ruby/object:Gem::Dependency
60
- name: rdoc
60
+ name: rest-client
61
61
  requirement: &id005 !ruby/object:Gem::Requirement
62
+ none: false
63
+ requirements:
64
+ - - ~>
65
+ - !ruby/object:Gem::Version
66
+ version: 1.6.3
67
+ type: :runtime
68
+ prerelease: false
69
+ version_requirements: *id005
70
+ - !ruby/object:Gem::Dependency
71
+ name: rdoc
72
+ requirement: &id006 !ruby/object:Gem::Requirement
62
73
  none: false
63
74
  requirements:
64
75
  - - "="
@@ -66,10 +77,10 @@ dependencies:
66
77
  version: "3.12"
67
78
  type: :development
68
79
  prerelease: false
69
- version_requirements: *id005
80
+ version_requirements: *id006
70
81
  - !ruby/object:Gem::Dependency
71
82
  name: rake
72
- requirement: &id006 !ruby/object:Gem::Requirement
83
+ requirement: &id007 !ruby/object:Gem::Requirement
73
84
  none: false
74
85
  requirements:
75
86
  - - ~>
@@ -77,10 +88,10 @@ dependencies:
77
88
  version: 0.9.2.2
78
89
  type: :development
79
90
  prerelease: false
80
- version_requirements: *id006
91
+ version_requirements: *id007
81
92
  - !ruby/object:Gem::Dependency
82
93
  name: webmock
83
- requirement: &id007 !ruby/object:Gem::Requirement
94
+ requirement: &id008 !ruby/object:Gem::Requirement
84
95
  none: false
85
96
  requirements:
86
97
  - - ~>
@@ -88,10 +99,10 @@ dependencies:
88
99
  version: "1.8"
89
100
  type: :development
90
101
  prerelease: false
91
- version_requirements: *id007
102
+ version_requirements: *id008
92
103
  - !ruby/object:Gem::Dependency
93
104
  name: mocha
94
- requirement: &id008 !ruby/object:Gem::Requirement
105
+ requirement: &id009 !ruby/object:Gem::Requirement
95
106
  none: false
96
107
  requirements:
97
108
  - - ~>
@@ -99,10 +110,10 @@ dependencies:
99
110
  version: 0.12.4
100
111
  type: :development
101
112
  prerelease: false
102
- version_requirements: *id008
113
+ version_requirements: *id009
103
114
  - !ruby/object:Gem::Dependency
104
115
  name: minitest
105
- requirement: &id009 !ruby/object:Gem::Requirement
116
+ requirement: &id010 !ruby/object:Gem::Requirement
106
117
  none: false
107
118
  requirements:
108
119
  - - ~>
@@ -110,10 +121,10 @@ dependencies:
110
121
  version: 3.4.0
111
122
  type: :development
112
123
  prerelease: false
113
- version_requirements: *id009
124
+ version_requirements: *id010
114
125
  - !ruby/object:Gem::Dependency
115
126
  name: rack
116
- requirement: &id010 !ruby/object:Gem::Requirement
127
+ requirement: &id011 !ruby/object:Gem::Requirement
117
128
  none: false
118
129
  requirements:
119
130
  - - ">="
@@ -121,10 +132,10 @@ dependencies:
121
132
  version: "0"
122
133
  type: :development
123
134
  prerelease: false
124
- version_requirements: *id010
135
+ version_requirements: *id011
125
136
  - !ruby/object:Gem::Dependency
126
137
  name: simplecov
127
- requirement: &id011 !ruby/object:Gem::Requirement
138
+ requirement: &id012 !ruby/object:Gem::Requirement
128
139
  none: false
129
140
  requirements:
130
141
  - - ~>
@@ -132,10 +143,10 @@ dependencies:
132
143
  version: 0.5.4
133
144
  type: :development
134
145
  prerelease: false
135
- version_requirements: *id011
146
+ version_requirements: *id012
136
147
  - !ruby/object:Gem::Dependency
137
148
  name: simplecov-rcov
138
- requirement: &id012 !ruby/object:Gem::Requirement
149
+ requirement: &id013 !ruby/object:Gem::Requirement
139
150
  none: false
140
151
  requirements:
141
152
  - - ">="
@@ -143,10 +154,10 @@ dependencies:
143
154
  version: "0"
144
155
  type: :development
145
156
  prerelease: false
146
- version_requirements: *id012
157
+ version_requirements: *id013
147
158
  - !ruby/object:Gem::Dependency
148
159
  name: gem_publisher
149
- requirement: &id013 !ruby/object:Gem::Requirement
160
+ requirement: &id014 !ruby/object:Gem::Requirement
150
161
  none: false
151
162
  requirements:
152
163
  - - ~>
@@ -154,10 +165,10 @@ dependencies:
154
165
  version: 1.1.1
155
166
  type: :development
156
167
  prerelease: false
157
- version_requirements: *id013
168
+ version_requirements: *id014
158
169
  - !ruby/object:Gem::Dependency
159
170
  name: timecop
160
- requirement: &id014 !ruby/object:Gem::Requirement
171
+ requirement: &id015 !ruby/object:Gem::Requirement
161
172
  none: false
162
173
  requirements:
163
174
  - - ~>
@@ -165,7 +176,7 @@ dependencies:
165
176
  version: 0.5.1
166
177
  type: :development
167
178
  prerelease: false
168
- version_requirements: *id014
179
+ version_requirements: *id015
169
180
  description: A set of adapters providing easy access to the GDS GOV.UK APIs
170
181
  email:
171
182
  - jystewart@gmail.com
@@ -225,7 +236,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
225
236
  requirements:
226
237
  - - ">="
227
238
  - !ruby/object:Gem::Version
228
- hash: -1781835164135696191
239
+ hash: 4441910162548111668
229
240
  segments:
230
241
  - 0
231
242
  version: "0"
@@ -234,7 +245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
234
245
  requirements:
235
246
  - - ">="
236
247
  - !ruby/object:Gem::Version
237
- hash: -1781835164135696191
248
+ hash: 4441910162548111668
238
249
  segments:
239
250
  - 0
240
251
  version: "0"