loading
Generated 2024-12-31T18:57:12+00:00

All Files ( 91.24% covered at 2.91 hits/line )

7 files in total.
571 relevant lines, 521 lines covered and 50 lines missed. ( 91.24% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/minato_ruby_api_client.rb 100.00 % 24 10 10 0 5.90
lib/minato_ruby_api_client/api_client.rb 70.06 % 389 167 117 50 2.23
lib/minato_ruby_api_client/api_error.rb 100.00 % 16 9 9 0 1.22
lib/minato_ruby_api_client/configuration.rb 100.00 % 247 85 85 0 6.72
spec/api_client_spec.rb 100.00 % 309 172 172 0 1.88
spec/configuration_spec.rb 100.00 % 204 119 119 0 2.66
spec/minato_ruby_api_client_spec.rb 100.00 % 17 9 9 0 1.11

lib/minato_ruby_api_client.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # Common files
  2. 1 require 'minato_ruby_api_client/api_client'
  3. 1 require 'minato_ruby_api_client/api_error'
  4. 1 require 'minato_ruby_api_client/version'
  5. 1 require 'minato_ruby_api_client/configuration'
  6. 1 module MinatoRubyApiClient
  7. 1 class << self
  8. # Customize default settings for the SDK using block.
  9. # MinatoRubyApiClient.configure do |config|
  10. # config.username = "xxx"
  11. # config.password = "xxx"
  12. # end
  13. # If no block given, return the default Configuration object.
  14. 1 def configure
  15. 26 if block_given?
  16. 25 yield(Configuration.default)
  17. else
  18. 1 Configuration.default
  19. end
  20. end
  21. end
  22. end

lib/minato_ruby_api_client/api_client.rb

70.06% lines covered

167 relevant lines. 117 lines covered and 50 lines missed.
    
  1. 1 require 'date'
  2. 1 require 'json'
  3. 1 require 'logger'
  4. 1 require 'tempfile'
  5. 1 require 'time'
  6. 1 require 'faraday'
  7. 1 require 'minato/trace'
  8. 1 require 'ostruct'
  9. 1 module MinatoRubyApiClient
  10. 1 class ApiClient
  11. # The Configuration object holding settings to be used in the API client.
  12. 1 attr_accessor :config
  13. # Defines the headers to be used in HTTP requests of all API calls by default.
  14. #
  15. # @return [Hash]
  16. 1 attr_accessor :default_headers
  17. # Initializes the ApiClient
  18. # @option config [Configuration] Configuration for initializing the object, default to Configuration.default
  19. 1 def initialize(config = Configuration.default)
  20. 11 @config = config
  21. 11 @user_agent = config.user_agent
  22. @default_headers = {
  23. 11 'Content-Type' => 'application/json',
  24. 'User-Agent' => @user_agent
  25. }
  26. 11 if defined?(Rails) && Rails.env.production? && Minato::Trace.enabled?
  27. 1 @config.use(Minato::Trace::Middleware::DistributedTraceContext)
  28. end
  29. end
  30. 1 def self.default
  31. 25 @@default ||= ApiClient.new
  32. end
  33. # Call an API with given options.
  34. #
  35. # @return [Array<(Object, Integer, Hash)>] an array of 3 elements:
  36. # the data deserialized from response body (could be nil), response status code and response headers.
  37. 1 def call_api(http_method, path, opts = {})
  38. ssl_options = {
  39. 4 :ca_file => @config.ssl_ca_file,
  40. :verify => @config.ssl_verify,
  41. :verify_mode => @config.ssl_verify_mode,
  42. :client_cert => @config.ssl_client_cert,
  43. :client_key => @config.ssl_client_key
  44. }
  45. 4 connection = Faraday.new(:url => config.base_url, :ssl => ssl_options) do |conn|
  46. 4 @config.configure_middleware(conn)
  47. 4 if opts[:header_params] && opts[:header_params]["Content-Type"] == "multipart/form-data"
  48. conn.request :multipart
  49. conn.request :url_encoded
  50. end
  51. 4 conn.adapter(Faraday.default_adapter)
  52. end
  53. 4 request = nil
  54. begin
  55. 4 response = connection.public_send(http_method.to_sym.downcase) do |req|
  56. 4 request = req
  57. 4 build_request(http_method, path, req, opts)
  58. end
  59. 3 @config.logger.info({ 'message' => "Receive response from #{response.env.url}",
  60. 'http_response' => response.to_hash })
  61. 3 unless response.success?
  62. 1 error = self.class.module_parent::ApiError.new(:status_code => response.status,
  63. :res => response, :req => request)
  64. 1 error.caused_by = response.reason_phrase
  65. 1 fail error
  66. end
  67. rescue Faraday::TimeoutError => e
  68. 1 error = self.class.module_parent::ApiError.new(req: request, res: response, status_code: 408)
  69. 1 error.caused_by = e
  70. 1 fail error
  71. end
  72. 2 if opts[:return_type]
  73. 1 data = deserialize(response, opts[:return_type])
  74. else
  75. 1 data = nil
  76. end
  77. 2 return data, response.status, response.headers
  78. end
  79. # Builds the HTTP request
  80. #
  81. # @param [String] http_method HTTP method/verb (e.g. POST)
  82. # @param [String] path URL path (e.g. /account/new)
  83. # @option opts [Hash] :header_params Header parameters
  84. # @option opts [Hash] :query_params Query parameters
  85. # @option opts [Hash] :form_params Query parameters
  86. # @option opts [Object] :body HTTP body (JSON/XML)
  87. # @return [Typhoeus::Request] A Typhoeus Request
  88. 1 def build_request(http_method, path, request, opts = {})
  89. 4 url = build_request_url(path)
  90. 4 http_method = http_method.to_sym.downcase
  91. 4 header_params = @default_headers.merge(opts[:header_params] || {})
  92. 4 query_params = opts[:query_params] || {}
  93. 4 form_params = opts[:form_params] || {}
  94. 4 update_params_for_auth! header_params, query_params, opts[:auth_names]
  95. req_opts = {
  96. 4 :params_encoding => @config.params_encoding,
  97. :timeout => @config.timeout,
  98. :verbose => @config.debugging
  99. }
  100. 4 if [:post, :patch, :put, :delete].include?(http_method)
  101. 3 req_body = build_request_body(header_params, form_params, opts[:body])
  102. end
  103. 4 request.headers = header_params
  104. 4 request.body = req_body
  105. 4 request.options = OpenStruct.new(req_opts)
  106. 4 request.url url
  107. 4 request.params = query_params
  108. 4 download_file(request) if opts[:return_type] == 'File'
  109. 4 @config.logger.info({ 'message' => "Starting request to #{url}",
  110. 'http_request' => request.to_h })
  111. 4 request
  112. end
  113. # Builds the HTTP request body
  114. #
  115. # @param [Hash] header_params Header parameters
  116. # @param [Hash] form_params Query parameters
  117. # @param [Object] body HTTP body (JSON/XML)
  118. # @return [String] HTTP body data in the form of string
  119. 1 def build_request_body(header_params, form_params, body)
  120. # http form
  121. 8 if header_params['Content-Type'] == 'application/x-www-form-urlencoded'
  122. 1 data = URI.encode_www_form(form_params)
  123. 7 elsif header_params['Content-Type'] == 'multipart/form-data'
  124. 1 data = {}
  125. 1 form_params.each do |key, value|
  126. 2 case value
  127. when ActionDispatch::Http::UploadedFile
  128. # TODO hardcode to application/octet-stream, need better way to detect content type
  129. data[key] = Faraday::UploadIO.new(value.path, value.content_type, value.original_filename)
  130. when ::File, ::Tempfile
  131. # TODO hardcode to application/octet-stream, need better way to detect content type
  132. data[key] = Faraday::UploadIO.new(value.path, 'application/octet-stream', value.path)
  133. when ::Array, nil
  134. # let Faraday handle Array and nil parameters
  135. data[key] = value
  136. else
  137. 2 data[key] = value.to_s
  138. end
  139. end
  140. 6 elsif body
  141. 5 data = body.is_a?(String) ? body : body.to_json
  142. else
  143. 1 data = nil
  144. end
  145. 8 data
  146. end
  147. 1 def download_file(request)
  148. @stream = []
  149. # handle streaming Responses
  150. request.options.on_data = Proc.new do |chunk, overall_received_bytes|
  151. @stream << chunk
  152. end
  153. end
  154. # Check if the given MIME is a JSON MIME.
  155. # JSON MIME examples:
  156. # application/json
  157. # application/json; charset=UTF8
  158. # APPLICATION/JSON
  159. # */*
  160. # @param [String] mime MIME
  161. # @return [Boolean] True if the MIME is application/json
  162. 1 def json_mime?(mime)
  163. 3 (mime == '*/*') || !(mime =~ /Application\/.*json(?!p)(;.*)?/i).nil?
  164. end
  165. # Deserialize the response to the given return type.
  166. #
  167. # @param [Response] response HTTP response
  168. # @param [String] return_type some examples: "User", "Array<User>", "Hash<String, Integer>"
  169. 1 def deserialize(response, return_type)
  170. 1 body = response.body
  171. # handle file downloading - return the File instance processed in request callbacks
  172. # note that response body is empty when the file is written in chunks in request on_body callback
  173. 1 if return_type == 'File'
  174. content_disposition = response.headers['Content-Disposition']
  175. if content_disposition && content_disposition =~ /filename=/i
  176. filename = content_disposition[/filename=['"]?([^'"\s]+)['"]?/, 1]
  177. prefix = sanitize_filename(filename)
  178. else
  179. prefix = 'download-'
  180. end
  181. prefix = prefix + '-' unless prefix.end_with?('-')
  182. encoding = body.encoding
  183. @tempfile = Tempfile.open(prefix, @config.temp_folder_path, encoding: encoding)
  184. @tempfile.write(@stream.join.force_encoding(encoding))
  185. @tempfile.close
  186. @config.logger.info "Temp file written to #{@tempfile.path}, please copy the file to a proper folder "\
  187. "with e.g. `FileUtils.cp(tempfile.path, '/new/file/path')` otherwise the temp file "\
  188. "will be deleted automatically with GC. It's also recommended to delete the temp file "\
  189. "explicitly with `tempfile.delete`"
  190. return @tempfile
  191. end
  192. 1 return nil if body.nil? || body.empty?
  193. # return response body directly for String return type
  194. 1 return body if return_type == 'String'
  195. # ensuring a default content type
  196. 1 content_type = response.headers['Content-Type'] || 'application/json'
  197. 1 fail "Content-Type is not supported: #{content_type}" unless json_mime?(content_type)
  198. begin
  199. 1 data = JSON.parse("[#{body}]", :symbolize_names => true)[0]
  200. rescue JSON::ParserError => e
  201. if %w(String Date Time).include?(return_type)
  202. data = body
  203. else
  204. raise e
  205. end
  206. end
  207. 1 convert_to_type data, return_type
  208. end
  209. # Convert data to the given return type.
  210. # @param [Object] data Data to be converted
  211. # @param [String] return_type Return type
  212. # @return [Mixed] Data in a particular type
  213. 1 def convert_to_type(data, return_type)
  214. 17 return nil if data.nil?
  215. 16 case return_type
  216. when 'String'
  217. 4 data.to_s
  218. when 'Integer'
  219. 3 data.to_i
  220. when 'Float'
  221. 1 data.to_f
  222. when 'Boolean'
  223. 1 data == true
  224. when 'Time'
  225. # parse date time (expecting ISO 8601 format)
  226. 1 Time.parse data
  227. when 'Date'
  228. # parse date time (expecting ISO 8601 format)
  229. 1 Date.parse data
  230. when 'Object'
  231. # generic object (usually a Hash), return directly
  232. 2 data
  233. when /\AArray<(.+)>\z/
  234. # e.g. Array<Pet>
  235. 1 sub_type = $1
  236. 4 data.map { |item| convert_to_type(item, sub_type) }
  237. when /\AHash\<String, (.+)\>\z/
  238. # e.g. Hash<String, Integer>
  239. 1 sub_type = $1
  240. 1 {}.tap do |hash|
  241. 3 data.each { |k, v| hash[k] = convert_to_type(v, sub_type) }
  242. end
  243. else
  244. # models (e.g. Pet) or oneOf
  245. 1 klass = self.class.module_parent.const_get(return_type)
  246. 1 klass.respond_to?(:openapi_one_of) ? klass.build(data) : klass.build_from_hash(data)
  247. end
  248. end
  249. # Sanitize filename by removing path.
  250. # e.g. ../../sun.gif becomes sun.gif
  251. #
  252. # @param [String] filename the filename to be sanitized
  253. # @return [String] the sanitized filename
  254. 1 def sanitize_filename(filename)
  255. filename.gsub(/.*[\/\\]/, '')
  256. end
  257. 1 def build_request_url(path)
  258. # Add leading and trailing slashes to path
  259. 4 path = "/#{path}".gsub(/\/+/, '/')
  260. 4 @config.base_url + path
  261. end
  262. # Update header and query params based on authentication settings.
  263. #
  264. # @param [Hash] header_params Header parameters
  265. # @param [Hash] query_params Query parameters
  266. # @param [String] auth_names Authentication scheme name
  267. 1 def update_params_for_auth!(header_params, query_params, auth_names)
  268. 8 Array(auth_names).each do |auth_name|
  269. 8 auth_setting = @config.auth_settings[auth_name]
  270. 8 next unless auth_setting
  271. 4 case auth_setting[:in]
  272. 4 when 'header' then header_params[auth_setting[:key]] = auth_setting[:value]
  273. when 'query' then query_params[auth_setting[:key]] = auth_setting[:value]
  274. else fail ArgumentError, 'Authentication token must be in `query` or `header`'
  275. end
  276. end
  277. end
  278. # Sets user agent in HTTP header
  279. #
  280. # @param [String] user_agent User agent (e.g. openapi-generator/ruby/1.0.0)
  281. 1 def user_agent=(user_agent)
  282. @user_agent = user_agent
  283. @default_headers['User-Agent'] = @user_agent
  284. end
  285. # Return Accept header based on an array of accepts provided.
  286. # @param [Array] accepts array for Accept
  287. # @return [String] the Accept header (e.g. application/json)
  288. 1 def select_header_accept(accepts)
  289. return nil if accepts.nil? || accepts.empty?
  290. # use JSON when present, otherwise use all of the provided
  291. json_accept = accepts.find { |s| json_mime?(s) }
  292. json_accept || accepts.join(',')
  293. end
  294. # Return Content-Type header based on an array of content types provided.
  295. # @param [Array] content_types array for Content-Type
  296. # @return [String] the Content-Type header (e.g. application/json)
  297. 1 def select_header_content_type(content_types)
  298. # return nil by default
  299. return if content_types.nil? || content_types.empty?
  300. # use JSON when present, otherwise use the first one
  301. json_content_type = content_types.find { |s| json_mime?(s) }
  302. json_content_type || content_types.first
  303. end
  304. # Convert object (array, hash, object, etc) to JSON string.
  305. # @param [Object] model object to be converted into JSON string
  306. # @return [String] JSON string representation of the object
  307. 1 def object_to_http_body(model)
  308. return model if model.nil? || model.is_a?(String)
  309. local_body = nil
  310. if model.is_a?(Array)
  311. local_body = model.map { |m| object_to_hash(m) }
  312. else
  313. local_body = object_to_hash(model)
  314. end
  315. local_body.to_json
  316. end
  317. # Convert object(non-array) to hash.
  318. # @param [Object] obj object to be converted into JSON string
  319. # @return [String] JSON string representation of the object
  320. 1 def object_to_hash(obj)
  321. if obj.respond_to?(:to_hash)
  322. obj.to_hash
  323. else
  324. obj
  325. end
  326. end
  327. # Build parameter value according to the given collection format.
  328. # @param [String] collection_format one of :csv, :ssv, :tsv, :pipes and :multi
  329. 1 def build_collection_param(param, collection_format)
  330. case collection_format
  331. when :csv
  332. param.join(',')
  333. when :ssv
  334. param.join(' ')
  335. when :tsv
  336. param.join("\t")
  337. when :pipes
  338. param.join('|')
  339. when :multi
  340. # return the array directly as typhoeus will handle it as expected
  341. param
  342. else
  343. fail "unknown collection format: #{collection_format.inspect}"
  344. end
  345. end
  346. end
  347. end

lib/minato_ruby_api_client/api_error.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 require 'minato_error_handler'
  2. 1 module MinatoRubyApiClient
  3. 1 class ApiError < MinatoErrorHandler::Errors::ExternalError
  4. 1 attr_reader :status_code
  5. 1 def initialize(res: nil, req: nil, status_code: 500)
  6. 2 super(req: req, res: res)
  7. 2 @status_code = status_code
  8. end
  9. 1 def message
  10. 1 "An error occurred while communicating with the API."
  11. end
  12. end
  13. end

lib/minato_ruby_api_client/configuration.rb

100.0% lines covered

85 relevant lines. 85 lines covered and 0 lines missed.
    
  1. 1 module MinatoRubyApiClient
  2. 1 class Configuration
  3. # Defines url scheme
  4. 1 attr_accessor :scheme
  5. # Defines url host
  6. 1 attr_accessor :host
  7. # Defines url base path
  8. 1 attr_accessor :base_path
  9. # Defines user agent
  10. 1 attr_accessor :user_agent
  11. # Defines API keys used with API Key authentications.
  12. #
  13. # @return [Hash] key: parameter name, value: parameter value (API key)
  14. #
  15. # @example parameter name is "api_key", API key is "xxx" (e.g. "api_key=xxx" in query string)
  16. # config.api_key['api_key'] = 'xxx'
  17. 1 attr_accessor :api_key
  18. # Defines API key prefixes used with API Key authentications.
  19. #
  20. # @return [Hash] key: parameter name, value: API key prefix
  21. #
  22. # @example parameter name is "Authorization", API key prefix is "Token" (e.g. "Authorization: Token xxx" in headers)
  23. # config.api_key_prefix['api_key'] = 'Token'
  24. 1 attr_accessor :api_key_prefix
  25. # Defines the username used with HTTP basic authentication.
  26. #
  27. # @return [String]
  28. 1 attr_accessor :username
  29. # Defines the password used with HTTP basic authentication.
  30. #
  31. # @return [String]
  32. 1 attr_accessor :password
  33. # Defines the access token (Bearer) used with OAuth2.
  34. 1 attr_accessor :access_token
  35. # Set this to enable/disable debugging. When enabled (set to true), HTTP request/response
  36. # details will be logged with `logger.debug` (see the `logger` attribute).
  37. # Default to false.
  38. #
  39. # @return [true, false]
  40. 1 attr_accessor :debugging
  41. # Defines the logger used for debugging.
  42. # Default to `Rails.logger` (when in Rails) or logging to STDOUT.
  43. #
  44. # @return [#debug]
  45. 1 attr_accessor :logger
  46. # Defines the temporary folder to store downloaded files
  47. # (for API endpoints that have file response).
  48. # Default to use `Tempfile`.
  49. #
  50. # @return [String]
  51. 1 attr_accessor :temp_folder_path
  52. # The time limit for HTTP request in seconds.
  53. # Default to 0 (never times out).
  54. 1 attr_accessor :timeout
  55. # Set this to false to skip client side validation in the operation.
  56. # Default to true.
  57. # @return [true, false]
  58. 1 attr_accessor :client_side_validation
  59. ### TLS/SSL setting
  60. # Set this to false to skip verifying SSL certificate when calling API from https server.
  61. # Default to true.
  62. #
  63. # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks.
  64. #
  65. # @return [true, false]
  66. 1 attr_accessor :ssl_verify
  67. ### TLS/SSL setting
  68. # Any `OpenSSL::SSL::` constant (see https://ruby-doc.org/stdlib-2.5.1/libdoc/openssl/rdoc/OpenSSL/SSL.html)
  69. #
  70. # @note Do NOT set it to false in production code, otherwise you would face multiple types of cryptographic attacks.
  71. #
  72. 1 attr_accessor :ssl_verify_mode
  73. ### TLS/SSL setting
  74. # Set this to customize the certificate file to verify the peer.
  75. #
  76. # @return [String] the path to the certificate file
  77. 1 attr_accessor :ssl_ca_file
  78. ### TLS/SSL setting
  79. # Client certificate file (for client certificate)
  80. 1 attr_accessor :ssl_client_cert
  81. ### TLS/SSL setting
  82. # Client private key file (for client certificate)
  83. 1 attr_accessor :ssl_client_key
  84. # Set this to customize parameters encoding of array parameter with multi collectionFormat.
  85. # Default to nil.
  86. #
  87. # @see The params_encoding option of Ethon. Related source code:
  88. # https://github.com/typhoeus/ethon/blob/master/lib/ethon/easy/queryable.rb#L96
  89. 1 attr_accessor :params_encoding
  90. 1 def initialize
  91. 13 @scheme = 'https'
  92. 13 @host = ''
  93. 13 @base_path = ''
  94. 13 @user_agent = ''
  95. 13 @subscription_key = ''
  96. 13 @api_key = {}
  97. 13 @api_key_prefix = {}
  98. 13 @client_side_validation = true
  99. 13 @ssl_verify = true
  100. 13 @ssl_verify_mode = nil
  101. 13 @ssl_ca_file = nil
  102. 13 @ssl_client_cert = nil
  103. 13 @ssl_client_key = nil
  104. 13 @middlewares = []
  105. 13 @request_middlewares = []
  106. 13 @response_middlewares = []
  107. 13 @timeout = 60
  108. 13 @debugging = false
  109. 13 @logger = Object.const_defined?(:Rails) ? Rails.logger : Logger.new(STDOUT)
  110. 13 yield(self) if block_given?
  111. end
  112. # The default Configuration object.
  113. 1 def self.default
  114. 54 @@default ||= Configuration.new
  115. end
  116. 1 def configure
  117. 2 yield(self) if block_given?
  118. end
  119. 1 def scheme=(scheme)
  120. # remove :// from scheme
  121. 1 @scheme = scheme.sub(/:\/\//, '')
  122. end
  123. 1 def host=(host)
  124. # remove http(s):// and anything after a slash
  125. 46 @host = host.sub(/https?:\/\//, '').split('/').first
  126. end
  127. 1 def base_path=(base_path)
  128. # Add leading and trailing slashes to base_path
  129. 30 @base_path = "/#{base_path}".gsub(/\/+/, '/')
  130. 30 @base_path = '' if @base_path == '/'
  131. end
  132. # Returns base URL
  133. 1 def base_url
  134. 13 "#{scheme}://#{[host, base_path].join('/').gsub(/\/+/, '/')}".sub(/\/+\z/, '')
  135. end
  136. 1 def debugging
  137. 7 return @debugging if @debugging == true
  138. 6 ENV['MINATO_RUBY_API_CLIENT_DEBUG'] == 'true'
  139. end
  140. # Gets API key (with prefix if set).
  141. # @param [String] param_name the parameter name of API key auth
  142. 1 def api_key_with_prefix(param_name, param_alias = nil)
  143. 5 key = @api_key[param_name]
  144. 5 key = @api_key.fetch(param_alias, key) unless param_alias.nil?
  145. 5 if @api_key_prefix[param_name]
  146. 2 "#{@api_key_prefix[param_name]} #{key}"
  147. else
  148. 3 key
  149. end
  150. end
  151. # Gets Basic Auth token string
  152. 1 def basic_auth_token
  153. 10 'Basic ' + ["#{username}:#{password}"].pack('m').delete("\r\n")
  154. end
  155. # Returns Auth Settings hash for api client.
  156. 1 def auth_settings
  157. auth_settings = {
  158. 9 'bearer_auth' =>
  159. {
  160. type: 'bearer',
  161. in: 'header',
  162. key: 'Authorization',
  163. value: "Bearer #{access_token}"
  164. },
  165. 'basic_auth' =>
  166. {
  167. type: 'basic',
  168. in: 'header',
  169. key: 'Authorization',
  170. value: basic_auth_token
  171. },
  172. }
  173. 9 api_key.each do |k, v|
  174. 3 auth_settings['apikey'] = {
  175. type: 'apikey',
  176. in: 'header',
  177. key: k,
  178. value: api_key_with_prefix(k)
  179. }
  180. end
  181. 9 auth_settings
  182. end
  183. # Adds middleware to the stack
  184. 1 def use(*middleware)
  185. 3 @middlewares << middleware
  186. end
  187. # Adds request middleware to the stack
  188. 1 def request(*middleware)
  189. 2 @request_middlewares << middleware
  190. end
  191. # Adds response middleware to the stack
  192. 1 def response(*middleware)
  193. 2 @response_middlewares << middleware
  194. end
  195. # Set up middleware on the connection
  196. 1 def configure_middleware(connection)
  197. 5 @middlewares.each do |middleware|
  198. 1 connection.use(*middleware)
  199. end
  200. 5 @request_middlewares.each do |middleware|
  201. 1 connection.request(*middleware)
  202. end
  203. 5 @response_middlewares.each do |middleware|
  204. 1 connection.response(*middleware)
  205. end
  206. end
  207. end
  208. end

spec/api_client_spec.rb

100.0% lines covered

172 relevant lines. 172 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 describe MinatoRubyApiClient::ApiClient do
  3. 1 [:config, :default_headers].each do |method|
  4. 4 it { should respond_to method }
  5. end
  6. 23 let(:url) { 'https://example.com/v1' }
  7. 1 let(:api_client) do
  8. 22 uri = URI.parse(url)
  9. 22 described_class.default.tap do |api_client|
  10. 22 api_client.config.host = uri.host
  11. end
  12. end
  13. 1 describe '.default' do
  14. 1 it 'memoizes the instance of MinatoRubyApiClient::ApiClient in subsequent calls' do
  15. 1 expect(described_class).to receive(:new).once.and_call_original
  16. 3 2.times { described_class.default }
  17. end
  18. 1 it 'returns a instance of MinatoRubyApiClient::ApiClient' do
  19. 1 expect(described_class.default).to be_a MinatoRubyApiClient::ApiClient
  20. end
  21. end
  22. 1 describe '#build_request_body' do
  23. 6 let(:form_params) { { foo: 'bar', bar: 'foobar' } }
  24. 4 let(:body) { { foo: 'bar', bar: 'foobar' } }
  25. 1 before do
  26. 5 @data = api_client.build_request_body(header_params, form_params, body)
  27. end
  28. 1 context 'when content type is form-url-encoded' do
  29. 2 let(:header_params) { { 'Content-Type' => 'application/x-www-form-urlencoded' } }
  30. 1 it 'returns data in url encoded format' do
  31. 1 expect(@data).to eq('foo=bar&bar=foobar')
  32. end
  33. end
  34. 1 context 'when content type is multipar/form-data' do
  35. 2 let(:header_params) { { 'Content-Type' => 'multipart/form-data' } }
  36. 1 it 'returns data in hash format' do
  37. 1 expect(@data).to eq(form_params)
  38. end
  39. end
  40. 1 context 'when body is a string' do
  41. 2 let(:header_params) { { 'Content-Type' => 'text/plain' } }
  42. 2 let(:body) { 'body is a string' }
  43. 1 it 'returns data in string format' do
  44. 1 expect(@data).to eq(body)
  45. end
  46. end
  47. 1 context 'when body is a hash' do
  48. 2 let(:header_params) { { 'Content-Type' => 'application/json' } }
  49. 1 it 'returns data in json format' do
  50. 1 expect(@data).to eq(body.to_json)
  51. end
  52. end
  53. 1 context 'when no body and form content types is passed' do
  54. 2 let(:header_params) { { 'Content-Type' => 'application/json' } }
  55. 2 let(:body) { nil }
  56. 1 it 'returns a null data' do
  57. 1 expect(@data).to be_nil
  58. end
  59. end
  60. end
  61. 1 describe '#json_mime?' do
  62. 1 context 'when content type is json' do
  63. 1 it 'returns true' do
  64. 1 expect(api_client.json_mime?('application/json')).to be true
  65. end
  66. end
  67. 1 context 'when content type is not json' do
  68. 1 it 'returns false' do
  69. 1 expect(api_client.json_mime?('application/xml')).to be false
  70. end
  71. end
  72. end
  73. 1 describe '#call_api' do
  74. 5 let(:opts) { { body: { name: 'Foo Bar' }, auth_names: 'apikey', return_type: 'Object' } }
  75. 1 let(:http_response) do
  76. 3 { status: 200, headers: { 'Context-type': 'application/json' },
  77. body: { message: 'success' }.to_json }
  78. end
  79. 1 before do
  80. 4 stub_request(:post, /test/)
  81. .to_return(http_response)
  82. 4 allow(api_client.config).to receive(:logger).and_return(Logger.new(STDOUT))
  83. 4 api_client.config.logger.level = Logger::WARN
  84. end
  85. 1 context 'when request is successful' do
  86. 1 it 'returns data, status and headers' do
  87. 1 data, status_code, headers = api_client.call_api(:post, '/test', opts)
  88. 1 expect(data).to eq({ message: 'success' })
  89. 1 expect(status_code).to eq(200)
  90. end
  91. 1 it 'returns nil when return_type is not defined' do
  92. 1 opts[:return_type] = nil
  93. 1 data, status_code, headers = api_client.call_api(:post, '/test', opts)
  94. 1 expect(data).to be_nil
  95. end
  96. end
  97. 1 context 'when request failed' do
  98. 1 let(:http_response) do
  99. 1 { status: [400, 'Bad Request'], headers: { 'Context-type': 'application/json' },
  100. body: { message: 'Dados inválidos' }.to_json }
  101. end
  102. 1 it 'raise an error MinatoRubyApiClient::ApiError' do
  103. 2 expect { api_client.call_api(:post, '/test', opts) }.to raise_error(
  104. an_instance_of(MinatoRubyApiClient::ApiError)
  105. .and having_attributes(message: 'An error occurred while communicating with the API.',
  106. caused_by: 'Bad Request', status_code: 400))
  107. end
  108. end
  109. 1 context 'when request timeout' do
  110. 2 before { stub_request(:get, /test/).to_raise(Faraday::TimeoutError) }
  111. 1 it 'raise an error caused by timeout' do
  112. 2 expect { api_client.call_api(:get, '/test', opts) }.to raise_error { |error|
  113. 1 expect(error).to be_a(MinatoRubyApiClient::ApiError)
  114. 1 expect(error.caused_by).to be_a(Faraday::TimeoutError)
  115. }
  116. end
  117. end
  118. end
  119. 1 describe 'trace' do
  120. 5 let(:config) { MinatoRubyApiClient::Configuration.new }
  121. 5 let(:api_client) { described_class.new(config) }
  122. 1 context 'when trace is enabled and Rails is defined' do
  123. 1 it 'add distributed trace middleware to middleware stack' do
  124. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(true)
  125. 1 allow(Rails.env).to receive(:production?).and_return(true)
  126. 1 allow(Minato::Trace).to receive(:enabled?).and_return(true)
  127. 1 expect(api_client.config.instance_variable_get('@middlewares')).to include([Minato::Trace::Middleware::DistributedTraceContext])
  128. end
  129. end
  130. 1 context 'when trace is disabled' do
  131. 1 it 'should not add ditributed trace middleware' do
  132. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(true)
  133. 1 allow(Rails.env).to receive(:production?).and_return(true)
  134. 1 allow(Minato::Trace).to receive(:enabled?).and_return(false)
  135. 1 expect(api_client.config.instance_variable_get('@middlewares')).to eq []
  136. end
  137. end
  138. 1 context 'when Rails is not defined' do
  139. 1 it 'should not add ditributed trace middleware' do
  140. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(false)
  141. 1 allow(Minato::Trace).to receive(:enabled?).and_return(true)
  142. 1 expect(api_client.config.instance_variable_get('@middlewares')).to eq []
  143. end
  144. end
  145. 1 context 'when Rails is defined but is not in productio mode' do
  146. 1 it 'should not add ditributed trace middleware' do
  147. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(true)
  148. 1 allow(Rails.env).to receive(:production?).and_return(false)
  149. 1 allow(Minato::Trace).to receive(:enabled?).and_return(true)
  150. 1 expect(api_client.config.instance_variable_get('@middlewares')).to eq []
  151. end
  152. end
  153. end
  154. 1 describe '#convert_to_type' do
  155. 1 context 'when data is nil' do
  156. 1 it 'returns nil' do
  157. 1 expect(api_client.convert_to_type(nil, 'String')).to be_nil
  158. end
  159. end
  160. 1 context 'when return type is String' do
  161. 1 it 'returns string' do
  162. 1 expect(api_client.convert_to_type(1, 'String')).to be_a(String)
  163. end
  164. end
  165. 1 context 'when return type is Integer' do
  166. 1 it 'returns integer' do
  167. 1 expect(api_client.convert_to_type('1', 'Integer')).to be_a(Integer)
  168. end
  169. end
  170. 1 context 'when return type is Float' do
  171. 1 it 'returns float' do
  172. 1 expect(api_client.convert_to_type('1', 'Float')).to be_a(Float)
  173. end
  174. end
  175. 1 context 'when return type is Boolean' do
  176. 1 it 'returns boolean' do
  177. 1 expect(api_client.convert_to_type(true, 'Boolean')).to be_truthy
  178. end
  179. end
  180. 1 context 'when return type is Time' do
  181. 1 it 'returns time' do
  182. 1 expect(api_client.convert_to_type('12:00:00', 'Time')).to be_a(Time)
  183. end
  184. end
  185. 1 context 'when return type is Date' do
  186. 1 it 'returns time' do
  187. 1 expect(api_client.convert_to_type('2020-12-25 12:00:00', 'Date')).to be_a(Date)
  188. end
  189. end
  190. 1 context 'when return type is Object' do
  191. 1 it 'returns data' do
  192. 1 data = { id: 1 }
  193. 1 expect(api_client.convert_to_type(data, 'Object')).to equal(data)
  194. end
  195. end
  196. 1 context 'when return type is Array<String>' do
  197. 1 it 'returns array list of type specified' do
  198. 1 data = [1, 2, 3]
  199. 1 expect(api_client.convert_to_type(data, 'Array<String>')).to eq(['1', '2', '3'])
  200. end
  201. end
  202. 1 context 'when return type is Hash<String, Integer>' do
  203. 1 it 'returns hash with key:value of types specifieds' do
  204. 1 data = { id: '22', qtd: '12' }
  205. 1 expect(api_client.convert_to_type(data, 'Hash<String, Integer>')).to eq({ 'id': 22, 'qtd': 12 })
  206. end
  207. end
  208. 1 context 'when return type is a custom type as a class' do
  209. 1 it 'returns a object from the class' do
  210. 1 klass = double('Pet', name: 'Bob')
  211. 1 allow(MinatoRubyApiClient).to receive(:const_get).with('Pet').and_return(klass)
  212. 1 allow(klass).to receive(:build_from_hash).and_return(true)
  213. 1 expect(api_client.convert_to_type({ name: 'Bob' }, 'Pet')).to be_truthy
  214. end
  215. end
  216. end
  217. 1 describe '#update_params_for_auth!' do
  218. 5 let(:header_params) { {} }
  219. 5 let(:query_params) { {} }
  220. 5 let(:config) { MinatoRubyApiClient::Configuration.new }
  221. 5 let(:api_client) { described_class.new(config) }
  222. 1 context 'when auth method is bearer auth' do
  223. 1 it 'add bearer auth in header' do
  224. 1 api_client.config.access_token = 'access_token'
  225. 1 api_client.update_params_for_auth!(header_params, query_params, ['bearer_auth'])
  226. 1 expect(header_params).to eq({ 'Authorization' => 'Bearer access_token' })
  227. end
  228. end
  229. 1 context 'when auth method is basic auth' do
  230. 1 it 'add basic auth in header' do
  231. 1 api_client.config.username = 'user'
  232. 1 api_client.config.password = 'pass'
  233. 1 basic_auth = Base64.encode64("user:pass").chomp.delete("\n")
  234. 1 api_client.update_params_for_auth!(header_params, query_params, ['basic_auth'])
  235. 1 expect(header_params).to eq({ 'Authorization' => "Basic #{basic_auth}" })
  236. end
  237. end
  238. 1 context 'when auth method is apikey' do
  239. 1 context 'when api_key prefix is not defined' do
  240. 1 it 'add apikey auth in header without prefix' do
  241. 1 api_client.config.api_key['api_key'] = 'key'
  242. 1 api_client.update_params_for_auth!(header_params, query_params, ['apikey'])
  243. 1 expect(header_params).to eq({ 'api_key' => 'key' })
  244. end
  245. end
  246. 1 context 'when api_key prefix is defined' do
  247. 1 it 'add apikey auth in header with prefix' do
  248. 1 api_client.config.api_key['api_key'] = 'key'
  249. 1 api_client.config.api_key_prefix['api_key'] = 'prefix'
  250. 1 api_client.update_params_for_auth!(header_params, query_params, ['apikey'])
  251. 1 expect(header_params).to eq({ 'api_key' => 'prefix key' })
  252. end
  253. end
  254. end
  255. end
  256. end

spec/configuration_spec.rb

100.0% lines covered

119 relevant lines. 119 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 describe MinatoRubyApiClient::Configuration do
  3. 25 let(:url) { 'https://example.com/v1' }
  4. 1 before :each do
  5. 24 require 'uri'
  6. 24 uri = URI.parse(url)
  7. 24 MinatoRubyApiClient.configure do |c|
  8. 24 c.host = uri.host
  9. 24 c.base_path = uri.path
  10. 24 c.api_key['api_key'] = 'test_key'
  11. end
  12. 24 @config = described_class.default
  13. end
  14. 1 describe '#scheme' do
  15. 1 it 'should have the default value https' do
  16. 1 expect(@config.scheme).to eq('https')
  17. end
  18. 1 it 'should remove :// from scheme' do
  19. 1 @config.scheme = 'https://'
  20. 1 expect(@config.scheme).to eq('https')
  21. end
  22. end
  23. 1 describe '#host' do
  24. 1 it 'should do not have the scheme value' do
  25. 1 expect(@config.host).not_to match(/https/)
  26. end
  27. end
  28. 1 describe '#base_path' do
  29. 1 it 'should add leading slashes if missing' do
  30. 1 @config.base_path = 'v1'
  31. 1 expect(@config.base_path).to eq('/v1')
  32. end
  33. 1 it 'should be empty if is /' do
  34. 1 @config.base_path = '/'
  35. 1 expect(@config.base_path).to eq('')
  36. end
  37. end
  38. 1 describe '#user_agent' do
  39. 1 it 'should have user agent' do
  40. 1 @config.user_agent = 'Minato'
  41. 1 expect(@config.user_agent).to eq('Minato')
  42. end
  43. end
  44. 1 describe '#base_url' do
  45. 1 it 'should have the default value' do
  46. 1 expect(@config.base_url).to eq(url)
  47. end
  48. 1 it 'should remove trailing slashes' do
  49. 1 [nil, '', '/', '//'].each do |base_path|
  50. 4 @config.base_path = base_path
  51. 4 expect(@config.base_url).to eq("https://example.com")
  52. end
  53. end
  54. end
  55. 1 describe '#timeout' do
  56. 1 it 'should have the default value' do
  57. 1 expect(@config.timeout).to eq(60)
  58. end
  59. end
  60. 1 describe '#debugging' do
  61. 1 it 'should have the default value falsy' do
  62. 1 expect(@config.debugging).to be_falsy
  63. end
  64. 1 context 'when MINATO_RUBY_API_CLIENT_DEBUG is true' do
  65. 1 it 'should be truthy' do
  66. 1 allow(ENV).to receive(:[]).with('MINATO_RUBY_API_CLIENT_DEBUG').and_return('true')
  67. 1 expect(@config.debugging).to be_truthy
  68. end
  69. end
  70. 1 context 'when debug is active' do
  71. 1 it 'should be truthy' do
  72. 1 config = described_class.new
  73. 1 config.debugging = true
  74. 1 expect(config.debugging).to be_truthy
  75. end
  76. end
  77. end
  78. 1 describe '#auth_settings' do
  79. 1 it 'should have default auth types' do
  80. 1 expect(@config.auth_settings.keys).to eq(%w[bearer_auth basic_auth apikey])
  81. end
  82. end
  83. 1 describe '#basic_auth_token' do
  84. 2 let(:username) { 'test' }
  85. 2 let(:password) { 'test' }
  86. 2 let(:token) { ["#{username}:#{password}"].pack('m').delete("\r\n") }
  87. 1 it 'should return basic auth string' do
  88. 1 @config.username = username
  89. 1 @config.password = password
  90. 1 expect(@config.basic_auth_token).to eq("Basic #{token}")
  91. end
  92. end
  93. 1 describe '#logger' do
  94. 2 let(:logger) { Logger.new(STDOUT) }
  95. 1 context 'when Rails is defined' do
  96. 1 it 'should return Rails.logger' do
  97. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(true)
  98. 1 allow(Rails).to receive(:logger).and_return(logger)
  99. 1 config = described_class.new
  100. 1 expect(config.logger).to eq(Rails.logger)
  101. end
  102. end
  103. 1 context 'when Rails is not defined' do
  104. 1 it 'should return Logger instance' do
  105. 1 allow(Object).to receive(:const_defined?).with(:Rails).and_return(nil)
  106. 1 config = described_class.new
  107. 1 expect(config.logger).to be_instance_of(Logger)
  108. end
  109. end
  110. end
  111. 1 describe '#api_key_with_prefix' do
  112. 1 context 'when api_key_prefix is not defined' do
  113. 1 it 'return the key without prefix' do
  114. 1 expect(@config.api_key_with_prefix('api_key')).to eq('test_key')
  115. end
  116. end
  117. 1 context 'when api_key_prefix is defined' do
  118. 1 it 'return the key with prefix' do
  119. 1 @config.api_key_prefix['api_key'] = 'Prefix'
  120. 1 expect(@config.api_key_with_prefix('api_key')).to eq('Prefix test_key')
  121. end
  122. end
  123. end
  124. 1 describe '#configure' do
  125. 1 context 'when block is given' do
  126. 1 it 'should yield the configuration object' do
  127. 2 expect { |b| @config.configure(&b) }.to yield_with_args
  128. end
  129. end
  130. 1 context 'when block is not given' do
  131. 1 it 'should return nil' do
  132. 1 expect(@config.configure).to be_nil
  133. end
  134. end
  135. end
  136. 1 describe '#use' do
  137. 1 it 'add middleware to the middlewares list' do
  138. 1 @config.use(:test)
  139. 1 expect(@config.instance_variable_get('@middlewares')).to include([:test])
  140. end
  141. end
  142. 1 describe '#request' do
  143. 1 it 'add middleware to the request middlewares list' do
  144. 1 @config.request(:test)
  145. 1 expect(@config.instance_variable_get('@request_middlewares')).to include([:test])
  146. end
  147. end
  148. 1 describe '#response' do
  149. 1 it 'add middleware to the response middlewares list' do
  150. 1 @config.response(:test)
  151. 1 expect(@config.instance_variable_get('@response_middlewares')).to include([:test])
  152. end
  153. end
  154. 1 describe '#configure_middleware' do
  155. 2 let(:conn) { double('conn') }
  156. 2 let(:config) { described_class.new }
  157. 1 before do
  158. 1 config.use(:use)
  159. 1 config.request(:request)
  160. 1 config.response(:response)
  161. 1 allow(conn).to receive(:use)
  162. 1 allow(conn).to receive(:request)
  163. 1 allow(conn).to receive(:response)
  164. 1 config.configure_middleware(conn)
  165. end
  166. 1 it 'add middlewares to connection middleware stack' do
  167. 1 expect(conn).to have_received(:use).once.with(:use)
  168. 1 expect(conn).to have_received(:request).once.with(:request)
  169. 1 expect(conn).to have_received(:response).once.with(:response)
  170. end
  171. end
  172. end

spec/minato_ruby_api_client_spec.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. 1 require 'spec_helper'
  2. 1 describe MinatoRubyApiClient do
  3. 1 describe '.configure' do
  4. 1 context 'when no block is given' do
  5. 1 it 'returns the default configuration' do
  6. 1 expect(MinatoRubyApiClient.configure).to eq MinatoRubyApiClient::Configuration.default
  7. end
  8. end
  9. 1 context 'when a block is given' do
  10. 1 it 'yields the configuration' do
  11. 2 expect { |b| MinatoRubyApiClient.configure(&b) }.to yield_control
  12. end
  13. end
  14. end
  15. end