content_gateway 0.1.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.
@@ -0,0 +1,23 @@
1
+ require "ostruct"
2
+ require "logger"
3
+ require "timeout"
4
+ require "benchmark"
5
+ require "json"
6
+ require "rest-client"
7
+ require "active_support/cache"
8
+ require "active_support/core_ext/object/blank"
9
+ require "active_support/core_ext/date_time/calculations"
10
+ require "active_support/core_ext/hash/indifferent_access"
11
+
12
+ module ContentGateway
13
+ extend self
14
+
15
+ def logger
16
+ end
17
+ end
18
+
19
+ require "content_gateway/version"
20
+ require "content_gateway/exceptions"
21
+ require "content_gateway/cache"
22
+ require "content_gateway/request"
23
+ require "content_gateway/gateway"
@@ -0,0 +1,410 @@
1
+ require "spec_helper"
2
+
3
+ describe ContentGateway::Gateway do
4
+ let! :url_generator do
5
+ url_generator = double('url_generator')
6
+ allow(url_generator).to receive(:generate).with(resource_path, {}).and_return("http://api.com/servico")
7
+ url_generator
8
+ end
9
+
10
+ let! :config do
11
+ OpenStruct.new(
12
+ cache: ActiveSupport::Cache::NullStore.new,
13
+ cache_expires_in: 15.minutes,
14
+ cache_stale_expires_in: 1.hour,
15
+ proxy: "proxy"
16
+ )
17
+ end
18
+
19
+ let :gateway do
20
+ ContentGateway::Gateway.new "API XPTO", config, url_generator, headers: headers
21
+ end
22
+
23
+ let :params do
24
+ { "a|b" => 1, name: "a|b|c" }
25
+ end
26
+
27
+ let :headers do
28
+ { key: 'value' }
29
+ end
30
+
31
+ let :resource_path do
32
+ "anything"
33
+ end
34
+
35
+ let(:timeout) { 0.1 }
36
+
37
+ let :cached_response do
38
+ response = "cached response"
39
+ response.instance_eval do
40
+ def code
41
+ 200
42
+ end
43
+ end
44
+ response
45
+ end
46
+
47
+ before do
48
+ config.cache.clear
49
+ end
50
+
51
+ describe ".new" do
52
+ it "default_params should be optional" do
53
+ expect(ContentGateway::Gateway.new("API XPTO", config, url_generator)).to be_kind_of(ContentGateway::Gateway)
54
+ end
55
+ end
56
+
57
+ describe "#get" do
58
+ let :resource_url do
59
+ url_generator.generate(resource_path, {})
60
+ end
61
+
62
+ let :stale_cache_key do
63
+ "stale:#{resource_url}"
64
+ end
65
+
66
+ let :default_expires_in do
67
+ config.cache_expires_in
68
+ end
69
+
70
+ let :default_stale_expires_in do
71
+ config.cache_stale_expires_in
72
+ end
73
+
74
+ context "with all request params" do
75
+ before do
76
+ stub_request(method: :get, proxy: config.proxy, url: resource_url, headers: headers)
77
+ end
78
+
79
+ it "should do request with http get" do
80
+ gateway.get resource_path
81
+ end
82
+
83
+ context "with cache" do
84
+ it "should cache responses" do
85
+ cache_store = double("cache_store")
86
+ expect(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in)
87
+ config.cache = cache_store
88
+
89
+ gateway.get resource_path
90
+ end
91
+
92
+ it "should keep stale cache" do
93
+ stub_request(url: resource_url, proxy: config.proxy, headers: headers) { cached_response }
94
+
95
+ cache_store = double("cache_store")
96
+ expect(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield
97
+ expect(cache_store).to receive(:write).with(stale_cache_key, cached_response, expires_in: default_stale_expires_in)
98
+ config.cache = cache_store
99
+
100
+ gateway.get resource_path
101
+ end
102
+
103
+ describe "timeout control" do
104
+ before do
105
+ stub_request(method: :get, url: resource_url, proxy: config.proxy, headers: headers) {
106
+ sleep(0.3)
107
+ }
108
+ end
109
+
110
+ it "should accept 'timeout' to overwrite the default value" do
111
+ expect(Timeout).to receive(:timeout).with(timeout)
112
+ gateway.get resource_path, timeout: timeout
113
+ end
114
+
115
+ it "should block requests that expire the configured timeout" do
116
+ expect { gateway.get resource_path, timeout: timeout }.to raise_error ContentGateway::TimeoutError
117
+ end
118
+
119
+ it "should block cache requests that expire the configured timeout" do
120
+ allow(config.cache).to receive(:fetch) { sleep(1) }
121
+ expect { gateway.get resource_path, timeout: timeout }.to raise_error ContentGateway::TimeoutError
122
+ end
123
+ end
124
+
125
+ context "with stale cache" do
126
+ context "on timeout" do
127
+ before do
128
+ cache_store = double("cache_store")
129
+ allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_raise(Timeout::Error)
130
+ allow(cache_store).to receive(:read).with(stale_cache_key).and_return(cached_response)
131
+ config.cache = cache_store
132
+ end
133
+
134
+ it "should serve stale" do
135
+ expect(gateway.get(resource_path, timeout: timeout)).to eql "cached response"
136
+ end
137
+ end
138
+
139
+ context "on server error" do
140
+ before do
141
+ stub_request_with_error({method: :get, url: resource_url, proxy: config.proxy, headers: headers}, RestClient::InternalServerError.new(nil, 500))
142
+
143
+ cache_store = double("cache_store")
144
+ allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield
145
+ allow(cache_store).to receive(:read).with(stale_cache_key).and_return(cached_response)
146
+ config.cache = cache_store
147
+ end
148
+
149
+ it "should serve stale" do
150
+ expect(gateway.get(resource_path)).to eql "cached response"
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ context "with skip_cache parameter" do
157
+ it "shouldn't cache requests" do
158
+ cache_store = double("cache_store")
159
+ expect(cache_store).not_to receive(:fetch).with(resource_url, expires_in: default_expires_in)
160
+ config.cache = cache_store
161
+
162
+ gateway.get resource_path, skip_cache: true
163
+ end
164
+
165
+ describe "timeout control" do
166
+ let(:timeout) { 0.1 }
167
+
168
+ before do
169
+ stub_request(method: :get, url: resource_url, proxy: config.proxy, headers: headers) {
170
+ sleep(0.3)
171
+ }
172
+ end
173
+
174
+ it "should ignore 'timeout' parameter" do
175
+ expect(Timeout).not_to receive(:timeout).with(timeout)
176
+ gateway.get resource_path, skip_cache: true, timeout: timeout
177
+ end
178
+ end
179
+
180
+ context "on server error" do
181
+ before do
182
+ stub_request_with_error({method: :get, url: resource_url, proxy: config.proxy, headers: headers}, RestClient::InternalServerError.new(nil, 500))
183
+
184
+ cache_store = double("cache_store")
185
+ expect(cache_store).not_to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield
186
+ config.cache = cache_store
187
+ end
188
+
189
+ it "should ignore cache" do
190
+ expect { gateway.get(resource_path, skip_cache: true) }.to raise_error ContentGateway::ServerError
191
+ end
192
+ end
193
+ end
194
+
195
+ it "should raise NotFound exception on 404 error" do
196
+ stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::ResourceNotFound.new)
197
+ expect { gateway.get resource_path }.to raise_error ContentGateway::ResourceNotFound
198
+ end
199
+
200
+ it "should raise Conflict exception on 409 error" do
201
+ stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Conflict.new)
202
+ expect { gateway.get resource_path }.to raise_error ContentGateway::ConflictError
203
+ end
204
+
205
+ it "should raise ServerError exception on 500 error" do
206
+ stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Exception.new(nil, 500))
207
+ expect { gateway.get resource_path }.to raise_error ContentGateway::ServerError
208
+ end
209
+
210
+ it "should raise ConnectionFailure exception on other errors" do
211
+ stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, SocketError.new)
212
+ expect { gateway.get resource_path }.to raise_error ContentGateway::ConnectionFailure
213
+ end
214
+
215
+ it "should accept a 'expires_in' parameter to overwrite the default value" do
216
+ expires_in = 3.minutes
217
+ cache_store = double("cache_store")
218
+ expect(cache_store).to receive(:fetch).with(resource_url, expires_in: expires_in)
219
+ config.cache = cache_store
220
+ gateway.get resource_path, expires_in: expires_in
221
+ end
222
+
223
+ it "should accept a 'stale_expires_in' parameter to overwrite the default value" do
224
+ stub_request(url: resource_url, proxy: config.proxy, headers: headers) { cached_response }
225
+
226
+ stale_expires_in = 5.minutes
227
+ cache_store = double("cache_store")
228
+ allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield
229
+ expect(cache_store).to receive(:write).with(stale_cache_key, cached_response, expires_in: stale_expires_in)
230
+ config.cache = cache_store
231
+
232
+ gateway.get resource_path, stale_expires_in: stale_expires_in
233
+ end
234
+ end
235
+
236
+ context "without proxy" do
237
+ before do
238
+ config.proxy = nil
239
+ stub_request(method: :get, url: resource_url, headers: headers)
240
+ end
241
+
242
+ it "should do request with http get" do
243
+ gateway.get resource_path
244
+ end
245
+ end
246
+
247
+ context "overwriting headers" do
248
+ let :novos_headers do
249
+ { key2: 'value2' }
250
+ end
251
+
252
+ before do
253
+ stub_request(method: :get, proxy: config.proxy, url: resource_url, headers: novos_headers)
254
+ end
255
+
256
+ it "should do request with http get" do
257
+ gateway.get resource_path, headers: novos_headers
258
+ end
259
+ end
260
+ end
261
+
262
+ describe "#get_json" do
263
+ it "should convert the get result to JSON" do
264
+ expect(gateway).to receive(:get).with(resource_path, params).and_return({ "a" => 1 }.to_json)
265
+ expect(gateway.get_json(resource_path, params)).to eql("a" => 1)
266
+ end
267
+ end
268
+
269
+ describe "#post_json" do
270
+ it "should convert the post result to JSON" do
271
+ expect(gateway).to receive(:post).with(resource_path, params).and_return({ "a" => 1 }.to_json)
272
+ expect(gateway.post_json(resource_path, params)).to eql("a" => 1)
273
+ end
274
+ end
275
+
276
+ describe "#put_json" do
277
+ it "should convert the put result to JSON" do
278
+ expect(gateway).to receive(:put).with(resource_path, params).and_return({ "a" => 1 }.to_json)
279
+ expect(gateway.put_json(resource_path, params)).to eql("a" => 1)
280
+ end
281
+ end
282
+
283
+ describe "#post" do
284
+ let :resource_url do
285
+ url_generator.generate(resource_path, {})
286
+ end
287
+
288
+ let :payload do
289
+ { param: "value" }
290
+ end
291
+
292
+ it "should do request with http post" do
293
+ stub_request(method: :post, url: resource_url, proxy: config.proxy, payload: payload)
294
+ gateway.post resource_path, payload: payload
295
+ end
296
+
297
+ it "should raise NotFound exception on 404 error" do
298
+ stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::ResourceNotFound.new)
299
+ expect { gateway.post resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound
300
+ end
301
+
302
+ it "should raise UnprocessableEntity exception on 401 error" do
303
+ stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::Unauthorized.new)
304
+ expect { gateway.post resource_path, payload: payload }.to raise_error(ContentGateway::UnauthorizedError)
305
+ end
306
+
307
+ it "should raise Forbidden exception on 403 error" do
308
+ stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::Forbidden.new)
309
+ expect { gateway.post resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden)
310
+ end
311
+
312
+ it "should raise ConnectionFailure exception on 500 error" do
313
+ stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload }, SocketError.new)
314
+ expect { gateway.post resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure
315
+ end
316
+ end
317
+
318
+ describe "#delete" do
319
+ let :resource_url do
320
+ url_generator.generate(resource_path, {})
321
+ end
322
+
323
+ let :payload do
324
+ { param: "value" }
325
+ end
326
+
327
+ it "should do request with http post" do
328
+ stub_request(method: :delete, url: resource_url, proxy: config.proxy, payload: payload)
329
+ gateway.delete resource_path, payload: payload
330
+ end
331
+
332
+ it "should raise NotFound exception on 404 error" do
333
+ stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::ResourceNotFound.new)
334
+ expect { gateway.delete resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound
335
+ end
336
+
337
+ it "should raise UnprocessableEntity exception on 401 error" do
338
+ stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::Unauthorized.new)
339
+ expect { gateway.delete resource_path, payload: payload }.to raise_error(ContentGateway::UnauthorizedError)
340
+ end
341
+
342
+ it "should raise Forbidden exception on 403 error" do
343
+ stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::Forbidden.new)
344
+ expect { gateway.delete resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden)
345
+ end
346
+
347
+ it "should raise ConnectionFailure exception on 500 error" do
348
+ stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, payload: payload }, SocketError.new)
349
+ expect { gateway.delete resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure
350
+ end
351
+ end
352
+
353
+ describe "#put" do
354
+ let :resource_url do
355
+ gateway.generate_url(resource_path)
356
+ end
357
+
358
+ let :payload do
359
+ { param: "value" }
360
+ end
361
+
362
+ it "should do request with http put" do
363
+ stub_request(method: :put, url: resource_url, proxy: config.proxy, payload: payload)
364
+ gateway.put resource_path, payload: payload
365
+ end
366
+
367
+ it "should raise NotFound exception on 404 error" do
368
+ stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::ResourceNotFound.new)
369
+ expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound
370
+ end
371
+
372
+ it "should raise UnprocessableEntity exception on 422 error" do
373
+ stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::UnprocessableEntity)
374
+ expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ValidationError
375
+ end
376
+
377
+ it "should raise Forbidden exception on 403 error" do
378
+ stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload }, RestClient::Forbidden.new)
379
+ expect { gateway.put resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden)
380
+ end
381
+
382
+ it "should raise ConnectionFailure exception on 500 error" do
383
+ stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload }, SocketError.new)
384
+ expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure
385
+ end
386
+ end
387
+
388
+ private
389
+
390
+ def stub_request(opts, payload = {}, &block)
391
+ opts = { method: :get, proxy: :none }.merge(opts)
392
+ request = RestClient::Request.new(opts)
393
+ allow(RestClient::Request).to receive(:new).with(opts).and_return(request)
394
+
395
+ allow(request).to receive(:execute) do
396
+ block.call if block_given?
397
+ end
398
+
399
+ request
400
+ end
401
+
402
+ def stub_request_with_error(opts, exc)
403
+ opts = { method: :get, proxy: :none }.merge(opts)
404
+
405
+ request = RestClient::Request.new(opts)
406
+ allow(RestClient::Request).to receive(:new).with(opts).and_return(request)
407
+
408
+ allow(request).to receive(:execute).and_raise(exc)
409
+ end
410
+ end
@@ -0,0 +1,35 @@
1
+ ENV['RACK_ENV'] = 'test'
2
+
3
+ $:.unshift(File.join(File.dirname(__FILE__), "..", "lib"))
4
+
5
+ require 'byebug'
6
+ require 'rspec'
7
+ require 'content_gateway'
8
+
9
+ begin
10
+ require 'simplecov'
11
+ SimpleCov.start do
12
+ add_filter '/spec/'
13
+ end
14
+ SimpleCov.coverage_dir 'coverage/rspec'
15
+ rescue LoadError
16
+ # ignore simplecov in ruby < 1.9
17
+ end
18
+
19
+ # This file was generated by the `rspec --init` command. Conventionally, all
20
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
21
+ # Require this file using `require "spec_helper"` to ensure that it is only
22
+ # loaded once.
23
+ #
24
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
25
+
26
+ RSpec.configure do |config|
27
+ config.run_all_when_everything_filtered = true
28
+ config.filter_run :focus
29
+
30
+ # Run specs in random order to surface order dependencies. If you find an
31
+ # order dependency and want to debug it, you can fix the order by providing
32
+ # the seed, which is printed after each run.
33
+ # --seed 1234
34
+ config.order = 'random'
35
+ end
@@ -0,0 +1,134 @@
1
+ require "spec_helper"
2
+
3
+ describe ContentGateway::Cache do
4
+ subject do
5
+ ContentGateway::Cache.new(config, url, method, params)
6
+ end
7
+
8
+ let(:config) { OpenStruct.new(cache: cache_store) }
9
+
10
+ let(:cache_store) { double("cache store", write: nil) }
11
+
12
+ let(:url) { "/url" }
13
+
14
+ let(:method) { :get }
15
+
16
+ let(:params) { {} }
17
+
18
+ describe "#use?" do
19
+ context "when skip_cache is true" do
20
+ let(:params) { { skip_cache: true } }
21
+
22
+ it "shouldn't use cache" do
23
+ expect(subject.use?).to eql false
24
+ end
25
+ end
26
+
27
+ context "when method isn't get or head" do
28
+ let(:method) { :post }
29
+
30
+ it "shouldn't use cache" do
31
+ expect(subject.use?).to eql false
32
+ end
33
+ end
34
+
35
+ context "when method is get" do
36
+ let(:method) { :get }
37
+
38
+ it "should use cache" do
39
+ expect(subject.use?).to eql true
40
+ end
41
+ end
42
+
43
+ context "when method is head" do
44
+ let(:method) { :head }
45
+
46
+ it "should use cache" do
47
+ expect(subject.use?).to eql true
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "#fetch" do
53
+ let(:request) { double("request", execute: "data") }
54
+
55
+ context "when cache hits" do
56
+ before do
57
+ expect(Timeout).to receive(:timeout) do |timeout, &arg|
58
+ arg.call
59
+ end
60
+
61
+ expect(cache_store).to receive(:fetch).with(url, expires_in: 100).and_return("cached data")
62
+ end
63
+
64
+ it "should return the cached data" do
65
+ expect(subject.fetch(request, expires_in: 100)).to eql "cached data"
66
+ end
67
+ end
68
+
69
+ context "when cache misses" do
70
+ context "and request succeeds" do
71
+ before do
72
+ expect(Timeout).to receive(:timeout) do |timeout, &arg|
73
+ arg.call
74
+ end
75
+
76
+ expect(cache_store).to receive(:fetch) do |url, params, &arg|
77
+ arg.call
78
+ end
79
+ end
80
+
81
+ it "should set status to 'MISS'" do
82
+ subject.fetch(request)
83
+
84
+ expect(subject.status).to eql "MISS"
85
+ end
86
+
87
+ it "should return the request data" do
88
+ expect(subject.fetch(request)).to eql "data"
89
+ end
90
+
91
+ it "should write the request data to stale cache" do
92
+ expect(cache_store).to receive(:write).with("stale:/url", "data", expires_in: 15)
93
+
94
+ subject.fetch(request, stale_expires_in: 15)
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ describe "#serve_stale" do
101
+ before do
102
+ expect(cache_store).to receive(:read).with("stale:/url").and_return(return_value)
103
+ end
104
+
105
+ context "when data are successfully read from stale cache" do
106
+ let(:return_value) { "stale cache data" }
107
+
108
+ it "should return the stale data" do
109
+ expect(subject.serve_stale).to eql "stale cache data"
110
+ end
111
+
112
+ it "should set status to 'STALE'" do
113
+ subject.serve_stale
114
+ expect(subject.status).to eql "STALE"
115
+ end
116
+ end
117
+
118
+ context "when data can't be read from stale cache" do
119
+ let(:return_value) { nil }
120
+
121
+ it "should raise ContentGateway::StaleCacheNotAvailableError" do
122
+ expect { subject.serve_stale }.to raise_error ContentGateway::StaleCacheNotAvailableError
123
+ end
124
+ end
125
+ end
126
+
127
+ describe "#stale_key" do
128
+ let(:url) { "http://example.com" }
129
+
130
+ it "should return the stale cache key" do
131
+ expect(subject.stale_key).to eql "stale:http://example.com"
132
+ end
133
+ end
134
+ end