content_gateway 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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