diff --git a/.gitignore b/.gitignore index 70da2bafc..eea05d17b 100644 --- a/.gitignore +++ b/.gitignore @@ -7,9 +7,9 @@ pkg/* tmp .rvmrc .ruby-version +.yardoc ## BUNDLER -bin *.gem .bundle Gemfile.lock diff --git a/.rspec b/.rspec new file mode 100644 index 000000000..bb697427e --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--require spec_helper +--format documentation +--color \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 5acd953df..1a2470ac7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,17 +1,11 @@ sudo: false language: ruby -script: bundle exec script/test cache: bundler - rvm: - 2.3 - 2.4 - 2.5 - ruby-head -env: - matrix: - - SSL=no - - SSL=yes deploy: provider: rubygems api_key: @@ -21,5 +15,4 @@ deploy: tags: true repo: lostisland/faraday rvm: 2.5 - condition: '"$SSL" = "yes"' diff --git a/Gemfile b/Gemfile index cbcddee4e..989acb943 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,10 @@ gem 'ffi-ncurses', '~> 0.3', :platforms => :jruby gem 'jruby-openssl', '~> 0.8.8', :platforms => :jruby gem 'rake' +group :development, :test do + gem 'pry' +end + group :test do gem 'coveralls', :require => false gem 'em-http-request', '>= 1.1', :require => 'em-http' @@ -22,6 +26,8 @@ group :test do gem 'simplecov' gem 'sinatra', '~> 1.3' gem 'typhoeus', '~> 1.3', :require => 'typhoeus' + gem 'rspec', '~> 3.7' + gem 'webmock', '~> 3.4' end gemspec diff --git a/Rakefile b/Rakefile index 2830fb212..e5a08338e 100644 --- a/Rakefile +++ b/Rakefile @@ -4,5 +4,5 @@ task :default => :test desc "Run all tests" task :test do - exec 'script/test' + exec 'rspec' end diff --git a/bin/console b/bin/console new file mode 100755 index 000000000..bf430ea37 --- /dev/null +++ b/bin/console @@ -0,0 +1,14 @@ +#!/usr/bin/env ruby + +require 'bundler/setup' +require 'faraday' + +# You can add fixtures and/or initialization code here to make experimenting +# with your gem easier. You can also use a different console, if you like. + +# (If you use this, don't forget to add pry to your Gemfile!) +# require "pry" +# Pry.start + +require 'irb' +IRB.start(__FILE__) \ No newline at end of file diff --git a/bin/setup b/bin/setup new file mode 100755 index 000000000..b64275cc4 --- /dev/null +++ b/bin/setup @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' +set -vx + +bundle install + +# Do any other automated setup that you need to do here \ No newline at end of file diff --git a/lib/faraday.rb b/lib/faraday.rb index 19ccc946b..de91fc08a 100644 --- a/lib/faraday.rb +++ b/lib/faraday.rb @@ -17,6 +17,8 @@ # module Faraday VERSION = "0.15.3" + METHODS_WITH_QUERY = %w[get head delete] + METHODS_WITH_BODY = %w[post put patch] class << self # The root path that Faraday is being loaded from. @@ -198,6 +200,13 @@ def register_middleware(autoload_path = nil, mapping = nil) end end + # Unregister a previously registered middleware class. + # + # @param key [Symbol] key for the registered middleware. + def unregister_middleware(key) + @registered_middleware.delete(key) + end + # Lookup middleware class with a registered Symbol shortcut. # # @param key [Symbol] key for the registered middleware. diff --git a/lib/faraday/adapter/patron.rb b/lib/faraday/adapter/patron.rb index 036fff922..d52836cfe 100644 --- a/lib/faraday/adapter/patron.rb +++ b/lib/faraday/adapter/patron.rb @@ -81,7 +81,8 @@ def configure_ssl(session, ssl) private - CURL_TIMEOUT_MESSAGES = [ "Connection time-out", + CURL_TIMEOUT_MESSAGES = [ + "Connection time-out", "Connection timed out", "Timed out before name resolve", "server connect has timed out", diff --git a/lib/faraday/connection.rb b/lib/faraday/connection.rb index d2dee45bc..6f278efca 100644 --- a/lib/faraday/connection.rb +++ b/lib/faraday/connection.rb @@ -160,7 +160,7 @@ def headers=(hash) # @return [Faraday::Response] # @!visibility private - %w[get head delete].each do |method| + METHODS_WITH_QUERY.each do |method| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(url = nil, params = nil, headers = nil) run_request(:#{method}, url, nil, headers) { |request| @@ -170,7 +170,7 @@ def #{method}(url = nil, params = nil, headers = nil) end RUBY end - + # @!method post(url = nil, body = nil, headers = nil) # Makes a POST HTTP request with a body. # @!scope class @@ -217,7 +217,7 @@ def #{method}(url = nil, params = nil, headers = nil) # @return [Faraday::Response] # @!visibility private - %w[post put patch].each do |method| + METHODS_WITH_BODY.each do |method| class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{method}(url = nil, body = nil, headers = nil, &block) run_request(:#{method}, url, body, headers, &block) diff --git a/lib/faraday/error.rb b/lib/faraday/error.rb index 2052a53c7..ec004ceff 100644 --- a/lib/faraday/error.rb +++ b/lib/faraday/error.rb @@ -1,6 +1,7 @@ module Faraday # Faraday error base class. - class Error < StandardError; end + class Error < StandardError; + end # Faraday client error class. class ClientError < Error @@ -45,15 +46,18 @@ def inspect end # A unified client error for failed connections. - class ConnectionFailed < ClientError; end + class ConnectionFailed < ClientError; + end # A 404 error used in the RaiseError middleware # # @see Faraday::Response::RaiseError - class ResourceNotFound < ClientError; end - + class ResourceNotFound < ClientError; + end + # Raised by FaradayMiddleware::ResponseMiddleware - class ParsingError < ClientError; end + class ParsingError < ClientError; + end # A unified client error for timeouts. class TimeoutError < ClientError @@ -69,12 +73,11 @@ class SSLError < ClientError # Exception used to control the Retry middleware. # # @see Faraday::Request::Retry - class RetriableResponse < ClientError; end + class RetriableResponse < ClientError; + end [:ClientError, :ConnectionFailed, :ResourceNotFound, :ParsingError, :TimeoutError, :SSLError, :RetriableResponse].each do |const| Error.const_set(const, Faraday.const_get(const)) end - - end diff --git a/lib/faraday/rack_builder.rb b/lib/faraday/rack_builder.rb index 513ce1275..85cef75b6 100644 --- a/lib/faraday/rack_builder.rb +++ b/lib/faraday/rack_builder.rb @@ -52,8 +52,8 @@ def build(app) end end - def initialize(handlers = []) - @adapter = nil + def initialize(handlers = [], adapter = nil) + @adapter = adapter @handlers = handlers if block_given? build(&Proc.new) @@ -176,11 +176,11 @@ def to_app(inner_app) end def ==(other) - other.is_a?(self.class) && @handlers == other.handlers + other.is_a?(self.class) && @handlers == other.handlers && @adapter == other.adapter end def dup - self.class.new(@handlers.dup) + self.class.new(@handlers.dup, @adapter.dup) end # ENV Keys diff --git a/lib/faraday/utils.rb b/lib/faraday/utils.rb index 344b58609..52a610a9b 100644 --- a/lib/faraday/utils.rb +++ b/lib/faraday/utils.rb @@ -1,197 +1,12 @@ require 'thread' +require_relative 'utils/headers' +require_relative 'utils/params_hash' + module Faraday module Utils extend self - # A case-insensitive Hash that preserves the original case of a header - # when set. - # - # Adapted from [Rack::Utils::HeaderHash](https://www.rubydoc.info/gems/rack/Rack/Utils/HeaderHash). - class Headers < ::Hash - def self.from(value) - new(value) - end - - def self.allocate - new_self = super - new_self.initialize_names - new_self - end - - def initialize(hash = nil) - super() - @names = {} - self.update(hash || {}) - end - - def initialize_names - @names = {} - end - - # on dup/clone, we need to duplicate @names hash - def initialize_copy(other) - super - @names = other.names.dup - end - - # need to synchronize concurrent writes to the shared KeyMap - keymap_mutex = Mutex.new - - # symbol -> string mapper + cache - KeyMap = Hash.new do |map, key| - value = if key.respond_to?(:to_str) - key - else - key.to_s.split('_'). # :user_agent => %w(user agent) - each { |w| w.capitalize! }. # => %w(User Agent) - join('-') # => "User-Agent" - end - keymap_mutex.synchronize { map[key] = value } - end - KeyMap[:etag] = "ETag" - - def [](k) - k = KeyMap[k] - super(k) || super(@names[k.downcase]) - end - - def []=(k, v) - k = KeyMap[k] - k = (@names[k.downcase] ||= k) - # join multiple values with a comma - v = v.to_ary.join(', ') if v.respond_to? :to_ary - super(k, v) - end - - def fetch(k, *args, &block) - k = KeyMap[k] - key = @names.fetch(k.downcase, k) - super(key, *args, &block) - end - - def delete(k) - k = KeyMap[k] - if k = @names[k.downcase] - @names.delete k.downcase - super(k) - end - end - - def include?(k) - @names.include? k.downcase - end - - alias_method :has_key?, :include? - alias_method :member?, :include? - alias_method :key?, :include? - - def merge!(other) - other.each { |k, v| self[k] = v } - self - end - alias_method :update, :merge! - - def merge(other) - hash = dup - hash.merge! other - end - - def replace(other) - clear - @names.clear - self.update other - self - end - - def to_hash() ::Hash.new.update(self) end - - def parse(header_string) - return unless header_string && !header_string.empty? - - headers = header_string.split(/\r\n/) - - # Find the last set of response headers. - start_index = headers.rindex { |x| x.match(/^HTTP\//) } || 0 - last_response = headers.slice(start_index, headers.size) - - last_response. - tap { |a| a.shift if a.first.index('HTTP/') == 0 }. # drop the HTTP status line - map { |h| h.split(/:\s*/, 2) }.reject { |p| p[0].nil? }. # split key and value, ignore blank lines - each { |key, value| - # join multiple values with a comma - if self[key] - self[key] << ', ' << value - else - self[key] = value - end - } - end - - protected - - def names - @names - end - end - - # A hash with stringified keys. - class ParamsHash < Hash - def [](key) - super(convert_key(key)) - end - - def []=(key, value) - super(convert_key(key), value) - end - - def delete(key) - super(convert_key(key)) - end - - def include?(key) - super(convert_key(key)) - end - - alias_method :has_key?, :include? - alias_method :member?, :include? - alias_method :key?, :include? - - def update(params) - params.each do |key, value| - self[key] = value - end - self - end - alias_method :merge!, :update - - def merge(params) - dup.update(params) - end - - def replace(other) - clear - update(other) - end - - def merge_query(query, encoder = nil) - if query && !query.empty? - update((encoder || Utils.default_params_encoder).decode(query)) - end - self - end - - def to_query(encoder = nil) - (encoder || Utils.default_params_encoder).encode(self) - end - - private - - def convert_key(key) - key.to_s - end - end - def build_query(params) FlatParamsEncoder.encode(params) end @@ -229,43 +44,6 @@ class << self attr_writer :default_params_encoder end - # Stolen from Rack - def normalize_params(params, name, v = nil) - name =~ %r(\A[\[\]]*([^\[\]]+)\]*) - k = $1 || '' - after = $' || '' - - return if k.empty? - - if after == "" - if params[k] - params[k] = Array[params[k]] unless params[k].kind_of?(Array) - params[k] << v - else - params[k] = v - end - elsif after == "[]" - params[k] ||= [] - raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) - params[k] << v - elsif after =~ %r(^\[\]\[([^\[\]]+)\]$) || after =~ %r(^\[\](.+)$) - child_key = $1 - params[k] ||= [] - raise TypeError, "expected Array (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Array) - if params[k].last.is_a?(Hash) && !params[k].last.key?(child_key) - normalize_params(params[k].last, child_key, v) - else - params[k] << normalize_params({}, child_key, v) - end - else - params[k] ||= {} - raise TypeError, "expected Hash (got #{params[k].class.name}) for param `#{k}'" unless params[k].is_a?(Hash) - params[k] = normalize_params(params[k], after, v) - end - - return params - end - # Normalize URI() behavior across Ruby versions # # url - A String or URI. diff --git a/lib/faraday/utils/headers.rb b/lib/faraday/utils/headers.rb new file mode 100644 index 000000000..4c22dd7ef --- /dev/null +++ b/lib/faraday/utils/headers.rb @@ -0,0 +1,137 @@ +module Faraday + module Utils + # A case-insensitive Hash that preserves the original case of a header + # when set. + # + # Adapted from Rack::Utils::HeaderHash + class Headers < ::Hash + def self.from(value) + new(value) + end + + def self.allocate + new_self = super + new_self.initialize_names + new_self + end + + def initialize(hash = nil) + super() + @names = {} + self.update(hash || {}) + end + + def initialize_names + @names = {} + end + + # on dup/clone, we need to duplicate @names hash + def initialize_copy(other) + super + @names = other.names.dup + end + + # need to synchronize concurrent writes to the shared KeyMap + keymap_mutex = Mutex.new + + # symbol -> string mapper + cache + KeyMap = Hash.new do |map, key| + value = if key.respond_to?(:to_str) + key + else + key.to_s.split('_') # :user_agent => %w(user agent) + .each { |w| w.capitalize! } # => %w(User Agent) + .join('-') # => "User-Agent" + end + keymap_mutex.synchronize { map[key] = value } + end + KeyMap[:etag] = "ETag" + + def [](k) + k = KeyMap[k] + super(k) || super(@names[k.downcase]) + end + + def []=(k, v) + k = KeyMap[k] + k = (@names[k.downcase] ||= k) + # join multiple values with a comma + v = v.to_ary.join(', ') if v.respond_to? :to_ary + super(k, v) + end + + def fetch(k, *args, &block) + k = KeyMap[k] + key = @names.fetch(k.downcase, k) + super(key, *args, &block) + end + + def delete(k) + k = KeyMap[k] + if (k = @names[k.downcase]) + @names.delete k.downcase + super(k) + end + end + + def include?(k) + @names.include? k.downcase + end + + alias_method :has_key?, :include? + alias_method :member?, :include? + alias_method :key?, :include? + + def merge!(other) + other.each { |k, v| self[k] = v } + self + end + + alias_method :update, :merge! + + def merge(other) + hash = dup + hash.merge! other + end + + def replace(other) + clear + @names.clear + self.update other + self + end + + def to_hash + ::Hash.new.update(self) + end + + def parse(header_string) + return unless header_string && !header_string.empty? + + headers = header_string.split(/\r\n/) + + # Find the last set of response headers. + start_index = headers.rindex { |x| x.match(/^HTTP\//) } || 0 + last_response = headers.slice(start_index, headers.size) + + last_response + .tap { |a| a.shift if a.first.index('HTTP/') == 0 } # drop the HTTP status line + .map { |h| h.split(/:\s*/, 2) } # split key and value + .reject { |p| p[0].nil? } # ignore blank lines + .each { |key, value| add_parsed(key, value) } # join multiple values with a comma + end + + protected + + def names + @names + end + + private + + def add_parsed(key, value) + self[key] ? self[key] << ', ' << value : self[key] = value + end + end + end +end diff --git a/lib/faraday/utils/params_hash.rb b/lib/faraday/utils/params_hash.rb new file mode 100644 index 000000000..8e6150800 --- /dev/null +++ b/lib/faraday/utils/params_hash.rb @@ -0,0 +1,60 @@ +module Faraday + module Utils + # A hash with stringified keys. + class ParamsHash < Hash + def [](key) + super(convert_key(key)) + end + + def []=(key, value) + super(convert_key(key), value) + end + + def delete(key) + super(convert_key(key)) + end + + def include?(key) + super(convert_key(key)) + end + + alias_method :has_key?, :include? + alias_method :member?, :include? + alias_method :key?, :include? + + def update(params) + params.each do |key, value| + self[key] = value + end + self + end + alias_method :merge!, :update + + def merge(params) + dup.update(params) + end + + def replace(other) + clear + update(other) + end + + def merge_query(query, encoder = nil) + if query && !query.empty? + update((encoder || Utils.default_params_encoder).decode(query)) + end + self + end + + def to_query(encoder = nil) + (encoder || Utils.default_params_encoder).encode(self) + end + + private + + def convert_key(key) + key.to_s + end + end + end +end diff --git a/spec/faraday/adapter/excon_spec.rb b/spec/faraday/adapter/excon_spec.rb new file mode 100644 index 000000000..f3faed49e --- /dev/null +++ b/spec/faraday/adapter/excon_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe Faraday::Adapter::Excon do + features :body_on_get, :reason_phrase_parse + + it_behaves_like 'an adapter' + + it 'allows to provide adapter specific configs' do + url = URI('https://example.com:1234') + + adapter = Faraday::Adapter::Excon.new(nil, debug_request: true) + + conn = adapter.create_connection({ url: url }, {}) + + expect(conn.data[:debug_request]).to be_truthy + end +end \ No newline at end of file diff --git a/spec/faraday/adapter/httpclient_spec.rb b/spec/faraday/adapter/httpclient_spec.rb new file mode 100644 index 000000000..3167af3b7 --- /dev/null +++ b/spec/faraday/adapter/httpclient_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe Faraday::Adapter::HTTPClient do + features :body_on_get, :reason_phrase_parse, :compression + + it_behaves_like 'an adapter' + + it 'allows to provide adapter specific configs' do + adapter = Faraday::Adapter::HTTPClient.new do |client| + client.keep_alive_timeout = 20 + client.ssl_config.timeout = 25 + end + + client = adapter.client + adapter.configure_client + + expect(client.keep_alive_timeout).to eq(20) + expect(client.ssl_config.timeout).to eq(25) + end + + it 'binds local socket' do + stub_request(:get, 'http://example.com') + + host = '1.2.3.4' + port = 1234 + conn = Faraday.new('http://example.com', request: { bind: { host: host, port: port } }) do |f| + f.adapter :httpclient + end + + conn.get('/') + + expect(conn.options[:bind][:host]).to eq(host) + expect(conn.options[:bind][:port]).to eq(port) + end +end \ No newline at end of file diff --git a/spec/faraday/adapter/net_http_persistent_spec.rb b/spec/faraday/adapter/net_http_persistent_spec.rb new file mode 100644 index 000000000..6a702185d --- /dev/null +++ b/spec/faraday/adapter/net_http_persistent_spec.rb @@ -0,0 +1,41 @@ +RSpec.describe Faraday::Adapter::NetHttpPersistent do + features :body_on_get, :reason_phrase_parse, :compression + + it_behaves_like 'an adapter' + + it 'allows to provide adapter specific configs' do + url = URI('https://example.com') + + adapter = Faraday::Adapter::NetHttpPersistent.new do |http| + http.idle_timeout = 123 + end + + http = adapter.send(:net_http_connection, url: url, request: {}) + adapter.send(:configure_request, http, {}) + + expect(http.idle_timeout).to eq(123) + end + + it 'sets max_retries to 0' do + url = URI('http://example.com') + + adapter = Faraday::Adapter::NetHttpPersistent.new + + http = adapter.send(:net_http_connection, url: url, request: {}) + adapter.send(:configure_request, http, {}) + + # `max_retries=` is only present in Ruby 2.5 + expect(http.max_retries).to eq(0) if http.respond_to?(:max_retries=) + end + + it 'allows to set pool_size on initialize' do + url = URI('https://example.com') + + adapter = Faraday::Adapter::NetHttpPersistent.new(nil, { pool_size: 5 }) + + http = adapter.send(:net_http_connection, url: url, request: {}) + + # `pool` is only present in net_http_persistent >= 3.0 + expect(http.pool.size).to eq(5) if http.respond_to?(:pool) + end +end diff --git a/spec/faraday/adapter/net_http_spec.rb b/spec/faraday/adapter/net_http_spec.rb new file mode 100644 index 000000000..df6d129e9 --- /dev/null +++ b/spec/faraday/adapter/net_http_spec.rb @@ -0,0 +1,48 @@ +RSpec.describe Faraday::Adapter::NetHttp do + features :body_on_get, :reason_phrase_parse, :compression, :streaming + + it_behaves_like 'an adapter' + + context 'checking http' do + let(:url) { URI('http://example.com') } + let(:adapter) { Faraday::Adapter::NetHttp.new } + let(:http) { adapter.send(:net_http_connection, url: url, request: {}) } + + it { expect(http.port).to eq(80) } + it 'sets max_retries to 0' do + adapter.send(:configure_request, http, {}) + + expect(http.max_retries).to eq(0) if http.respond_to?(:max_retries=) + end + it 'supports write_timeout' do + adapter.send(:configure_request, http, { write_timeout: 10 }) + + expect(http.write_timeout).to eq(10) if http.respond_to?(:write_timeout=) + end + + context 'with https url' do + let(:url) { URI('https://example.com') } + + it { expect(http.port).to eq(443) } + end + + context 'with http url including port' do + let(:url) { URI('https://example.com:1234') } + + it { expect(http.port).to eq(1234) } + end + + context 'with custom adapter config' do + let(:adapter) do + Faraday::Adapter::NetHttp.new do |http| + http.continue_timeout = 123 + end + end + + it do + adapter.send(:configure_request, http, {}) + expect(http.continue_timeout).to eq(123) + end + end + end +end \ No newline at end of file diff --git a/spec/faraday/adapter/patron_spec.rb b/spec/faraday/adapter/patron_spec.rb new file mode 100644 index 000000000..e5bc00651 --- /dev/null +++ b/spec/faraday/adapter/patron_spec.rb @@ -0,0 +1,16 @@ +RSpec.describe Faraday::Adapter::Patron do + features :reason_phrase_parse + + it_behaves_like 'an adapter' + + it 'allows to provide adapter specific configs' do + conn = Faraday.new do |f| + f.adapter :patron do |session| + session.max_redirects = 10 + raise RuntimeError, 'Configuration block called' + end + end + + expect { conn.get('/') }.to raise_error(RuntimeError, 'Configuration block called') + end +end \ No newline at end of file diff --git a/spec/faraday/adapter/typhoeus_spec.rb b/spec/faraday/adapter/typhoeus_spec.rb new file mode 100644 index 000000000..c62b527eb --- /dev/null +++ b/spec/faraday/adapter/typhoeus_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Faraday::Adapter::Typhoeus do + features :body_on_get, :parallel + + it_behaves_like 'an adapter' +end \ No newline at end of file diff --git a/spec/faraday/composite_read_io_spec.rb b/spec/faraday/composite_read_io_spec.rb new file mode 100644 index 000000000..790febc79 --- /dev/null +++ b/spec/faraday/composite_read_io_spec.rb @@ -0,0 +1,78 @@ +require 'stringio' + +RSpec.describe Faraday::CompositeReadIO do + Part = Struct.new(:to_io) do + def length + to_io.string.length + end + end + + def part(str) + Part.new StringIO.new(str) + end + + def composite_io(*parts) + Faraday::CompositeReadIO.new(*parts) + end + + context 'with empty composite_io' do + subject { composite_io } + + it { expect(subject.length).to eq(0) } + it { expect(subject.read).to eq('') } + it { expect(subject.read(1)).to be_nil } + end + + context 'with empty parts' do + subject { composite_io(part(''), part('')) } + + it { expect(subject.length).to eq(0) } + it { expect(subject.read).to eq('') } + it { expect(subject.read(1)).to be_nil } + end + + context 'with 2 parts' do + subject { composite_io(part('abcd'), part('1234')) } + + it { expect(subject.length).to eq(8) } + it { expect(subject.read).to eq('abcd1234') } + it 'allows to read in chunks' do + expect(subject.read(3)).to eq('abc') + expect(subject.read(3)).to eq('d12') + expect(subject.read(3)).to eq('34') + expect(subject.read(3)).to be_nil + end + it 'allows to rewind while reading in chunks' do + expect(subject.read(3)).to eq('abc') + expect(subject.read(3)).to eq('d12') + subject.rewind + expect(subject.read(3)).to eq('abc') + expect(subject.read(5)).to eq('d1234') + expect(subject.read(3)).to be_nil + subject.rewind + expect(subject.read(2)).to eq('ab') + end + end + + context 'with mix of empty and non-empty parts' do + subject { composite_io(part(''), part('abcd'), part(''), part('1234'), part('')) } + + it 'allows to read in chunks' do + expect(subject.read(6)).to eq('abcd12') + expect(subject.read(6)).to eq('34') + expect(subject.read(6)).to be_nil + end + end + + context 'with utf8 multibyte part' do + subject { composite_io(part("\x86"), part("ファイル")) } + + it { expect(subject.read).to eq("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB".force_encoding("BINARY"))} + it 'allows to read in chunks' do + expect(subject.read(3)).to eq("\x86\xE3\x83".force_encoding("BINARY")) + expect(subject.read(3)).to eq("\x95\xE3\x82".force_encoding("BINARY")) + expect(subject.read(8)).to eq("\xA1\xE3\x82\xA4\xE3\x83\xAB".force_encoding("BINARY")) + expect(subject.read(3)).to be_nil + end + end +end \ No newline at end of file diff --git a/spec/faraday/connection_spec.rb b/spec/faraday/connection_spec.rb new file mode 100644 index 000000000..6415fb10c --- /dev/null +++ b/spec/faraday/connection_spec.rb @@ -0,0 +1,645 @@ +shared_examples 'initializer with url' do + context 'with simple url' do + let(:address) { 'http://sushi.com' } + + it { expect(subject.host).to eq('sushi.com') } + it { expect(subject.port).to eq(80) } + it { expect(subject.scheme).to eq('http') } + it { expect(subject.path_prefix).to eq('/') } + it { expect(subject.params).to eq({}) } + end + + context 'with complex url' do + let(:address) { 'http://sushi.com:815/fish?a=1' } + + it { expect(subject.port).to eq(815) } + it { expect(subject.path_prefix).to eq('/fish') } + it { expect(subject.params).to eq({ 'a' => '1' }) } + end +end + +shared_examples 'default connection options' do + after { Faraday.default_connection_options = nil } + + it 'works with implicit url' do + conn = Faraday.new 'http://sushi.com/foo' + expect(conn.options.timeout).to eq(10) + end + + it 'works with option url' do + conn = Faraday.new :url => 'http://sushi.com/foo' + expect(conn.options.timeout).to eq(10) + end + + it 'works with instance connection options' do + conn = Faraday.new 'http://sushi.com/foo', request: { open_timeout: 1 } + expect(conn.options.timeout).to eq(10) + expect(conn.options.open_timeout).to eq(1) + end + + it 'default connection options persist with an instance overriding' do + conn = Faraday.new 'http://nigiri.com/bar' + conn.options.timeout = 1 + expect(Faraday.default_connection_options.request.timeout).to eq(10) + + other = Faraday.new :url => 'https://sushi.com/foo' + other.options.timeout = 1 + + expect(Faraday.default_connection_options.request.timeout).to eq(10) + end + + it 'default connection uses default connection options' do + expect(Faraday.default_connection.options.timeout).to eq(10) + end +end + +RSpec.describe Faraday::Connection do + let(:conn) { Faraday::Connection.new(url, options) } + let(:url) { nil } + let(:options) { nil } + + describe '.new' do + subject { conn } + + context 'with implicit url param' do + # Faraday::Connection.new('http://sushi.com') + let(:url) { address } + + it_behaves_like 'initializer with url' + end + + context 'with explicit url param' do + # Faraday::Connection.new(url: 'http://sushi.com') + let(:url) { { url: address } } + + it_behaves_like 'initializer with url' + end + + context 'with custom builder' do + let(:custom_builder) { Faraday::RackBuilder.new } + let(:options) { { builder: custom_builder } } + + it { expect(subject.builder).to eq(custom_builder) } + end + + context 'with custom params' do + let(:options) { { params: { a: 1 } } } + + it { expect(subject.params).to eq({ 'a' => 1 }) } + end + + context 'with custom params and params in url' do + let(:url) { 'http://sushi.com/fish?a=1&b=2' } + let(:options) { { params: { a: 3 } } } + it { expect(subject.params).to eq({ 'a' => 3, 'b' => '2' }) } + end + + context 'with custom headers' do + let(:options) { { headers: { user_agent: 'Faraday' } } } + + it { expect(subject.headers['User-agent']).to eq('Faraday') } + end + + context 'with ssl false' do + let(:options) { { ssl: { verify: false } } } + + it { expect(subject.ssl.verify?).to be_falsey } + end + + context 'with empty block' do + let(:conn) { Faraday::Connection.new {} } + + it { expect(conn.builder.handlers.size).to eq(0) } + end + + context 'with block' do + let(:conn) do + Faraday::Connection.new(params: { 'a' => '1' }) do |faraday| + faraday.adapter :net_http + faraday.url_prefix = 'http://sushi.com/omnom' + end + end + + it { expect(conn.builder.handlers.size).to eq(0) } + it { expect(conn.path_prefix).to eq('/omnom') } + end + end + + describe 'basic_auth' do + subject { conn } + + context 'calling the #basic_auth method' do + before { subject.basic_auth 'Aladdin', 'open sesame' } + + it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } + end + + context 'adding basic auth info to url' do + let(:url) { 'http://Aladdin:open%20sesame@sushi.com/fish' } + + it { expect(subject.headers['Authorization']).to eq('Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==') } + end + end + + describe '#token_auth' do + before { subject.token_auth('abcdef', nonce: 'abc') } + + it { expect(subject.headers['Authorization']).to eq('Token nonce="abc", token="abcdef"') } + end + + describe '#build_exclusive_url' do + context 'with relative path' do + subject { conn.build_exclusive_url('sake.html') } + + it 'uses connection host as default host' do + conn.host = 'sushi.com' + expect(subject.host).to eq('sushi.com') + expect(subject.scheme).to eq('http') + end + + it do + conn.path_prefix = '/fish' + expect(subject.path).to eq('/fish/sake.html') + end + + it do + conn.path_prefix = '/' + expect(subject.path).to eq('/sake.html') + end + + it do + conn.path_prefix = 'fish' + expect(subject.path).to eq('/fish/sake.html') + end + + it do + conn.path_prefix = '/fish/' + expect(subject.path).to eq('/fish/sake.html') + end + end + + context 'with absolute path' do + subject { conn.build_exclusive_url('/sake.html') } + + after { expect(subject.path).to eq('/sake.html') } + + it { conn.path_prefix = '/fish' } + it { conn.path_prefix = '/' } + it { conn.path_prefix = 'fish' } + it { conn.path_prefix = '/fish/' } + end + + context 'with complete url' do + subject { conn.build_exclusive_url('http://sushi.com/sake.html?a=1') } + + it { expect(subject.scheme).to eq('http') } + it { expect(subject.host).to eq('sushi.com') } + it { expect(subject.port).to eq(80) } + it { expect(subject.path).to eq('/sake.html') } + it { expect(subject.query).to eq('a=1') } + end + + it 'overrides connection port for absolute url' do + conn.port = 23 + uri = conn.build_exclusive_url('http://sushi.com') + expect(uri.port).to eq(80) + end + + it 'does not add ending slash given nil url' do + conn.url_prefix = 'http://sushi.com/nigiri' + uri = conn.build_exclusive_url + expect(uri.path).to eq('/nigiri') + end + + it 'does not add ending slash given empty url' do + conn.url_prefix = 'http://sushi.com/nigiri' + uri = conn.build_exclusive_url('') + expect(uri.path).to eq('/nigiri') + end + + it 'does not use connection params' do + conn.url_prefix = 'http://sushi.com/nigiri' + conn.params = { :a => 1 } + expect(conn.build_exclusive_url.to_s).to eq('http://sushi.com/nigiri') + end + + it 'allows to provide params argument' do + conn.url_prefix = 'http://sushi.com/nigiri' + conn.params = { :a => 1 } + params = Faraday::Utils::ParamsHash.new + params[:a] = 2 + uri = conn.build_exclusive_url(nil, params) + expect(uri.to_s).to eq('http://sushi.com/nigiri?a=2') + end + + it 'handles uri instances' do + uri = conn.build_exclusive_url(URI('/sake.html')) + expect(uri.path).to eq('/sake.html') + end + + context 'with url_prefixed connection' do + let(:url) { 'http://sushi.com/sushi/' } + + it 'parses url and changes scheme' do + conn.scheme = 'https' + uri = conn.build_exclusive_url('sake.html') + expect(uri.to_s).to eq('https://sushi.com/sushi/sake.html') + end + + it 'joins url to base with ending slash' do + uri = conn.build_exclusive_url('sake.html') + expect(uri.to_s).to eq('http://sushi.com/sushi/sake.html') + end + + it 'used default base with ending slash' do + uri = conn.build_exclusive_url + expect(uri.to_s).to eq('http://sushi.com/sushi/') + end + + it 'overrides base' do + uri = conn.build_exclusive_url('/sake/') + expect(uri.to_s).to eq('http://sushi.com/sake/') + end + end + end + + describe '#build_url' do + let(:url) { 'http://sushi.com/nigiri' } + + it 'uses params' do + conn.params = { a: 1, b: 1 } + expect(conn.build_url.to_s).to eq('http://sushi.com/nigiri?a=1&b=1') + end + + it 'merges params' do + conn.params = { a: 1, b: 1 } + url = conn.build_url(nil, b: 2, c: 3) + expect(url.to_s).to eq('http://sushi.com/nigiri?a=1&b=2&c=3') + end + end + + describe '#build_request' do + let(:url) { 'https://asushi.com/sake.html' } + let(:request) { conn.build_request(:get) } + + before do + conn.headers = { 'Authorization' => 'token abc123' } + request.headers.delete('Authorization') + end + + it { expect(conn.headers.keys).to eq(['Authorization']) } + it { expect(conn.headers.include?('Authorization')).to be_truthy } + it { expect(request.headers.keys).to be_empty } + it { expect(request.headers.include?('Authorization')).to be_falsey } + end + + describe '#to_env' do + subject { conn.build_request(:get).to_env(conn).url } + + let(:url) { 'http://sushi.com/sake.html' } + let(:options) { { params: @params } } + + it 'parses url params into query' do + @params = { 'a[b]' => '1 + 2' } + expect(subject.query).to eq('a%5Bb%5D=1+%2B+2') + end + + it 'escapes per spec' do + @params = { 'a' => '1+2 foo~bar.-baz' } + expect(subject.query).to eq('a=1%2B2+foo~bar.-baz') + end + + it 'bracketizes nested params in query' do + @params = { 'a' => { 'b' => 'c' } } + expect(subject.query).to eq('a%5Bb%5D=c') + end + + it 'bracketizes repeated params in query' do + @params = { 'a' => [1, 2] } + expect(subject.query).to eq('a%5B%5D=1&a%5B%5D=2') + end + + it 'without braketizing repeated params in query' do + @params = { 'a' => [1, 2] } + conn.options.params_encoder = Faraday::FlatParamsEncoder + expect(subject.query).to eq('a=1&a=2') + end + end + + describe 'proxy support' do + it 'accepts string' do + with_env 'http_proxy' => 'http://proxy.com:80' do + conn.proxy = 'http://proxy.com' + expect(conn.proxy.host).to eq('proxy.com') + end + end + + it 'accepts uri' do + with_env 'http_proxy' => 'http://proxy.com:80' do + conn.proxy = URI.parse('http://proxy.com') + expect(conn.proxy.host).to eq('proxy.com') + end + end + + it 'accepts hash with string uri' do + with_env 'http_proxy' => 'http://proxy.com:80' do + conn.proxy = { :uri => 'http://proxy.com', :user => 'rick' } + expect(conn.proxy.host).to eq('proxy.com') + expect(conn.proxy.user).to eq('rick') + end + end + + it 'accepts hash' do + with_env 'http_proxy' => 'http://proxy.com:80' do + conn.proxy = { :uri => URI.parse('http://proxy.com'), :user => 'rick' } + expect(conn.proxy.host).to eq('proxy.com') + expect(conn.proxy.user).to eq('rick') + end + end + + it 'accepts http env' do + with_env 'http_proxy' => 'http://proxy.com:80' do + expect(conn.proxy.host).to eq('proxy.com') + end + end + + it 'accepts http env with auth' do + with_env 'http_proxy' => 'http://a%40b:my%20pass@proxy.com:80' do + expect(conn.proxy.user).to eq('a@b') + expect(conn.proxy.password).to eq('my pass') + end + end + + it 'accepts env without scheme' do + with_env 'http_proxy' => 'localhost:8888' do + uri = conn.proxy[:uri] + expect(uri.host).to eq('localhost') + expect(uri.port).to eq(8888) + end + end + + it 'fetches no proxy from nil env' do + with_env 'http_proxy' => nil do + expect(conn.proxy).to be_nil + end + end + + it 'fetches no proxy from blank env' do + with_env 'http_proxy' => '' do + expect(conn.proxy).to be_nil + end + end + + it 'does not accept uppercase env' do + with_env 'HTTP_PROXY' => 'http://localhost:8888/' do + expect(conn.proxy).to be_nil + end + end + + it 'allows when url in no proxy list' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do + conn = Faraday::Connection.new('http://example.com') + expect(conn.proxy).to be_nil + end + end + + it 'allows when prefixed url is not in no proxy list' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do + conn = Faraday::Connection.new('http://prefixedexample.com') + expect(conn.proxy.host).to eq('proxy.com') + end + end + + it 'allows when subdomain url is in no proxy list' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do + conn = Faraday::Connection.new('http://subdomain.example.com') + expect(conn.proxy).to be_nil + end + end + + it 'allows when url not in no proxy list' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example2.com' do + conn = Faraday::Connection.new('http://example.com') + expect(conn.proxy.host).to eq('proxy.com') + end + end + + it 'allows when ip address is not in no proxy list but url is' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'localhost' do + conn = Faraday::Connection.new('http://127.0.0.1') + expect(conn.proxy).to be_nil + end + end + + it 'allows when url is not in no proxy list but ip address is' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => '127.0.0.1' do + conn = Faraday::Connection.new('http://localhost') + expect(conn.proxy).to be_nil + end + end + + it 'allows in multi element no proxy list' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example0.com,example.com,example1.com' do + expect(Faraday::Connection.new('http://example0.com').proxy).to be_nil + expect(Faraday::Connection.new('http://example.com').proxy).to be_nil + expect(Faraday::Connection.new('http://example1.com').proxy).to be_nil + expect(Faraday::Connection.new('http://example2.com').proxy.host).to eq('proxy.com') + end + end + + it 'test proxy requires uri' do + expect { conn.proxy = { uri: :bad_uri, user: 'rick' } }.to raise_error(ArgumentError) + end + + it 'gives priority to manually set proxy' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do + conn = Faraday.new + conn.proxy = 'http://proxy2.com' + + expect(conn.instance_variable_get('@manual_proxy')).to be_truthy + expect(conn.proxy_for_request('https://google.co.uk').host).to eq('proxy2.com') + end + end + + it 'ignores env proxy if set that way' do + with_env_proxy_disabled do + with_env 'http_proxy' => 'http://duncan.proxy.com:80' do + expect(conn.proxy).to be_nil + end + end + end + + context 'performing a request' do + before { stub_request(:get, 'http://example.com') } + + it 'dynamically checks proxy' do + with_env 'http_proxy' => 'http://proxy.com:80' do + conn = Faraday.new + conn.get('http://example.com') + expect(conn.instance_variable_get('@temp_proxy').host).to eq('proxy.com') + end + + conn.get('http://example.com') + expect(conn.instance_variable_get('@temp_proxy')).to be_nil + end + + it 'dynamically check no proxy' do + with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do + conn = Faraday.new + + expect(conn.instance_variable_get('@temp_proxy').host).to eq('proxy.com') + conn.get('http://example.com') + expect(conn.instance_variable_get('@temp_proxy')).to be_nil + end + end + end + end + + describe '#dup' do + subject { conn.dup } + + let(:url) { 'http://sushi.com/foo' } + let(:options) do + { + ssl: { verify: :none }, + headers: { 'content-type' => 'text/plain' }, + params: { 'a' => '1' }, + request: { timeout: 5 } + } + end + + it { expect(subject.build_exclusive_url).to eq(conn.build_exclusive_url) } + it { expect(subject.headers['content-type']).to eq('text/plain') } + it { expect(subject.params['a']).to eq('1') } + + context 'after manual changes' do + before do + subject.basic_auth('', '') + subject.headers['content-length'] = 12 + subject.params['b'] = '2' + subject.options[:open_timeout] = 10 + end + + it { expect(subject.builder.handlers.size).to eq(1) } + it { expect(conn.builder.handlers.size).to eq(1) } + it { expect(conn.headers.key?('content-length')).to be_falsey } + it { expect(conn.params.key?('b')).to be_falsey } + it { expect(subject.options[:timeout]).to eq(5) } + it { expect(conn.options[:open_timeout]).to be_nil } + end + end + + describe '#respond_to?' do + it { expect(Faraday.respond_to?(:get)).to be_truthy } + it { expect(Faraday.respond_to?(:post)).to be_truthy } + end + + describe 'default_connection_options' do + context 'assigning a default value' do + before do + Faraday.default_connection_options = nil + Faraday.default_connection_options.request.timeout = 10 + end + + it_behaves_like 'default connection options' + end + + context 'assigning a hash' do + before { Faraday.default_connection_options = { request: { timeout: 10 } } } + + it_behaves_like 'default connection options' + end + end + + describe 'request params' do + context 'with simple url' do + let(:url) { 'http://example.com' } + let!(:stubbed) { stub_request(:get, 'http://example.com?a=a&p=3') } + + after { expect(stubbed).to have_been_made.once } + + it 'test_overrides_request_params' do + conn.get('?p=2&a=a', p: 3) + end + + it 'test_overrides_request_params_block' do + conn.get('?p=1&a=a', p: 2) do |req| + req.params[:p] = 3 + end + end + + it 'test_overrides_request_params_block_url' do + conn.get(nil, p: 2) do |req| + req.url('?p=1&a=a', 'p' => 3) + end + end + end + + context 'with url and extra params' do + let(:url) { 'http://example.com?a=1&b=2' } + let(:options) { { params: { c: 3 } } } + + it 'merges connection and request params' do + stubbed = stub_request(:get, 'http://example.com?a=1&b=2&c=3&limit=5&page=1') + conn.get('?page=1', limit: 5) + expect(stubbed).to have_been_made.once + end + + it 'allows to override all params' do + stubbed = stub_request(:get, 'http://example.com?b=b') + conn.get('?p=1&a=a', p: 2) do |req| + expect(req.params[:a]).to eq('a') + expect(req.params['c']).to eq(3) + expect(req.params['p']).to eq(2) + req.params = { :b => 'b' } + expect(req.params['b']).to eq('b') + end + expect(stubbed).to have_been_made.once + end + + it 'allows to set params_encoder for single request' do + encoder = Object.new + def encoder.encode(params) + params.map { |k,v| "#{k.upcase}-#{v.to_s.upcase}" }.join(',') + end + stubbed = stub_request(:get, 'http://example.com/?A-1,B-2,C-3,FEELING-BLUE') + + conn.get('/', feeling: 'blue') do |req| + req.options.params_encoder = encoder + end + expect(stubbed).to have_been_made.once + end + end + + context 'with default params encoder' do + let!(:stubbed) { stub_request(:get, 'http://example.com?color%5B%5D=red&color%5B%5D=blue') } + after { expect(stubbed).to have_been_made.once } + + it 'supports array params in url' do + conn.get('http://example.com?color[]=red&color[]=blue') + end + + it 'supports array params in params' do + conn.get('http://example.com', { color: %w(red blue) }) + end + end + + context 'with flat params encoder' do + let(:options) { { request: { params_encoder: Faraday::FlatParamsEncoder } } } + let!(:stubbed) { stub_request(:get, 'http://example.com?color=blue') } + after { expect(stubbed).to have_been_made.once } + + it 'supports array params in params' do + conn.get('http://example.com', { color: %w(red blue) }) + end + + context 'with array param in url' do + let(:url) { 'http://example.com?color[]=red&color[]=blue' } + + it do + conn.get('/') + end + end + end + end +end \ No newline at end of file diff --git a/spec/faraday/error_spec.rb b/spec/faraday/error_spec.rb new file mode 100644 index 000000000..ef9e2337c --- /dev/null +++ b/spec/faraday/error_spec.rb @@ -0,0 +1,45 @@ +RSpec.describe Faraday::Error do + describe Faraday::Error::ClientError do + describe '.initialize' do + subject { described_class.new(exception, response) } + let(:response) { nil } + + context 'with exception only' do + let(:exception) { RuntimeError.new('test') } + + it { expect(subject.wrapped_exception).to eq(exception) } + it { expect(subject.response).to be_nil } + it { expect(subject.message).to eq(exception.message) } + it { expect(subject.backtrace).to eq(exception.backtrace) } + it { expect(subject.inspect).to eq('#>') } + end + + context 'with response hash' do + let(:exception) { { status: 400 } } + + it { expect(subject.wrapped_exception).to be_nil } + it { expect(subject.response).to eq(exception) } + it { expect(subject.message).to eq('the server responded with status 400') } + it { expect(subject.inspect).to eq('#400}>') } + end + + context 'with string' do + let(:exception) { 'custom message' } + + it { expect(subject.wrapped_exception).to be_nil } + it { expect(subject.response).to be_nil } + it { expect(subject.message).to eq('custom message') } + it { expect(subject.inspect).to eq('#>') } + end + + context 'with anything else #to_s' do + let(:exception) { %w(error1 error2) } + + it { expect(subject.wrapped_exception).to be_nil } + it { expect(subject.response).to be_nil } + it { expect(subject.message).to eq('["error1", "error2"]') } + it { expect(subject.inspect).to eq('#>') } + end + end + end +end \ No newline at end of file diff --git a/spec/faraday/options/env_spec.rb b/spec/faraday/options/env_spec.rb new file mode 100644 index 000000000..6ff52e07f --- /dev/null +++ b/spec/faraday/options/env_spec.rb @@ -0,0 +1,37 @@ +RSpec.describe Faraday::Env do + subject { Faraday::Env.new } + it 'allows to access members' do + expect(subject.method).to be_nil + subject.method = :get + expect(subject.method).to eq(:get) + end + + it 'allows to access symbol non members' do + expect(subject[:custom]).to be_nil + subject[:custom] = :boom + expect(subject[:custom]).to eq(:boom) + end + + it 'allows to access string non members' do + expect(subject['custom']).to be_nil + subject['custom'] = :boom + expect(subject['custom']).to eq(:boom) + end + + it 'ignores false when fetching' do + ssl = Faraday::SSLOptions.new + ssl.verify = false + expect(ssl.fetch(:verify, true)).to be_falsey + end + + it 'retains custom members' do + subject[:foo] = "custom 1" + subject[:bar] = :custom_2 + env2 = Faraday::Env.from(subject) + env2[:baz] = "custom 3" + + expect(env2[:foo]).to eq("custom 1") + expect(env2[:bar]).to eq(:custom_2) + expect(subject[:baz]).to be_nil + end +end \ No newline at end of file diff --git a/spec/faraday/options/options_spec.rb b/spec/faraday/options/options_spec.rb new file mode 100644 index 000000000..545e33764 --- /dev/null +++ b/spec/faraday/options/options_spec.rb @@ -0,0 +1,284 @@ +RSpec.describe Faraday::Options do + SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b)) + class ParentOptions < Faraday::Options.new(:a, :b, :c) + options c: SubOptions + end + + describe '#merge' do + it 'merges options with hashes' do + options = ParentOptions.new(1) + expect(options.a).to eq(1) + expect(options.b).to be_nil + + dup = options.merge a: 2, b: 3 + expect(dup.a).to eq(2) + expect(dup.b).to eq(3) + expect(options.a).to eq(1) + expect(options.b).to be_nil + end + + it 'deeply merges two options' do + sub_opts1 = SubOptions.from(sub_a: 3) + sub_opts2 = SubOptions.from(sub_b: 4) + opt1 = ParentOptions.from(a: 1, c: sub_opts1) + opt2 = ParentOptions.from(b: 2, c: sub_opts2) + + merged = opt1.merge(opt2) + + expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4) + expected = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts) + expect(merged).to eq(expected) + end + + it 'deeply merges options with hashes' do + sub_opts1 = SubOptions.from(sub_a: 3) + sub_opts2 = { sub_b: 4 } + opt1 = ParentOptions.from(a: 1, c: sub_opts1) + opt2 = { b: 2, c: sub_opts2 } + + merged = opt1.merge(opt2) + + expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4) + expected = ParentOptions.from(a: 1, b: 2, c: expected_sub_opts) + expect(merged).to eq(expected) + end + + it 'deeply merges options with nil' do + sub_opts = SubOptions.new(3, 4) + options = ParentOptions.new(1, 2, sub_opts) + expect(options.a).to eq(1) + expect(options.b).to eq(2) + expect(options.c.sub_a).to eq(3) + expect(options.c.sub_b).to eq(4) + + options2 = ParentOptions.from(b: 5, c: nil) + + merged = options.merge(options2) + + expect(merged.b).to eq(5) + expect(merged.c).to eq(sub_opts) + end + + it 'deeply merges options with options having nil sub-options' do + options = ParentOptions.from(a: 1) + + sub_opts = SubOptions.new(3, 4) + options2 = ParentOptions.from(b: 2, c: sub_opts) + + expect(options.a).to eq(1) + expect(options2.b).to eq(2) + expect(options2.c.sub_a).to eq(3) + expect(options2.c.sub_b).to eq(4) + + merged = options.merge(options2) + + expect(merged.c).to eq(sub_opts) + end + + describe '#dup' do + it 'duplicate options but not sub-options' do + sub_opts = SubOptions.from(sub_a: 3) + opts = ParentOptions.from(b: 1, c: sub_opts) + + duped = opts.dup + duped.b = 2 + duped.c.sub_a = 4 + + expect(opts.b).to eq(1) + expect(opts.c.sub_a).to eq(4) + end + end + + describe '#deep_dup' do + it 'duplicate options and also suboptions' do + sub_opts = SubOptions.from(sub_a: 3) + opts = ParentOptions.from(b: 1, c: sub_opts) + + duped = opts.deep_dup + duped.b = 2 + duped.c.sub_a = 4 + + expect(opts.b).to eq(1) + expect(opts.c.sub_a).to eq(3) + end + end + + describe '#clear' do + it 'clears the options' do + options = SubOptions.new(1) + expect(options.empty?).not_to be_truthy + options.clear + expect(options.empty?).to be_truthy + end + end + + + describe '#empty?' do + it 'returns true only if all options are nil' do + options = SubOptions.new + expect(options.empty?).to be_truthy + options.sub_a = 1 + expect(options.empty?).not_to be_truthy + options.delete(:sub_a) + expect(options.empty?).to be_truthy + end + end + + describe '#each_key' do + it 'allows to iterate through keys' do + options = ParentOptions.new(1, 2, 3) + enum = options.each_key + expect(enum.next.to_sym).to eq(:a) + expect(enum.next.to_sym).to eq(:b) + expect(enum.next.to_sym).to eq(:c) + end + end + + describe '#key?' do + it 'returns true if the key exists and is not nil' do + options = SubOptions.new + expect(options.key?(:sub_a)).not_to be_truthy + options.sub_a = 1 + expect(options.key?(:sub_a)).to be_truthy + end + end + + describe '#each_value' do + it 'allows to iterate through values' do + options = ParentOptions.new(1, 2, 3) + enum = options.each_value + expect(enum.next).to eq(1) + expect(enum.next).to eq(2) + expect(enum.next).to eq(3) + end + end + + describe '#value?' do + it 'returns true if any key has that value' do + options = SubOptions.new + expect(options.value?(1)).not_to be_truthy + options.sub_a = 1 + expect(options.value?(1)).to be_truthy + end + end + + describe '#update' do + it 'updates options from hashes' do + options = ParentOptions.new(1) + expect(options.a).to eq(1) + expect(options.b).to be_nil + + updated = options.update :a => 2, :b => 3 + expect(options.a).to eq(2) + expect(options.b).to eq(3) + expect(updated).to eq(options) + end + end + + describe '#delete' do + it 'allows to remove value for key' do + options = ParentOptions.new(1) + expect(options.a).to eq(1) + expect(options.delete(:a)).to eq(1) + expect(options.a).to be_nil + end + end + + describe '#from' do + it { expect { ParentOptions.from invalid: 1 }.to raise_error(NoMethodError) } + + it 'works with options' do + options = ParentOptions.new(1) + + value = ParentOptions.from(options) + expect(value.a).to eq(1) + expect(value.b).to be_nil + end + + it 'works with options with sub object' do + sub = SubOptions.new(1) + options = ParentOptions.from a: 1, c: sub + expect(options).to be_a_kind_of(ParentOptions) + expect(options.a).to eq(1) + expect(options.b).to be_nil + expect(options.c).to be_a_kind_of(SubOptions) + expect(options.c.sub_a).to eq(1) + end + + it 'works with hash' do + options = ParentOptions.from a: 1 + expect(options).to be_a_kind_of(ParentOptions) + expect(options.a).to eq(1) + expect(options.b).to be_nil + end + + it 'works with hash with sub object' do + options = ParentOptions.from a: 1, c: { sub_a: 1 } + expect(options).to be_a_kind_of(ParentOptions) + expect(options.a).to eq(1) + expect(options.b).to be_nil + expect(options.c).to be_a_kind_of(SubOptions) + expect(options.c.sub_a).to eq(1) + end + + it 'works with deep hash' do + hash = { b: 1 } + options = ParentOptions.from a: hash + expect(options.a[:b]).to eq(1) + + hash[:b] = 2 + expect(options.a[:b]).to eq(1) + + options.a[:b] = 3 + expect(hash[:b]).to eq(2) + expect(options.a[:b]).to eq(3) + end + + it 'works with nil' do + options = ParentOptions.from(nil) + expect(options).to be_a_kind_of(ParentOptions) + expect(options.a).to be_nil + expect(options.b).to be_nil + end + + it 'respects inheritance' do + subclass = Class.new(ParentOptions) + options = subclass.from(c: { sub_a: 'hello' }) + expect(options.c).to be_a_kind_of(SubOptions) + expect(options.c.sub_a).to eq('hello') + end + end + + describe '#fetch' do + subject { SubOptions.new } + + context 'when the fetched key has no value' do + it 'uses falsey default' do + expect(subject.fetch(:sub_a, false) { |k| :blah }).to be_falsey + end + + it 'accepts block' do + expect(subject.fetch(:sub_a) { |k| "yo #{k.inspect}" }).to eq('yo :sub_a') + end + + it 'needs a default if key is missing' do + expect { subject.fetch(:sub_a) }.to raise_error(Faraday::Options.fetch_error_class) + end + end + + context 'when the fetched key has a value' do + before do + subject.sub_a = 1 + end + + it 'grabs value' do + expect(subject.fetch(:sub_a, false) { |k| :blah }).to eq(1) + end + + it 'works with key' do + expect(subject.fetch(:sub_a)).to eq(1) + end + end + end + end +end \ No newline at end of file diff --git a/spec/faraday/options/proxy_options_spec.rb b/spec/faraday/options/proxy_options_spec.rb new file mode 100644 index 000000000..048f325bb --- /dev/null +++ b/spec/faraday/options/proxy_options_spec.rb @@ -0,0 +1,33 @@ +RSpec.describe Faraday::ProxyOptions do + describe '#from' do + it 'works with string' do + options = Faraday::ProxyOptions.from 'http://user:pass@example.org' + expect(options.user).to eq('user') + expect(options.password).to eq('pass') + expect(options.uri).to be_a_kind_of(URI) + expect(options.path).to eq('') + expect(options.port).to eq(80) + expect(options.host).to eq('example.org') + expect(options.scheme).to eq('http') + end + + it 'works with nil' do + options = Faraday::ProxyOptions.from nil + expect(options).to be_a_kind_of(Faraday::ProxyOptions) + end + + it 'works with no auth' do + proxy = Faraday::ProxyOptions.from 'http://example.org' + expect(proxy.user).to be_nil + expect(proxy.password).to be_nil + end + end + + it 'allows hash access' do + proxy = Faraday::ProxyOptions.from 'http://a%40b:pw%20d@example.org' + expect(proxy.user).to eq('a@b') + expect(proxy[:user]).to eq('a@b') + expect(proxy.password).to eq('pw d') + expect(proxy[:password]).to eq('pw d') + end +end \ No newline at end of file diff --git a/spec/faraday/options/request_options_spec.rb b/spec/faraday/options/request_options_spec.rb new file mode 100644 index 000000000..6c8e28616 --- /dev/null +++ b/spec/faraday/options/request_options_spec.rb @@ -0,0 +1,15 @@ +RSpec.describe Faraday::RequestOptions do + it 'allows to set the request proxy' do + options = Faraday::RequestOptions.new + expect(options.proxy).to be_nil + + expect { options[:proxy] = { booya: 1 } }.to raise_error(NoMethodError) + + options[:proxy] = { user: 'user' } + expect(options.proxy).to be_a_kind_of(Faraday::ProxyOptions) + expect(options.proxy.user).to eq('user') + + options.proxy = nil + expect(options.proxy).to be_nil + end +end \ No newline at end of file diff --git a/spec/faraday/params_encoders/flat_spec.rb b/spec/faraday/params_encoders/flat_spec.rb new file mode 100644 index 000000000..3c33937bc --- /dev/null +++ b/spec/faraday/params_encoders/flat_spec.rb @@ -0,0 +1,11 @@ +require 'rack/utils' + +RSpec.describe Faraday::FlatParamsEncoder do + it_behaves_like 'a params encoder' + + it 'decodes arrays' do + query = 'a=one&a=two&a=three' + expected = {'a' => %w(one two three) } + expect(subject.decode(query)).to eq(expected) + end +end \ No newline at end of file diff --git a/spec/faraday/params_encoders/nested_spec.rb b/spec/faraday/params_encoders/nested_spec.rb new file mode 100644 index 000000000..52386ac20 --- /dev/null +++ b/spec/faraday/params_encoders/nested_spec.rb @@ -0,0 +1,122 @@ +require 'rack/utils' + +RSpec.describe Faraday::NestedParamsEncoder do + it_behaves_like 'a params encoder' + + it 'decodes arrays' do + query = 'a[1]=one&a[2]=two&a[3]=three' + expected = { 'a' => %w(one two three) } + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes hashes' do + query = 'a[b1]=one&a[b2]=two&a[b][c]=foo' + expected = { "a" => { "b1" => "one", "b2" => "two", "b" => { "c" => "foo" } } } + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested arrays rack compat' do + query = 'a[][one]=1&a[][two]=2&a[][one]=3&a[][two]=4' + expected = Rack::Utils.parse_nested_query(query) + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested array mixed types' do + query = 'a[][one]=1&a[]=2&a[]=&a[]' + expected = Rack::Utils.parse_nested_query(query) + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested ignores invalid array' do + query = '[][a]=1&b=2' + expected = { "a" => "1", "b" => "2" } + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested ignores repeated array notation' do + query = 'a[][][]=1' + expected = { "a" => ["1"] } + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested ignores malformed keys' do + query = '=1&[]=2' + expected = {} + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested subkeys dont have to be in brackets' do + query = 'a[b]c[d]e=1' + expected = { "a" => { "b" => { "c" => { "d" => { "e" => "1" } } } } } + expect(subject.decode(query)).to eq(expected) + end + + it 'decodes nested final value overrides any type' do + query = 'a[b][c]=1&a[b]=2' + expected = { "a" => { "b" => "2" } } + expect(subject.decode(query)).to eq(expected) + end + + it 'encodes rack compat' do + params = { :a => [{ :one => "1", :two => "2" }, "3", ""] } + result = Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split('&') + expected = Rack::Utils.build_nested_query(params).split('&') + expect(result).to match_array(expected) + end + + it 'encodes empty string array value' do + expected = 'baz=&foo%5Bbar%5D=' + result = Faraday::NestedParamsEncoder.encode(foo: {bar: ''}, baz: '') + expect(result).to eq(expected) + end + + it 'encodes nil array value' do + expected = 'baz&foo%5Bbar%5D' + result = Faraday::NestedParamsEncoder.encode(foo: {bar: nil}, baz: nil) + expect(result).to eq(expected) + end + + it 'encodes empty array value' do + expected = 'baz%5B%5D&foo%5Bbar%5D%5B%5D' + result = Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: []) + expect(result).to eq(expected) + end + + shared_examples 'a wrong decoding' do + it do + expect { subject.decode(query) }.to raise_error(TypeError) do |e| + expect(e.message).to eq(error_message) + end + end + end + + context 'when expecting hash but getting string' do + let(:query) { 'a=1&a[b]=2' } + let(:error_message) { "expected Hash (got String) for param `a'" } + it_behaves_like 'a wrong decoding' + end + + context 'when expecting hash but getting array' do + let(:query) { 'a[]=1&a[b]=2' } + let(:error_message) { "expected Hash (got Array) for param `a'" } + it_behaves_like 'a wrong decoding' + end + + context 'when expecting nested hash but getting non nested' do + let(:query) { 'a[b]=1&a[b][c]=2' } + let(:error_message) { "expected Hash (got String) for param `b'" } + it_behaves_like 'a wrong decoding' + end + + context 'when expecting array but getting hash' do + let(:query) { 'a[b]=1&a[]=2' } + let(:error_message) { "expected Array (got Hash) for param `a'" } + it_behaves_like 'a wrong decoding' + end + + context 'when expecting array but getting string' do + let(:query) { 'a=1&a[]=2' } + let(:error_message) { "expected Array (got String) for param `a'" } + it_behaves_like 'a wrong decoding' + end +end \ No newline at end of file diff --git a/spec/faraday/rack_builder_spec.rb b/spec/faraday/rack_builder_spec.rb new file mode 100644 index 000000000..62e4b896f --- /dev/null +++ b/spec/faraday/rack_builder_spec.rb @@ -0,0 +1,193 @@ +RSpec.describe Faraday::RackBuilder do + # mock handler classes + class Handler < Struct.new(:app) + def call(env) + (env[:request_headers]['X-Middleware'] ||= '') << ":#{self.class.name.split('::').last}" + app.call(env) + end + end + + class Apple < Handler; + end + class Orange < Handler; + end + class Banana < Handler; + end + + class Broken < Faraday::Middleware + dependency 'zomg/i_dont/exist' + end + + subject { conn.builder } + + context 'with default stack' do + let(:conn) { Faraday::Connection.new } + + it { expect(subject[0]).to eq(Faraday::Request.lookup_middleware(:url_encoded)) } + it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(Faraday.default_adapter)) } + end + + context 'with custom empty block' do + let(:conn) { Faraday::Connection.new {} } + + it { expect(subject[0]).to be_nil } + it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(Faraday.default_adapter)) } + end + + context 'with custom adapter only' do + let(:conn) do + Faraday::Connection.new do |builder| + builder.adapter :test do |stub| + stub.get('/') { |_| [200, {}, ''] } + end + end + end + + it { expect(subject[0]).to be_nil } + it { expect(subject.adapter).to eq(Faraday::Adapter.lookup_middleware(:test)) } + end + + context 'with custom handler and adapter' do + let(:conn) do + Faraday::Connection.new do |builder| + builder.use Apple + builder.adapter :test do |stub| + stub.get('/') { |_| [200, {}, ''] } + end + end + end + + it 'locks the stack after making a request' do + expect(subject.locked?).to be_falsey + conn.get('/') + expect(subject.locked?).to be_truthy + expect { subject.use(Orange) }.to raise_error(Faraday::RackBuilder::StackLocked) + end + + it 'dup stack is unlocked' do + expect(subject.locked?).to be_falsey + subject.lock! + expect(subject.locked?).to be_truthy + dup = subject.dup + expect(dup).to eq(subject) + expect(dup.locked?).to be_falsey + end + + it 'allows to compare handlers' do + expect(subject.handlers.first).to eq(Faraday::RackBuilder::Handler.new(Apple)) + end + end + + context 'when having a single handler' do + let(:conn) { Faraday::Connection.new {} } + + before { subject.use(Apple) } + + it { expect(subject.handlers).to eq([Apple]) } + + it 'allows rebuilding' do + subject.build do |builder| + builder.use(Orange) + end + expect(subject.handlers).to eq([Orange]) + end + + it 'allows use' do + subject.use(Orange) + expect(subject.handlers).to eq([Apple, Orange]) + end + + it 'allows insert_before' do + subject.insert_before(Apple, Orange) + expect(subject.handlers).to eq([Orange, Apple]) + end + + it 'allows insert_after' do + subject.insert_after(Apple, Orange) + expect(subject.handlers).to eq([Apple, Orange]) + end + + it 'raises an error trying to use an unregistered symbol' do + expect { subject.use(:apple) }.to raise_error(Faraday::Error) do |err| + expect(err.message).to eq(':apple is not registered on Faraday::Middleware') + end + end + end + + context 'with custom registered middleware' do + let(:conn) { Faraday::Connection.new {} } + + after { Faraday::Middleware.unregister_middleware(:apple) } + + it 'allows to register with constant' do + Faraday::Middleware.register_middleware(apple: Apple) + subject.use(:apple) + expect(subject.handlers).to eq([Apple]) + end + + it 'allows to register with symbol' do + Faraday::Middleware.register_middleware(apple: :Apple) + subject.use(:apple) + expect(subject.handlers).to eq([Apple]) + end + + it 'allows to register with string' do + Faraday::Middleware.register_middleware(apple: 'Apple') + subject.use(:apple) + expect(subject.handlers).to eq([Apple]) + end + + it 'allows to register with Proc' do + Faraday::Middleware.register_middleware(apple: lambda { Apple }) + subject.use(:apple) + expect(subject.handlers).to eq([Apple]) + end + end + + context 'when having two handlers' do + let(:conn) { Faraday::Connection.new {} } + + before do + subject.use(Apple) + subject.use(Orange) + end + + it 'allows insert_before' do + subject.insert_before(Orange, Banana) + expect(subject.handlers).to eq([Apple, Banana, Orange]) + end + + it 'allows insert_after' do + subject.insert_after(Apple, Banana) + expect(subject.handlers).to eq([Apple, Banana, Orange]) + end + + it 'allows to swap handlers' do + subject.swap(Apple, Banana) + expect(subject.handlers).to eq([Banana, Orange]) + end + + it 'allows to delete a handler' do + subject.delete(Apple) + expect(subject.handlers).to eq([Orange]) + end + end + + context 'when having a handler with broken dependency' do + let(:conn) do + Faraday::Connection.new do |builder| + builder.adapter :test do |stub| + stub.get('/') { |_| [200, {}, ''] } + end + end + end + + before { subject.use(Broken) } + + it 'raises an error while making a request' do + expect { conn.get('/') }.to raise_error(RuntimeError) do |err| + expect(err.message).to eq('missing dependency for Broken: cannot load such file -- zomg/i_dont/exist') + end + end + end +end \ No newline at end of file diff --git a/spec/faraday/request/authorization_spec.rb b/spec/faraday/request/authorization_spec.rb new file mode 100644 index 000000000..e802ef06b --- /dev/null +++ b/spec/faraday/request/authorization_spec.rb @@ -0,0 +1,86 @@ +RSpec.describe Faraday::Request::Authorization do + let(:conn) do + Faraday.new do |b| + b.request auth_type, *auth_config + b.adapter :test do |stub| + stub.get('/auth-echo') do |env| + [200, {}, env[:request_headers]['Authorization']] + end + end + end + end + + shared_examples 'does not interfere with existing authentication' do + context 'and request already has an authentication header' do + let(:response) { conn.get('/auth-echo', nil, authorization: 'Token token="bar"') } + + it 'does not interfere with existing authorization' do + expect(response.body).to eq('Token token="bar"') + end + end + end + + let(:response) { conn.get('/auth-echo') } + + describe 'basic_auth' do + let(:auth_type) { :basic_auth } + + context 'when passed correct params' do + let(:auth_config) { %w(aladdin opensesame) } + + it { expect(response.body).to eq('Basic YWxhZGRpbjpvcGVuc2VzYW1l') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when passed very long values' do + let(:auth_config) { ['A' * 255, ''] } + + it { expect(response.body).to eq("Basic #{'QUFB' * 85}Og==") } + + include_examples 'does not interfere with existing authentication' + end + end + + describe 'token_auth' do + let(:auth_type) { :token_auth } + + context 'when passed correct params' do + let(:auth_config) { 'quux' } + + it { expect(response.body).to eq('Token token="quux"') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when other values are provided' do + let(:auth_config) { ['baz', foo: 42] } + + it { expect(response.body).to match(/^Token /) } + it { expect(response.body).to match(/token="baz"/) } + it { expect(response.body).to match(/foo="42"/) } + + include_examples 'does not interfere with existing authentication' + end + end + + describe 'authorization' do + let(:auth_type) { :authorization } + + context 'when passed two strings' do + let(:auth_config) { ['custom', 'abc def'] } + + it { expect(response.body).to eq('custom abc def') } + + include_examples 'does not interfere with existing authentication' + end + + context 'when passed a string and a hash' do + let(:auth_config) { ['baz', foo: 42] } + + it { expect(response.body).to eq('baz foo="42"') } + + include_examples 'does not interfere with existing authentication' + end + end +end diff --git a/spec/faraday/request/instrumentation_spec.rb b/spec/faraday/request/instrumentation_spec.rb new file mode 100644 index 000000000..a90f6f0f5 --- /dev/null +++ b/spec/faraday/request/instrumentation_spec.rb @@ -0,0 +1,74 @@ +RSpec.describe Faraday::Request::Instrumentation do + class FakeInstrumenter + attr_reader :instrumentations + + def initialize + @instrumentations = [] + end + + def instrument(name, env) + @instrumentations << [name, env] + yield + end + end + + let(:config) { {} } + let(:options) { Faraday::Request::Instrumentation::Options.from config } + let(:instrumenter) { FakeInstrumenter.new } + let(:conn) do + Faraday.new do |f| + f.request :instrumentation, config.merge(instrumenter: instrumenter) + f.adapter :test do |stub| + stub.get '/' do + [200, {}, 'ok'] + end + end + end + end + + it { expect(options.name).to eq('request.faraday') } + it 'defaults to ActiveSupport::Notifications' do + begin + res = options.instrumenter + rescue NameError => err + expect(err.to_s).to match('ActiveSupport') + else + expect(res).to eq(ActiveSupport::Notifications) + end + end + + it 'instruments with default name' do + expect(instrumenter.instrumentations.size).to eq(0) + + res = conn.get '/' + expect(res.body).to eq('ok') + expect(instrumenter.instrumentations.size).to eq(1) + + name, env = instrumenter.instrumentations.first + expect(name).to eq('request.faraday') + expect(env[:url].path).to eq('/') + end + + context 'with custom name' do + let(:config) { { name: 'custom' } } + + it { expect(options.name).to eq('custom') } + it 'instruments with custom name' do + expect(instrumenter.instrumentations.size).to eq(0) + + res = conn.get '/' + expect(res.body).to eq('ok') + expect(instrumenter.instrumentations.size).to eq(1) + + name, env = instrumenter.instrumentations.first + expect(name).to eq('custom') + expect(env[:url].path).to eq('/') + end + end + + context 'with custom instrumenter' do + let(:config) { { instrumenter: :custom } } + + it { expect(options.instrumenter).to eq(:custom) } + end +end \ No newline at end of file diff --git a/spec/faraday/request/multipart_spec.rb b/spec/faraday/request/multipart_spec.rb new file mode 100644 index 000000000..ac1337fd1 --- /dev/null +++ b/spec/faraday/request/multipart_spec.rb @@ -0,0 +1,56 @@ +Faraday::CompositeReadIO.class_eval { attr_reader :ios } + +RSpec.describe Faraday::Request::Multipart do + let(:conn) do + Faraday.new do |b| + b.request :multipart + b.request :url_encoded + b.adapter :test do |stub| + stub.post('/echo') do |env| + posted_as = env[:request_headers]['Content-Type'] + [200, { 'Content-Type' => posted_as }, env[:body]] + end + end + end + end + + shared_examples 'a multipart request' do + it 'forms a multipart request' do + response = conn.post('/echo', payload) + + expect(response.body).to be_a_kind_of(Faraday::CompositeReadIO) + match = "multipart/form-data; boundary=%s" % Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX + expect(response.headers['Content-Type']).to start_with(match) + + response.body.send(:ios).map { |io| io.read }.each do |io| + re = regexes.detect { |r| io =~ r } + regexes.delete(re) if re + end + expect(regexes).to eq([]) + end + + it 'generates a unique boundary for each request' do + response1 = conn.post('/echo', payload) + response2 = conn.post('/echo', payload) + expect(response1.headers['Content-Type']).not_to eq(response2.headers['Content-Type']) + end + end + + context 'when multipart objects in param' do + let(:regexes) { [/name\=\"a\"/, + /name=\"b\[c\]\"\; filename\=\"multipart_spec\.rb\"/, + /name=\"b\[d\]\"/] } + + let(:payload) { { :a => 1, :b => { :c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2 } } } + it_behaves_like 'a multipart request' + end + + context 'when multipart objects in array param' do + let(:regexes) { [/name\=\"a\"/, + /name=\"b\[\]\[c\]\"\; filename\=\"multipart_spec\.rb\"/, + /name=\"b\[\]\[d\]\"/] } + + let(:payload) { { :a => 1, :b => [{ :c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2 }] } } + it_behaves_like 'a multipart request' + end +end \ No newline at end of file diff --git a/spec/faraday/request/retry_spec.rb b/spec/faraday/request/retry_spec.rb new file mode 100644 index 000000000..8d9841a7b --- /dev/null +++ b/spec/faraday/request/retry_spec.rb @@ -0,0 +1,226 @@ +RSpec.describe Faraday::Request::Retry do + let(:calls) { [] } + let(:times_called) { calls.size } + let(:options) { [] } + let(:conn) do + Faraday.new do |b| + b.request :retry, *options + + b.adapter :test do |stub| + %w(get post).each do |method| + stub.send(method, '/unstable') do |env| + calls << env.dup + env[:body] = nil # simulate blanking out response body + callback.call + end + end + end + end + end + + context 'when an unexpected error happens' do + let(:callback) { lambda { raise 'boom!' } } + + before { expect { conn.get('/unstable') }.to raise_error(RuntimeError) } + + it { expect(times_called).to eq(1) } + + context 'and this is passed as a custom exception' do + let(:options) { [{ exceptions: StandardError }] } + + it { expect(times_called).to eq(3) } + end + end + + context 'when an expected error happens' do + let(:callback) { lambda { raise Errno::ETIMEDOUT } } + + before do + @started = Time.now + expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) + end + + it { expect(times_called).to eq(3) } + + context 'and legacy max_retry set to 1' do + let(:options) { [1] } + + it { expect(times_called).to eq(2) } + end + + context 'and legacy max_retry set to -9' do + let(:options) { [-9] } + + it { expect(times_called).to eq(1) } + end + + context 'and new max_retry set to 3' do + let(:options) { [{ max: 3 }] } + + it { expect(times_called).to eq(4) } + end + + context 'and new max_retry set to -9' do + let(:options) { [{ max: -9 }] } + + it { expect(times_called).to eq(1) } + end + + context 'and both max_retry and interval are set' do + let(:options) { [{ max: 2, interval: 0.1 }] } + + it { expect(Time.now - @started).to be_within(0.04).of(0.2) } + end + end + + context 'when no exception raised' do + let(:options) { [{ max: 1, retry_statuses: 429 }] } + + before { conn.get('/unstable') } + + context 'and response code is in retry_statuses' do + let(:callback) { lambda { [429, {}, ''] } } + + it { expect(times_called).to eq(2) } + end + + context 'and response code is not in retry_statuses' do + let(:callback) { lambda { [503, {}, ''] } } + + it { expect(times_called).to eq(1) } + end + end + + describe '#calculate_retry_interval' do + context 'with exponential backoff' do + let(:options) { { max: 5, interval: 0.1, backoff_factor: 2 } } + let(:middleware) { Faraday::Request::Retry.new(nil, options) } + + it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } + it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } + it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.4) } + end + + context 'with exponential backoff and max_interval' do + let(:options) { { max: 5, interval: 0.1, backoff_factor: 2, max_interval: 0.3 } } + let(:middleware) { Faraday::Request::Retry.new(nil, options) } + + it { expect(middleware.send(:calculate_retry_interval, 5)).to eq(0.1) } + it { expect(middleware.send(:calculate_retry_interval, 4)).to eq(0.2) } + it { expect(middleware.send(:calculate_retry_interval, 3)).to eq(0.3) } + it { expect(middleware.send(:calculate_retry_interval, 2)).to eq(0.3) } + end + + context 'with exponential backoff and interval_randomness' do + let(:options) { { max: 2, interval: 0.1, interval_randomness: 0.05 } } + let(:middleware) { Faraday::Request::Retry.new(nil, options) } + + it { expect(middleware.send(:calculate_retry_interval, 2)).to be_between(0.1, 0.15) } + end + end + + context 'when method is not idempotent' do + let(:callback) { lambda { raise Errno::ETIMEDOUT } } + + before { expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) } + + it { expect(times_called).to eq(1) } + end + + describe 'retry_if option' do + let(:callback) { lambda { raise Errno::ETIMEDOUT } } + let(:options) { [{ retry_if: @check }] } + + it 'retries if retry_if block always returns true' do + body = { foo: :bar } + @check = lambda { |_, _| true } + expect { conn.post('/unstable', body) }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(3) + expect(calls.all? { |env| env[:body] == body }).to be_truthy + end + + it 'does not retry if retry_if block returns false checking env' do + @check = lambda { |env, _| env[:method] != :post } + expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(1) + end + + it 'does not retry if retry_if block returns false checking exception' do + @check = lambda { |_, exception| !exception.kind_of?(Errno::ETIMEDOUT) } + expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(1) + end + + it 'should rewind files on retry' do + io = StringIO.new('Test data') + upload_io = Faraday::UploadIO.new(io, 'application/octet/stream') + + rewound = 0 + rewind = lambda { rewound += 1 } + + @check = lambda { |_, _| true } + allow(upload_io).to receive(:rewind, &rewind) + expect { conn.post('/unstable', { file: upload_io }) }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(3) + expect(rewound).to eq(2) + end + + context 'when explicitly specifying methods to retry' do + let(:options) { [{ retry_if: @check, methods: [:post] }] } + + it 'does not call retry_if for specified methods' do + @check = lambda { |_, _| raise "this should have never been called" } + expect { conn.post('/unstable') }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(3) + end + end + + context 'with empty list of methods to retry' do + let(:options) { [{ retry_if: @check, methods: [] }] } + + it 'calls retry_if for all methods' do + @check = lambda { |_, _| calls.size < 2 } + expect { conn.get('/unstable') }.to raise_error(Errno::ETIMEDOUT) + expect(times_called).to eq(2) + end + end + end + + describe 'retry_after header support' do + let(:callback) { lambda { [504, headers, ''] } } + let(:elapsed) { Time.now - @started } + + before do + @started = Time.now + conn.get('/unstable') + end + + context 'when retry_after bigger than interval' do + let(:headers) { { 'Retry-After' => '0.5' } } + let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } + + it { expect(elapsed).to be > 0.5 } + end + + context 'when retry_after smaller than interval' do + let(:headers) { { 'Retry-After' => '0.1' } } + let(:options) { [{ max: 1, interval: 0.2, retry_statuses: 504 }] } + + it { expect(elapsed).to be > 0.2 } + end + + context 'when retry_after is a timestamp' do + let(:headers) { { 'Retry-After' => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } } + let(:options) { [{ max: 1, interval: 0.1, retry_statuses: 504 }] } + + it { expect(elapsed).to be > 1 } + end + + context 'when retry_after is bigger than max_interval' do + let(:headers) { { 'Retry-After' => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } } + let(:options) { [{ max: 2, interval: 0.1, max_interval: 5, retry_statuses: 504 }] } + + it { expect(times_called).to eq(1) } + end + end +end diff --git a/spec/faraday/request/url_encoded_spec.rb b/spec/faraday/request/url_encoded_spec.rb new file mode 100644 index 000000000..14f12c5e1 --- /dev/null +++ b/spec/faraday/request/url_encoded_spec.rb @@ -0,0 +1,68 @@ +RSpec.describe Faraday::Request::UrlEncoded do + let(:conn) do + Faraday.new do |b| + b.request :multipart + b.request :url_encoded + b.adapter :test do |stub| + stub.post('/echo') do |env| + posted_as = env[:request_headers]['Content-Type'] + [200, {'Content-Type' => posted_as}, env[:body]] + end + end + end + end + + it 'does nothing without payload' do + response = conn.post('/echo') + expect(response.headers['Content-Type']).to be_nil + expect(response.body.empty?).to be_truthy + end + + it 'ignores custom content type' do + response = conn.post('/echo', { :some => 'data' }, 'content-type' => 'application/x-foo') + expect(response.headers['Content-Type']).to eq('application/x-foo') + expect(response.body).to eq({ :some => 'data' }) + end + + it 'works with no headers' do + response = conn.post('/echo', { :fruit => %w[apples oranges] }) + expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') + expect(response.body).to eq('fruit%5B%5D=apples&fruit%5B%5D=oranges') + end + + it 'works with with headers' do + response = conn.post('/echo', {'a'=>123}, 'content-type' => 'application/x-www-form-urlencoded') + expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') + expect(response.body).to eq('a=123') + end + + it 'works with nested params' do + response = conn.post('/echo', { :user => {:name => 'Mislav', :web => 'mislav.net'} }) + expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') + expected = { 'user' => {'name' => 'Mislav', 'web' => 'mislav.net'} } + expect(Faraday::Utils.parse_nested_query(response.body)).to eq(expected) + end + + it 'works with non nested params' do + response = conn.post('/echo', { :dimensions => ['date', 'location']}) do |req| + req.options.params_encoder = Faraday::FlatParamsEncoder + end + expect(response.headers['Content-Type']).to eq('application/x-www-form-urlencoded') + expected = { 'dimensions' => ['date', 'location'] } + expect(Faraday::Utils.parse_query(response.body)).to eq(expected) + expect(response.body).to eq('dimensions=date&dimensions=location') + end + + it 'works with unicode' do + err = capture_warnings { + response = conn.post('/echo', {:str => "eé cç aã aâ"}) + expect(response.body).to eq('str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2') + } + expect(err.empty?).to be_truthy + end + + it 'works with nested keys' do + response = conn.post('/echo', {'a'=>{'b'=>{'c'=>['d']}}}) + expect(response.body).to eq("a%5Bb%5D%5Bc%5D%5B%5D=d") + end +end \ No newline at end of file diff --git a/spec/faraday/request_spec.rb b/spec/faraday/request_spec.rb new file mode 100644 index 000000000..77b2a136b --- /dev/null +++ b/spec/faraday/request_spec.rb @@ -0,0 +1,107 @@ +RSpec.describe Faraday::Request do + let(:conn) do + Faraday.new(url: 'http://sushi.com/api', + headers: { 'Mime-Version' => '1.0' }, + request: { oauth: { consumer_key: 'anonymous' } }) + end + let(:method) { :get } + let(:block) { nil } + + subject { conn.build_request(method, &block) } + + context 'when nothing particular is configured' do + it { expect(subject.method).to eq(:get) } + it { expect(subject.to_env(conn).ssl.verify).to be_falsey } + end + + context 'when method is post' do + let(:method) { :post } + + it { expect(subject.method).to eq(:post) } + end + + context 'when setting the url on setup with a URI' do + let(:block) { Proc.new { |req| req.url URI.parse('foo.json?a=1') } } + + it { expect(subject.path).to eq(URI.parse('foo.json')) } + it { expect(subject.params).to eq({ 'a' => '1' }) } + it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') } + end + + context 'when setting the url on setup with a string path and params' do + let(:block) { Proc.new { |req| req.url 'foo.json', { 'a' => 1 } } } + + it { expect(subject.path).to eq('foo.json') } + it { expect(subject.params).to eq({ 'a' => 1 }) } + it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1') } + end + + context 'when setting the url on setup with a path including params' do + let(:block) { Proc.new { |req| req.url 'foo.json?b=2&a=1#qqq' } } + + it { expect(subject.path).to eq('foo.json') } + it { expect(subject.params).to eq({ 'a' => '1', 'b' => '2' }) } + it { expect(subject.to_env(conn).url.to_s).to eq('http://sushi.com/api/foo.json?a=1&b=2') } + end + + context 'when setting a header on setup with []= syntax' do + let(:block) { Proc.new { |req| req['Server'] = 'Faraday' } } + let(:headers) { subject.to_env(conn).request_headers } + + it { expect(subject.headers['Server']).to eq('Faraday') } + it { expect(headers['mime-version']).to eq('1.0') } + it { expect(headers['server']).to eq('Faraday') } + end + + context 'when setting the body on setup' do + let(:block) { Proc.new { |req| req.body = 'hi' } } + + it { expect(subject.body).to eq('hi') } + it { expect(subject.to_env(conn).body).to eq('hi') } + end + + context 'with global request options set' do + let(:env_request) { subject.to_env(conn).request } + + before do + conn.options.timeout = 3 + conn.options.open_timeout = 5 + conn.ssl.verify = false + conn.proxy = 'http://proxy.com' + end + + it { expect(subject.options.timeout).to eq(3) } + it { expect(subject.options.open_timeout).to eq(5) } + it { expect(env_request.timeout).to eq(3) } + it { expect(env_request.open_timeout).to eq(5) } + + context 'and per-request options set' do + let(:block) do + Proc.new do |req| + req.options.timeout = 10 + req.options.boundary = 'boo' + req.options.oauth[:consumer_secret] = 'xyz' + req.options.context = { + foo: 'foo', + bar: 'bar' + } + end + end + + it { expect(subject.options.timeout).to eq(10) } + it { expect(subject.options.open_timeout).to eq(5) } + it { expect(env_request.timeout).to eq(10) } + it { expect(env_request.open_timeout).to eq(5) } + it { expect(env_request.boundary).to eq('boo') } + it { expect(env_request.context).to eq({ foo: 'foo', bar: 'bar' }) } + it do + oauth_expected = { consumer_secret: 'xyz', consumer_key: 'anonymous' } + expect(env_request.oauth).to eq(oauth_expected) + end + end + end + + it 'supports marshal serialization' do + expect(Marshal.load(Marshal.dump(subject))).to eq(subject) + end +end diff --git a/spec/faraday/response/logger_spec.rb b/spec/faraday/response/logger_spec.rb new file mode 100644 index 000000000..9324454f0 --- /dev/null +++ b/spec/faraday/response/logger_spec.rb @@ -0,0 +1,137 @@ +require 'stringio' +require 'logger' + +RSpec.describe Faraday::Response::Logger do + let(:string_io) { StringIO.new } + let(:logger) { Logger.new(string_io) } + let(:logger_options) { {} } + let(:conn) do + rubbles = ['Barney', 'Betty', 'Bam Bam'] + + Faraday.new do |b| + b.response :logger, logger, logger_options do |logger| + logger.filter(/(soylent green is) (.+)/, '\1 tasty') + logger.filter(/(api_key:).*"(.+)."/, '\1[API_KEY]') + logger.filter(/(password)=(.+)/, '\1=[HIDDEN]') + end + b.adapter :test do |stubs| + stubs.get('/hello') { [200, { 'Content-Type' => 'text/html' }, 'hello'] } + stubs.post('/ohai') { [200, { 'Content-Type' => 'text/html' }, 'fred'] } + stubs.post('/ohyes') { [200, { 'Content-Type' => 'text/html' }, 'pebbles'] } + stubs.get('/rubbles') { [200, { 'Content-Type' => 'application/json' }, rubbles] } + stubs.get('/filtered_body') { [200, { 'Content-Type' => 'text/html' }, 'soylent green is people'] } + stubs.get('/filtered_headers') { [200, { 'Content-Type' => 'text/html' }, 'headers response'] } + stubs.get('/filtered_params') { [200, { 'Content-Type' => 'text/html' }, 'params response'] } + stubs.get('/filtered_url') { [200, { 'Content-Type' => 'text/html' }, 'url response'] } + end + end + end + + before do + logger.level = Logger::DEBUG + end + + it 'still returns output' do + resp = conn.get '/hello', nil, accept: 'text/html' + expect(resp.body).to eq('hello') + end + + it 'logs method and url' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).to match("GET http:/hello") + end + + it 'logs request headers by default' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).to match(%(Accept: "text/html)) + end + + it 'logs response headers by default' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).to match(%(Content-Type: "text/html)) + end + + it 'does not log request body by default' do + conn.post '/ohai', 'name=Unagi', accept: 'text/html' + expect(string_io.string).not_to match(%(name=Unagi)) + end + + it 'does not log response body by default' do + conn.post '/ohai', 'name=Toro', accept: 'text/html' + expect(string_io.string).not_to match(%(fred)) + end + + it 'logs filter headers' do + conn.headers = { 'api_key' => 'ABC123' } + conn.get '/filtered_headers', nil, accept: 'text/html' + expect(string_io.string).to match(%(api_key:)) + expect(string_io.string).to match(%([API_KEY])) + expect(string_io.string).not_to match(%(ABC123)) + end + + it 'logs filter url' do + conn.get '/filtered_url?password=hunter2', nil, accept: 'text/html' + expect(string_io.string).to match(%([HIDDEN])) + expect(string_io.string).not_to match(%(hunter2)) + end + + context 'when not logging request headers' do + let(:logger_options) { { headers: { request: false } } } + + it 'does not log request headers if option is false' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).not_to match(%(Accept: "text/html)) + end + end + + context 'when not logging response headers' do + let(:logger_options) { { headers: { response: false } } } + + it 'does log response headers if option is false' do + conn.get '/hello', nil, accept: 'text/html' + expect(string_io.string).not_to match(%(Content-Type: "text/html)) + end + end + + context 'when logging request body' do + let(:logger_options) { { bodies: { request: true } } } + + it 'log only request body' do + conn.post '/ohyes', 'name=Tamago', accept: 'text/html' + expect(string_io.string).to match(%(name=Tamago)) + expect(string_io.string).not_to match(%(pebbles)) + end + end + + context 'when logging response body' do + let(:logger_options) { { bodies: { response: true } } } + + it 'log only response body' do + conn.post '/ohyes', 'name=Hamachi', accept: 'text/html' + expect(string_io.string).to match(%(pebbles)) + expect(string_io.string).not_to match(%(name=Hamachi)) + end + end + + context 'when logging request and response bodies' do + let(:logger_options) { { bodies: true } } + + it 'log request and response body' do + conn.post '/ohyes', 'name=Ebi', accept: 'text/html' + expect(string_io.string).to match(%(name=Ebi)) + expect(string_io.string).to match(%(pebbles)) + end + + it 'log response body object' do + conn.get '/rubbles', nil, accept: 'text/html' + expect(string_io.string).to match(%([\"Barney\", \"Betty\", \"Bam Bam\"]\n)) + end + + it 'logs filter body' do + conn.get '/filtered_body', nil, accept: 'text/html' + expect(string_io.string).to match(%(soylent green is)) + expect(string_io.string).to match(%(tasty)) + expect(string_io.string).not_to match(%(people)) + end + end +end diff --git a/spec/faraday/response/middleware_spec.rb b/spec/faraday/response/middleware_spec.rb new file mode 100644 index 000000000..c041d217e --- /dev/null +++ b/spec/faraday/response/middleware_spec.rb @@ -0,0 +1,50 @@ +RSpec.describe Faraday::Response::Middleware do + let(:conn) do + Faraday.new do |b| + b.use custom_middleware + b.adapter :test do |stub| + stub.get('ok') { [200, { 'Content-Type' => 'text/html' }, ''] } + stub.get('not_modified') { [304, nil, nil] } + stub.get('no_content') { [204, nil, nil] } + end + end + end + + context 'with a custom ResponseMiddleware' do + let(:custom_middleware) do + Class.new(Faraday::Response::Middleware) do + def parse(body) + body.upcase + end + end + end + + it 'parses the response' do + expect(conn.get('ok').body).to eq('') + end + end + + context 'with a custom ResponseMiddleware but empty response' do + let(:custom_middleware) do + Class.new(Faraday::Response::Middleware) do + def parse(body) + raise 'this should not be called' + end + end + end + + it 'raises exception for 200 responses' do + expect { conn.get('ok') }.to raise_error(StandardError) + end + + it 'doesn\'t call the middleware for 204 responses' do + expect_any_instance_of(custom_middleware).not_to receive(:parse) + expect(conn.get('no_content').body).to be_nil + end + + it 'doesn\'t call the middleware for 304 responses' do + expect_any_instance_of(custom_middleware).not_to receive(:parse) + expect(conn.get('not_modified').body).to be_nil + end + end +end \ No newline at end of file diff --git a/spec/faraday/response/raise_error_spec.rb b/spec/faraday/response/raise_error_spec.rb new file mode 100644 index 000000000..448a179c5 --- /dev/null +++ b/spec/faraday/response/raise_error_spec.rb @@ -0,0 +1,30 @@ +RSpec.describe Faraday::Response::RaiseError do + let(:conn) do + Faraday.new do |b| + b.response :raise_error + b.adapter :test do |stub| + stub.get('ok') { [200, {'Content-Type' => 'text/html'}, ''] } + stub.get('not-found') { [404, {'X-Reason' => 'because'}, 'keep looking'] } + stub.get('error') { [500, {'X-Error' => 'bailout'}, 'fail'] } + end + end + end + + it 'raises no exceptio for 200 responses' do + expect { conn.get('ok') }.not_to raise_error + end + + it 'raise Faraday::Error::ResourceNotFound for 404 responses' do + expect { conn.get('not-found') }.to raise_error(Faraday::Error::ResourceNotFound) do |ex| + expect(ex.message).to eq('the server responded with status 404') + expect(ex.response[:headers]['X-Reason']).to eq('because') + end + end + + it 'raise Faraday::Error::ClientError for 500 responses' do + expect { conn.get('error') }.to raise_error(Faraday::Error::ClientError) do |ex| + expect(ex.message).to eq('the server responded with status 500') + expect(ex.response[:headers]['X-Error']).to eq('bailout') + end + end +end diff --git a/spec/faraday/response_spec.rb b/spec/faraday/response_spec.rb new file mode 100644 index 000000000..805e9ea26 --- /dev/null +++ b/spec/faraday/response_spec.rb @@ -0,0 +1,74 @@ +RSpec.describe Faraday::Response do + subject { Faraday::Response.new(env) } + + let(:env) do + Faraday::Env.from(status: 404, body: 'yikes', + response_headers: { 'Content-Type' => 'text/plain' }) + end + + it { expect(subject.finished?).to be_truthy } + it { expect { subject.finish({}) }.to raise_error(RuntimeError) } + it { expect(subject.success?).to be_falsey } + it { expect(subject.status).to eq(404) } + it { expect(subject.body).to eq('yikes') } + it { expect(subject.headers['Content-Type']).to eq('text/plain') } + it { expect(subject['content-type']).to eq('text/plain') } + + describe '#apply_request' do + before { subject.apply_request(body: 'a=b', method: :post) } + + it { expect(subject.body).to eq('yikes') } + it { expect(subject.env[:method]).to eq(:post) } + end + + describe '#to_hash' do + let(:hash) { subject.to_hash } + + it { expect(hash).to be_a(Hash) } + it { expect(hash).to eq(env.to_hash) } + it { expect(hash[:status]).to eq(subject.status) } + it { expect(hash[:response_headers]).to eq(subject.headers) } + it { expect(hash[:body]).to eq(subject.body) } + end + + describe 'marshal serialization support' do + subject { Faraday::Response.new } + let(:loaded) { Marshal.load(Marshal.dump(subject)) } + + before do + subject.on_complete {} + subject.finish(env.merge(params: 'moo')) + end + + it { expect(loaded.env[:params]).to be_nil } + it { expect(loaded.env[:body]).to eq(env[:body]) } + it { expect(loaded.env[:response_headers]).to eq(env[:response_headers]) } + it { expect(loaded.env[:status]).to eq(env[:status]) } + end + + describe '#on_complete' do + subject { Faraday::Response.new } + + it 'parse body on finish' do + subject.on_complete { |env| env[:body] = env[:body].upcase } + subject.finish(env) + + expect(subject.body).to eq('YIKES') + end + + it 'can access response body in on_complete callback' do + subject.on_complete { |env| env[:body] = subject.body.upcase } + subject.finish(env) + + expect(subject.body).to eq('YIKES') + end + + it 'can access response body in on_complete callback' do + callback_env = nil + subject.on_complete { |env| callback_env = env } + subject.finish({}) + + expect(subject.env).to eq(callback_env) + end + end +end diff --git a/spec/faraday/utils/headers_spec.rb b/spec/faraday/utils/headers_spec.rb new file mode 100644 index 000000000..1dd5c4172 --- /dev/null +++ b/spec/faraday/utils/headers_spec.rb @@ -0,0 +1,80 @@ +RSpec.describe Faraday::Utils::Headers do + subject { Faraday::Utils::Headers.new } + + context 'when Content-Type is set to application/json' do + before { subject['Content-Type'] = 'application/json' } + + it { expect(subject.keys).to eq(['Content-Type']) } + it { expect(subject['Content-Type']).to eq('application/json') } + it { expect(subject['CONTENT-TYPE']).to eq('application/json') } + it { expect(subject['content-type']).to eq('application/json') } + it { is_expected.to include('content-type') } + end + + context 'when Content-Type is set to application/xml' do + before { subject['Content-Type'] = 'application/xml' } + + it { expect(subject.keys).to eq(['Content-Type']) } + it { expect(subject['Content-Type']).to eq('application/xml') } + it { expect(subject['CONTENT-TYPE']).to eq('application/xml') } + it { expect(subject['content-type']).to eq('application/xml') } + it { is_expected.to include('content-type') } + end + + describe '#fetch' do + before { subject['Content-Type'] = 'application/json' } + + it { expect(subject.fetch('Content-Type')).to eq('application/json') } + it { expect(subject.fetch('CONTENT-TYPE')).to eq('application/json') } + it { expect(subject.fetch(:content_type)).to eq('application/json') } + it { expect(subject.fetch('invalid', 'default')).to eq('default') } + it { expect(subject.fetch('invalid', false)).to eq(false) } + it { expect(subject.fetch('invalid', nil)).to be_nil } + it { expect(subject.fetch('Invalid') { |key| "#{key} key" }).to eq('Invalid key') } + it 'calls a block when provided' do + block_called = false + expect(subject.fetch('content-type') { block_called = true }).to eq('application/json') + expect(block_called).to be_falsey + end + it 'raises an error if key not found' do + expected_error = defined?(KeyError) ? KeyError : IndexError + expect { subject.fetch('invalid') }.to raise_error(expected_error) + end + end + + describe '#delete' do + before do + subject['Content-Type'] = 'application/json' + @deleted = subject.delete('content-type') + end + + it { expect(@deleted).to eq('application/json') } + it { expect(subject.size).to eq(0) } + it { is_expected.not_to include('content-type') } + it { expect(subject.delete('content-type')).to be_nil } + end + + describe '#parse' do + before { subject.parse(headers) } + + context 'when response headers leave http status line out' do + let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n" } + + it { expect(subject.keys).to eq(%w(Content-Type)) } + it { expect(subject['Content-Type']).to eq('text/html') } + it { expect(subject['content-type']).to eq('text/html') } + end + + context 'when response headers values include a colon' do + let(:headers) { "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://sushi.com/\r\n\r\n" } + + it { expect(subject['location']).to eq('http://sushi.com/') } + end + + context 'when response headers include a blank line' do + let(:headers) { "HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n" } + + it { expect(subject['content-type']).to eq('text/html') } + end + end +end diff --git a/spec/faraday/utils_spec.rb b/spec/faraday/utils_spec.rb new file mode 100644 index 000000000..ae058b6c3 --- /dev/null +++ b/spec/faraday/utils_spec.rb @@ -0,0 +1,54 @@ +RSpec.describe Faraday::Utils do + describe 'headers parsing' do + let(:multi_response_headers) { + "HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \ + "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n" + } + + it 'parse headers for aggregated responses' do + headers = Faraday::Utils::Headers.new + headers.parse(multi_response_headers) + + result = headers.to_hash + + expect(result["Content-Type"]).to eq("application/json; charset=UTF-8") + end + end + + describe 'URI parsing' do + let(:url) { "http://example.com/abc" } + + it 'escapes safe buffer' do + str = FakeSafeBuffer.new('$32,000.00') + expect(Faraday::Utils.escape(str)).to eq('%2432%2C000.00') + end + + it 'parses with default parser' do + with_default_uri_parser(nil) do + uri = normalize(url) + expect(uri.host).to eq('example.com') + end + end + + it 'parses with URI' do + with_default_uri_parser(::URI) do + uri = normalize(url) + expect(uri.host).to eq('example.com') + end + end + + it 'parses with block' do + with_default_uri_parser(lambda {|u| "booya#{"!" * u.size}" }) do + expect(normalize(url)).to eq('booya!!!!!!!!!!!!!!!!!!!!!!') + end + end + + it 'replaces headers hash' do + headers = Faraday::Utils::Headers.new('authorization' => 't0ps3cr3t!') + expect(headers).to have_key('authorization') + + headers.replace({'content-type' => 'text/plain'}) + expect(headers).not_to have_key('authorization') + end + end +end \ No newline at end of file diff --git a/spec/faraday_spec.rb b/spec/faraday_spec.rb new file mode 100644 index 000000000..2b9fc81f8 --- /dev/null +++ b/spec/faraday_spec.rb @@ -0,0 +1,5 @@ +RSpec.describe Faraday do + it 'has a version number' do + expect(Faraday::VERSION).not_to be nil + end +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 000000000..1164bc67e --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,141 @@ +# This file was generated by the `rspec --init` command. Conventionally, all +# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. +# The generated `.rspec` file contains `--require spec_helper` which will cause +# this file to always be loaded, without a need to explicitly require it in any +# files. +# +# Given that it is always loaded, you are encouraged to keep this file as +# light-weight as possible. Requiring heavyweight dependencies from this file +# will add to the boot time of your test suite on EVERY test run, even for an +# individual file that may not need all of that loaded. Instead, consider making +# a separate helper file that requires the additional dependencies and performs +# the additional setup, and require it from the spec files that actually need +# it. +# +# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration + +require 'simplecov' +require 'coveralls' +require 'webmock/rspec' +WebMock.disable_net_connect!(allow_localhost: true) + +SimpleCov.formatters = [SimpleCov::Formatter::HTMLFormatter, Coveralls::SimpleCov::Formatter] + +SimpleCov.start do + add_filter '/spec/' + minimum_coverage 90 + minimum_coverage_by_file 70 +end + +require 'faraday' +require 'pry' + +Dir['./spec/support/**/*.rb'].each { |f| require f } + +RSpec.configure do |config| + # rspec-expectations config goes here. You can use an alternate + # assertion/expectation library such as wrong or the stdlib/minitest + # assertions if you prefer. + config.expect_with :rspec do |expectations| + # This option will default to `true` in RSpec 4. It makes the `description` + # and `failure_message` of custom matchers include text for helper methods + # defined using `chain`, e.g.: + # be_bigger_than(2).and_smaller_than(4).description + # # => "be bigger than 2 and smaller than 4" + # ...rather than: + # # => "be bigger than 2" + expectations.include_chain_clauses_in_custom_matcher_descriptions = true + end + + # rspec-mocks config goes here. You can use an alternate test double + # library (such as bogus or mocha) by changing the `mock_with` option here. + config.mock_with :rspec do |mocks| + # Prevents you from mocking or stubbing a method that does not exist on + # a real object. This is generally recommended, and will default to + # `true` in RSpec 4. + mocks.verify_partial_doubles = true + end + + # This option will default to `:apply_to_host_groups` in RSpec 4 (and will + # have no way to turn it off -- the option exists only for backwards + # compatibility in RSpec 3). It causes shared context metadata to be + # inherited by the metadata hash of host groups and examples, rather than + # triggering implicit auto-inclusion in groups with matching metadata. + config.shared_context_metadata_behavior = :apply_to_host_groups + + # This allows you to limit a spec run to individual examples or groups + # you care about by tagging them with `:focus` metadata. When nothing + # is tagged with `:focus`, all examples get run. RSpec also provides + # aliases for `it`, `describe`, and `context` that include `:focus` + # metadata: `fit`, `fdescribe` and `fcontext`, respectively. + # config.filter_run_when_matching :focus + + # Allows RSpec to persist some state between runs in order to support + # the `--only-failures` and `--next-failure` CLI options. We recommend + # you configure your source control system to ignore this file. + # config.example_status_persistence_file_path = "spec/examples.txt" + + # Limits the available syntax to the non-monkey patched syntax that is + # recommended. For more details, see: + # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/ + # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/ + # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode + # config.disable_monkey_patching! + + # This setting enables warnings. It's recommended, but in some cases may + # be too noisy due to issues in dependencies. + # config.warnings = true + + # Many RSpec users commonly either run the entire suite or an individual + # file, and it's useful to allow more verbose output when running an + # individual spec file. + # if config.files_to_run.one? + # # Use the documentation formatter for detailed output, + # # unless a formatter has already been configured + # # (e.g. via a command-line flag). + # config.default_formatter = "doc" + # end + + # Print the 10 slowest examples and example groups at the + # end of the spec run, to help surface which specs are running + # particularly slow. + # config.profile_examples = 10 + + # Run specs in random order to surface order dependencies. If you find an + # order dependency and want to debug it, you can fix the order by providing + # the seed, which is printed after each run. + # --seed 1234 + config.order = :random + + # Seed global randomization in this process using the `--seed` CLI option. + # Setting this allows you to use `--seed` to deterministically reproduce + # test failures related to randomization by passing the same `--seed` value + # as the one that triggered the failure. + Kernel.srand config.seed + + config.include Faraday::HelperMethods +end + +# Extends RSpec DocumentationFormatter to hide skipped tests. +module FormatterOverrides + def example_pending(_) + end + + def dump_pending(_) + end + + RSpec::Core::Formatters::DocumentationFormatter.prepend self +end + +# Allows to disable WebMock stubs +module DisablingStub + def disable + @disabled = true + end + + def disabled? + @disabled + end + + WebMock::RequestStub.prepend self +end diff --git a/spec/support/fake_safe_buffer.rb b/spec/support/fake_safe_buffer.rb new file mode 100644 index 000000000..fc7b365a2 --- /dev/null +++ b/spec/support/fake_safe_buffer.rb @@ -0,0 +1,10 @@ +# emulates ActiveSupport::SafeBuffer#gsub +FakeSafeBuffer = Struct.new(:string) do + def to_s; self end + def gsub(regex) + string.gsub(regex) { + match, = $&, '' =~ /a/ + yield(match) + } + end +end \ No newline at end of file diff --git a/spec/support/helper_methods.rb b/spec/support/helper_methods.rb new file mode 100644 index 000000000..76319ea21 --- /dev/null +++ b/spec/support/helper_methods.rb @@ -0,0 +1,97 @@ +module Faraday + module HelperMethods + def self.included(base) + base.extend ClassMethods + end + + module ClassMethods + def features(*features) + @features = features + end + + def on_feature(name, &block) + if @features.nil? + superclass.on_feature(name, &block) if superclass.respond_to?(:on_feature) + else + yield if block_given? and @features.include?(name) + end + end + + def method_with_body?(method) + METHODS_WITH_BODY.include?(method.to_s) + end + end + + def ssl_mode? + ENV['SSL'] == 'yes' + end + + def normalize(url) + Faraday::Utils::URI(url) + end + + def with_default_uri_parser(parser) + old_parser = Faraday::Utils.default_uri_parser + begin + Faraday::Utils.default_uri_parser = parser + yield + ensure + Faraday::Utils.default_uri_parser = old_parser + end + end + + def with_env(new_env) + old_env = {} + + new_env.each do |key, value| + old_env[key] = ENV.fetch(key, false) + ENV[key] = value + end + + begin + yield + ensure + old_env.each do |key, value| + if value == false + ENV.delete key + else + ENV[key] = value + end + end + end + end + + def with_env_proxy_disabled + Faraday.ignore_env_proxy = true + + begin + yield + ensure + Faraday.ignore_env_proxy = false + end + end + + def capture_warnings + old, $stderr = $stderr, StringIO.new + begin + yield + $stderr.string + ensure + $stderr = old + end + end + + def multipart_file + Faraday::UploadIO.new(__FILE__, 'text/x-ruby') + end + + def method_with_body?(method) + self.class.method_with_body?(method) + end + + def big_string + kb = 1024 + (32..126).map{|i| i.chr}.cycle.take(50*kb).join + end + end +end \ No newline at end of file diff --git a/spec/support/shared_examples/adapter.rb b/spec/support/shared_examples/adapter.rb new file mode 100644 index 000000000..040eac967 --- /dev/null +++ b/spec/support/shared_examples/adapter.rb @@ -0,0 +1,102 @@ +shared_examples 'an adapter' do |**options| + context 'with SSL enabled' do + before { ENV['SSL'] = 'yes' } + include_examples 'adapter examples', options + end + + context 'with SSL disabled' do + before { ENV['SSL'] = 'no' } + include_examples 'adapter examples' + end +end + +shared_examples 'adapter examples' do |**options| + include Faraday::StreamingResponseChecker + + let(:adapter) { described_class.name.split('::').last } + + let(:conn_options) { { headers: { 'X-Faraday-Adapter' => adapter } }.merge(options[:conn_options] || {}) } + + let(:adapter_options) do + return [] unless options[:adapter_options] + if options[:adapter_options].is_a?(Array) + options[:adapter_options] + else + [options[:adapter_options]] + end + end + + let(:protocol) { ssl_mode? ? 'https' : 'http' } + let(:remote) { "#{protocol}://example.com" } + + let(:conn) do + conn_options[:ssl] ||= {} + conn_options[:ssl][:ca_file] ||= ENV['SSL_FILE'] + + Faraday.new(remote, conn_options) do |conn| + conn.request :multipart + conn.request :url_encoded + conn.response :raise_error + conn.adapter described_class, *adapter_options + end + end + + let!(:request_stub) { stub_request(http_method, remote) } + + after do + expect(request_stub).to have_been_requested unless request_stub.disabled? + end + + describe '#get' do + let(:http_method) { :get } + + it_behaves_like 'a request method', :get + + on_feature :body_on_get do + it 'handles request body' do + body = { bodyrock: 'true' } + request_stub.with(body: body) + conn.get('/') do |req| + req.body = body + end + end + end + end + + describe '#post' do + let(:http_method) { :post } + + it_behaves_like 'a request method', :post + end + + describe '#put' do + let(:http_method) { :put } + + it_behaves_like 'a request method', :put + end + + describe '#delete' do + let(:http_method) { :delete } + + it_behaves_like 'a request method', :delete + end + + describe '#patch' do + let(:http_method) { :patch } + + it_behaves_like 'a request method', :patch + end + + describe '#head' do + let(:http_method) { :head } + + it_behaves_like 'a request method', :head + end + + # TODO: Enable after adding API for options method + # describe '#options' do + # let(:http_method) { :options } + # + # it_behaves_like 'a request method' + # end +end \ No newline at end of file diff --git a/spec/support/shared_examples/params_encoder.rb b/spec/support/shared_examples/params_encoder.rb new file mode 100644 index 000000000..784625c95 --- /dev/null +++ b/spec/support/shared_examples/params_encoder.rb @@ -0,0 +1,16 @@ +shared_examples 'a params encoder' do + it 'escapes safe buffer' do + monies = FakeSafeBuffer.new('$32,000.00') + expect(subject.encode('a' => monies)).to eq('a=%2432%2C000.00') + end + + it 'raises type error for empty string' do + expect { subject.encode('') }.to raise_error(TypeError) do |error| + expect(error.message).to eq("Can't convert String into Hash.") + end + end + + it 'encodes nil' do + expect(subject.encode('a' => nil)).to eq('a') + end +end \ No newline at end of file diff --git a/spec/support/shared_examples/request_method.rb b/spec/support/shared_examples/request_method.rb new file mode 100644 index 000000000..6f76260d5 --- /dev/null +++ b/spec/support/shared_examples/request_method.rb @@ -0,0 +1,181 @@ +shared_examples 'a request method' do |http_method| + let(:query_or_body) { method_with_body?(http_method) ? :body : :query } + let(:response) { conn.public_send(http_method, '/') } + + it 'retrieves the response body' do + res_body = 'test' + request_stub.to_return(body: res_body) + expect(conn.public_send(http_method, '/').body).to eq(res_body) + end + + it 'handles headers with multiple values' do + request_stub.to_return(headers: { 'Set-Cookie' => 'one, two' }) + expect(response.headers['set-cookie']).to eq('one, two') + end + + it 'retrieves the response headers' do + request_stub.to_return(headers: { 'Content-Type' => 'text/plain' }) + expect(response.headers['Content-Type']).to match(/text\/plain/) + expect(response.headers['content-type']).to match(/text\/plain/) + end + + it 'sends user agent' do + request_stub.with(headers: { 'User-Agent' => 'Agent Faraday' }) + conn.public_send(http_method, '/', nil, user_agent: 'Agent Faraday') + end + + it 'represents empty body response as blank string' do + expect(response.body).to eq('') + end + + it 'handles connection error' do + request_stub.disable + expect { conn.public_send(http_method, 'http://localhost:4') }.to raise_error(Faraday::Error::ConnectionFailed) + end + + # context 'when wrong ssl certificate is provided' do + # let(:ca_file_path) { 'tmp/faraday-different-ca-cert.crt' } + # before { conn_options.merge!(ssl: { ca_file: ca_file_path }) } + # + # it do + # expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::SSLError) # do |ex| + # expect(ex.message).to include?('certificate') + # end + # end + # end + + it 'sends url encoded parameters' do + payload = { name: 'zack' } + request_stub.with(Hash[query_or_body, payload]) + conn.public_send(http_method, '/', payload) + end + + it 'sends url encoded nested parameters' do + payload = { name: { first: 'zack' } } + request_stub.with(Hash[query_or_body, payload]) + conn.public_send(http_method, '/', payload) + end + + # TODO: This needs reimplementation: see https://github.com/lostisland/faraday/issues/718 + # Should raise Faraday::TimeoutError + it 'supports timeout option' do + conn_options[:request] = { timeout: 1 } + request_stub.to_timeout + exc = adapter == 'NetHttp' ? Faraday::Error::ConnectionFailed : Faraday::TimeoutError + expect { conn.public_send(http_method, '/') }.to raise_error(exc) + end + + # TODO: This needs reimplementation: see https://github.com/lostisland/faraday/issues/718 + # Should raise Faraday::Error::ConnectionFailed + it 'supports open_timeout option' do + conn_options[:request] = { open_timeout: 1 } + request_stub.to_timeout + exc = adapter == 'NetHttp' ? Faraday::Error::ConnectionFailed : Faraday::TimeoutError + expect { conn.public_send(http_method, '/') }.to raise_error(exc) + end + + # Can't send files on get, head and delete methods + if method_with_body?(http_method) + it 'sends files' do + payload = { uploaded_file: multipart_file } + request_stub.with(headers: { "Content-Type" => %r[\Amultipart/form-data] }) do |request| + # WebMock does not support matching body for multipart/form-data requests yet :( + # https://github.com/bblimke/webmock/issues/623 + request.body =~ %r[RubyMultipartPost] + end + conn.public_send(http_method, '/', payload) + end + end + + on_feature :reason_phrase_parse do + it 'parses the reason phrase' do + request_stub.to_return(status: [200, 'OK']) + expect(response.reason_phrase).to eq('OK') + end + end + + on_feature :compression do + # Accept-Encoding header not sent for HEAD requests as body is not expected in the response. + unless http_method == :head + it 'handles gzip compression' do + request_stub.with(headers: { 'Accept-Encoding' => %r[\bgzip\b] }) + conn.public_send(http_method, '/') + end + + it 'handles deflate compression' do + request_stub.with(headers: { 'Accept-Encoding' => %r[\bdeflate\b] }) + conn.public_send(http_method, '/') + end + end + end + + on_feature :streaming do + describe 'streaming' do + let(:streamed) { [] } + + context 'when response is empty' do + it do + conn.public_send(http_method, '/') do |req| + req.options.on_data = Proc.new { |*args| streamed << args } + end + + expect(streamed).to eq([["", 0]]) + end + end + + context 'when response contains big data' do + before { request_stub.to_return(body: big_string) } + + + it 'handles streaming' do + response = conn.public_send(http_method, '/') do |req| + req.options.on_data = Proc.new { |*args| streamed << args } + end + + expect(response.body).to eq('') + check_streaming_response(streamed, chunk_size: 16 * 1024) + end + end + end + end + + on_feature :parallel do + it 'handles parallel requests' do + resp1, resp2 = nil, nil + payload1 = { a: '1' } + payload2 = { b: '2' } + request_stub.with(Hash[query_or_body, payload1]) + .to_return(body: payload1.to_json) + stub_request(http_method, remote).with(Hash[query_or_body, payload2]) + .to_return(body: payload2.to_json) + + conn.in_parallel do + resp1 = conn.public_send(http_method, '/', payload1) + resp2 = conn.public_send(http_method, '/', payload2) + + expect(conn.in_parallel?).to be_truthy + expect(resp1.body).to be_nil + expect(resp2.body).to be_nil + end + + expect(conn.in_parallel?).to be_falsey + expect(resp1.body).to eq(payload1.to_json) + expect(resp2.body).to eq(payload2.to_json) + end + end + + it 'handles requests with proxy' do + conn_options[:proxy] = 'http://google.co.uk' + + # binding.pry + res = conn.public_send(http_method, '/') + expect(res.status).to eq(200) + end + + it 'handles proxy failures' do + conn_options[:proxy] = 'http://google.co.uk' + request_stub.to_return(status: 407) + + expect { conn.public_send(http_method, '/') }.to raise_error(Faraday::Error::ConnectionFailed) + end +end \ No newline at end of file diff --git a/spec/support/streaming_response_checker.rb b/spec/support/streaming_response_checker.rb new file mode 100644 index 000000000..d8c5f6684 --- /dev/null +++ b/spec/support/streaming_response_checker.rb @@ -0,0 +1,33 @@ +module Faraday + module StreamingResponseChecker + def check_streaming_response(streamed, options = {}) + opts = { + :prefix => '', + :streaming? => true + }.merge(options) + + expected_response = opts[:prefix] + big_string + + chunks, sizes = streamed.transpose + + # Check that the total size of the chunks (via the last size returned) + # is the same size as the expected_response + expect(sizes.last).to eq(expected_response.bytesize) + + start_index = 0 + expected_chunks = [] + chunks.each do |actual_chunk| + expected_chunk = expected_response[start_index..((start_index + actual_chunk.bytesize) - 1)] + expected_chunks << expected_chunk + start_index += expected_chunk.bytesize + end + + # it's easier to read a smaller portion, so we check that first + expect(expected_chunks[0][0..255]).to eq(chunks[0][0..255]) + + [expected_chunks, chunks].transpose.each do |expected, actual| + expect(actual).to eq(expected) + end + end + end +end \ No newline at end of file diff --git a/test/adapters/default_test.rb b/test/adapters/default_test.rb deleted file mode 100644 index 4d30064fb..000000000 --- a/test/adapters/default_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class DefaultTest < Faraday::TestCase - - def adapter() :default end - - Integration.apply(self, :NonParallel, :Streaming) do - # default stack is not configured with Multipart - undef :test_POST_sends_files - end - - end -end diff --git a/test/adapters/excon_test.rb b/test/adapters/excon_test.rb deleted file mode 100644 index dd1b82fdc..000000000 --- a/test/adapters/excon_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class ExconTest < Faraday::TestCase - - def adapter() :excon end - - Integration.apply(self, :NonParallel, :NonStreaming) do - # https://github.com/geemus/excon/issues/126 ? - undef :test_timeout if ssl_mode? - - # Excon lets OpenSSL::SSL::SSLError be raised without any way to - # distinguish whether it happened because of a 407 proxy response - undef :test_proxy_auth_fail if ssl_mode? - - # https://github.com/geemus/excon/issues/358 - undef :test_connection_error if RUBY_VERSION >= '2.1.0' - end - - def test_custom_adapter_config - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::Excon.new nil, debug_request: true - - conn = adapter.create_connection({url: url}, {}) - - assert_equal true, conn.data[:debug_request] - end - end -end diff --git a/test/adapters/httpclient_test.rb b/test/adapters/httpclient_test.rb deleted file mode 100644 index e8b87f281..000000000 --- a/test/adapters/httpclient_test.rb +++ /dev/null @@ -1,34 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class HttpclientTest < Faraday::TestCase - - def adapter() :httpclient end - - Integration.apply(self, :NonParallel, :Compression) do - def setup - require 'httpclient' unless defined?(HTTPClient) - HTTPClient::NO_PROXY_HOSTS.delete('localhost') - end - - def test_binds_local_socket - host = '1.2.3.4' - conn = create_connection :request => { :bind => { :host => host } } - assert_equal host, conn.options[:bind][:host] - end - - def test_custom_adapter_config - adapter = Faraday::Adapter::HTTPClient.new do |client| - client.keep_alive_timeout = 20 - client.ssl_config.timeout = 25 - end - - client = adapter.client - adapter.configure_client - - assert_equal 20, client.keep_alive_timeout - assert_equal 25, client.ssl_config.timeout - end - end - end -end diff --git a/test/adapters/integration.rb b/test/adapters/integration.rb index ac0646331..d36227c1e 100644 --- a/test/adapters/integration.rb +++ b/test/adapters/integration.rb @@ -14,7 +14,7 @@ def self.apply(base, *extra_features) features = [:Common] features.concat extra_features features << :SSL if base.ssl_mode? - features.each {|name| base.send(:include, self.const_get(name)) } + features.each { |name| base.send(:include, self.const_get(name)) } yield if block_given? elsif !defined? @warned warn "Warning: Not running integration tests against a live server." @@ -24,24 +24,6 @@ def self.apply(base, *extra_features) end end - module Parallel - def test_in_parallel - resp1, resp2 = nil, nil - - connection = create_connection - connection.in_parallel do - resp1 = connection.get('echo?a=1') - resp2 = connection.get('echo?b=2') - assert connection.in_parallel? - assert_nil resp1.body - assert_nil resp2.body - end - assert !connection.in_parallel? - assert_equal 'get ?{"a"=>"1"}', resp1.body - assert_equal 'get ?{"b"=>"2"}', resp2.body - end - end - module NonParallel def test_no_parallel_support connection = create_connection @@ -67,7 +49,7 @@ def test_callback_is_called_in_parallel_with_no_streaming_support err = capture_warnings do connection.in_parallel do resp1, streamed1 = streaming_request(connection, :get, 'stream?a=1') - resp2, streamed2 = streaming_request(connection, :get, 'stream?b=2', :chunk_size => 16*1024) + resp2, streamed2 = streaming_request(connection, :get, 'stream?b=2', :chunk_size => 16 * 1024) assert connection.in_parallel? assert_nil resp1.body assert_nil resp2.body @@ -77,39 +59,15 @@ def test_callback_is_called_in_parallel_with_no_streaming_support end assert !connection.in_parallel? assert_match(/Streaming .+ not yet implemented/, err) - opts = {:streaming? => false, :chunk_size => 16*1024} + opts = { :streaming? => false, :chunk_size => 16 * 1024 } check_streaming_response(streamed1, opts.merge(:prefix => '{"a"=>"1"}')) check_streaming_response(streamed2, opts.merge(:prefix => '{"b"=>"2"}')) end end - module Streaming - def test_GET_streaming - response, streamed = streaming_request(create_connection, :get, 'stream') - check_streaming_response(streamed, :chunk_size => 16*1024) - assert_equal "", response.body - end - - def test_non_GET_streaming - response, streamed = streaming_request(create_connection, :post, 'stream') - check_streaming_response(streamed, :chunk_size => 16*1024) - - assert_equal "", response.body - end - - def test_GET_streaming_empty_response - _, streamed = streaming_request(create_connection, :get, 'empty_stream') - assert_equal [["", 0]], streamed - end - - def test_non_GET_streaming_empty_response - _, streamed = streaming_request(create_connection, :post, 'empty_stream') - assert_equal [["", 0]], streamed - end - end - module NonStreaming include Faraday::Shared + def test_GET_streaming response, streamed = nil err = capture_warnings do @@ -133,18 +91,10 @@ def test_non_GET_streaming end end - module Compression - def test_GET_handles_compression - res = get('echo_header', :name => 'accept-encoding') - assert_match(/\bgzip\b/, res.body) - assert_match(/\bdeflate\b/, res.body) - end - end - module SSL def test_GET_ssl_fails_with_bad_cert ca_file = 'tmp/faraday-different-ca-cert.crt' - conn = create_connection(:ssl => {:ca_file => ca_file}) + conn = create_connection(:ssl => { :ca_file => ca_file }) err = assert_raises Faraday::SSLError do conn.get('/ssl') end @@ -156,152 +106,6 @@ module Common extend Forwardable def_delegators :create_connection, :get, :head, :put, :post, :patch, :delete, :run_request - def test_GET_retrieves_the_response_body - assert_equal 'get', get('echo').body - end - - def test_GET_send_url_encoded_params - assert_equal %(get ?{"name"=>"zack"}), get('echo', :name => 'zack').body - end - - def test_GET_retrieves_the_response_headers - response = get('echo') - assert_match(/text\/plain/, response.headers['Content-Type'], 'original case fail') - assert_match(/text\/plain/, response.headers['content-type'], 'lowercase fail') - end - - def test_GET_handles_headers_with_multiple_values - assert_equal 'one, two', get('multi').headers['set-cookie'] - end - - def test_GET_with_body - response = get('echo') do |req| - req.body = {'bodyrock' => true} - end - assert_equal %(get {"bodyrock"=>"true"}), response.body - end - - def test_GET_sends_user_agent - response = get('echo_header', {:name => 'user-agent'}, :user_agent => 'Agent Faraday') - assert_equal 'Agent Faraday', response.body - end - - def test_GET_ssl - expected = self.class.ssl_mode?.to_s - assert_equal expected, get('ssl').body - end - - def test_GET_reason_phrase - response = get('echo') - assert_equal "OK", response.reason_phrase - end - - def test_POST_send_url_encoded_params - assert_equal %(post {"name"=>"zack"}), post('echo', :name => 'zack').body - end - - def test_POST_send_url_encoded_nested_params - resp = post('echo', 'name' => {'first' => 'zack'}) - assert_equal %(post {"name"=>{"first"=>"zack"}}), resp.body - end - - def test_POST_retrieves_the_response_headers - assert_match(/text\/plain/, post('echo').headers['content-type']) - end - - def test_POST_sends_files - resp = post('file') do |req| - req.body = {'uploaded_file' => Faraday::UploadIO.new(__FILE__, 'text/x-ruby')} - end - assert_equal "file integration.rb text/x-ruby #{File.size(__FILE__)}", resp.body - end - - def test_PUT_send_url_encoded_params - assert_equal %(put {"name"=>"zack"}), put('echo', :name => 'zack').body - end - - def test_PUT_send_url_encoded_nested_params - resp = put('echo', 'name' => {'first' => 'zack'}) - assert_equal %(put {"name"=>{"first"=>"zack"}}), resp.body - end - - def test_PUT_retrieves_the_response_headers - assert_match(/text\/plain/, put('echo').headers['content-type']) - end - - def test_PATCH_send_url_encoded_params - assert_equal %(patch {"name"=>"zack"}), patch('echo', :name => 'zack').body - end - - def test_OPTIONS - resp = run_request(:options, 'echo', nil, {}) - assert_equal 'options', resp.body - end - - def test_HEAD_retrieves_no_response_body - assert_equal '', head('echo').body - end - - def test_HEAD_retrieves_the_response_headers - assert_match(/text\/plain/, head('echo').headers['content-type']) - end - - def test_DELETE_retrieves_the_response_headers - assert_match(/text\/plain/, delete('echo').headers['content-type']) - end - - def test_DELETE_retrieves_the_body - assert_equal %(delete), delete('echo').body - end - - def test_timeout - conn = create_connection(:request => {:timeout => 1, :open_timeout => 1}) - assert_raises Faraday::Error::TimeoutError do - conn.get '/slow' - end - end - - def test_connection_error - assert_raises Faraday::Error::ConnectionFailed do - get 'http://localhost:4' - end - end - - def test_proxy - proxy_uri = URI(ENV['LIVE_PROXY']) - conn = create_connection(:proxy => proxy_uri) - - res = conn.get '/echo' - assert_equal 'get', res.body - - unless self.class.ssl_mode? - # proxy can't append "Via" header for HTTPS responses - assert_match(/:#{proxy_uri.port}$/, res['via']) - end - end - - def test_proxy_auth_fail - proxy_uri = URI(ENV['LIVE_PROXY']) - proxy_uri.password = 'WRONG' - conn = create_connection(:proxy => proxy_uri) - - err = assert_raises Faraday::Error::ConnectionFailed do - conn.get '/echo' - end - - unless self.class.ssl_mode? && (self.class.jruby? || - adapter == :em_http || adapter == :em_synchrony) - # JRuby raises "End of file reached" which cannot be distinguished from a 407 - # EM raises "connection closed by server" due to https://github.com/igrigorik/em-socksify/pull/19 - assert_equal %{407 "Proxy Authentication Required "}, err.message - end - end - - def test_empty_body_response_represented_as_blank_string - response = get('204') - assert_equal '', response.body - end - def adapter raise NotImplementedError.new("Need to override #adapter") end @@ -334,44 +138,6 @@ def create_connection(options = {}, &optional_connection_config_blk) conn.builder.insert_before adapter_handler, Faraday::Response::RaiseError end end - - def streaming_request(connection, method, path, options={}) - streamed = [] - response = connection.send(method, path) do |req| - req.options.on_data = Proc.new{|*args| streamed << args} - end - - [response, streamed] - end - - def check_streaming_response(streamed, options={}) - opts = { - :prefix => '', - :streaming? => true - }.merge(options) - expected_response = opts[:prefix] + Faraday::Shared.big_string - - chunks, sizes = streamed.transpose - - # Check that the total size of the chunks (via the last size returned) - # is the same size as the expected_response - assert_equal sizes.last, expected_response.bytesize - - start_index = 0 - expected_chunks = [] - chunks.each do |actual_chunk| - expected_chunk = expected_response[start_index..((start_index + actual_chunk.bytesize)-1)] - expected_chunks << expected_chunk - start_index += expected_chunk.bytesize - end - - # it's easier to read a smaller portion, so we check that first - assert_equal expected_chunks[0][0..255], chunks[0][0..255] - - [expected_chunks, chunks].transpose.each do |expected, actual| - assert_equal expected, actual - end - end end end end diff --git a/test/adapters/logger_test.rb b/test/adapters/logger_test.rb deleted file mode 100644 index 618efe267..000000000 --- a/test/adapters/logger_test.rb +++ /dev/null @@ -1,136 +0,0 @@ -require File.expand_path('../../helper', __FILE__) -require 'stringio' -require 'logger' - -module Adapters - class LoggerTest < Faraday::TestCase - def conn(logger, logger_options={}) - rubbles = ['Barney', 'Betty', 'Bam Bam'] - - Faraday.new do |b| - b.response :logger, @logger, logger_options do | logger | - logger.filter(/(soylent green is) (.+)/,'\1 tasty') - logger.filter(/(api_key:).*"(.+)."/,'\1[API_KEY]') - logger.filter(/(password)=(.+)/,'\1=[HIDDEN]') - end - b.adapter :test do |stubs| - stubs.get('/hello') { [200, {'Content-Type' => 'text/html'}, 'hello'] } - stubs.post('/ohai') { [200, {'Content-Type' => 'text/html'}, 'fred'] } - stubs.post('/ohyes') { [200, {'Content-Type' => 'text/html'}, 'pebbles'] } - stubs.get('/rubbles') { [200, {'Content-Type' => 'application/json'}, rubbles] } - stubs.get('/filtered_body') { [200, {'Content-Type' => 'text/html'}, 'soylent green is people'] } - stubs.get('/filtered_headers') { [200, {'Content-Type' => 'text/html'}, 'headers response'] } - stubs.get('/filtered_params') { [200, {'Content-Type' => 'text/html'}, 'params response'] } - stubs.get('/filtered_url') { [200, {'Content-Type' => 'text/html'}, 'url response'] } - end - end - end - - def setup - @io = StringIO.new - @logger = Logger.new(@io) - @logger.level = Logger::DEBUG - - @conn = conn(@logger) - end - - def test_still_returns_output - resp = @conn.get '/hello', nil, :accept => 'text/html' - assert_equal 'hello', resp.body - end - - def test_logs_method_and_url - @conn.get '/hello', nil, :accept => 'text/html' - assert_match 'request: GET http:/hello', @io.string - end - - def test_logs_status_code - @conn.get '/hello', nil, :accept => 'text/html' - assert_match 'response: Status 200', @io.string - end - - def test_logs_request_headers_by_default - @conn.get '/hello', nil, :accept => 'text/html' - assert_match %(Accept: "text/html), @io.string - end - - def test_logs_response_headers_by_default - @conn.get '/hello', nil, :accept => 'text/html' - assert_match %(Content-Type: "text/html), @io.string - end - - def test_does_not_log_request_headers_if_option_is_false - app = conn(@logger, :headers => { :request => false }) - app.get '/hello', nil, :accept => 'text/html' - refute_match %(Accept: "text/html), @io.string - end - - def test_does_log_response_headers_if_option_is_false - app = conn(@logger, :headers => { :response => false }) - app.get '/hello', nil, :accept => 'text/html' - refute_match %(Content-Type: "text/html), @io.string - end - - def test_does_not_log_request_body_by_default - @conn.post '/ohai', 'name=Unagi', :accept => 'text/html' - refute_match %(name=Unagi), @io.string - end - - def test_does_not_log_response_body_by_default - @conn.post '/ohai', 'name=Toro', :accept => 'text/html' - refute_match %(fred), @io.string - end - - def test_log_only_request_body - app = conn(@logger, :bodies => { :request => true }) - app.post '/ohyes', 'name=Tamago', :accept => 'text/html' - assert_match %(name=Tamago), @io.string - refute_match %(pebbles), @io.string - end - - def test_log_only_response_body - app = conn(@logger, :bodies => { :response => true }) - app.post '/ohyes', 'name=Hamachi', :accept => 'text/html' - assert_match %(pebbles), @io.string - refute_match %(name=Hamachi), @io.string - end - - def test_log_request_and_response_body - app = conn(@logger, :bodies => true) - app.post '/ohyes', 'name=Ebi', :accept => 'text/html' - assert_match %(name=Ebi), @io.string - assert_match %(pebbles), @io.string - end - - def test_log_response_body_object - app = conn(@logger, :bodies => true) - app.get '/rubbles', nil, :accept => 'text/html' - assert_match %([\"Barney\", \"Betty\", \"Bam Bam\"]\n), @io.string - end - - def test_logs_filter_body - app = conn(@logger, :bodies => true) - app.get '/filtered_body', nil, :accept => 'text/html' - assert_match %(soylent green is), @io.string - assert_match %(tasty), @io.string - refute_match %(people), @io.string - end - - def test_logs_filter_headers - app = conn(@logger) - app.headers = {'api_key' => 'ABC123'} - app.get '/filtered_headers', nil, :accept => 'text/html' - assert_match %(api_key:), @io.string - assert_match %([API_KEY]), @io.string - refute_match %(ABC123), @io.string - end - - def test_logs_filter_url - app = conn(@logger) - app.get '/filtered_url?password=hunter2', nil, :accept => 'text/html' - assert_match %(password=[HIDDEN]), @io.string - refute_match %(hunter2), @io.string - end - - end -end diff --git a/test/adapters/net_http_persistent_test.rb b/test/adapters/net_http_persistent_test.rb deleted file mode 100644 index e9adcb3dd..000000000 --- a/test/adapters/net_http_persistent_test.rb +++ /dev/null @@ -1,114 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class NetHttpPersistentTest < Faraday::TestCase - - def adapter() :net_http_persistent end - - Integration.apply(self, :NonParallel) do - def setup - if defined?(Net::HTTP::Persistent) - # work around problems with mixed SSL certificates - # https://github.com/drbrain/net-http-persistent/issues/45 - if Net::HTTP::Persistent.instance_method(:initialize).parameters.first == [:key, :name] - Net::HTTP::Persistent.new(name: 'Faraday').reconnect_ssl - else - Net::HTTP::Persistent.new('Faraday').ssl_cleanup(4) - end - end - end if ssl_mode? - - def test_reuses_tcp_sockets - # Ensure that requests are not reused from previous tests - Thread.current.keys - .select { |key| key.to_s =~ /\Anet_http_persistent_Faraday_/ } - .each { |key| Thread.current[key] = nil } - - sockets = [] - tcp_socket_open_wrapped = Proc.new do |*args, &block| - socket = TCPSocket.__minitest_stub__open(*args, &block) - sockets << socket - socket - end - - TCPSocket.stub :open, tcp_socket_open_wrapped do - conn = create_connection - conn.post("/echo", :foo => "bar") - conn.post("/echo", :foo => "baz") - end - - assert_equal 1, sockets.count - end - - def test_does_not_reuse_tcp_sockets_when_proxy_changes - # Ensure that requests are not reused from previous tests - Thread.current.keys - .select { |key| key.to_s =~ /\Anet_http_persistent_Faraday_/ } - .each { |key| Thread.current[key] = nil } - - sockets = [] - tcp_socket_open_wrapped = Proc.new do |*args, &block| - socket = TCPSocket.__minitest_stub__open(*args, &block) - sockets << socket - socket - end - - TCPSocket.stub :open, tcp_socket_open_wrapped do - conn = create_connection - conn.post("/echo", :foo => "bar") - conn.proxy = URI(ENV["LIVE_PROXY"]) - conn.post("/echo", :foo => "bar") - end - - assert_equal 2, sockets.count - end - end - - def test_without_custom_connection_config - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::NetHttpPersistent.new - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - - # `pool` is only present in net_http_persistent >= 3.0 - assert http.pool.size != nil if http.respond_to?(:pool) - end - - def test_custom_connection_config - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::NetHttpPersistent.new(nil, {pool_size: 5}) - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - - # `pool` is only present in net_http_persistent >= 3.0 - assert_equal 5, http.pool.size if http.respond_to?(:pool) - end - - def test_custom_adapter_config - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::NetHttpPersistent.new do |http| - http.idle_timeout = 123 - end - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - adapter.send(:configure_request, http, {}) - - assert_equal 123, http.idle_timeout - end - - def test_max_retries - url = URI('http://example.com') - - adapter = Faraday::Adapter::NetHttpPersistent.new - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - adapter.send(:configure_request, http, {}) - - # `max_retries=` is only present in Ruby 2.5 - assert_equal 0, http.max_retries if http.respond_to?(:max_retries=) - end - end -end diff --git a/test/adapters/net_http_test.rb b/test/adapters/net_http_test.rb deleted file mode 100644 index ead1a5a10..000000000 --- a/test/adapters/net_http_test.rb +++ /dev/null @@ -1,79 +0,0 @@ -require File.expand_path('../integration', __FILE__) -require 'ostruct' -require 'uri' - -module Adapters - class NetHttpTest < Faraday::TestCase - - def adapter() :net_http end - - behaviors = [:NonParallel, :Compression, :Streaming] - - Integration.apply(self, *behaviors) - - def test_no_explicit_http_port_number - url = URI('http://example.com') - url.port = nil - - adapter = Faraday::Adapter::NetHttp.new - http = adapter.send(:net_http_connection, :url => url, :request => {}) - - assert_equal 80, http.port - end - - def test_no_explicit_https_port_number - url = URI('https://example.com') - url.port = nil - - adapter = Faraday::Adapter::NetHttp.new - http = adapter.send(:net_http_connection, :url => url, :request => {}) - - assert_equal 443, http.port - end - - def test_explicit_port_number - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::NetHttp.new - http = adapter.send(:net_http_connection, :url => url, :request => {}) - - assert_equal 1234, http.port - end - - def test_custom_adapter_config - url = URI('https://example.com:1234') - - adapter = Faraday::Adapter::NetHttp.new do |http| - http.continue_timeout = 123 - end - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - adapter.send(:configure_request, http, {}) - - assert_equal 123, http.continue_timeout - end - - def test_no_retries - url = URI('http://example.com') - - adapter = Faraday::Adapter::NetHttp.new - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - adapter.send(:configure_request, http, {}) - - # `max_retries=` is only present in Ruby 2.5 - assert_equal 0, http.max_retries if http.respond_to?(:max_retries=) - end - - def test_write_timeout - url = URI('http://example.com') - - adapter = Faraday::Adapter::NetHttp.new - - http = adapter.send(:net_http_connection, :url => url, :request => {}) - adapter.send(:configure_request, http, { write_timeout: 10 }) - - assert_equal 10, http.write_timeout if http.respond_to?(:write_timeout=) - end - end -end diff --git a/test/adapters/patron_test.rb b/test/adapters/patron_test.rb deleted file mode 100644 index 5f488e73f..000000000 --- a/test/adapters/patron_test.rb +++ /dev/null @@ -1,40 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class Patron < Faraday::TestCase - - def adapter() :patron end - - unless jruby? - Integration.apply(self, :NonParallel, :NonStreaming) do - # https://github.com/toland/patron/issues/34 - undef :test_PATCH_send_url_encoded_params - - # https://github.com/toland/patron/issues/52 - undef :test_GET_with_body - - # no support for SSL peer verification - undef :test_GET_ssl_fails_with_bad_cert if ssl_mode? - end - - def test_custom_adapter_config - conn = create_connection do |session| - assert_kind_of ::Patron::Session, session - session.max_redirects = 10 - throw :config_block_called - end - - assert_throws(:config_block_called) do - conn.get 'http://8.8.8.8:88' - end - end - - def test_connection_timeout - conn = create_connection(:request => {:timeout => 10, :open_timeout => 1}) - assert_raises Faraday::Error::ConnectionFailed do - conn.get 'http://8.8.8.8:88' - end - end - end - end -end diff --git a/test/adapters/typhoeus_test.rb b/test/adapters/typhoeus_test.rb deleted file mode 100644 index d85f4afe1..000000000 --- a/test/adapters/typhoeus_test.rb +++ /dev/null @@ -1,38 +0,0 @@ -require File.expand_path('../integration', __FILE__) - -module Adapters - class TyphoeusTest < Faraday::TestCase - - def adapter() :typhoeus end - - Integration.apply(self, :Parallel) do - # inconsistent outcomes ranging from successful response to connection error - undef :test_proxy_auth_fail if ssl_mode? - - # Typhoeus adapter not supporting Faraday::SSLError - undef :test_GET_ssl_fails_with_bad_cert if ssl_mode? - - def test_binds_local_socket - host = '1.2.3.4' - conn = create_connection :request => { :bind => { :host => host } } - assert_equal host, conn.options[:bind][:host] - end - - # Typhoeus::Response doesn't provide an easy way to access the reason phrase, - # so override the shared test from Common. - def test_GET_reason_phrase - response = get('echo') - assert_nil response.reason_phrase - end - end - - def test_custom_adapter_config - adapter = Faraday::Adapter::Typhoeus.new(nil, { :forbid_reuse => true, :maxredirs => 1 }) - - request = adapter.method(:typhoeus_request).call({}) - - assert_equal true, request.options[:forbid_reuse] - assert_equal 1, request.options[:maxredirs] - end - end -end diff --git a/test/authentication_middleware_test.rb b/test/authentication_middleware_test.rb deleted file mode 100644 index 1fbad6169..000000000 --- a/test/authentication_middleware_test.rb +++ /dev/null @@ -1,65 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class AuthenticationMiddlewareTest < Faraday::TestCase - def conn - Faraday::Connection.new('http://example.net/') do |builder| - yield(builder) - builder.adapter :test do |stub| - stub.get('/auth-echo') do |env| - [200, {}, env[:request_headers]['Authorization']] - end - end - end - end - - def test_basic_middleware_adds_basic_header - response = conn { |b| b.request :basic_auth, 'aladdin', 'opensesame' }.get('/auth-echo') - assert_equal 'Basic YWxhZGRpbjpvcGVuc2VzYW1l', response.body - end - - def test_basic_middleware_adds_basic_header_correctly_with_long_values - response = conn { |b| b.request :basic_auth, 'A' * 255, '' }.get('/auth-echo') - assert_equal "Basic #{'QUFB' * 85}Og==", response.body - end - - def test_basic_middleware_does_not_interfere_with_existing_authorization - response = conn { |b| b.request :basic_auth, 'aladdin', 'opensesame' }. - get('/auth-echo', nil, :authorization => 'Token token="bar"') - assert_equal 'Token token="bar"', response.body - end - - def test_token_middleware_adds_token_header - response = conn { |b| b.request :token_auth, 'quux' }.get('/auth-echo') - assert_equal 'Token token="quux"', response.body - end - - def test_token_middleware_includes_other_values_if_provided - response = conn { |b| - b.request :token_auth, 'baz', :foo => 42 - }.get('/auth-echo') - assert_match(/^Token /, response.body) - assert_match(/token="baz"/, response.body) - assert_match(/foo="42"/, response.body) - end - - def test_token_middleware_does_not_interfere_with_existing_authorization - response = conn { |b| b.request :token_auth, 'quux' }. - get('/auth-echo', nil, :authorization => 'Token token="bar"') - assert_equal 'Token token="bar"', response.body - end - - def test_authorization_middleware_with_string - response = conn { |b| - b.request :authorization, 'custom', 'abc def' - }.get('/auth-echo') - assert_match(/^custom abc def$/, response.body) - end - - def test_authorization_middleware_with_hash - response = conn { |b| - b.request :authorization, 'baz', :foo => 42 - }.get('/auth-echo') - assert_match(/^baz /, response.body) - assert_match(/foo="42"/, response.body) - end -end diff --git a/test/composite_read_io_test.rb b/test/composite_read_io_test.rb deleted file mode 100644 index 9338e2a1e..000000000 --- a/test/composite_read_io_test.rb +++ /dev/null @@ -1,109 +0,0 @@ -require File.expand_path(File.join(File.dirname(__FILE__), 'helper')) -require 'stringio' - -class CompositeReadIOTest < Faraday::TestCase - Part = Struct.new(:to_io) do - def length() to_io.string.length end - end - - def part(str) - Part.new StringIO.new(str) - end - - def composite_io(*parts) - Faraday::CompositeReadIO.new(*parts) - end - - def test_empty - io = composite_io - assert_equal 0, io.length - assert_equal "", io.read - end - - def test_empty_returns_nil_for_limited_read - assert_nil composite_io.read(1) - end - - def test_empty_parts_returns_nil_for_limited_read - io = composite_io(part(""), part("")) - assert_nil io.read(1) - end - - def test_multipart_read_all - io = composite_io(part("abcd"), part("1234")) - assert_equal 8, io.length - assert_equal "abcd1234", io.read - end - - def test_multipart_read_limited - io = composite_io(part("abcd"), part("1234")) - assert_equal "abc", io.read(3) - assert_equal "d12", io.read(3) - assert_equal "34", io.read(3) - assert_nil io.read(3) - assert_nil io.read(3) - end - - def test_multipart_read_limited_size_larger_than_part - io = composite_io(part("abcd"), part("1234")) - assert_equal "abcd12", io.read(6) - assert_equal "34", io.read(6) - assert_nil io.read(6) - end - - def test_multipart_read_with_blank_parts - io = composite_io(part(""), part("abcd"), part(""), part("1234"), part("")) - assert_equal "abcd12", io.read(6) - assert_equal "34", io.read(6) - assert_nil io.read(6) - end - - def test_multipart_rewind - io = composite_io(part("abcd"), part("1234")) - assert_equal "abc", io.read(3) - assert_equal "d12", io.read(3) - io.rewind - assert_equal "abc", io.read(3) - assert_equal "d1234", io.read(5) - assert_nil io.read(3) - io.rewind - assert_equal "ab", io.read(2) - end - - # JRuby enforces types to copy_stream to be String or IO - if IO.respond_to?(:copy_stream) && !jruby? - def test_compatible_with_copy_stream - target_io = StringIO.new - def target_io.ensure_open_and_writable - # Rubinius compatibility - end - io = composite_io(part("abcd"), part("1234")) - - Faraday::Timer.timeout(1) do - IO.copy_stream(io, target_io) - end - assert_equal "abcd1234", target_io.string - end - end - - def test_read_from_multibyte - File.open(File.dirname(__FILE__) + '/multibyte.txt') do |utf8| - io = composite_io(part("\x86"), Part.new(utf8)) - assert_equal bin("\x86\xE3\x83\x95\xE3\x82\xA1\xE3\x82\xA4\xE3\x83\xAB\n"), io.read - end - end - - def test_limited_from_multibyte - File.open(File.dirname(__FILE__) + '/multibyte.txt') do |utf8| - io = composite_io(part("\x86"), Part.new(utf8)) - assert_equal bin("\x86\xE3\x83"), io.read(3) - assert_equal bin("\x95\xE3\x82"), io.read(3) - assert_equal bin("\xA1\xE3\x82\xA4\xE3\x83\xAB\n"), io.read(8) - end - end - - def bin(str) - str.force_encoding("BINARY") if str.respond_to?(:force_encoding) - str - end -end diff --git a/test/connection_test.rb b/test/connection_test.rb deleted file mode 100644 index f6737ad29..000000000 --- a/test/connection_test.rb +++ /dev/null @@ -1,738 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class TestConnection < Faraday::TestCase - def teardown - Faraday.default_connection_options = nil - end - - def with_test_conn - old_conn = Faraday.default_connection - Faraday.default_connection = Faraday::Connection.new do |builder| - builder.adapter :test do |stub| - stub.get('/') do |_| - [200, nil, nil] - end - end - end - - begin - yield - ensure - Faraday.default_connection = old_conn - end - end - - def with_env_proxy_disabled - Faraday.ignore_env_proxy = true - - begin - yield - ensure - Faraday.ignore_env_proxy = false - end - end - - def with_env(new_env) - old_env = {} - - new_env.each do |key, value| - old_env[key] = ENV.fetch(key, false) - ENV[key] = value - end - - begin - yield - ensure - old_env.each do |key, value| - if value == false - ENV.delete key - else - ENV[key] = value - end - end - end - end - - def test_initialize_parses_host_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com' - assert_equal 'sushi.com', conn.host - end - - def test_initialize_inherits_default_port_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com' - assert_equal 80, conn.port - end - - def test_initialize_parses_scheme_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com' - assert_equal 'http', conn.scheme - end - - def test_initialize_parses_port_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com:815' - assert_equal 815, conn.port - end - - def test_initialize_parses_nil_path_prefix_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com' - assert_equal '/', conn.path_prefix - end - - def test_initialize_parses_path_prefix_out_of_given_url - conn = Faraday::Connection.new 'http://sushi.com/fish' - assert_equal '/fish', conn.path_prefix - end - - def test_initialize_parses_path_prefix_out_of_given_url_option - conn = Faraday::Connection.new :url => 'http://sushi.com/fish' - assert_equal '/fish', conn.path_prefix - end - - def test_initialize_stores_default_params_from_options - conn = Faraday::Connection.new :params => {:a => 1} - assert_equal({'a' => 1}, conn.params) - end - - def test_initialize_stores_default_params_from_uri - conn = Faraday::Connection.new 'http://sushi.com/fish?a=1' - assert_equal({'a' => '1'}, conn.params) - end - - def test_initialize_stores_default_params_from_uri_and_options - conn = Faraday::Connection.new 'http://sushi.com/fish?a=1&b=2', :params => {'a' => 3} - assert_equal({'a' => 3, 'b' => '2'}, conn.params) - end - - def test_initialize_stores_default_headers_from_options - conn = Faraday::Connection.new :headers => {:user_agent => 'Faraday'} - assert_equal 'Faraday', conn.headers['User-agent'] - end - - def test_basic_auth_sets_header - conn = Faraday::Connection.new - assert_nil conn.headers['Authorization'] - - conn.basic_auth 'Aladdin', 'open sesame' - assert auth = conn.headers['Authorization'] - assert_equal 'Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==', auth - end - - def test_auto_parses_basic_auth_from_url_and_unescapes - conn = Faraday::Connection.new :url => 'http://foo%40bar.com:pass%20word@sushi.com/fish' - assert auth = conn.headers['Authorization'] - assert_equal Faraday::Request::BasicAuthentication.header('foo@bar.com', 'pass word'), auth - end - - def test_token_auth_sets_header - conn = Faraday::Connection.new - assert_nil conn.headers['Authorization'] - - conn.token_auth 'abcdef', :nonce => 'abc' - assert auth = conn.headers['Authorization'] - assert_match(/^Token /, auth) - assert_match(/token="abcdef"/, auth) - assert_match(/nonce="abc"/, auth) - end - - def test_build_exclusive_url_uses_connection_host_as_default_uri_host - conn = Faraday::Connection.new - conn.host = 'sushi.com' - uri = conn.build_exclusive_url('/sake.html') - assert_equal 'sushi.com', uri.host - end - - def test_build_exclusive_url_overrides_connection_port_for_absolute_urls - conn = Faraday::Connection.new - conn.port = 23 - uri = conn.build_exclusive_url('http://sushi.com') - assert_equal 80, uri.port - end - - def test_build_exclusive_url_uses_connection_scheme_as_default_uri_scheme - conn = Faraday::Connection.new 'http://sushi.com' - uri = conn.build_exclusive_url('/sake.html') - assert_equal 'http', uri.scheme - end - - def test_build_exclusive_url_uses_connection_path_prefix_to_customize_path - conn = Faraday::Connection.new - conn.path_prefix = '/fish' - uri = conn.build_exclusive_url('sake.html') - assert_equal '/fish/sake.html', uri.path - end - - def test_build_exclusive_url_uses_root_connection_path_prefix_to_customize_path - conn = Faraday::Connection.new - conn.path_prefix = '/' - uri = conn.build_exclusive_url('sake.html') - assert_equal '/sake.html', uri.path - end - - def test_build_exclusive_url_forces_connection_path_prefix_to_be_absolute - conn = Faraday::Connection.new - conn.path_prefix = 'fish' - uri = conn.build_exclusive_url('sake.html') - assert_equal '/fish/sake.html', uri.path - end - - def test_build_exclusive_url_ignores_connection_path_prefix_trailing_slash - conn = Faraday::Connection.new - conn.path_prefix = '/fish/' - uri = conn.build_exclusive_url('sake.html') - assert_equal '/fish/sake.html', uri.path - end - - def test_build_exclusive_url_allows_absolute_uri_to_ignore_connection_path_prefix - conn = Faraday::Connection.new - conn.path_prefix = '/fish' - uri = conn.build_exclusive_url('/sake.html') - assert_equal '/sake.html', uri.path - end - - def test_build_exclusive_url_parses_url_params_into_path - conn = Faraday::Connection.new - uri = conn.build_exclusive_url('http://sushi.com/sake.html') - assert_equal '/sake.html', uri.path - end - - def test_build_exclusive_url_doesnt_add_ending_slash_given_nil_url - conn = Faraday::Connection.new - conn.url_prefix = 'http://sushi.com/nigiri' - uri = conn.build_exclusive_url - assert_equal '/nigiri', uri.path - end - - def test_build_exclusive_url_doesnt_add_ending_slash_given_empty_url - conn = Faraday::Connection.new - conn.url_prefix = 'http://sushi.com/nigiri' - uri = conn.build_exclusive_url('') - assert_equal '/nigiri', uri.path - end - - def test_build_exclusive_url_doesnt_use_connection_params - conn = Faraday::Connection.new 'http://sushi.com/nigiri' - conn.params = {:a => 1} - assert_equal 'http://sushi.com/nigiri', conn.build_exclusive_url.to_s - end - - def test_build_exclusive_url_uses_argument_params - conn = Faraday::Connection.new 'http://sushi.com/nigiri' - conn.params = {:a => 1} - params = Faraday::Utils::ParamsHash.new - params[:a] = 2 - url = conn.build_exclusive_url(nil, params) - assert_equal 'http://sushi.com/nigiri?a=2', url.to_s - end - - def test_build_url_uses_params - conn = Faraday::Connection.new 'http://sushi.com/nigiri' - conn.params = {:a => 1, :b => 1} - assert_equal 'http://sushi.com/nigiri?a=1&b=1', conn.build_url.to_s - end - - def test_build_url_merges_params - conn = Faraday::Connection.new 'http://sushi.com/nigiri' - conn.params = {:a => 1, :b => 1} - url = conn.build_url(nil, :b => 2, :c => 3) - assert_equal 'http://sushi.com/nigiri?a=1&b=2&c=3', url.to_s - end - - def test_request_header_change_does_not_modify_connection_header - connection = Faraday.new(:url => 'https://asushi.com/sake.html') - connection.headers = {'Authorization' => 'token abc123'} - - request = connection.build_request(:get) - request.headers.delete('Authorization') - - assert_equal connection.headers.keys.sort, ['Authorization'] - assert connection.headers.include?('Authorization') - - assert_equal request.headers.keys.sort, [] - assert !request.headers.include?('Authorization') - end - - def test_env_url_parses_url_params_into_query - uri = env_url('http://sushi.com/sake.html', 'a[b]' => '1 + 2') - assert_equal 'a%5Bb%5D=1+%2B+2', uri.query - end - - def test_env_url_escapes_per_spec - uri = env_url(nil, 'a' => '1+2 foo~bar.-baz') - assert_equal 'a=1%2B2+foo~bar.-baz', uri.query - end - - def test_env_url_bracketizes_nested_params_in_query - url = env_url nil, 'a' => {'b' => 'c'} - assert_equal 'a%5Bb%5D=c', url.query - end - - def test_env_url_bracketizes_repeated_params_in_query - uri = env_url('http://sushi.com/sake.html', 'a' => [1, 2]) - assert_equal 'a%5B%5D=1&a%5B%5D=2', uri.query - end - - def test_env_url_without_braketizing_repeated_params_in_query - uri = env_url 'http://sushi.com', 'a' => [1, 2] do |conn| - conn.options.params_encoder = Faraday::FlatParamsEncoder - end - assert_equal 'a=1&a=2', uri.query - end - - def test_build_exclusive_url_parses_url - conn = Faraday::Connection.new - uri = conn.build_exclusive_url('http://sushi.com/sake.html') - assert_equal 'http', uri.scheme - assert_equal 'sushi.com', uri.host - assert_equal '/sake.html', uri.path - end - - def test_build_exclusive_url_parses_url_and_changes_scheme - conn = Faraday::Connection.new :url => 'http://sushi.com/sushi' - conn.scheme = 'https' - uri = conn.build_exclusive_url('sake.html') - assert_equal 'https://sushi.com/sushi/sake.html', uri.to_s - end - - def test_build_exclusive_url_joins_url_to_base_with_ending_slash - conn = Faraday::Connection.new :url => 'http://sushi.com/sushi/' - uri = conn.build_exclusive_url('sake.html') - assert_equal 'http://sushi.com/sushi/sake.html', uri.to_s - end - - def test_build_exclusive_url_used_default_base_with_ending_slash - conn = Faraday::Connection.new :url => 'http://sushi.com/sushi/' - uri = conn.build_exclusive_url - assert_equal 'http://sushi.com/sushi/', uri.to_s - end - - def test_build_exclusive_url_overrides_base - conn = Faraday::Connection.new :url => 'http://sushi.com/sushi/' - uri = conn.build_exclusive_url('/sake/') - assert_equal 'http://sushi.com/sake/', uri.to_s - end - - def test_build_exclusive_url_handles_uri_instances - conn = Faraday::Connection.new - uri = conn.build_exclusive_url(URI('/sake.html')) - assert_equal '/sake.html', uri.path - end - - def test_proxy_accepts_string - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new - conn.proxy = 'http://proxy.com' - assert_equal 'proxy.com', conn.proxy.host - end - end - - def test_proxy_accepts_uri - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new - conn.proxy = URI.parse('http://proxy.com') - assert_equal 'proxy.com', conn.proxy.host - end - end - - def test_proxy_accepts_hash_with_string_uri - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new - conn.proxy = {:uri => 'http://proxy.com', :user => 'rick'} - assert_equal 'proxy.com', conn.proxy.host - assert_equal 'rick', conn.proxy.user - end - end - - def test_proxy_accepts_hash - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new - conn.proxy = {:uri => URI.parse('http://proxy.com'), :user => 'rick'} - assert_equal 'proxy.com', conn.proxy.host - assert_equal 'rick', conn.proxy.user - end - end - - def test_proxy_accepts_http_env - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new - assert_equal 'duncan.proxy.com', conn.proxy.host - end - end - - def test_proxy_accepts_http_env_with_auth - with_env 'http_proxy' => 'http://a%40b:my%20pass@duncan.proxy.com:80' do - conn = Faraday::Connection.new - assert_equal 'a@b', conn.proxy.user - assert_equal 'my pass', conn.proxy.password - end - end - - def test_proxy_accepts_env_without_scheme - with_env 'http_proxy' => 'localhost:8888' do - uri = Faraday::Connection.new.proxy[:uri] - assert_equal 'localhost', uri.host - assert_equal 8888, uri.port - end - end - - def test_no_proxy_from_env - with_env 'http_proxy' => nil do - conn = Faraday::Connection.new - assert_nil conn.proxy - end - end - - def test_no_proxy_from_blank_env - with_env 'http_proxy' => '' do - conn = Faraday::Connection.new - assert_nil conn.proxy - end - end - - def test_proxy_doesnt_accept_uppercase_env - with_env 'HTTP_PROXY' => 'http://localhost:8888/' do - conn = Faraday::Connection.new - assert_nil conn.proxy - end - end - - def test_dynamic_proxy - with_test_conn do - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - Faraday.get('http://google.co.uk') - assert_equal 'duncan.proxy.com', Faraday.default_connection.instance_variable_get('@temp_proxy').host - end - Faraday.get('http://google.co.uk') - assert_nil Faraday.default_connection.instance_variable_get('@temp_proxy') - end - end - - def test_ignore_env_proxy - with_env_proxy_disabled do - with_env 'http_proxy' => 'http://duncan.proxy.com:80' do - conn = Faraday::Connection.new(proxy: nil) - assert_nil conn.proxy - end - end - end - - if URI.parse('').respond_to?(:find_proxy) - def test_proxy_allowed_when_url_in_no_proxy_list - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do - conn = Faraday::Connection.new('http://example.com') - assert_nil conn.proxy - end - end - - def test_proxy_allowed_when_prefixed_url_is_not_in_no_proxy_list - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do - conn = Faraday::Connection.new('http://prefixedexample.com') - assert_equal 'proxy.com', conn.proxy.host - end - end - - def test_proxy_allowed_when_subdomain_url_is_in_no_proxy_list - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example.com' do - conn = Faraday::Connection.new('http://subdomain.example.com') - assert_nil conn.proxy - end - end - - def test_proxy_allowed_when_url_not_in_no_proxy_list - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example2.com' do - conn = Faraday::Connection.new('http://example.com') - assert_equal 'proxy.com', conn.proxy.host - end - end - - def test_proxy_allowed_when_ip_address_is_not_in_no_proxy_list_but_url_is - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'localhost' do - conn = Faraday::Connection.new('http://127.0.0.1') - assert_nil conn.proxy - end - end - - def test_proxy_allowed_when_url_is_not_in_no_proxy_list_but_ip_address_is - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => '127.0.0.1' do - conn = Faraday::Connection.new('http://localhost') - assert_nil conn.proxy - end - end - - def test_proxy_allowed_in_multi_element_no_proxy_list - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'example0.com,example.com,example1.com' do - assert_nil Faraday::Connection.new('http://example0.com').proxy - assert_nil Faraday::Connection.new('http://example.com').proxy - assert_nil Faraday::Connection.new('http://example1.com').proxy - assert_equal 'proxy.com', Faraday::Connection.new('http://example2.com').proxy.host - end - end - - def test_dynamic_no_proxy - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do - conn = Faraday.new - - assert_equal 'proxy.com', conn.instance_variable_get('@temp_proxy').host - conn.get('https://google.co.uk') - assert_nil conn.instance_variable_get('@temp_proxy') - end - end - - def test_issue - with_env 'http_proxy' => 'http://proxy.com', 'no_proxy' => 'google.co.uk' do - conn = Faraday.new - conn.proxy = 'http://proxy2.com' - - assert_equal true, conn.instance_variable_get('@manual_proxy') - assert_equal 'proxy2.com', conn.proxy_for_request('https://google.co.uk').host - end - end - end - - def test_proxy_requires_uri - conn = Faraday::Connection.new - assert_raises ArgumentError do - conn.proxy = {:uri => :bad_uri, :user => 'rick'} - end - end - - def test_dups_connection_object - conn = Faraday::Connection.new 'http://sushi.com/foo', - :ssl => { :verify => :none }, - :headers => {'content-type' => 'text/plain'}, - :params => {'a'=>'1'}, - :request => {:timeout => 5} - - other = conn.dup - - assert_equal conn.build_exclusive_url, other.build_exclusive_url - assert_equal 'text/plain', other.headers['content-type'] - assert_equal '1', other.params['a'] - - other.basic_auth('', '') - other.headers['content-length'] = 12 - other.params['b'] = '2' - other.options[:open_timeout] = 10 - - assert_equal 1, other.builder.handlers.size - assert_equal 1, conn.builder.handlers.size - assert !conn.headers.key?('content-length') - assert !conn.params.key?('b') - assert_equal 5, other.options[:timeout] - assert_nil conn.options[:open_timeout] - end - - def test_initialize_with_false_option - conn = Faraday::Connection.new :ssl => {:verify => false} - assert !conn.ssl.verify? - end - - def test_init_with_block - conn = Faraday::Connection.new { } - assert_equal 0, conn.builder.handlers.size - end - - def test_init_with_block_yields_connection - conn = Faraday::Connection.new(:params => {'a'=>'1'}) { |faraday| - faraday.adapter :net_http - faraday.url_prefix = 'http://sushi.com/omnom' - assert_equal '1', faraday.params['a'] - } - assert_equal 0, conn.builder.handlers.size - assert_equal '/omnom', conn.path_prefix - end - - def test_respond_to - assert Faraday.respond_to?(:get) - assert Faraday.respond_to?(:post) - end - - def test_default_connection_options - Faraday.default_connection_options.request.timeout = 10 - conn = Faraday.new 'http://sushi.com/foo' - assert_equal 10, conn.options.timeout - end - - def test_default_connection_options_without_url - Faraday.default_connection_options.request.timeout = 10 - conn = Faraday.new :url => 'http://sushi.com/foo' - assert_equal 10, conn.options.timeout - end - - def test_default_connection_options_as_hash - Faraday.default_connection_options = { request: { timeout: 10 } } - conn = Faraday.new 'http://sushi.com/foo' - assert_equal 10, conn.options.timeout - end - - def test_default_connection_options_as_hash_without_url - Faraday.default_connection_options = { request: { timeout: 10 } } - conn = Faraday.new :url => 'http://sushi.com/foo' - assert_equal 10, conn.options.timeout - end - - def test_default_connection_options_as_hash_with_instance_connection_options - Faraday.default_connection_options = { request: { timeout: 10 } } - conn = Faraday.new 'http://sushi.com/foo', request: { open_timeout: 1 } - assert_equal 1, conn.options.open_timeout - assert_equal 10, conn.options.timeout - end - - def test_default_connection_options_persist_with_an_instance_overriding - Faraday.default_connection_options.request.timeout = 10 - conn = Faraday.new 'http://nigiri.com/bar' - conn.options.timeout = 1 - assert_equal 10, Faraday.default_connection_options.request.timeout - - other = Faraday.new :url => 'https://sushi.com/foo' - other.options.timeout = 1 - - assert_equal 10, Faraday.default_connection_options.request.timeout - end - - def test_default_connection_uses_default_connection_options - Faraday.default_connection_options.request.timeout = 10 - default_conn = Faraday.default_connection - - assert_equal 10, default_conn.options.timeout - end - - def env_url(url, params) - conn = Faraday::Connection.new(url, :params => params) - yield(conn) if block_given? - req = conn.build_request(:get) - req.to_env(conn).url - end -end - -class TestRequestParams < Faraday::TestCase - def create_connection(*args) - @conn = Faraday::Connection.new(*args) do |conn| - yield(conn) if block_given? - class << conn.builder - undef app - def app() lambda { |env| env } end - end - end - end - - def assert_query_equal(expected, query) - assert_equal expected, query.split('&').sort - end - - def with_default_params_encoder(encoder) - old_encoder = Faraday::Utils.default_params_encoder - begin - Faraday::Utils.default_params_encoder = encoder - yield - ensure - Faraday::Utils.default_params_encoder = old_encoder - end - end - - def test_merges_connection_and_request_params - create_connection 'http://a.co/?token=abc', :params => {'format' => 'json'} - query = get '?page=1', :limit => 5 - assert_query_equal %w[format=json limit=5 page=1 token=abc], query - end - - def test_overrides_connection_params - create_connection 'http://a.co/?a=a&b=b&c=c', :params => {:a => 'A'} do |conn| - conn.params[:b] = 'B' - assert_equal 'c', conn.params[:c] - end - assert_query_equal %w[a=A b=B c=c], get - end - - def test_all_overrides_connection_params - create_connection 'http://a.co/?a=a', :params => {:c => 'c'} do |conn| - conn.params = {'b' => 'b'} - end - assert_query_equal %w[b=b], get - end - - def test_overrides_request_params - create_connection - query = get '?p=1&a=a', :p => 2 - assert_query_equal %w[a=a p=2], query - end - - def test_overrides_request_params_block - create_connection - query = get '?p=1&a=a', :p => 2 do |req| - req.params[:p] = 3 - end - assert_query_equal %w[a=a p=3], query - end - - def test_overrides_request_params_block_url - create_connection - query = get nil, :p => 2 do |req| - req.url '?p=1&a=a', 'p' => 3 - end - assert_query_equal %w[a=a p=3], query - end - - def test_overrides_all_request_params - create_connection :params => {:c => 'c'} - query = get '?p=1&a=a', :p => 2 do |req| - assert_equal 'a', req.params[:a] - assert_equal 'c', req.params['c'] - assert_equal 2, req.params['p'] - req.params = {:b => 'b'} - assert_equal 'b', req.params['b'] - end - assert_query_equal %w[b=b], query - end - - def test_array_params_in_url - with_default_params_encoder(nil) do - create_connection 'http://a.co/page1?color[]=red&color[]=blue' - query = get - assert_equal 'color%5B%5D=red&color%5B%5D=blue', query - end - end - - def test_array_params_in_params - with_default_params_encoder(nil) do - create_connection 'http://a.co/page1', :params => {:color => ['red', 'blue']} - query = get - assert_equal 'color%5B%5D=red&color%5B%5D=blue', query - end - end - - def test_array_params_in_url_with_flat_params - with_default_params_encoder(Faraday::FlatParamsEncoder) do - create_connection 'http://a.co/page1?color=red&color=blue' - query = get - assert_equal 'color=red&color=blue', query - end - end - - def test_array_params_in_params_with_flat_params - with_default_params_encoder(Faraday::FlatParamsEncoder) do - create_connection 'http://a.co/page1', :params => {:color => ['red', 'blue']} - query = get - assert_equal 'color=red&color=blue', query - end - end - - def test_params_with_connection_options - encoder = Object.new - def encoder.encode(params) - params.map { |k,v| "#{k.upcase}-#{v.upcase}" }.join(',') - end - - create_connection :params => {:color => 'red'} - query = get('', :feeling => 'blue') do |req| - req.options.params_encoder = encoder - end - assert_equal ['COLOR-RED', 'FEELING-BLUE'], query.split(',').sort - end - - def get(*args) - env = @conn.get(*args) do |req| - yield(req) if block_given? - end - env[:url].query - end -end diff --git a/test/env_test.rb b/test/env_test.rb deleted file mode 100644 index fd1cb2242..000000000 --- a/test/env_test.rb +++ /dev/null @@ -1,268 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class EnvTest < Faraday::TestCase - def setup - @conn = Faraday.new :url => 'http://sushi.com/api', - :headers => {'Mime-Version' => '1.0'}, - :request => {:oauth => {:consumer_key => 'anonymous'}} - - @conn.options.timeout = 3 - @conn.options.open_timeout = 5 - @conn.ssl.verify = false - @conn.proxy = 'http://proxy.com' - end - - def test_request_create_stores_method - env = make_env(:get) - assert_equal :get, env.method - end - - def test_request_create_stores_uri - env = make_env do |req| - req.url 'foo.json', 'a' => 1 - end - assert_equal 'http://sushi.com/api/foo.json?a=1', env.url.to_s - end - - def test_request_create_stores_uri_with_anchor - env = make_env do |req| - req.url 'foo.json?b=2&a=1#qqq' - end - assert_equal 'http://sushi.com/api/foo.json?a=1&b=2', env.url.to_s - end - - def test_request_create_stores_headers - env = make_env do |req| - req['Server'] = 'Faraday' - end - headers = env.request_headers - assert_equal '1.0', headers['mime-version'] - assert_equal 'Faraday', headers['server'] - end - - def test_request_create_stores_body - env = make_env do |req| - req.body = 'hi' - end - assert_equal 'hi', env.body - end - - def test_global_request_options - env = make_env - assert_equal 3, env.request.timeout - assert_equal 5, env.request.open_timeout - end - - def test_per_request_options - env = make_env do |req| - req.options.timeout = 10 - req.options.boundary = 'boo' - req.options.oauth[:consumer_secret] = 'xyz' - req.options.context = { - foo: 'foo', - bar: 'bar' - } - end - - assert_equal 10, env.request.timeout - assert_equal 5, env.request.open_timeout - assert_equal 'boo', env.request.boundary - assert_equal env.request.context, { foo: 'foo', bar: 'bar' } - - oauth_expected = {:consumer_secret => 'xyz', :consumer_key => 'anonymous'} - assert_equal oauth_expected, env.request.oauth - end - - def test_request_create_stores_ssl_options - env = make_env - assert_equal false, env.ssl.verify - end - - def test_custom_members_are_retained - env = make_env - env[:foo] = "custom 1" - env[:bar] = :custom_2 - env2 = Faraday::Env.from(env) - assert_equal "custom 1", env2[:foo] - assert_equal :custom_2, env2[:bar] - env2[:baz] = "custom 3" - assert_nil env[:baz] - end - - private - - def make_env(method = :get, connection = @conn, &block) - request = connection.build_request(method, &block) - request.to_env(connection) - end -end - -class HeadersTest < Faraday::TestCase - def setup - @headers = Faraday::Utils::Headers.new - end - - def test_normalizes_different_capitalizations - @headers['Content-Type'] = 'application/json' - assert_equal ['Content-Type'], @headers.keys - assert_equal 'application/json', @headers['Content-Type'] - assert_equal 'application/json', @headers['CONTENT-TYPE'] - assert_equal 'application/json', @headers['content-type'] - assert @headers.include?('content-type') - - @headers['content-type'] = 'application/xml' - assert_equal ['Content-Type'], @headers.keys - assert_equal 'application/xml', @headers['Content-Type'] - assert_equal 'application/xml', @headers['CONTENT-TYPE'] - assert_equal 'application/xml', @headers['content-type'] - end - - def test_fetch_key - @headers['Content-Type'] = 'application/json' - block_called = false - assert_equal 'application/json', @headers.fetch('content-type') { block_called = true } - assert_equal 'application/json', @headers.fetch('Content-Type') - assert_equal 'application/json', @headers.fetch('CONTENT-TYPE') - assert_equal 'application/json', @headers.fetch(:content_type) - assert_equal false, block_called - - assert_equal 'default', @headers.fetch('invalid', 'default') - assert_equal false, @headers.fetch('invalid', false) - assert_nil @headers.fetch('invalid', nil) - - assert_equal 'Invalid key', @headers.fetch('Invalid') { |key| "#{key} key" } - - expected_error = defined?(KeyError) ? KeyError : IndexError - assert_raises(expected_error) { @headers.fetch('invalid') } - end - - def test_delete_key - @headers['Content-Type'] = 'application/json' - assert_equal 1, @headers.size - assert @headers.include?('content-type') - assert_equal 'application/json', @headers.delete('content-type') - assert_equal 0, @headers.size - assert !@headers.include?('content-type') - assert_nil @headers.delete('content-type') - end - - def test_parse_response_headers_leaves_http_status_line_out - @headers.parse("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n") - assert_equal %w(Content-Type), @headers.keys - end - - def test_parse_response_headers_parses_lower_cased_header_name_and_value - @headers.parse("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n") - assert_equal 'text/html', @headers['content-type'] - end - - def test_parse_response_headers_parses_lower_cased_header_name_and_value_with_colon - @headers.parse("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nLocation: http://sushi.com/\r\n\r\n") - assert_equal 'http://sushi.com/', @headers['location'] - end - - def test_parse_response_headers_parses_blank_lines - @headers.parse("HTTP/1.1 200 OK\r\n\r\nContent-Type: text/html\r\n\r\n") - assert_equal 'text/html', @headers['content-type'] - end -end - -class ResponseTest < Faraday::TestCase - def setup - @env = Faraday::Env.from \ - :status => 404, :body => 'yikes', - :response_headers => {'Content-Type' => 'text/plain'} - @response = Faraday::Response.new @env - end - - def test_finished - assert @response.finished? - end - - def test_error_on_finish - assert_raises RuntimeError do - @response.finish({}) - end - end - - def test_body_is_parsed_on_finish - response = Faraday::Response.new - response.on_complete { |env| env[:body] = env[:body].upcase } - response.finish(@env) - - assert_equal "YIKES", response.body - end - - def test_response_body_is_available_during_on_complete - response = Faraday::Response.new - response.on_complete { |env| env[:body] = response.body.upcase } - response.finish(@env) - - assert_equal "YIKES", response.body - end - - def test_env_in_on_complete_is_identical_to_response_env - response = Faraday::Response.new - callback_env = nil - response.on_complete { |env| callback_env = env } - response.finish({}) - - assert_same response.env, callback_env - end - - def test_not_success - assert !@response.success? - end - - def test_status - assert_equal 404, @response.status - end - - def test_body - assert_equal 'yikes', @response.body - end - - def test_headers - assert_equal 'text/plain', @response.headers['Content-Type'] - assert_equal 'text/plain', @response['content-type'] - end - - def test_apply_request - @response.apply_request :body => 'a=b', :method => :post - assert_equal 'yikes', @response.body - assert_equal :post, @response.env[:method] - end - - def test_marshal_response - @response = Faraday::Response.new - @response.on_complete { } - @response.finish @env.merge(:params => 'moo') - - loaded = Marshal.load Marshal.dump(@response) - assert_nil loaded.env[:params] - assert_equal %w[body response_headers status], loaded.env.keys.map { |k| k.to_s }.sort - end - - def test_marshal_request - @request = Faraday::Request.create(:post) do |request| - request.options = Faraday::RequestOptions.new - request.params = Faraday::Utils::ParamsHash.new({ 'a' => 'c' }) - request.headers = { 'b' => 'd' } - request.body = 'hello, world!' - request.url 'http://localhost/foo' - end - - loaded = Marshal.load(Marshal.dump(@request)) - - assert_equal @request, loaded - end - - def test_hash - hash = @response.to_hash - assert_kind_of Hash, hash - assert_equal @env.to_hash, hash - assert_equal hash[:status], @response.status - assert_equal hash[:response_headers], @response.headers - assert_equal hash[:body], @response.body - end -end diff --git a/test/helper.rb b/test/helper.rb index 371723138..dd0150711 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -6,7 +6,7 @@ SimpleCov.start do add_filter '/bundle/' add_filter '/test/' - minimum_coverage(87) + # minimum_coverage(87) end gem 'minitest' if defined? Bundler diff --git a/test/middleware/instrumentation_test.rb b/test/middleware/instrumentation_test.rb deleted file mode 100644 index 364f13cb5..000000000 --- a/test/middleware/instrumentation_test.rb +++ /dev/null @@ -1,88 +0,0 @@ -require File.expand_path("../../helper", __FILE__) - -module Middleware - class InstrumentationTest < Faraday::TestCase - def setup - @instrumenter = FakeInstrumenter.new - end - - def test_default_name - assert_equal 'request.faraday', options.name - end - - def test_default_instrumenter - begin - instrumenter = options.instrumenter - rescue NameError => err - assert_match 'ActiveSupport', err.to_s - else - assert_equal ActiveSupport::Notifications, instrumenter - end - end - - def test_name - assert_equal 'booya', options(:name => 'booya').name - end - - def test_instrumenter - assert_equal :boom, options(:instrumenter => :boom).instrumenter - end - - def test_instrumentation_with_default_name - assert_equal 0, @instrumenter.instrumentations.size - - faraday = conn - res = faraday.get '/' - assert_equal 'ok', res.body - - assert_equal 1, @instrumenter.instrumentations.size - name, env = @instrumenter.instrumentations.first - assert_equal 'request.faraday', name - assert_equal '/', env[:url].path - end - - def test_instrumentation - assert_equal 0, @instrumenter.instrumentations.size - - faraday = conn :name => 'booya' - res = faraday.get '/' - assert_equal 'ok', res.body - - assert_equal 1, @instrumenter.instrumentations.size - name, env = @instrumenter.instrumentations.first - assert_equal 'booya', name - assert_equal '/', env[:url].path - end - - class FakeInstrumenter - attr_reader :instrumentations - - def initialize - @instrumentations = [] - end - - def instrument(name, env) - @instrumentations << [name, env] - yield - end - end - - def options(hash = nil) - Faraday::Request::Instrumentation::Options.from hash - end - - def conn(hash = nil) - hash ||= {} - hash[:instrumenter] = @instrumenter - - Faraday.new do |f| - f.request :instrumentation, hash - f.adapter :test do |stub| - stub.get '/' do - [200, {}, 'ok'] - end - end - end - end - end -end diff --git a/test/middleware/retry_test.rb b/test/middleware/retry_test.rb deleted file mode 100644 index 2d7b86e0e..000000000 --- a/test/middleware/retry_test.rb +++ /dev/null @@ -1,282 +0,0 @@ -require File.expand_path("../../helper", __FILE__) - -module Middleware - class RetryTest < Faraday::TestCase - def setup - @times_called = 0 - @envs = [] - end - - def conn(*retry_args) - Faraday.new do |b| - b.request :retry, *retry_args - - b.adapter :test do |stub| - ['get', 'post'].each do |method| - stub.send(method, '/unstable') do |env| - @times_called += 1 - @envs << env.dup - env[:body] = nil # simulate blanking out response body - @explode.call @times_called - end - - stub.send(method, '/throttled') do |env| - @times_called += 1 - @envs << env.dup - - params = env[:params] - - status = (params['status'] || 429).to_i - headers = {} - - retry_after = params['retry_after'] - - headers['Retry-After'] = retry_after if retry_after - - [status, headers, ''] - end - end - end - end - end - - def test_unhandled_error - @explode = lambda {|n| raise "boom!" } - assert_raises(RuntimeError) { conn.get("/unstable") } - assert_equal 1, @times_called - end - - def test_handled_error - @explode = lambda {|n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { conn.get("/unstable") } - assert_equal 3, @times_called - end - - def test_legacy_max_retries - @explode = lambda {|n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { conn(1).get("/unstable") } - assert_equal 2, @times_called - end - - def test_legacy_max_negative_retries - @explode = lambda {|n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { conn(-9).get("/unstable") } - assert_equal 1, @times_called - end - - def test_new_max_retries - @explode = lambda {|n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { conn(:max => 3).get("/unstable") } - assert_equal 4, @times_called - end - - def test_new_max_negative_retries - @explode = lambda { |n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { conn(:max => -9).get("/unstable") } - assert_equal 1, @times_called - end - - def test_interval - @explode = lambda {|n| raise Errno::ETIMEDOUT } - started = Time.now - assert_raises(Errno::ETIMEDOUT) { - conn(:max => 2, :interval => 0.1).get("/unstable") - } - assert_in_delta 0.2, Time.now - started, 0.04 - end - - def test_calls_calculate_sleep_amount - explode_app = MiniTest::Mock.new - explode_app.expect(:call, nil, [{:body=>nil}]) - def explode_app.call(env) - raise Errno::ETIMEDOUT - end - - retry_middleware = Faraday::Request::Retry.new(explode_app) - class << retry_middleware - attr_accessor :sleep_amount_retries - - def calculate_sleep_amount(retries, env) - self.sleep_amount_retries.delete(retries) - 0 - end - end - retry_middleware.sleep_amount_retries = [2, 1] - - assert_raises(Errno::ETIMEDOUT) { - retry_middleware.call({:method => :get}) - } - - assert_empty retry_middleware.sleep_amount_retries - end - - def test_exponential_backoff - middleware = Faraday::Request::Retry.new(nil, :max => 5, :interval => 0.1, :backoff_factor => 2) - assert_equal middleware.send(:calculate_retry_interval, 5), 0.1 - assert_equal middleware.send(:calculate_retry_interval, 4), 0.2 - assert_equal middleware.send(:calculate_retry_interval, 3), 0.4 - end - - def test_exponential_backoff_with_max_interval - middleware = Faraday::Request::Retry.new(nil, :max => 5, :interval => 1, :max_interval => 3, :backoff_factor => 2) - assert_equal middleware.send(:calculate_retry_interval, 5), 1 - assert_equal middleware.send(:calculate_retry_interval, 4), 2 - assert_equal middleware.send(:calculate_retry_interval, 3), 3 - assert_equal middleware.send(:calculate_retry_interval, 2), 3 - end - - def test_random_additional_interval_amount - middleware = Faraday::Request::Retry.new(nil, :max => 2, :interval => 0.1, :interval_randomness => 1.0) - sleep_amount = middleware.send(:calculate_retry_interval, 2) - assert_operator sleep_amount, :>=, 0.1 - assert_operator sleep_amount, :<=, 0.2 - middleware = Faraday::Request::Retry.new(nil, :max => 2, :interval => 0.1, :interval_randomness => 0.5) - sleep_amount = middleware.send(:calculate_retry_interval, 2) - assert_operator sleep_amount, :>=, 0.1 - assert_operator sleep_amount, :<=, 0.15 - middleware = Faraday::Request::Retry.new(nil, :max => 2, :interval => 0.1, :interval_randomness => 0.25) - sleep_amount = middleware.send(:calculate_retry_interval, 2) - assert_operator sleep_amount, :>=, 0.1 - assert_operator sleep_amount, :<=, 0.125 - end - - def test_custom_exceptions - @explode = lambda {|n| raise "boom!" } - assert_raises(RuntimeError) { - conn(:exceptions => StandardError).get("/unstable") - } - assert_equal 3, @times_called - end - - def test_should_retry_with_body_if_block_returns_true_for_non_idempotent_request - body = { :foo => :bar } - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| true } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check).post("/unstable", body) - } - assert_equal 3, @times_called - assert @envs.all? { |env| env[:body] === body } - end - - def test_should_stop_retrying_if_block_returns_false_checking_env - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| env[:method] != :post } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check).post("/unstable") - } - assert_equal 1, @times_called - end - - def test_should_stop_retrying_if_block_returns_false_checking_exception - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| !exception.kind_of?(Errno::ETIMEDOUT) } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check).post("/unstable") - } - assert_equal 1, @times_called - end - - def test_should_not_call_retry_if_for_idempotent_methods_if_methods_unspecified - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| raise "this should have never been called" } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check).get("/unstable") - } - assert_equal 3, @times_called - end - - def test_should_not_retry_for_non_idempotent_method_if_methods_unspecified - @explode = lambda {|n| raise Errno::ETIMEDOUT } - assert_raises(Errno::ETIMEDOUT) { - conn.post("/unstable") - } - assert_equal 1, @times_called - end - - def test_should_not_call_retry_if_for_specified_methods - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| raise "this should have never been called" } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check, :methods => [:post]).post("/unstable") - } - assert_equal 3, @times_called - end - - def test_should_call_retry_if_for_empty_method_list - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| @times_called < 2 } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check, :methods => []).get("/unstable") - } - assert_equal 2, @times_called - end - - def test_should_rewind_files_on_retry - io = StringIO.new("Test data") - upload_io = Faraday::UploadIO.new(io, "application/octet/stream") - - rewound = 0 - rewind = lambda { rewound += 1 } - - upload_io.stub :rewind, rewind do - @explode = lambda {|n| raise Errno::ETIMEDOUT } - check = lambda { |env,exception| true } - assert_raises(Errno::ETIMEDOUT) { - conn(:retry_if => check).post("/unstable", { :file => upload_io }) - } - end - assert_equal 3, @times_called - assert_equal 2, rewound - end - - def test_should_retry_retriable_response - params = { status: 429 } - response = conn(:max => 1, :retry_statuses => 429).get("/throttled", params) - - assert_equal 2, @times_called - assert_equal 429, response.status - end - - def test_should_not_retry_non_retriable_response - params = { status: 503 } - conn(:max => 1, :retry_statuses => 429).get("/throttled", params) - - assert_equal 1, @times_called - end - - def test_interval_if_retry_after_present - started = Time.now - - params = { :retry_after => 0.5 } - conn(:max => 1, :interval => 0.1, :retry_statuses => [429]).get("/throttled", params) - - assert Time.now - started > 0.5 - end - - def test_should_ignore_retry_after_if_less_then_calculated - started = Time.now - - params = { :retry_after => 0.1 } - conn(:max => 1, :interval => 0.2, :retry_statuses => [429]).get("/throttled", params) - - assert Time.now - started > 0.2 - end - - def test_interval_when_retry_after_is_timestamp - started = Time.now - - params = { :retry_after => (Time.now.utc + 2).strftime('%a, %d %b %Y %H:%M:%S GMT') } - conn(:max => 1, :interval => 0.1, :retry_statuses => [429]).get("/throttled", params) - - assert Time.now - started > 1 - end - - def test_should_not_retry_when_retry_after_greater_then_max_interval - params = { :retry_after => (Time.now.utc + 20).strftime('%a, %d %b %Y %H:%M:%S GMT') } - conn(:max => 2, :interval => 0.1, :retry_statuses => [429], max_interval: 5).get("/throttled", params) - - assert_equal 1, @times_called - end - end -end diff --git a/test/middleware_stack_test.rb b/test/middleware_stack_test.rb deleted file mode 100644 index 5ee5b5a45..000000000 --- a/test/middleware_stack_test.rb +++ /dev/null @@ -1,217 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class MiddlewareStackTest < Faraday::TestCase - # mock handler classes - class Handler < Struct.new(:app) - def call(env) - (env[:request_headers]['X-Middleware'] ||= '') << ":#{self.class.name.split('::').last}" - app.call(env) - end - end - class Apple < Handler; end - class Orange < Handler; end - class Banana < Handler; end - - class Broken < Faraday::Middleware - dependency 'zomg/i_dont/exist' - end - - def setup - @conn = Faraday::Connection.new - @builder = @conn.builder - end - - def test_default_stack - default_middleware = Faraday::Request.lookup_middleware(:url_encoded) - default_adapter_klass = Faraday::Adapter.lookup_middleware(Faraday.default_adapter) - assert @builder[0] == default_middleware - assert @builder.adapter == default_adapter_klass - end - - def test_sets_default_adapter_if_none_set - @conn = Faraday::Connection.new do |_| - # custom stack, but missing adapter - end - @builder = @conn.builder - default_adapter_klass = Faraday::Adapter.lookup_middleware(Faraday.default_adapter) - assert_nil @builder[0] - assert @builder.adapter == default_adapter_klass - end - - def test_use_provided_adapter - @conn = Faraday::Connection.new do |builder| - builder.adapter :test - end - @builder = @conn.builder - test_adapter_klass = Faraday::Adapter.lookup_middleware(:test) - assert_nil @builder[0] - assert @builder.adapter == test_adapter_klass - end - - def test_allows_rebuilding - build_stack Apple - build_stack Orange - assert_handlers %w[Orange] - end - - def test_allows_extending - build_handlers_stack Apple - @builder.use Orange - @builder.adapter :test, &test_adapter - assert_handlers %w[Apple Orange] - end - - def test_builder_is_passed_to_new_faraday_connection - new_conn = Faraday::Connection.new :builder => @builder - assert_equal @builder, new_conn.builder - end - - def test_insert_before - build_handlers_stack Apple, Orange - @builder.insert_before Apple, Banana - @builder.adapter :test, &test_adapter - assert_handlers %w[Banana Apple Orange] - end - - def test_insert_after - build_handlers_stack Apple, Orange - @builder.insert_after Apple, Banana - @builder.adapter :test, &test_adapter - assert_handlers %w[Apple Banana Orange] - end - - def test_swap_handlers - build_handlers_stack Apple, Orange - @builder.swap Apple, Banana - @builder.adapter :test, &test_adapter - assert_handlers %w[Banana Orange] - end - - def test_delete_handler - build_stack Apple, Orange - @builder.delete Apple - assert_handlers %w[Orange] - end - - def test_stack_is_locked_after_making_requests - build_stack Apple - assert !@builder.locked? - @conn.get('/') - assert @builder.locked? - - assert_raises Faraday::RackBuilder::StackLocked do - @conn.use Orange - end - end - - def test_duped_stack_is_unlocked - build_stack Apple - assert !@builder.locked? - @builder.lock! - assert @builder.locked? - - duped_connection = @conn.dup - assert_equal @builder, duped_connection.builder - assert !duped_connection.builder.locked? - end - - def test_handler_comparison - build_stack Apple - assert_equal @builder.handlers.first, Apple - assert_equal @builder.handlers[0,1], [Apple] - assert_equal @builder.handlers.first, Faraday::RackBuilder::Handler.new(Apple) - end - - def test_unregistered_symbol - err = assert_raises(Faraday::Error){ build_stack :apple } - assert_equal ":apple is not registered on Faraday::Middleware", err.message - end - - def test_registered_symbol - Faraday::Middleware.register_middleware :apple => Apple - begin - build_stack :apple - assert_handlers %w[Apple] - ensure - unregister_middleware Faraday::Middleware, :apple - end - end - - def test_registered_symbol_with_proc - Faraday::Middleware.register_middleware :apple => lambda { Apple } - begin - build_stack :apple - assert_handlers %w[Apple] - ensure - unregister_middleware Faraday::Middleware, :apple - end - end - - def test_registered_symbol_with_array - Faraday::Middleware.register_middleware File.expand_path("..", __FILE__), - :strawberry => [lambda { Strawberry }, 'strawberry'] - begin - build_stack :strawberry - assert_handlers %w[Strawberry] - ensure - unregister_middleware Faraday::Middleware, :strawberry - end - end - - def test_missing_dependencies - build_stack Broken - err = assert_raises RuntimeError do - @conn.get('/') - end - assert_match "missing dependency for MiddlewareStackTest::Broken: ", err.message - assert_match "zomg/i_dont/exist", err.message - end - - def test_env_stored_on_middleware_response_has_reference_to_the_response - env = response = nil - build_stack Struct.new(:app) { - define_method(:call) { |e| env, response = e, app.call(e) } - } - @conn.get("/") - assert_same env.response, response.env.response - end - - private - - # make a stack with test adapter that reflects the order of middleware - def build_stack(*handlers) - @builder.build do |b| - handlers.each { |handler| b.use(*handler) } - yield(b) if block_given? - - @builder.adapter :test, &test_adapter - end - end - - def build_handlers_stack(*handlers) - @builder.build do |b| - handlers.each { |handler| b.use(*handler) } - end - end - - def test_adapter - Proc.new do |stub| - stub.get '/' do |env| - # echo the "X-Middleware" request header in the body - [200, {}, env[:request_headers]['X-Middleware'].to_s] - end - end - end - - def assert_handlers(list) - echoed_list = @conn.get('/').body.to_s.split(':') - echoed_list.shift if echoed_list.first == '' - assert_equal list, echoed_list - end - - def unregister_middleware(component, key) - # TODO: unregister API? - component.instance_variable_get('@registered_middleware').delete(key) - end -end - diff --git a/test/multibyte.txt b/test/multibyte.txt deleted file mode 100644 index 24a84b045..000000000 --- a/test/multibyte.txt +++ /dev/null @@ -1 +0,0 @@ -ファイル diff --git a/test/options_test.rb b/test/options_test.rb deleted file mode 100644 index f006213e8..000000000 --- a/test/options_test.rb +++ /dev/null @@ -1,333 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class OptionsTest < Faraday::TestCase - SubOptions = Class.new(Faraday::Options.new(:sub_a, :sub_b)) - class ParentOptions < Faraday::Options.new(:a, :b, :c) - options :c => SubOptions - end - - def test_deep_merge - sub_opts1 = SubOptions.from(sub_a: 3) - sub_opts2 = SubOptions.from(sub_b: 4) - opt1 = ParentOptions.from(a: 1, c: sub_opts1) - opt2 = ParentOptions.from(b: 2, c: sub_opts2) - - merged = opt1.merge(opt2) - - expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4) - assert_equal merged, ParentOptions.from(a: 1, b: 2, c: expected_sub_opts) - end - - def test_deep_merge_with_hash - sub_opts1 = SubOptions.from(sub_a: 3) - sub_opts2 = { sub_b: 4 } - opt1 = ParentOptions.from(a: 1, c: sub_opts1) - opt2 = { b: 2, c: sub_opts2 } - - merged = opt1.merge(opt2) - - expected_sub_opts = SubOptions.from(sub_a: 3, sub_b: 4) - assert_equal merged, ParentOptions.from(a: 1, b: 2, c: expected_sub_opts) - end - - def test_deep_merge_with_nil - sub_opts = SubOptions.new(3, 4) - options = ParentOptions.new(1, 2, sub_opts) - assert_equal options.a, 1 - assert_equal options.b, 2 - assert_equal options.c.sub_a, 3 - assert_equal options.c.sub_b, 4 - - options2 = ParentOptions.from(b: 5, c: nil) - - merged = options.merge(options2) - - assert_equal merged.b, 5 - assert_equal merged.c, sub_opts - end - - def test_deep_merge_with_sub_nil - options = ParentOptions.from(a: 1) - - sub_opts = SubOptions.new(3, 4) - options2 = ParentOptions.from(b: 2, c: sub_opts) - - assert_equal options.a, 1 - assert_equal options2.b, 2 - assert_equal options2.c.sub_a, 3 - assert_equal options2.c.sub_b, 4 - - merged = options.merge(options2) - - assert_equal merged.c, sub_opts - end - - def test_dup_is_shallow - sub_opts = SubOptions.from(sub_a: 3) - opts = ParentOptions.from(b: 1, c: sub_opts) - - duped = opts.dup - duped.b = 2 - duped.c.sub_a = 4 - - assert_equal opts.b, 1 - assert_equal opts.c.sub_a, 4 - end - - def test_deep_dup - sub_opts = SubOptions.from(sub_a: 3) - opts = ParentOptions.from(b: 1, c: sub_opts) - - duped = opts.deep_dup - duped.b = 2 - duped.c.sub_a = 4 - - assert_equal opts.b, 1 - assert_equal opts.c.sub_a, 3 - end - - def test_clear - options = SubOptions.new(1) - assert !options.empty? - assert options.clear - assert options.empty? - end - - def test_empty - options = SubOptions.new - assert options.empty? - options.sub_a = 1 - assert !options.empty? - options.delete(:sub_a) - assert options.empty? - end - - def test_each_key - options = ParentOptions.new(1, 2, 3) - enum = options.each_key - assert_equal enum.next.to_sym, :a - assert_equal enum.next.to_sym, :b - assert_equal enum.next.to_sym, :c - end - - def test_key? - options = SubOptions.new - assert !options.key?(:sub_a) - options.sub_a = 1 - assert options.key?(:sub_a) - end - - def test_each_value - options = ParentOptions.new(1, 2, 3) - enum = options.each_value - assert_equal enum.next, 1 - assert_equal enum.next, 2 - assert_equal enum.next, 3 - end - - def test_value? - options = SubOptions.new - assert !options.value?(1) - options.sub_a = 1 - assert options.value?(1) - end - - def test_request_proxy_setter - options = Faraday::RequestOptions.new - assert_nil options.proxy - - assert_raises NoMethodError do - options[:proxy] = {:booya => 1} - end - - options[:proxy] = {:user => 'user'} - assert_kind_of Faraday::ProxyOptions, options.proxy - assert_equal 'user', options.proxy.user - - options.proxy = nil - assert_nil options.proxy - end - - def test_proxy_options_from_string - options = Faraday::ProxyOptions.from 'http://user:pass@example.org' - assert_equal 'user', options.user - assert_equal 'pass', options.password - assert_kind_of URI, options.uri - assert_equal '', options.path - assert_equal 80, options.port - assert_equal 'example.org', options.host - assert_equal 'http', options.scheme - end - - def test_proxy_options_from_nil - options = Faraday::ProxyOptions.from nil - assert_kind_of Faraday::ProxyOptions, options - end - - def test_proxy_options_hash_access - proxy = Faraday::ProxyOptions.from 'http://a%40b:pw%20d@example.org' - assert_equal 'a@b', proxy[:user] - assert_equal 'a@b', proxy.user - assert_equal 'pw d', proxy[:password] - assert_equal 'pw d', proxy.password - end - - def test_proxy_options_no_auth - proxy = Faraday::ProxyOptions.from 'http://example.org' - assert_nil proxy.user - assert_nil proxy.password - end - - def test_from_options - options = ParentOptions.new(1) - - value = ParentOptions.from(options) - assert_equal 1, value.a - assert_nil value.b - end - - def test_from_options_with_sub_object - sub = SubOptions.new(1) - options = ParentOptions.from :a => 1, :c => sub - assert_kind_of ParentOptions, options - assert_equal 1, options.a - assert_nil options.b - assert_kind_of SubOptions, options.c - assert_equal 1, options.c.sub_a - end - - def test_from_hash - options = ParentOptions.from :a => 1 - assert_kind_of ParentOptions, options - assert_equal 1, options.a - assert_nil options.b - end - - def test_from_hash_with_sub_object - options = ParentOptions.from :a => 1, :c => {:sub_a => 1} - assert_kind_of ParentOptions, options - assert_equal 1, options.a - assert_nil options.b - assert_kind_of SubOptions, options.c - assert_equal 1, options.c.sub_a - end - - def test_inheritance - subclass = Class.new(ParentOptions) - options = subclass.from(:c => {:sub_a => 'hello'}) - assert_kind_of SubOptions, options.c - assert_equal 'hello', options.c.sub_a - end - - def test_from_deep_hash - hash = {:b => 1} - options = ParentOptions.from :a => hash - assert_equal 1, options.a[:b] - - hash[:b] = 2 - assert_equal 1, options.a[:b] - - options.a[:b] = 3 - assert_equal 2, hash[:b] - assert_equal 3, options.a[:b] - end - - def test_from_nil - options = ParentOptions.from(nil) - assert_kind_of ParentOptions, options - assert_nil options.a - assert_nil options.b - end - - def test_invalid_key - assert_raises NoMethodError do - ParentOptions.from :invalid => 1 - end - end - - def test_update - options = ParentOptions.new(1) - assert_equal 1, options.a - assert_nil options.b - - updated = options.update :a => 2, :b => 3 - assert_equal 2, options.a - assert_equal 3, options.b - assert_equal options, updated - end - - def test_delete - options = ParentOptions.new(1) - assert_equal 1, options.a - assert_equal 1, options.delete(:a) - assert_nil options.a - end - - def test_merge - options = ParentOptions.new(1) - assert_equal 1, options.a - assert_nil options.b - - dup = options.merge :a => 2, :b => 3 - assert_equal 2, dup.a - assert_equal 3, dup.b - assert_equal 1, options.a - assert_nil options.b - end - - def test_env_access_member - e = Faraday::Env.new - assert_nil e.method - e.method = :get - assert_equal :get, e.method - end - - def test_env_access_symbol_non_member - e = Faraday::Env.new - assert_nil e[:custom] - e[:custom] = :boom - assert_equal :boom, e[:custom] - end - - def test_env_access_string_non_member - e = Faraday::Env.new - assert_nil e["custom"] - e["custom"] = :boom - assert_equal :boom, e["custom"] - end - - def test_env_fetch_ignores_false - ssl = Faraday::SSLOptions.new - ssl.verify = false - assert !ssl.fetch(:verify, true) - end - - def test_fetch_grabs_value - opt = Faraday::SSLOptions.new - opt.verify = 1 - assert_equal 1, opt.fetch(:verify, false) { |k| :blah } - end - - def test_fetch_uses_falsey_default - opt = Faraday::SSLOptions.new - assert_equal false, opt.fetch(:verify, false) { |k| :blah } - end - - def test_fetch_accepts_block - opt = Faraday::SSLOptions.new - assert_equal "yo :verify", opt.fetch(:verify) { |k| "yo #{k.inspect}"} - end - - def test_fetch_needs_a_default_if_key_is_missing - opt = Faraday::SSLOptions.new - assert_raises Faraday::Options.fetch_error_class do - opt.fetch :verify - end - end - - def test_fetch_works_with_key - opt = Faraday::SSLOptions.new - opt.verify = 1 - assert_equal 1, opt.fetch(:verify) - end -end diff --git a/test/parameters_test.rb b/test/parameters_test.rb deleted file mode 100644 index e97bbf04d..000000000 --- a/test/parameters_test.rb +++ /dev/null @@ -1,157 +0,0 @@ -require File.expand_path("../helper", __FILE__) -require "rack/utils" - -class TestParameters < Faraday::TestCase - # emulates ActiveSupport::SafeBuffer#gsub - FakeSafeBuffer = Struct.new(:string) do - def to_s() self end - def gsub(regex) - string.gsub(regex) { - match, = $&, '' =~ /a/ - yield(match) - } - end - end - - def test_escaping_safe_buffer_nested - monies = FakeSafeBuffer.new("$32,000.00") - assert_equal "a=%2432%2C000.00", Faraday::NestedParamsEncoder.encode("a" => monies) - end - - def test_escaping_safe_buffer_flat - monies = FakeSafeBuffer.new("$32,000.00") - assert_equal "a=%2432%2C000.00", Faraday::FlatParamsEncoder.encode("a" => monies) - end - - def test_raises_typeerror_nested - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.encode("") - end - assert_equal "Can't convert String into Hash.", error.message - end - - def test_raises_typeerror_flat - error = assert_raises TypeError do - Faraday::FlatParamsEncoder.encode("") - end - assert_equal "Can't convert String into Hash.", error.message - end - - def test_decode_array_nested - query = "a[1]=one&a[2]=two&a[3]=three" - expected = {"a" => ["one", "two", "three"]} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_array_flat - query = "a=one&a=two&a=three" - expected = {"a" => ["one", "two", "three"]} - assert_equal expected, Faraday::FlatParamsEncoder.decode(query) - end - - def test_nested_decode_hash - query = "a[b1]=one&a[b2]=two&a[b][c]=foo" - expected = {"a" => {"b1" => "one", "b2" => "two", "b" => {"c" => "foo"}}} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_encode_nil_nested - assert_equal "a", Faraday::NestedParamsEncoder.encode("a" => nil) - end - - def test_encode_nil_flat - assert_equal "a", Faraday::FlatParamsEncoder.encode("a" => nil) - end - - def test_decode_nested_array_rack_compat - query = "a[][one]=1&a[][two]=2&a[][one]=3&a[][two]=4" - expected = Rack::Utils.parse_nested_query(query) - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_array_mixed_types - query = "a[][one]=1&a[]=2&a[]=&a[]" - expected = Rack::Utils.parse_nested_query(query) - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_ignores_invalid_array - query = "[][a]=1&b=2" - expected = {"a" => "1", "b" => "2"} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_ignores_repeated_array_notation - query = "a[][][]=1" - expected = {"a" => ["1"]} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_ignores_malformed_keys - query = "=1&[]=2" - expected = {} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_subkeys_dont_have_to_be_in_brackets - query = "a[b]c[d]e=1" - expected = {"a" => {"b" => {"c" => {"d" => {"e" => "1"}}}}} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_decode_nested_raises_error_when_expecting_hash - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.decode("a=1&a[b]=2") - end - assert_equal "expected Hash (got String) for param `a'", error.message - - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.decode("a[]=1&a[b]=2") - end - assert_equal "expected Hash (got Array) for param `a'", error.message - - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.decode("a[b]=1&a[]=2") - end - assert_equal "expected Array (got Hash) for param `a'", error.message - - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.decode("a=1&a[]=2") - end - assert_equal "expected Array (got String) for param `a'", error.message - - error = assert_raises TypeError do - Faraday::NestedParamsEncoder.decode("a[b]=1&a[b][c]=2") - end - assert_equal "expected Hash (got String) for param `b'", error.message - end - - def test_decode_nested_final_value_overrides_any_type - query = "a[b][c]=1&a[b]=2" - expected = {"a" => {"b" => "2"}} - assert_equal expected, Faraday::NestedParamsEncoder.decode(query) - end - - def test_encode_rack_compat_nested - params = { :a => [{:one => "1", :two => "2"}, "3", ""] } - expected = Rack::Utils.build_nested_query(params) - assert_equal expected.split("&").sort, - Faraday::Utils.unescape(Faraday::NestedParamsEncoder.encode(params)).split("&").sort - end - - def test_encode_empty_string_array_value - expected = 'baz=&foo%5Bbar%5D=' - assert_equal expected, Faraday::NestedParamsEncoder.encode(foo: {bar: ''}, baz: '') - end - - def test_encode_nil_array_value - expected = 'baz&foo%5Bbar%5D' - assert_equal expected, Faraday::NestedParamsEncoder.encode(foo: {bar: nil}, baz: nil) - end - - def test_encode_empty_array_value - expected = 'baz%5B%5D&foo%5Bbar%5D%5B%5D' - Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: []) - assert_equal expected, Faraday::NestedParamsEncoder.encode(foo: { bar: [] }, baz: []) - end -end diff --git a/test/request_middleware_test.rb b/test/request_middleware_test.rb deleted file mode 100644 index e3b99470c..000000000 --- a/test/request_middleware_test.rb +++ /dev/null @@ -1,126 +0,0 @@ -# encoding: utf-8 -require File.expand_path('../helper', __FILE__) - -Faraday::CompositeReadIO.class_eval { attr_reader :ios } - -class RequestMiddlewareTest < Faraday::TestCase - def conn - Faraday.new do |b| - b.request :multipart - b.request :url_encoded - b.adapter :test do |stub| - stub.post('/echo') do |env| - posted_as = env[:request_headers]['Content-Type'] - [200, {'Content-Type' => posted_as}, env[:body]] - end - end - end - end - - def test_does_nothing_without_payload - response = conn.post('/echo') - assert_nil response.headers['Content-Type'] - assert response.body.empty? - end - - def test_ignores_custom_content_type - response = conn.post('/echo', { :some => 'data' }, 'content-type' => 'application/x-foo') - assert_equal 'application/x-foo', response.headers['Content-Type'] - assert_equal({ :some => 'data' }, response.body) - end - - def test_url_encoded_no_header - response = conn.post('/echo', { :fruit => %w[apples oranges] }) - assert_equal 'application/x-www-form-urlencoded', response.headers['Content-Type'] - assert_equal 'fruit%5B%5D=apples&fruit%5B%5D=oranges', response.body - end - - def test_url_encoded_with_header - response = conn.post('/echo', {'a'=>123}, 'content-type' => 'application/x-www-form-urlencoded') - assert_equal 'application/x-www-form-urlencoded', response.headers['Content-Type'] - assert_equal 'a=123', response.body - end - - def test_url_encoded_nested - response = conn.post('/echo', { :user => {:name => 'Mislav', :web => 'mislav.net'} }) - assert_equal 'application/x-www-form-urlencoded', response.headers['Content-Type'] - expected = { 'user' => {'name' => 'Mislav', 'web' => 'mislav.net'} } - assert_equal expected, Faraday::Utils.parse_nested_query(response.body) - end - - def test_url_encoded_non_nested - response = conn.post('/echo', { :dimensions => ['date', 'location']}) do |req| - req.options.params_encoder = Faraday::FlatParamsEncoder - end - assert_equal 'application/x-www-form-urlencoded', response.headers['Content-Type'] - expected = { 'dimensions' => ['date', 'location'] } - assert_equal expected, Faraday::Utils.parse_query(response.body) - assert_equal 'dimensions=date&dimensions=location', response.body - end - - def test_url_encoded_unicode - err = capture_warnings { - response = conn.post('/echo', {:str => "eé cç aã aâ"}) - assert_equal "str=e%C3%A9+c%C3%A7+a%C3%A3+a%C3%A2", response.body - } - assert err.empty?, "stderr did include: #{err}" - end - - def test_url_encoded_nested_keys - response = conn.post('/echo', {'a'=>{'b'=>{'c'=>['d']}}}) - assert_equal "a%5Bb%5D%5Bc%5D%5B%5D=d", response.body - end - - def test_multipart - # assume params are out of order - regexes = [ - /name\=\"a\"/, - /name=\"b\[c\]\"\; filename\=\"request_middleware_test\.rb\"/, - /name=\"b\[d\]\"/] - - payload = {:a => 1, :b => {:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}} - response = conn.post('/echo', payload) - - assert_kind_of Faraday::CompositeReadIO, response.body - assert response.headers['Content-Type'].start_with?( - "multipart/form-data; boundary=%s" % Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX, - ) - - response.body.send(:ios).map{|io| io.read}.each do |io| - if re = regexes.detect { |r| io =~ r } - regexes.delete re - end - end - assert_equal [], regexes - end - - def test_multipart_with_arrays - # assume params are out of order - regexes = [ - /name\=\"a\"/, - /name=\"b\[\]\[c\]\"\; filename\=\"request_middleware_test\.rb\"/, - /name=\"b\[\]\[d\]\"/] - - payload = {:a => 1, :b =>[{:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}]} - response = conn.post('/echo', payload) - - assert_kind_of Faraday::CompositeReadIO, response.body - assert response.headers['Content-Type'].start_with?( - "multipart/form-data; boundary=%s" % Faraday::Request::Multipart::DEFAULT_BOUNDARY_PREFIX, - ) - - response.body.send(:ios).map{|io| io.read}.each do |io| - if re = regexes.detect { |r| io =~ r } - regexes.delete re - end - end - assert_equal [], regexes - end - - def test_multipart_unique_boundary - payload = {:a => 1, :b =>[{:c => Faraday::UploadIO.new(__FILE__, 'text/x-ruby'), :d => 2}]} - response1 = conn.post('/echo', payload) - response2 = conn.post('/echo', payload) - assert response1.headers['Content-Type'] != response2.headers['Content-Type'] - end -end diff --git a/test/response_middleware_test.rb b/test/response_middleware_test.rb deleted file mode 100644 index c22215ee6..000000000 --- a/test/response_middleware_test.rb +++ /dev/null @@ -1,72 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class ResponseMiddlewareTest < Faraday::TestCase - def setup - @conn = Faraday.new do |b| - b.response :raise_error - b.adapter :test do |stub| - stub.get('ok') { [200, {'Content-Type' => 'text/html'}, ''] } - stub.get('not-found') { [404, {'X-Reason' => 'because'}, 'keep looking'] } - stub.get('error') { [500, {'X-Error' => 'bailout'}, 'fail'] } - end - end - end - - class ResponseUpcaser < Faraday::Response::Middleware - def parse(body) - body.upcase - end - end - - def test_success - assert @conn.get('ok') - end - - def test_raises_not_found - error = assert_raises Faraday::Error::ResourceNotFound do - @conn.get('not-found') - end - assert_equal 'the server responded with status 404', error.message - assert_equal 'because', error.response[:headers]['X-Reason'] - end - - def test_raises_error - error = assert_raises Faraday::Error::ClientError do - @conn.get('error') - end - assert_equal 'the server responded with status 500', error.message - assert_equal 'bailout', error.response[:headers]['X-Error'] - end - - def test_upcase - @conn.builder.insert(0, ResponseUpcaser) - assert_equal '', @conn.get('ok').body - end -end - -class ResponseNoBodyMiddleWareTest < Faraday::TestCase - def setup - @conn = Faraday.new do |b| - b.response :raise_error - b.adapter :test do |stub| - stub.get('not_modified') { [304, nil, nil] } - stub.get('no_content') { [204, nil, nil] } - end - end - @conn.builder.insert(0, NotCalled) - end - - class NotCalled < Faraday::Response::Middleware - def parse(body) - raise "this should not be called" - end - end - - def test_204 - assert_nil @conn.get('no_content').body - end - - def test_304 - assert_nil @conn.get('not_modified').body - end -end diff --git a/test/strawberry.rb b/test/strawberry.rb deleted file mode 100644 index 80c8d0b34..000000000 --- a/test/strawberry.rb +++ /dev/null @@ -1,2 +0,0 @@ -class MiddlewareStackTest::Strawberry < MiddlewareStackTest::Handler -end diff --git a/test/utils_test.rb b/test/utils_test.rb deleted file mode 100644 index 1b116c5f9..000000000 --- a/test/utils_test.rb +++ /dev/null @@ -1,98 +0,0 @@ -require File.expand_path('../helper', __FILE__) - -class TestUtils < Faraday::TestCase - - # Headers parsing - - def test_headers - "HTTP/1.x 500 OK\r\nContent-Type: text/html; charset=UTF-8\r\n" \ - "HTTP/1.x 200 OK\r\nContent-Type: application/json; charset=UTF-8\r\n\r\n" - end - - def test_headers_parsing_for_aggregated_responses - headers = Faraday::Utils::Headers.new() - headers.parse(test_headers) - - result = headers.to_hash - - assert_equal result["Content-Type"], "application/json; charset=UTF-8" - end - - # URI parsing - - def setup - @url = "http://example.com/abc" - end - - # emulates ActiveSupport::SafeBuffer#gsub - FakeSafeBuffer = Struct.new(:string) do - def to_s() self end - def gsub(regex) - string.gsub(regex) { - match, = $&, '' =~ /a/ - yield(match) - } - end - end - - def test_escaping_safe_buffer - str = FakeSafeBuffer.new('$32,000.00') - assert_equal '%2432%2C000.00', Faraday::Utils.escape(str) - end - - def test_parses_with_default - with_default_uri_parser(nil) do - uri = normalize(@url) - assert_equal 'example.com', uri.host - end - end - - def test_parses_with_URI - with_default_uri_parser(::URI) do - uri = normalize(@url) - assert_equal 'example.com', uri.host - end - end - - def test_parses_with_block - with_default_uri_parser(lambda {|u| "booya#{"!" * u.size}" }) do - assert_equal 'booya!!!!!!!!!!!!!!!!!!!!!!', normalize(@url) - end - end - - def test_replace_header_hash - headers = Faraday::Utils::Headers.new('authorization' => 't0ps3cr3t!') - assert headers.include?('authorization') - - headers.replace({'content-type' => 'text/plain'}) - - assert !headers.include?('authorization') - end - - def normalize(url) - Faraday::Utils::URI(url) - end - - def with_default_uri_parser(parser) - old_parser = Faraday::Utils.default_uri_parser - begin - Faraday::Utils.default_uri_parser = parser - yield - ensure - Faraday::Utils.default_uri_parser = old_parser - end - end - - # YAML parsing - - def test_headers_yaml_roundtrip - headers = Faraday::Utils::Headers.new('User-Agent' => 'safari', 'Content-type' => 'text/html') - result = YAML.load(headers.to_yaml) - - assert result.include?('user-agent'), 'Unable to hydrate to a correct Headers' - assert result.include?('content-type'), 'Unable to hydrate to a correct Headers' - assert result['user-agent'] == 'safari', 'Unable to access rehydrated Headers' - assert result['content-type'] == 'text/html', 'Unable to access rehydrated Headers' - end -end -