gds-api-adapters 4.2.0 → 4.3.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.
@@ -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"