├── .gitignore ├── .rspec ├── .ruby-gemset ├── .ruby-version ├── .travis.yml ├── Changelog ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── content-gateway.gemspec ├── lib ├── content_gateway.rb └── content_gateway │ ├── cache.rb │ ├── exceptions.rb │ ├── gateway.rb │ ├── request.rb │ └── version.rb └── spec ├── integration └── content_gateway │ └── gateway_spec.rb ├── spec_helper.rb └── unit └── content_gateway ├── cache_spec.rb ├── gateway_spec.rb └── request_spec.rb /.gitignore: -------------------------------------------------------------------------------- 1 | pkg/* 2 | docs/* 3 | *.gem 4 | .bundle 5 | .rvmrc 6 | coverage/* 7 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --format documentation 3 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | content-gateway 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.3.8 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | sudo: false 3 | script: bundle exec rake spec 4 | -------------------------------------------------------------------------------- /Changelog: -------------------------------------------------------------------------------- 1 | 2022-02-11 [0.6.0] 2 | 3 | * Bugfix: Updated the rest-client gem from 1.0 to 2.1 to fix memory leak issues caused by the old version 4 | 5 | 2016-12-19 [0.5.2] 6 | 7 | * Bugfix: Now stale_on_error returns true if theres no config 8 | * Add default url generator 9 | 10 | 2015-11-19 [0.5.1] 11 | 12 | * Bugfix proxy implementation wrapper on rest-client 13 | 14 | 2015-07-24 [0.5.0] 15 | 16 | * Mapping JSON parse errors to ContentGateway::ParserError 17 | 18 | 2015-06-16 [0.4.0] 19 | 20 | * Adding ssl_version support to ssl_certificate hash 21 | 22 | 2015-01-05 [0.3.0] 23 | 24 | * Optional url generator. (Without the url generator on boot the content gateway will use the get/post/delete/put resource argument as full url for request). Closes #4 25 | 26 | 2014-11-06 [0.2.1] 27 | 28 | * Fix: Do not send connection parameters to url generator. Closes #2 29 | * Fix: Send http headers to request object. Closes #3 30 | 31 | 2014-10-31 [0.2.0] 32 | 33 | * Adding ssl support in requests 34 | * Solving cache problem when using ssl in requests. The solution was convert the result of the request to string before save the cache value. 35 | 36 | 2014-10-21 [0.1.0] 37 | 38 | * Adding delete_json method. 39 | * A lot of refactoring. 40 | 41 | 2014-10-07 [0.0.14] 42 | 43 | * Ignoring cache after an internal server error when skip_cache = true. 44 | * Mapping 5xx errors to ContentGateway::ServerError. 45 | 46 | 2014-04-01 [0.0.13] 47 | 48 | * Added support for HTTP 409 Conflict error. 49 | 50 | 2014-04-01 [0.0.12] 51 | 52 | * Require debugger on spec_helper. 53 | 54 | 2014-01-17 [0.0.11] 55 | 56 | * Adding delete method support 57 | 58 | 2014-01-17 [0.0.10] 59 | 60 | * Do not depend on a specific version of activesupport. 61 | 62 | 2013-08-15 [0.0.9] 63 | 64 | * Using try to get proxy from config to avoid error when not exists. 65 | 66 | 2013-08-15 [0.0.8] 67 | 68 | * Return empty when result is not a RestClient object and do not have "code" method. 69 | 70 | 2013-08-15 [0.0.7] 71 | 72 | * Default params now are optional 73 | 74 | 2013-08-14 [0.0.6] 75 | 76 | * First release 77 | * Replaces Esportes API gateway.rb 78 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | ruby '2.3.8' 3 | 4 | gem 'coveralls', require: false 5 | 6 | gemspec 7 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | PATH 2 | remote: . 3 | specs: 4 | content_gateway (0.6.0) 5 | activesupport (>= 3) 6 | json (~> 1.0) 7 | rest-client (~> 2.1) 8 | 9 | GEM 10 | remote: https://rubygems.org/ 11 | specs: 12 | activesupport (5.2.6) 13 | concurrent-ruby (~> 1.0, >= 1.0.2) 14 | i18n (>= 0.7, < 2) 15 | minitest (~> 5.1) 16 | tzinfo (~> 1.1) 17 | byebug (11.0.1) 18 | concurrent-ruby (1.1.9) 19 | coveralls (0.7.1) 20 | multi_json (~> 1.3) 21 | rest-client 22 | simplecov (>= 0.7) 23 | term-ansicolor 24 | thor 25 | diff-lcs (1.5.0) 26 | docile (1.3.5) 27 | domain_name (0.5.20190701) 28 | unf (>= 0.0.5, < 1.0.0) 29 | http-accept (1.7.0) 30 | http-cookie (1.0.4) 31 | domain_name (~> 0.5) 32 | i18n (1.9.1) 33 | concurrent-ruby (~> 1.0) 34 | json (1.8.6) 35 | mime-types (3.4.1) 36 | mime-types-data (~> 3.2015) 37 | mime-types-data (3.2022.0105) 38 | minitest (5.15.0) 39 | multi_json (1.15.0) 40 | netrc (0.11.0) 41 | rake (13.0.6) 42 | rest-client (2.1.0) 43 | http-accept (>= 1.7.0, < 2.0) 44 | http-cookie (>= 1.0.2, < 2.0) 45 | mime-types (>= 1.16, < 4.0) 46 | netrc (~> 0.8) 47 | rspec (3.10.0) 48 | rspec-core (~> 3.10.0) 49 | rspec-expectations (~> 3.10.0) 50 | rspec-mocks (~> 3.10.0) 51 | rspec-core (3.10.2) 52 | rspec-support (~> 3.10.0) 53 | rspec-expectations (3.10.2) 54 | diff-lcs (>= 1.2.0, < 2.0) 55 | rspec-support (~> 3.10.0) 56 | rspec-mocks (3.10.3) 57 | diff-lcs (>= 1.2.0, < 2.0) 58 | rspec-support (~> 3.10.0) 59 | rspec-support (3.10.3) 60 | simplecov (0.17.1) 61 | docile (~> 1.1) 62 | json (>= 1.8, < 3) 63 | simplecov-html (~> 0.10.0) 64 | simplecov-html (0.10.2) 65 | sync (0.5.0) 66 | term-ansicolor (1.7.1) 67 | tins (~> 1.0) 68 | thor (1.2.1) 69 | thread_safe (0.3.6) 70 | tins (1.31.0) 71 | sync 72 | tzinfo (1.2.9) 73 | thread_safe (~> 0.1) 74 | unf (0.1.4) 75 | unf_ext 76 | unf_ext (0.0.8) 77 | 78 | PLATFORMS 79 | ruby 80 | 81 | DEPENDENCIES 82 | byebug 83 | content_gateway! 84 | coveralls 85 | rake 86 | rspec (>= 2.3.0) 87 | simplecov (>= 0.7.1) 88 | 89 | RUBY VERSION 90 | ruby 2.3.8p459 91 | 92 | BUNDLED WITH 93 | 2.3.6 94 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Globo.com - Webmedia 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Content Gateway 2 | 3 | [![Build Status](https://travis-ci.org/globocom/content-gateway-ruby.svg)](https://travis-ci.org/globocom/content-gateway-ruby) 4 | [![Coverage Status](https://coveralls.io/repos/github/globocom/content-gateway-ruby/badge.svg?branch=master)](https://coveralls.io/github/globocom/content-gateway-ruby?branch=master) 5 | 6 | An easy way to get external content with two cache levels. The first is a performance cache and second is the stale. 7 | 8 | Content Gateway lets you set a timeout for any request. 9 | If the configured timeout is reached without response, it searches for cached data. 10 | If cache is unavailable or expired, it returns the stale cache data. 11 | Only then, if stale cache is also unavailable or expired, it raises an exception 12 | 13 | ## Dependencies 14 | 15 | - Ruby >= 1.9 16 | - ActiveSupport (for cache store) 17 | 18 | ## Installation 19 | 20 | Add this line to your application's Gemfile: 21 | 22 | gem 'content_gateway' 23 | 24 | And then execute: 25 | 26 | $ bundle 27 | 28 | Or install it yourself as: 29 | 30 | $ gem install content_gateway 31 | 32 | ## Configuration 33 | 34 | `ContentGateway::Gateway` class accepts a configuration object with the following parameters: 35 | 36 | - `timeout`: request timeout in seconds 37 | - `cache_expires_in`: cache data expiration time, in seconds 38 | - `cache_stale_expires_in`: stale cache data expiration time, in seconds 39 | - `stale_on_error`: if `true`, returns value from cache stale (if available) after a server error. Default value: `true` 40 | - `cache`: cache store instance. This may be an instance of `ActiveSupport::Cache` 41 | - `proxy`: proxy address, if needed 42 | 43 | Configuration object example: 44 | 45 | ```ruby 46 | config = OpenStruct.new( 47 | timeout: 2, 48 | cache_expires_in: 1800, 49 | cache_stale_expires_in: 86400, 50 | stale_on_error: false, 51 | cache: ActiveSupport::Cache.lookup_store(:memory_store), 52 | proxy: "http://proxy.example.com:3128" 53 | ) 54 | ``` 55 | 56 | ## Usage 57 | 58 | `ContentGateway::Gateway` expects four parameters: 59 | 60 | - a label, which is used in the log messages 61 | - a config object, just as described above 62 | - (optional) an URL Generator object. This may be any object that responds to a `generate` method, like this: 63 | - (optional) a hash with default params. Currently, it only supports default headers 64 | 65 | ```ruby 66 | class UrlGenerator 67 | def generate(resource_path, params = {}) 68 | args = "?#{params.map {|k, v| "#{k}=#{v}"}.join("&")}" if params.any? 69 | "http://example.com/#{resource_path}#{args}" 70 | end 71 | end 72 | 73 | default_params = { headers: { Accept: "application/json" } } 74 | 75 | gateway = ContentGateway::Gateway.new("My API", config, UrlGenerator.new, default_params) 76 | ``` 77 | 78 | If ommited, the default URL Generator adds the method call params as query string parameters. Every param may be overrided on each request. 79 | 80 | This Gateway object supports the following methods: 81 | 82 | ### GET 83 | 84 | To do a GET request, you may use the `get` or `get_json` methods. The second one parses the response as JSON. 85 | Optional parameters are supported: 86 | 87 | - `timeout`: overwrites the default timeout 88 | - `expires_in`: overwrites the default cache expiration time 89 | - `stale_expires_in`: overwrites the default stale cache expiration time 90 | - `skip_cache`: if set to `true`, ignores cache and stale cache 91 | - `headers`: a hash with request headers 92 | - `ssl_certificate`: a hash with ssl cert, key, ssl version (see ssl support section below) 93 | 94 | Every other parameter is passed to URLGenerator `generate` method (like query string parameters). 95 | 96 | Examples: 97 | 98 | ```ruby 99 | gateway.get("/path", timeout: 3) 100 | 101 | gateway.get_json("/path.json", skip_cache: true) 102 | ``` 103 | 104 | ### POST, PUT and DELETE 105 | 106 | POST, PUT and DELETE verbs are also supported, but ignore cache and stale cache. 107 | The gateway object offers the equivalent methods for these verbs (`post`, `post_json`, `put`, `put_json`, `delete` and `delete_json`). 108 | The only optional parameters supported by these methods are `payload` and `ssl_certificate`. 109 | Every other parameter is passed to URLGenerator `generate` method (like query string parameters). 110 | 111 | Examples: 112 | 113 | ```ruby 114 | gateway.post("/api/post_example", payload: { param1: "value" }) 115 | 116 | gateway.put_json("/api/put_example.json", query_string_param: "value") 117 | 118 | gateway.delete("/api/delete_example", id: "100") 119 | ``` 120 | 121 | ### SSL Support 122 | 123 | You can use ssl certificates to run all supported requests (get, post, put, delete). 124 | 125 | Just pass the path of cert file (x509 certificate) and key file (rsa key) to the request method. See exemple below: 126 | 127 | ```ruby 128 | ssl = { 129 | ssl_client_cert: "path/client.cert", 130 | ssl_client_key: "path/client.key" 131 | } 132 | 133 | gateway.get("/path", timeout: 3, ssl_certificate: ssl) 134 | 135 | gateway.get_json("/path.json", skip_cache: true, ssl_certificate: ssl) 136 | 137 | gateway.post("/api/post_example", payload: { param1: "value" }, ssl_certificate: ssl) 138 | ``` 139 | 140 | You can use ssl_version to specify which version you need. (You can use with client cert and key or use it alone) See example below: 141 | 142 | ```ruby 143 | ssl = { 144 | ssl_version: "SSLv23" 145 | } 146 | 147 | gateway.get("/path", timeout: 3, ssl_certificate: ssl) 148 | 149 | gateway.get_json("/path.json", skip_cache: true, ssl_certificate: ssl) 150 | 151 | gateway.post("/api/post_example", payload: { param1: "value" }, ssl_certificate: ssl) 152 | ``` 153 | 154 | ## Authors 155 | 156 | - [Túlio Ornelas](https://github.com/tulios) 157 | - [Roberto Soares](https://github.com/roberto) 158 | - [Emerson Macedo](https://github.com/emerleite) 159 | - [Guilherme Garnier](https://github.com/ggarnier) 160 | - [Daniel Martins](https://github.com/danielfm) 161 | - [Rafael Biriba](https://github.com/rafaelbiriba) 162 | - [Célio Latorraca](https://github.com/celiofonseca) 163 | 164 | ## Contributing 165 | 166 | 1. Fork it 167 | 2. Create your feature branch (`git checkout -b my-new-feature`) 168 | 3. Commit your changes (`git commit -am 'Add some feature'`) 169 | 4. Push to the branch (`git push origin my-new-feature`) 170 | 5. Create new Pull Request 171 | 172 | ## License 173 | 174 | Copyright (c) 2016 Globo.com - Webmedia. See [LICENSE.txt](https://github.com/globocom/content-gateway-ruby/blob/master/LICENSE.txt) for more details. 175 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | # Rake tasks 4 | Dir.glob('lib/tasks/**/*.rake').each {|r| import r} 5 | 6 | require "rspec/core/rake_task" 7 | desc "Run all examples" 8 | RSpec::Core::RakeTask.new(:spec) 9 | 10 | task :default => :spec 11 | -------------------------------------------------------------------------------- /content-gateway.gemspec: -------------------------------------------------------------------------------- 1 | lib = File.expand_path('../lib', __FILE__) 2 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 3 | require 'content_gateway/version' 4 | 5 | Gem::Specification.new do |gem| 6 | gem.name = "content_gateway" 7 | gem.version = ContentGateway::VERSION 8 | gem.authors = ["Túlio Ornelas", "Roberto Soares", "Emerson Macedo", "Guilherme Garnier", "Daniel Martins", "Rafael Biriba", "Célio Latorraca"] 9 | gem.email = ["ornelas.tulio@gmail.com", "roberto.tech@gmail.com", "emerleite@gmail.com", "guilherme.garnier@gmail.com", "daniel.tritone@gmail.com", "biribarj@gmail.com", "celio.la@gmail.com"] 10 | gem.description = %q{An easy way to get external content with two cache levels. The first is a performance cache and second is the stale} 11 | gem.summary = %q{Content Gateway} 12 | gem.homepage = "https://github.com/globocom/content-gateway-ruby" 13 | 14 | gem.files = `git ls-files`.split($/) 15 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 16 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 17 | gem.require_paths = ["lib"] 18 | 19 | gem.add_dependency "activesupport", ">= 3" 20 | gem.add_dependency "rest-client", "~> 2.1" 21 | gem.add_dependency "json", "~> 1.0" 22 | 23 | gem.add_development_dependency "rspec", ">= 2.3.0" 24 | gem.add_development_dependency "simplecov", ">= 0.7.1" 25 | gem.add_development_dependency "byebug" 26 | gem.add_development_dependency "rake" 27 | end 28 | -------------------------------------------------------------------------------- /lib/content_gateway.rb: -------------------------------------------------------------------------------- 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/notifications" 9 | require "active_support/core_ext/object/blank" 10 | require "active_support/core_ext/date_time/calculations" 11 | require "active_support/core_ext/hash/indifferent_access" 12 | 13 | module ContentGateway 14 | extend self 15 | 16 | def logger 17 | end 18 | end 19 | 20 | require "content_gateway/version" 21 | require "content_gateway/exceptions" 22 | require "content_gateway/cache" 23 | require "content_gateway/request" 24 | require "content_gateway/gateway" 25 | -------------------------------------------------------------------------------- /lib/content_gateway/cache.rb: -------------------------------------------------------------------------------- 1 | module ContentGateway 2 | class Cache 3 | attr_reader :status 4 | 5 | def initialize(config, url, method, params = {}) 6 | @config = config 7 | @url = url 8 | @method = method.to_sym 9 | @skip_cache = params[:skip_cache] || false 10 | end 11 | 12 | def use? 13 | !@skip_cache && [:get, :head].include?(@method) 14 | end 15 | 16 | def fetch(request, params = {}) 17 | timeout = params[:timeout] || @config.timeout 18 | expires_in = params[:expires_in] || @config.cache_expires_in 19 | stale_expires_in = params[:stale_expires_in] || @config.cache_stale_expires_in 20 | stale_on_error = config_stale_on_error params, @config 21 | 22 | begin 23 | Timeout.timeout(timeout) do 24 | @config.cache.fetch(@url, expires_in: expires_in) do 25 | @status = "MISS" 26 | response = request.execute 27 | response = String.new(response) if response 28 | 29 | @config.cache.write(stale_key, response, expires_in: stale_expires_in) 30 | response 31 | end 32 | end 33 | 34 | rescue Timeout::Error => e 35 | begin 36 | serve_stale 37 | rescue ContentGateway::StaleCacheNotAvailableError 38 | raise ContentGateway::TimeoutError.new(@url, e, timeout) 39 | end 40 | 41 | rescue ContentGateway::ServerError => e 42 | begin 43 | raise e unless stale_on_error 44 | serve_stale 45 | rescue ContentGateway::StaleCacheNotAvailableError 46 | raise e 47 | end 48 | end 49 | end 50 | 51 | def serve_stale 52 | @config.cache.read(stale_key).tap do |cached| 53 | raise ContentGateway::StaleCacheNotAvailableError.new unless cached 54 | @status = "STALE" 55 | end 56 | end 57 | 58 | def stale_key 59 | @stale_key ||= "stale:#{@url}" 60 | end 61 | 62 | private 63 | def config_stale_on_error params, config 64 | return params[:stale_on_error] unless params[:stale_on_error].nil? 65 | return config.stale_on_error unless config.try(:stale_on_error).nil? 66 | true 67 | end 68 | end 69 | end 70 | -------------------------------------------------------------------------------- /lib/content_gateway/exceptions.rb: -------------------------------------------------------------------------------- 1 | module ContentGateway 2 | class BaseError < StandardError 3 | attr_reader :resource_url, :wrapped_exception, :status_code, :info 4 | 5 | def initialize(resource_url, wrapped_exception = nil, status_code = nil, info = nil) 6 | @resource_url = resource_url 7 | @wrapped_exception = wrapped_exception 8 | @status_code = status_code 9 | @info = info 10 | 11 | message = @resource_url.dup 12 | if @wrapped_exception 13 | message << " - #{@wrapped_exception.message}" 14 | message << " - #{@info}" if @info 15 | end 16 | 17 | super(message) 18 | end 19 | end 20 | 21 | class UnauthorizedError < BaseError 22 | def initialize(resource_url, wrapped_exception = nil) 23 | super(resource_url, wrapped_exception, 401) 24 | end 25 | end 26 | 27 | class Forbidden < BaseError 28 | def initialize(resource_url, wrapped_exception = nil) 29 | super(resource_url, wrapped_exception, 403) 30 | end 31 | end 32 | 33 | class ResourceNotFound < BaseError 34 | def initialize(resource_url, wrapped_exception = nil) 35 | super(resource_url, wrapped_exception, 404) 36 | end 37 | end 38 | 39 | class TimeoutError < BaseError 40 | def initialize(resource_url, wrapped_exception = nil, timeout = nil) 41 | info = "TIMEOUT (max #{timeout} s)" if timeout 42 | 43 | super(resource_url, wrapped_exception, 408, info) 44 | end 45 | end 46 | 47 | class ConflictError < BaseError 48 | def initialize(resource_url, wrapped_exception = nil) 49 | super(resource_url, wrapped_exception, 409) 50 | end 51 | end 52 | 53 | class ValidationError < BaseError 54 | attr_reader :errors 55 | 56 | def initialize(resource_url, wrapped_exception = nil) 57 | super(resource_url, wrapped_exception, 422) 58 | 59 | if wrapped_exception 60 | response = wrapped_exception.response 61 | @errors = JSON.parse(response) if response.present? 62 | end 63 | end 64 | end 65 | 66 | class ServerError < BaseError 67 | def initialize(resource_url, wrapped_exception = nil, status_code = nil) 68 | super(resource_url, wrapped_exception, status_code, "SERVER ERROR") 69 | end 70 | end 71 | 72 | class ConnectionFailure < BaseError 73 | def initialize(resource_url, wrapped_exception = nil) 74 | super(resource_url, wrapped_exception, 500) 75 | end 76 | end 77 | 78 | class OpenSSLFailure < BaseError 79 | def initialize(resource_url, wrapped_exception = nil, info=nil) 80 | super(resource_url, wrapped_exception, 406, info) 81 | end 82 | end 83 | 84 | class ParserError < BaseError; end 85 | 86 | class StaleCacheNotAvailableError < StandardError; end 87 | end 88 | -------------------------------------------------------------------------------- /lib/content_gateway/gateway.rb: -------------------------------------------------------------------------------- 1 | module ContentGateway 2 | class Gateway 3 | def initialize(label, config, url_generator = nil, default_params = {}) 4 | @label = label 5 | @config = config 6 | @url_generator = url_generator || DefaultUrlGenerator 7 | @default_params = default_params 8 | end 9 | 10 | def get(resource_path, params = {}) 11 | aux_params = remove_aux_parameters! params 12 | headers = aux_params.delete :headers 13 | 14 | url = self.generate_url(resource_path, params) 15 | 16 | measure("GET - #{url}") do 17 | data = { method: :get, url: url }.tap do |h| 18 | h[:headers] = headers if headers.present? 19 | end 20 | 21 | request_params = aux_params.merge(params) 22 | send_request(data, request_params) 23 | end 24 | end 25 | 26 | def post(resource_path, params = {}) 27 | aux_params = remove_aux_parameters! params 28 | headers = aux_params.delete :headers 29 | payload = aux_params.delete :payload 30 | timeout = aux_params.delete :timeout 31 | ssl_certificate = aux_params.delete :ssl_certificate 32 | 33 | url = self.generate_url(resource_path, params) 34 | 35 | measure("POST - #{url}") do 36 | data = { method: :post, url: url, payload: payload }.tap do |h| 37 | h[:headers] = headers if headers.present? 38 | end 39 | 40 | request_params = { timeout: timeout }.merge(params).tap do |h| 41 | h[:ssl_certificate] = ssl_certificate unless ssl_certificate.nil? 42 | end 43 | send_request(data, request_params) 44 | end 45 | end 46 | 47 | def put(resource_path, params = {}) 48 | aux_params = remove_aux_parameters! params 49 | headers = aux_params.delete :headers 50 | payload = aux_params.delete :payload 51 | timeout = aux_params.delete :timeout 52 | ssl_certificate = aux_params.delete :ssl_certificate 53 | 54 | url = self.generate_url(resource_path, params) 55 | 56 | measure("PUT - #{url}") do 57 | data = { method: :put, url: url, payload: payload }.tap do |h| 58 | h[:headers] = headers if headers.present? 59 | end 60 | 61 | request_params = { timeout: timeout }.merge(params).tap do |h| 62 | h[:ssl_certificate] = ssl_certificate unless ssl_certificate.nil? 63 | end 64 | 65 | send_request(data, request_params) 66 | end 67 | end 68 | 69 | def delete(resource_path, params = {}) 70 | aux_params = remove_aux_parameters! params 71 | headers = aux_params.delete :headers 72 | timeout = aux_params.delete :timeout 73 | ssl_certificate = aux_params.delete :ssl_certificate 74 | 75 | url = self.generate_url(resource_path, params) 76 | 77 | measure("DELETE - #{url}") do 78 | data = { method: :delete, url: url }.tap do |h| 79 | h[:headers] = headers if headers.present? 80 | end 81 | 82 | request_params = { timeout: timeout }.tap do |h| 83 | h[:ssl_certificate] = ssl_certificate unless ssl_certificate.nil? 84 | end 85 | 86 | send_request(data, request_params) 87 | end 88 | end 89 | 90 | def get_json(resource_path, params = {}) 91 | json_request(:get, resource_path, params) 92 | end 93 | 94 | def post_json(resource_path, params = {}) 95 | json_request(:post, resource_path, params) 96 | end 97 | 98 | def put_json(resource_path, params = {}) 99 | json_request(:put, resource_path, params) 100 | end 101 | 102 | def delete_json(resource_path, params = {}) 103 | json_request(:delete, resource_path, params) 104 | end 105 | 106 | def generate_url(resource_path, params = {}) 107 | if @url_generator.respond_to? :generate 108 | @url_generator.generate(resource_path, params) 109 | else 110 | resource_path 111 | end 112 | end 113 | 114 | private 115 | 116 | def remove_aux_parameters! params 117 | aux_params = params.select do |k, v| 118 | [:timeout, :expires_in, :stale_expires_in, :skip_cache, :headers, :payload, :ssl_certificate].include? k 119 | end 120 | 121 | aux_params.tap do |p| 122 | p[:headers] = p[:headers] || @default_params[:headers] 123 | end 124 | 125 | params.delete_if do |k,v| 126 | aux_params.keys.include? k 127 | end 128 | 129 | aux_params 130 | end 131 | 132 | def send_request(request_data, params = {}) 133 | method = request_data[:method] || :get 134 | url = request_data[:url] 135 | headers = request_data[:headers] 136 | payload = request_data[:payload] 137 | 138 | @cache = ContentGateway::Cache.new(@config, url, method, params) 139 | @request = ContentGateway::Request.new(method, url, headers, payload, @config.try(:proxy), params) 140 | 141 | begin 142 | do_request(params) 143 | 144 | rescue ContentGateway::BaseError => e 145 | message = "#{prefix(e.status_code)} :: #{color_message(e.resource_url)}" 146 | message << " - #{e.info}" if e.info 147 | logger.info message 148 | 149 | raise e 150 | end 151 | end 152 | 153 | def do_request(params = {}) 154 | if @cache.use? 155 | @cache.fetch(@request, timeout: params[:timeout], expires_in: params[:expires_in], stale_expires_in: params[:stale_expires_in]) 156 | else 157 | @request.execute 158 | end 159 | end 160 | 161 | def json_request(verb, resource_path, params = {}) 162 | JSON.parse(self.send(verb, resource_path, params)) 163 | rescue JSON::ParserError => e 164 | url = generate_url(resource_path, params) rescue resource_path 165 | raise ContentGateway::ParserError.new(url, e) 166 | end 167 | 168 | def measure(message) 169 | result = nil 170 | time_elapsed = Benchmark.measure { result = yield } 171 | sufix = "finished in #{humanize_elapsed_time(time_elapsed.real)}. " 172 | cache_log = (@cache.status || "HIT").to_s.ljust(4, " ") 173 | log_message = "#{prefix(code(result))} :: #{cache_log} #{color_message(message)} #{sufix}" 174 | 175 | logger.info log_message 176 | result 177 | end 178 | 179 | def code(result) 180 | result.respond_to?(:code) ? result.code : "" 181 | end 182 | 183 | def humanize_elapsed_time(time_elapsed) 184 | time_elapsed >= 1 ? "%.3f secs" % time_elapsed : "#{(time_elapsed * 1000).to_i} ms" 185 | end 186 | 187 | def prefix(code = nil) 188 | "[#{@label}] #{color_code(code)}" 189 | end 190 | 191 | def color_message(message) 192 | "\033[1;33m#{message}\033[0m" 193 | end 194 | 195 | def color_code(code) 196 | color = code == 200 ? "32" : "31" 197 | code_message = code.to_s.ljust(3, " ") 198 | "\033[#{color}m#{code_message}\033[0m" 199 | end 200 | 201 | def logger 202 | @logger || lambda do 203 | if defined?(Rails) 204 | Rails.logger 205 | else 206 | log = ::Logger.new STDOUT 207 | log.formatter = lambda {|severity, datetime, progname, msg| 208 | "#{datetime.strftime("%Y-%m-%d %H:%M:%S")} #{severity.upcase} #{msg}\n" 209 | } 210 | 211 | log 212 | end 213 | end.yield 214 | end 215 | 216 | class DefaultUrlGenerator 217 | def generate(resource_path, params = {}) 218 | args = "?#{params.map {|k, v| "#{k}=#{v}"}.join("&")}" if params.any? 219 | "#{resource_path}#{args}" 220 | end 221 | end 222 | end 223 | end 224 | -------------------------------------------------------------------------------- /lib/content_gateway/request.rb: -------------------------------------------------------------------------------- 1 | module ContentGateway 2 | class Request 3 | def initialize(method, url, headers = {}, payload = {}, proxy = nil, params = {}) 4 | data = { method: method, url: url, proxy: proxy }.tap do |h| 5 | h[:payload] = payload if payload.present? 6 | h[:headers] = headers if headers.present? 7 | h = load_ssl_params(h, params) if params.has_key?(:ssl_certificate) 8 | end 9 | RestClient.proxy = proxy 10 | @client = RestClient::Request.new(data) 11 | end 12 | 13 | def execute 14 | @client.execute.to_s 15 | rescue RestClient::ResourceNotFound => e1 16 | raise ContentGateway::ResourceNotFound.new url, e1 17 | 18 | rescue RestClient::Unauthorized => e2 19 | raise ContentGateway::UnauthorizedError.new url, e2 20 | 21 | rescue RestClient::UnprocessableEntity => e3 22 | raise ContentGateway::ValidationError.new url, e3 23 | 24 | rescue RestClient::Forbidden => e4 25 | raise ContentGateway::Forbidden.new url, e4 26 | 27 | rescue RestClient::Conflict => e5 28 | raise ContentGateway::ConflictError.new url, e5 29 | 30 | rescue RestClient::Exception => e6 31 | status_code = e6.http_code 32 | if status_code && status_code < 500 33 | raise e6 34 | else 35 | raise ContentGateway::ServerError.new url, e6, status_code 36 | end 37 | 38 | rescue StandardError => e7 39 | raise ContentGateway::ConnectionFailure.new url, e7 40 | end 41 | 42 | private 43 | 44 | def load_ssl_params h, params 45 | ssl_client_cert = params[:ssl_certificate][:ssl_client_cert] 46 | ssl_client_key = params[:ssl_certificate][:ssl_client_key] 47 | if ssl_client_cert || ssl_client_key 48 | client_cert_file = File.read ssl_client_cert 49 | client_cert_key = File.read ssl_client_key 50 | 51 | h[:ssl_client_cert] = OpenSSL::X509::Certificate.new(client_cert_file) 52 | h[:ssl_client_key] = OpenSSL::PKey::RSA.new(client_cert_key) 53 | h[:verify_ssl] = OpenSSL::SSL::VERIFY_NONE 54 | end 55 | 56 | ssl_version = params[:ssl_certificate][:ssl_version] 57 | h[:ssl_version] = ssl_version if ssl_version 58 | 59 | h 60 | 61 | rescue Errno::ENOENT => e0 62 | raise ContentGateway::OpenSSLFailure.new h[:url], e0 63 | 64 | rescue OpenSSL::X509::CertificateError => e1 65 | raise ContentGateway::OpenSSLFailure.new h[:url], e1, "invalid ssl client cert" 66 | 67 | rescue OpenSSL::PKey::RSAError => e2 68 | raise ContentGateway::OpenSSLFailure.new h[:url], e2, "invalid ssl client key" 69 | end 70 | 71 | def url 72 | @client.url 73 | end 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /lib/content_gateway/version.rb: -------------------------------------------------------------------------------- 1 | module ContentGateway 2 | VERSION = "0.6.0" 3 | end 4 | -------------------------------------------------------------------------------- /spec/integration/content_gateway/gateway_spec.rb: -------------------------------------------------------------------------------- 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 | class FakeConfig 12 | attr_accessor :cache, :cache_expires_in, :cache_stale_expires_in, :proxy, :timeout 13 | 14 | def initialize(params) 15 | @cache = params[:cache] 16 | @cache_expires_in = params[:cache_expires_in] 17 | @cache_stale_expires_in = params[:cache_stale_expires_in] 18 | @proxy = params[:proxy] 19 | @timeout = params[:timeout] 20 | end 21 | end 22 | 23 | FakeConfig.new( 24 | cache: ActiveSupport::Cache::NullStore.new, 25 | cache_expires_in: 15.minutes, 26 | cache_stale_expires_in: 1.hour, 27 | proxy: "proxy", 28 | timeout: 2 29 | ) 30 | end 31 | 32 | let :gateway do 33 | ContentGateway::Gateway.new "API XPTO", config, url_generator, headers: headers 34 | end 35 | 36 | let :params do 37 | { "a|b" => 1, name: "a|b|c" } 38 | end 39 | 40 | let :headers do 41 | { key: 'value' } 42 | end 43 | 44 | let :resource_path do 45 | "anything" 46 | end 47 | 48 | let(:timeout) { 0.1 } 49 | 50 | let :cached_response do 51 | response = "cached response" 52 | response.instance_eval do 53 | def code 54 | 200 55 | end 56 | end 57 | response 58 | end 59 | 60 | before do 61 | config.cache.clear 62 | end 63 | 64 | describe ".new" do 65 | it "default_params should be optional" do 66 | expect(ContentGateway::Gateway.new("API XPTO", config, url_generator)).to be_kind_of(ContentGateway::Gateway) 67 | end 68 | end 69 | 70 | describe "#get" do 71 | let :resource_url do 72 | url_generator.generate(resource_path, {}) 73 | end 74 | 75 | let :stale_cache_key do 76 | "stale:#{resource_url}" 77 | end 78 | 79 | let :default_expires_in do 80 | config.cache_expires_in 81 | end 82 | 83 | let :default_stale_expires_in do 84 | config.cache_stale_expires_in 85 | end 86 | 87 | context "with all request params" do 88 | before do 89 | stub_request(method: :get, proxy: config.proxy, url: resource_url, headers: headers) 90 | end 91 | 92 | it "should do request with http get" do 93 | gateway.get resource_path 94 | end 95 | 96 | context "with cache" do 97 | it "should cache responses" do 98 | cache_store = double("cache_store") 99 | expect(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in) 100 | config.cache = cache_store 101 | 102 | gateway.get resource_path 103 | end 104 | 105 | it "should keep stale cache" do 106 | stub_request(url: resource_url, proxy: config.proxy, headers: headers) { cached_response } 107 | 108 | cache_store = double("cache_store") 109 | expect(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield 110 | expect(cache_store).to receive(:write).with(stale_cache_key, cached_response, expires_in: default_stale_expires_in) 111 | config.cache = cache_store 112 | 113 | gateway.get resource_path 114 | end 115 | 116 | describe "timeout control" do 117 | before do 118 | stub_request(method: :get, url: resource_url, proxy: config.proxy, headers: headers) { 119 | sleep(0.3) 120 | } 121 | end 122 | 123 | it "should accept 'timeout' to overwrite the default value" do 124 | expect(Timeout).to receive(:timeout).with(timeout) 125 | gateway.get resource_path, timeout: timeout 126 | end 127 | 128 | it "should block requests that expire the configured timeout" do 129 | expect { gateway.get resource_path, timeout: timeout }.to raise_error ContentGateway::TimeoutError 130 | end 131 | 132 | it "should block cache requests that expire the configured timeout" do 133 | allow(config.cache).to receive(:fetch) { sleep(1) } 134 | expect { gateway.get resource_path, timeout: timeout }.to raise_error ContentGateway::TimeoutError 135 | end 136 | end 137 | 138 | context "with stale cache" do 139 | let(:cache_store) {double("cache_store")} 140 | 141 | context "on timeout" do 142 | before do 143 | allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_raise(Timeout::Error) 144 | allow(cache_store).to receive(:read).with(stale_cache_key).and_return(cached_response) 145 | config.cache = cache_store 146 | end 147 | 148 | it "should serve stale" do 149 | expect(gateway.get(resource_path, timeout: timeout)).to eql "cached response" 150 | end 151 | end 152 | 153 | context "on server error" do 154 | before do 155 | stub_request_with_error({method: :get, url: resource_url, proxy: config.proxy, headers: headers}, RestClient::InternalServerError.new(nil, 500)) 156 | 157 | allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield 158 | allow(cache_store).to receive(:read).with(stale_cache_key).and_return(cached_response) 159 | config.cache = cache_store 160 | end 161 | 162 | context "give a config without stale_on_error" do 163 | context "uses the default value" do 164 | it "returns cached stale" do 165 | expect(gateway.get(resource_path)).to eql "cached response" 166 | end 167 | end 168 | end 169 | 170 | context "when stale_on_error configuration is false" do 171 | let :gateway do 172 | config_with_stale_on_error = config.dup 173 | allow(config_with_stale_on_error).to receive(:stale_on_error).and_return(false) 174 | ContentGateway::Gateway.new "API XPTO", config_with_stale_on_error, url_generator, headers: headers 175 | end 176 | 177 | it "raises error" do 178 | expect { gateway.get resource_path }.to raise_error ContentGateway::ServerError 179 | end 180 | end 181 | end 182 | end 183 | end 184 | 185 | context "with skip_cache parameter" do 186 | it "shouldn't cache requests" do 187 | cache_store = double("cache_store") 188 | expect(cache_store).not_to receive(:fetch).with(resource_url, expires_in: default_expires_in) 189 | config.cache = cache_store 190 | 191 | gateway.get resource_path, skip_cache: true 192 | end 193 | 194 | describe "timeout control" do 195 | let(:timeout) { 0.1 } 196 | 197 | before do 198 | stub_request(method: :get, url: resource_url, proxy: config.proxy, headers: headers) { 199 | sleep(0.3) 200 | } 201 | end 202 | 203 | it "should ignore 'timeout' parameter" do 204 | expect(Timeout).not_to receive(:timeout).with(timeout) 205 | gateway.get resource_path, skip_cache: true, timeout: timeout 206 | end 207 | end 208 | 209 | context "on server error" do 210 | before do 211 | stub_request_with_error({method: :get, url: resource_url, proxy: config.proxy, headers: headers}, RestClient::InternalServerError.new(nil, 500)) 212 | 213 | cache_store = double("cache_store") 214 | expect(cache_store).not_to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield 215 | config.cache = cache_store 216 | end 217 | 218 | it "should ignore cache" do 219 | expect { gateway.get(resource_path, skip_cache: true) }.to raise_error ContentGateway::ServerError 220 | end 221 | end 222 | end 223 | 224 | it "should raise NotFound exception on 404 error" do 225 | stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::ResourceNotFound.new) 226 | expect { gateway.get resource_path }.to raise_error ContentGateway::ResourceNotFound 227 | end 228 | 229 | it "should raise Conflict exception on 409 error" do 230 | stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Conflict.new) 231 | expect { gateway.get resource_path }.to raise_error ContentGateway::ConflictError 232 | end 233 | 234 | it "should raise ServerError exception on 500 error" do 235 | stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Exception.new(nil, 500)) 236 | expect { gateway.get resource_path }.to raise_error ContentGateway::ServerError 237 | end 238 | 239 | it "should raise ConnectionFailure exception on other errors" do 240 | stub_request_with_error({ method: :get, url: resource_url, proxy: config.proxy, headers: headers }, SocketError.new) 241 | expect { gateway.get resource_path }.to raise_error ContentGateway::ConnectionFailure 242 | end 243 | 244 | it "should accept a 'expires_in' parameter to overwrite the default value" do 245 | expires_in = 3.minutes 246 | cache_store = double("cache_store") 247 | expect(cache_store).to receive(:fetch).with(resource_url, expires_in: expires_in) 248 | config.cache = cache_store 249 | gateway.get resource_path, expires_in: expires_in 250 | end 251 | 252 | it "should accept a 'stale_expires_in' parameter to overwrite the default value" do 253 | stub_request(url: resource_url, proxy: config.proxy, headers: headers) { cached_response } 254 | 255 | stale_expires_in = 5.minutes 256 | cache_store = double("cache_store") 257 | allow(cache_store).to receive(:fetch).with(resource_url, expires_in: default_expires_in).and_yield 258 | expect(cache_store).to receive(:write).with(stale_cache_key, cached_response, expires_in: stale_expires_in) 259 | config.cache = cache_store 260 | 261 | gateway.get resource_path, stale_expires_in: stale_expires_in 262 | end 263 | end 264 | 265 | context "without proxy" do 266 | before do 267 | config.proxy = nil 268 | stub_request(method: :get, url: resource_url, headers: headers) 269 | end 270 | 271 | it "should do request with http get" do 272 | gateway.get resource_path 273 | end 274 | end 275 | 276 | context "overwriting headers" do 277 | let :novos_headers do 278 | { key2: 'value2' } 279 | end 280 | 281 | before do 282 | stub_request(method: :get, proxy: config.proxy, url: resource_url, headers: novos_headers) 283 | end 284 | 285 | it "should do request with http get" do 286 | gateway.get resource_path, headers: novos_headers 287 | end 288 | end 289 | end 290 | 291 | describe "#get_json" do 292 | it "should convert the get result to JSON" do 293 | expect(gateway).to receive(:get).with(resource_path, params).and_return({ "a" => 1 }.to_json) 294 | expect(gateway.get_json(resource_path, params)).to eql("a" => 1) 295 | end 296 | end 297 | 298 | describe "#post_json" do 299 | it "should convert the post result to JSON" do 300 | expect(gateway).to receive(:post).with(resource_path, params).and_return({ "a" => 1 }.to_json) 301 | expect(gateway.post_json(resource_path, params)).to eql("a" => 1) 302 | end 303 | end 304 | 305 | describe "#put_json" do 306 | it "should convert the put result to JSON" do 307 | expect(gateway).to receive(:put).with(resource_path, params).and_return({ "a" => 1 }.to_json) 308 | expect(gateway.put_json(resource_path, params)).to eql("a" => 1) 309 | end 310 | end 311 | 312 | describe "#post" do 313 | let :resource_url do 314 | url_generator.generate(resource_path, {}) 315 | end 316 | 317 | let :payload do 318 | { param: "value" } 319 | end 320 | 321 | it "should do request with http post" do 322 | stub_request(method: :post, url: resource_url, proxy: config.proxy, payload: payload, headers: headers) 323 | gateway.post resource_path, payload: payload 324 | end 325 | 326 | it "should raise NotFound exception on 404 error" do 327 | stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::ResourceNotFound.new) 328 | expect { gateway.post resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound 329 | end 330 | 331 | it "should raise UnprocessableEntity exception on 401 error" do 332 | stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::Unauthorized.new) 333 | expect { gateway.post resource_path, payload: payload }.to raise_error(ContentGateway::UnauthorizedError) 334 | end 335 | 336 | it "should raise Forbidden exception on 403 error" do 337 | stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::Forbidden.new) 338 | expect { gateway.post resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden) 339 | end 340 | 341 | it "should raise ConnectionFailure exception on 500 error" do 342 | stub_request_with_error({ method: :post, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, SocketError.new) 343 | expect { gateway.post resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure 344 | end 345 | end 346 | 347 | describe "#delete" do 348 | let :resource_url do 349 | url_generator.generate(resource_path, {}) 350 | end 351 | 352 | let :payload do 353 | { param: "value" } 354 | end 355 | 356 | it "should do request with http post" do 357 | stub_request(method: :delete, url: resource_url, proxy: config.proxy, headers: headers) 358 | gateway.delete resource_path, payload: payload 359 | end 360 | 361 | it "should raise NotFound exception on 404 error" do 362 | stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::ResourceNotFound.new) 363 | expect { gateway.delete resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound 364 | end 365 | 366 | it "should raise UnprocessableEntity exception on 401 error" do 367 | stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Unauthorized.new) 368 | expect { gateway.delete resource_path, payload: payload }.to raise_error(ContentGateway::UnauthorizedError) 369 | end 370 | 371 | it "should raise Forbidden exception on 403 error" do 372 | stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, headers: headers }, RestClient::Forbidden.new) 373 | expect { gateway.delete resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden) 374 | end 375 | 376 | it "should raise ConnectionFailure exception on 500 error" do 377 | stub_request_with_error({ method: :delete, url: resource_url, proxy: config.proxy, headers: headers }, SocketError.new) 378 | expect { gateway.delete resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure 379 | end 380 | end 381 | 382 | describe "#put" do 383 | let :resource_url do 384 | gateway.generate_url(resource_path) 385 | end 386 | 387 | let :payload do 388 | { param: "value" } 389 | end 390 | 391 | it "should do request with http put" do 392 | stub_request(method: :put, url: resource_url, proxy: config.proxy, payload: payload, headers: headers) 393 | gateway.put resource_path, payload: payload 394 | end 395 | 396 | it "should raise NotFound exception on 404 error" do 397 | stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::ResourceNotFound.new) 398 | expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ResourceNotFound 399 | end 400 | 401 | it "should raise UnprocessableEntity exception on 422 error" do 402 | stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::UnprocessableEntity) 403 | expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ValidationError 404 | end 405 | 406 | it "should raise Forbidden exception on 403 error" do 407 | stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, RestClient::Forbidden.new) 408 | expect { gateway.put resource_path, payload: payload }.to raise_error(ContentGateway::Forbidden) 409 | end 410 | 411 | it "should raise ConnectionFailure exception on 500 error" do 412 | stub_request_with_error({ method: :put, url: resource_url, proxy: config.proxy, payload: payload, headers: headers }, SocketError.new) 413 | expect { gateway.put resource_path, payload: payload }.to raise_error ContentGateway::ConnectionFailure 414 | end 415 | end 416 | 417 | private 418 | 419 | def stub_request(opts, payload = {}, &block) 420 | opts = { method: :get, proxy: nil }.merge(opts) 421 | request = RestClient::Request.new(opts) 422 | allow(RestClient::Request).to receive(:new).with(opts).and_return(request) 423 | 424 | allow(request).to receive(:execute) do 425 | block.call if block_given? 426 | end 427 | 428 | request 429 | end 430 | 431 | def stub_request_with_error(opts, exc) 432 | opts = { method: :get, proxy: nil }.merge(opts) 433 | 434 | request = RestClient::Request.new(opts) 435 | allow(RestClient::Request).to receive(:new).with(opts).and_return(request) 436 | 437 | allow(request).to receive(:execute).and_raise(exc) 438 | end 439 | end 440 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RACK_ENV'] = 'test' 2 | 3 | $:.unshift(File.join(File.dirname(__FILE__), "..", "lib")) 4 | 5 | require 'coveralls' 6 | Coveralls.wear! 7 | 8 | require 'byebug' 9 | require 'rspec' 10 | require 'content_gateway' 11 | 12 | begin 13 | require 'simplecov' 14 | SimpleCov.start do 15 | add_filter '/spec/' 16 | end 17 | SimpleCov.coverage_dir 'coverage/rspec' 18 | rescue LoadError 19 | # ignore simplecov in ruby < 1.9 20 | end 21 | 22 | # This file was generated by the `rspec --init` command. Conventionally, all 23 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 24 | # Require this file using `require "spec_helper"` to ensure that it is only 25 | # loaded once. 26 | # 27 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 28 | 29 | RSpec.configure do |config| 30 | config.run_all_when_everything_filtered = true 31 | config.filter_run :focus 32 | 33 | # Run specs in random order to surface order dependencies. If you find an 34 | # order dependency and want to debug it, you can fix the order by providing 35 | # the seed, which is printed after each run. 36 | # --seed 1234 37 | config.order = 'random' 38 | end 39 | -------------------------------------------------------------------------------- /spec/unit/content_gateway/cache_spec.rb: -------------------------------------------------------------------------------- 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 convert request response into string" do 88 | expect(String).to receive(:new).with("data") 89 | subject.fetch(request) 90 | end 91 | 92 | it "should return the request data" do 93 | expect(subject.fetch(request)).to eql "data" 94 | end 95 | 96 | it "should write the request data to stale cache" do 97 | expect(cache_store).to receive(:write).with("stale:/url", "data", expires_in: 15) 98 | 99 | subject.fetch(request, stale_expires_in: 15) 100 | end 101 | end 102 | end 103 | end 104 | 105 | describe "#serve_stale" do 106 | before do 107 | expect(cache_store).to receive(:read).with("stale:/url").and_return(return_value) 108 | end 109 | 110 | context "when data are successfully read from stale cache" do 111 | let(:return_value) { "stale cache data" } 112 | 113 | it "should return the stale data" do 114 | expect(subject.serve_stale).to eql "stale cache data" 115 | end 116 | 117 | it "should set status to 'STALE'" do 118 | subject.serve_stale 119 | expect(subject.status).to eql "STALE" 120 | end 121 | end 122 | 123 | context "when data can't be read from stale cache" do 124 | let(:return_value) { nil } 125 | 126 | it "should raise ContentGateway::StaleCacheNotAvailableError" do 127 | expect { subject.serve_stale }.to raise_error ContentGateway::StaleCacheNotAvailableError 128 | end 129 | end 130 | end 131 | 132 | describe "#stale_key" do 133 | let(:url) { "http://example.com" } 134 | 135 | it "should return the stale cache key" do 136 | expect(subject.stale_key).to eql "stale:http://example.com" 137 | end 138 | end 139 | end 140 | -------------------------------------------------------------------------------- /spec/unit/content_gateway/gateway_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ContentGateway::Gateway do 4 | subject do 5 | ContentGateway::Gateway.new("label", config, url_generator) 6 | end 7 | 8 | let(:gateway_without_url_generator) do 9 | ContentGateway::Gateway.new("label", config) 10 | end 11 | 12 | let(:config) { double("config", proxy: nil) } 13 | let(:headers) { { "Content-Type" => "application/json" } } 14 | let(:payload) { { "data" => 1234 } } 15 | let(:url_generator) { double("URL generator") } 16 | let(:path) { "/api/test.json" } 17 | let(:fullpath) { "www.teste.com/api/test.json" } 18 | let(:cache) { double("cache", use?: false, status: "HIT") } 19 | let(:request) { double("request", execute: data) } 20 | let(:data) { '{"param": "value"}' } 21 | let(:invalid_data) { "" } 22 | let(:cache_params) { { timeout: 2, expires_in: 30, stale_expires_in: 180, skip_cache: false, ssl_certificate: {ssl_client_cert: "test", ssl_client_key: "test"} } } 23 | let(:connection_params) {{ timeout: 2, ssl_certificate: {ssl_client_cert: "test", ssl_client_key: "test"} }} 24 | 25 | before do 26 | allow(File).to receive(:read).with("test").and_return("cert_content") 27 | allow(OpenSSL::X509::Certificate).to receive(:new).with("cert_content").and_return("cert") 28 | allow(OpenSSL::PKey::RSA).to receive(:new).with("cert_content").and_return("key") 29 | end 30 | 31 | shared_examples "request" do 32 | describe "doing a request" do 33 | it "should do a # {verb} request passing the correct parameters" do 34 | expect(subject.send(verb, path, params)).to eql data 35 | end 36 | end 37 | 38 | describe "doing a json request" do 39 | it "should parse the response as JSON" do 40 | expect(subject.send("#{verb}_json", path, params)).to eql JSON.parse(data) 41 | end 42 | 43 | context "when the answer is not a valid JSON" do 44 | let(:data) { invalid_data } 45 | 46 | it "should raise ContentGateway::ParserError" do 47 | expect { subject.send("#{verb}_json", path, params) }. 48 | to raise_error(ContentGateway::ParserError) 49 | end 50 | end 51 | end 52 | end 53 | 54 | describe "Without url generator" do 55 | describe "GET method" do 56 | let(:query_string) { { a: 1, b: 2 } } 57 | 58 | before do 59 | expect(ContentGateway::Request). 60 | to receive(:new). 61 | with(:get, fullpath, headers, nil, config.proxy, cache_params.merge(query_string)). 62 | and_return(request) 63 | 64 | expect(ContentGateway::Cache). 65 | to receive(:new). 66 | with(config, fullpath, :get, cache_params.merge(query_string)). 67 | and_return(cache) 68 | end 69 | 70 | describe "#get" do 71 | it "should do a get request passing the correct parameters" do 72 | expect(gateway_without_url_generator.get(fullpath, cache_params.merge(query_string).merge(headers: headers))).to eql data 73 | end 74 | end 75 | end 76 | 77 | describe "POST method" do 78 | before do 79 | expect(ContentGateway::Request). 80 | to receive(:new). 81 | with(:post, fullpath, nil, payload, config.proxy, connection_params). 82 | and_return(request) 83 | 84 | expect(ContentGateway::Cache). 85 | to receive(:new). 86 | with(config, fullpath, :post, connection_params). 87 | and_return(cache) 88 | end 89 | 90 | describe "#post" do 91 | it "should do a post request passing the correct parameters" do 92 | expect(gateway_without_url_generator.post(fullpath, cache_params.merge(payload: payload))).to eql data 93 | end 94 | end 95 | end 96 | 97 | describe "PUT method" do 98 | before do 99 | expect(ContentGateway::Request). 100 | to receive(:new). 101 | with(:put, fullpath, nil, payload, config.proxy, connection_params). 102 | and_return(request) 103 | 104 | expect(ContentGateway::Cache). 105 | to receive(:new). 106 | with(config, fullpath, :put, connection_params). 107 | and_return(cache) 108 | end 109 | 110 | describe "#put" do 111 | it "should do a put request passing the correct parameters" do 112 | expect(gateway_without_url_generator.put(fullpath, cache_params.merge(payload: payload))).to eql data 113 | end 114 | end 115 | end 116 | 117 | describe "DELETE method" do 118 | before do 119 | expect(ContentGateway::Request). 120 | to receive(:new). 121 | with(:delete, fullpath, nil, nil, config.proxy, connection_params). 122 | and_return(request) 123 | 124 | expect(ContentGateway::Cache). 125 | to receive(:new). 126 | with(config, fullpath, :delete, connection_params). 127 | and_return(cache) 128 | end 129 | 130 | describe "#delete" do 131 | it "should do a delete request passing the correct parameters" do 132 | expect(gateway_without_url_generator.delete(fullpath, cache_params.merge(payload: payload))).to eql data 133 | end 134 | end 135 | end 136 | end 137 | 138 | describe "With url generator" do 139 | before do 140 | expect(url_generator).to receive(:generate).at_least(:once).with(path, {}).and_return("url") 141 | end 142 | 143 | describe "GET method" do 144 | before do 145 | expect(ContentGateway::Request). 146 | to receive(:new). 147 | with(:get, "url", headers, nil, config.proxy, cache_params). 148 | and_return(request) 149 | 150 | expect(ContentGateway::Cache). 151 | to receive(:new). 152 | with(config, "url", :get, cache_params). 153 | and_return(cache) 154 | end 155 | 156 | it_should_behave_like "request" do 157 | let(:verb) { "get" } 158 | let(:params) { cache_params.merge(headers: headers) } 159 | end 160 | end 161 | 162 | describe "POST method" do 163 | before do 164 | expect(ContentGateway::Request). 165 | to receive(:new). 166 | with(:post, "url", nil, payload, config.proxy, connection_params). 167 | and_return(request) 168 | 169 | expect(ContentGateway::Cache). 170 | to receive(:new). 171 | with(config, "url", :post, connection_params). 172 | and_return(cache) 173 | end 174 | 175 | it_should_behave_like "request" do 176 | let(:verb) { "post" } 177 | let(:params) { cache_params.merge(payload: payload) } 178 | end 179 | end 180 | 181 | describe "PUT method" do 182 | before do 183 | expect(ContentGateway::Request). 184 | to receive(:new). 185 | with(:put, "url", nil, payload, config.proxy, connection_params). 186 | and_return(request) 187 | 188 | expect(ContentGateway::Cache). 189 | to receive(:new). 190 | with(config, "url", :put, connection_params). 191 | and_return(cache) 192 | end 193 | 194 | it_should_behave_like "request" do 195 | let(:verb) { "put" } 196 | let(:params) { cache_params.merge(payload: payload) } 197 | end 198 | end 199 | 200 | describe "DELETE method" do 201 | before do 202 | expect(ContentGateway::Request). 203 | to receive(:new). 204 | with(:delete, "url", nil, nil, config.proxy, connection_params). 205 | and_return(request) 206 | 207 | expect(ContentGateway::Cache). 208 | to receive(:new). 209 | with(config, "url", :delete, connection_params). 210 | and_return(cache) 211 | end 212 | 213 | it_should_behave_like "request" do 214 | let(:verb) { "delete" } 215 | let(:params) { cache_params.merge(payload: payload) } 216 | end 217 | end 218 | end 219 | end 220 | -------------------------------------------------------------------------------- /spec/unit/content_gateway/request_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe ContentGateway::Request do 4 | subject do 5 | ContentGateway::Request.new(:get, "/url") 6 | end 7 | 8 | before do 9 | allow(RestClient::Request).to receive(:new).with(request_params).and_return(client) 10 | end 11 | 12 | let(:client) { double("rest client", execute: "data", url: "/url") } 13 | 14 | let(:request_params) { { method: :get, url: "/url", proxy: nil } } 15 | 16 | describe "#execute" do 17 | context "when request is successful" do 18 | it "should return request data" do 19 | expect(subject.execute).to eql "data" 20 | end 21 | end 22 | 23 | context "when request fails" do 24 | context "with RestClient::ResourceNotFound exception" do 25 | before do 26 | expect(client).to receive(:execute).and_raise(RestClient::ResourceNotFound) 27 | end 28 | 29 | it "should raise ContentGateway::ResourceNotFound" do 30 | expect { subject.execute }.to raise_error ContentGateway::ResourceNotFound 31 | end 32 | end 33 | 34 | context "with RestClient::Unauthorized exception" do 35 | before do 36 | expect(client).to receive(:execute).and_raise(RestClient::Unauthorized) 37 | end 38 | 39 | it "should raise ContentGateway::UnauthorizedError" do 40 | expect { subject.execute }.to raise_error ContentGateway::UnauthorizedError 41 | end 42 | end 43 | 44 | context "with RestClient::UnprocessableEntity exception" do 45 | before do 46 | expect(client).to receive(:execute).and_raise(RestClient::UnprocessableEntity) 47 | end 48 | 49 | it "should raise ContentGateway::ValidationError" do 50 | expect { subject.execute }.to raise_error ContentGateway::ValidationError 51 | end 52 | end 53 | 54 | context "with RestClient::Forbidden exception" do 55 | before do 56 | expect(client).to receive(:execute).and_raise(RestClient::Forbidden) 57 | end 58 | 59 | it "should raise ContentGateway::Forbidden" do 60 | expect { subject.execute }.to raise_error ContentGateway::Forbidden 61 | end 62 | end 63 | 64 | context "with RestClient::Conflict exception" do 65 | before do 66 | expect(client).to receive(:execute).and_raise(RestClient::Conflict) 67 | end 68 | 69 | it "should raise ContentGateway::ConflictError" do 70 | expect { subject.execute }.to raise_error ContentGateway::ConflictError 71 | end 72 | end 73 | 74 | context "with a 5xx error" do 75 | before do 76 | expect(client).to receive(:execute).and_raise(RestClient::Exception.new(nil, 502)) 77 | end 78 | 79 | it "should raise ContentGateway::ServerError" do 80 | expect { subject.execute }.to raise_error ContentGateway::ServerError 81 | end 82 | end 83 | 84 | context "with other error codes from RestClient" do 85 | before do 86 | expect(client).to receive(:execute).and_raise(RestClient::Exception.new(nil, 418)) 87 | end 88 | 89 | it "should raise the original exception" do 90 | expect { subject.execute }.to raise_error RestClient::Exception 91 | end 92 | end 93 | 94 | context "with unmapped exceptions" do 95 | before do 96 | expect(client).to receive(:execute).and_raise(StandardError) 97 | end 98 | 99 | it "should raise ContentGateway::ConnectionFailure" do 100 | expect { subject.execute }.to raise_error ContentGateway::ConnectionFailure 101 | end 102 | end 103 | end 104 | 105 | context "requests with SSL" do 106 | 107 | let(:ssl_certificate_params) { { ssl_client_cert: "test", ssl_client_key: "test", ssl_version: "SSLv23"} } 108 | let(:restclient_ssl_params) { { ssl_client_cert: "cert", ssl_client_key: "key", verify_ssl: 0, ssl_version: "SSLv23" } } 109 | let(:request_params_ssl) { request_params.merge! restclient_ssl_params } 110 | 111 | let :subject_ssl do 112 | ContentGateway::Request.new(:get, "/url", {}, {}, nil, ssl_certificate: ssl_certificate_params) 113 | end 114 | 115 | context "only with ssl version" do 116 | let(:ssl_certificate_params) { { ssl_version: "SSLv23" } } 117 | let(:restclient_ssl_params) { { ssl_version: "SSLv23" } } 118 | let(:request_params_ssl) { request_params.merge! restclient_ssl_params } 119 | 120 | it "should setup request with ssl version" do 121 | expect(RestClient::Request).to receive(:new).with(request_params_ssl) 122 | subject_ssl.execute 123 | end 124 | 125 | it "should not setup ssl certificates" do 126 | allow(RestClient::Request).to receive(:new).with(request_params_ssl).and_return(client) 127 | expect(OpenSSL::X509::Certificate).to_not receive(:new) 128 | expect(OpenSSL::PKey::RSA).to_not receive(:new) 129 | subject_ssl.execute 130 | end 131 | end 132 | 133 | context "when request is successful" do 134 | before do 135 | allow(File).to receive(:read).with("test").and_return("cert_content") 136 | allow(OpenSSL::X509::Certificate).to receive(:new).with("cert_content").and_return("cert") 137 | allow(OpenSSL::PKey::RSA).to receive(:new).with("cert_content").and_return("key") 138 | allow(RestClient::Request).to receive(:new).with(request_params_ssl).and_return(client) 139 | end 140 | 141 | it "should setup ssl certificates" do 142 | expect(OpenSSL::X509::Certificate).to receive(:new).with("cert_content") 143 | expect(OpenSSL::PKey::RSA).to receive(:new).with("cert_content") 144 | subject_ssl.execute 145 | end 146 | 147 | it "should setup request with ssl params" do 148 | expect(RestClient::Request).to receive(:new).with(request_params_ssl) 149 | subject_ssl.execute 150 | end 151 | 152 | it "should return request data with ssl params" do 153 | expect(subject_ssl.execute).to eql "data" 154 | end 155 | end 156 | 157 | context "when request fails" do 158 | it "should return ssl failure error if certificate was not found" do 159 | expect { subject_ssl.execute }.to raise_error(ContentGateway::OpenSSLFailure).with_message(/^\/url - No such file or directory/) 160 | end 161 | 162 | it "should return ssl failure error if certificate cert was not valid" do 163 | allow(File).to receive(:read).with("test").and_return("cert_content") 164 | expect { subject_ssl.execute }.to raise_error(ContentGateway::OpenSSLFailure).with_message("/url - not enough data - invalid ssl client cert") 165 | end 166 | 167 | it "should return ssl failure error if certificate key was not valid" do 168 | allow(File).to receive(:read).with("test").and_return("cert_content") 169 | allow(OpenSSL::X509::Certificate).to receive(:new).with("cert_content").and_return("cert") 170 | expect { subject_ssl.execute }.to raise_error(ContentGateway::OpenSSLFailure).with_message("/url - Neither PUB key nor PRIV key: not enough data - invalid ssl client key") 171 | end 172 | end 173 | end 174 | 175 | context "when proxy is used" do 176 | let(:proxy) { 'http://proxy.test:3128' } 177 | subject { ContentGateway::Request.new(:get, "/url", {}, {}, proxy) } 178 | let(:request_params) { { method: :get, url: "/url", proxy: proxy } } 179 | 180 | it "should set proxy on RestClient" do 181 | expect(subject.execute).to eql "data" 182 | expect(RestClient.proxy).to eql(proxy) 183 | end 184 | end 185 | 186 | end 187 | end 188 | --------------------------------------------------------------------------------