[DRE-commits] [ruby-rest-client] 01/12: New upstream version 2.0.2
Lucas Nussbaum
lucas at moszumanska.debian.org
Sat Jul 8 08:38:20 UTC 2017
This is an automated email from the git hooks/post-receive script.
lucas pushed a commit to branch master
in repository ruby-rest-client.
commit 97574ac53ec6e155c23ceb82f48f2d396b818e38
Author: Lucas Nussbaum <lucas at debian.org>
Date: Sat Jul 1 20:35:47 2017 +0200
New upstream version 2.0.2
---
.gitignore | 1 +
.rspec | 3 +-
.rubocop-disables.yml | 384 ++++++++++++
.rubocop.yml | 3 +
.travis.yml | 46 +-
AUTHORS | 19 +-
README.md | 784 +++++++++++++++++++++++
README.rdoc | 324 ----------
Rakefile | 18 +-
bin/restclient | 8 +-
history.md | 128 ++++
lib/restclient.rb | 23 +-
lib/restclient/abstract_response.rb | 201 ++++--
lib/restclient/exceptions.rb | 145 +++--
lib/restclient/params_array.rb | 72 +++
lib/restclient/payload.rb | 113 ++--
lib/restclient/platform.rb | 19 +
lib/restclient/raw_response.rb | 7 +-
lib/restclient/request.rb | 742 ++++++++++++++--------
lib/restclient/resource.rb | 6 +-
lib/restclient/response.rb | 71 ++-
lib/restclient/utils.rb | 235 +++++++
lib/restclient/version.rb | 3 +-
metadata.yml | 253 --------
rest-client.gemspec | 19 +-
spec/helpers.rb | 22 +
spec/integration/_lib.rb | 1 +
spec/integration/httpbin_spec.rb | 87 +++
spec/integration/integration_spec.rb | 108 +++-
spec/integration/request_spec.rb | 27 +-
spec/spec_helper.rb | 29 +-
spec/unit/_lib.rb | 1 +
spec/unit/abstract_response_spec.rb | 127 ++--
spec/unit/exceptions_spec.rb | 69 +-
spec/unit/params_array_spec.rb | 36 ++
spec/unit/payload_spec.rb | 126 ++--
spec/unit/raw_response_spec.rb | 8 +-
spec/unit/request2_spec.rb | 42 +-
spec/unit/request_spec.rb | 1139 ++++++++++++++++++++++------------
spec/unit/resource_spec.rb | 58 +-
spec/unit/response_spec.rb | 180 ++++--
spec/unit/restclient_spec.rb | 30 +-
spec/unit/utils_spec.rb | 147 +++++
spec/unit/windows/root_certs_spec.rb | 6 +-
44 files changed, 4175 insertions(+), 1695 deletions(-)
diff --git a/.gitignore b/.gitignore
index 1a68400..0ce7bb3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -5,3 +5,4 @@
/doc
/pkg
/rdoc
+/.yardoc
diff --git a/.rspec b/.rspec
index 37bd1a9..5f16476 100644
--- a/.rspec
+++ b/.rspec
@@ -1 +1,2 @@
---colour --format progress --order random
+--color
+--format progress
diff --git a/.rubocop-disables.yml b/.rubocop-disables.yml
new file mode 100644
index 0000000..ef15b6b
--- /dev/null
+++ b/.rubocop-disables.yml
@@ -0,0 +1,384 @@
+# This configuration was generated by `rubocop --auto-gen-config`
+# on 2014-07-08 08:57:44 +0000 using RuboCop version 0.24.1.
+# The point is for the user to remove these configuration records
+# one by one as the offenses are removed from the code base.
+# Note that changes in the inspected code, or installation of new
+# versions of RuboCop, may require this file to be generated again.
+
+# TODO
+# Offense count: 1
+# Cop supports --auto-correct.
+Lint/StringConversionInInterpolation:
+ Enabled: false
+
+# Tests only
+# Offense count: 16
+# Cop supports --auto-correct.
+Lint/UnusedBlockArgument:
+ Enabled: false
+
+Lint/Eval:
+ Exclude:
+ - rest-client.windows.gemspec
+
+Lint/HandleExceptions:
+ Exclude:
+ - lib/restclient/utils.rb
+
+Lint/UselessAccessModifier:
+ Exclude:
+ - lib/restclient/windows/root_certs.rb
+
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/Alias:
+ Enabled: false
+
+# TODO
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/AndOr:
+ Enabled: false
+
+# TODO
+# Offense count: 3
+# Cop supports --auto-correct.
+Style/BlockDelimiters:
+ Enabled: false
+
+# Offense count: 48
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BracesAroundHashParameters:
+ Enabled: false
+
+# Offense count: 1
+Style/ClassAndModuleCamelCase:
+ Enabled: false
+
+# Offense count: 2
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/ClassAndModuleChildren:
+ Enabled: false
+
+# TODO?
+# Offense count: 14
+Metrics/AbcSize:
+ Max: 75
+
+# TODO?
+Metrics/MethodLength:
+ Max: 66
+
+# TODO?
+# Offense count: 4
+Metrics/PerceivedComplexity:
+ Max: 24
+
+# Offense count: 1
+# Configuration parameters: CountComments.
+Metrics/ClassLength:
+ Max: 411
+
+# TODO
+# Offense count: 5
+Style/ClassVars:
+ Enabled: false
+
+# TODO
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredMethods.
+Style/CollectionMethods:
+ Enabled: false
+
+# TODO
+# Offense count: 4
+# Cop supports --auto-correct.
+Style/ColonMethodCall:
+ Enabled: false
+
+Style/ConditionalAssignment:
+ EnforcedStyle: assign_inside_condition
+
+# Offense count: 2
+Style/ConstantName:
+ Enabled: false
+
+# TODO: eh?
+# Offense count: 4
+Metrics/CyclomaticComplexity:
+ Max: 22
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/DeprecatedHashMethods:
+ Enabled: false
+
+# TODO: docs
+# Offense count: 17
+Style/Documentation:
+ Enabled: false
+
+# Offense count: 9
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/DotPosition:
+ Enabled: false
+
+# Offense count: 1
+Style/DoubleNegation:
+ Enabled: false
+
+# TODO
+# Offense count: 2
+Style/EachWithObject:
+ Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/EmptyLines:
+ Enabled: false
+
+# Offense count: 11
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundClassBody:
+ Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/EmptyLinesAroundMethodBody:
+ Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/EmptyLinesAroundModuleBody:
+ Enabled: false
+
+# Offense count: 31
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Encoding:
+ Enabled: false
+
+# TODO: exclude
+# Offense count: 1
+# Configuration parameters: Exclude.
+Style/FileName:
+ Enabled: false
+
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/FormatString:
+ Enabled: false
+
+# TODO: enable
+# Cop supports --auto-correct.
+# Configuration parameters: SupportedStyles.
+Style/HashSyntax:
+ Enabled: false
+
+# NOTABUG
+# Offense count: 8
+# Configuration parameters: MaxLineLength.
+Style/IfUnlessModifier:
+ Enabled: false
+
+# TODO: configure
+# Offense count: 6
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/IndentHash:
+ Enabled: false
+
+# NOTABUG
+# Offense count: 19
+Style/Lambda:
+ Enabled: false
+
+# TODO
+# Offense count: 14
+# Cop supports --auto-correct.
+Style/LeadingCommentSpace:
+ Enabled: false
+
+# TODO
+# Offense count: 218
+# Configuration parameters: AllowURI.
+Metrics/LineLength:
+ Max: 340
+
+# TODO
+# Offense count: 28
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/MethodDefParentheses:
+ Enabled: false
+
+# TODO
+# Offense count: 1
+Style/ModuleFunction:
+ Enabled: false
+
+# Offense count: 4
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/Next:
+ Enabled: false
+
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: IncludeSemanticChanges.
+Style/NonNilCheck:
+ Enabled: false
+
+# TODO: exclude
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/Not:
+ Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/NumericLiterals:
+ MinDigits: 11
+
+# TODO?
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: AllowSafeAssignment.
+Style/ParenthesesAroundCondition:
+ Enabled: false
+
+# Offense count: 8
+# Cop supports --auto-correct.
+# Configuration parameters: PreferredDelimiters.
+Style/PercentLiteralDelimiters:
+ PreferredDelimiters:
+ '%w': '{}'
+ '%W': '{}'
+ '%Q': '{}'
+ Exclude:
+ - 'bin/restclient'
+
+# Offense count: 3
+# Configuration parameters: NamePrefixBlacklist.
+Style/PredicateName:
+ Enabled: false
+
+# TODO: configure
+# Offense count: 3
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/RaiseArgs:
+ Enabled: false
+
+# TODO
+# Offense count: 1
+# Cop supports --auto-correct.
+Style/RedundantBegin:
+ Enabled: false
+
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/RedundantSelf:
+ Enabled: false
+
+# Offense count: 1
+Style/RescueModifier:
+ Enabled: false
+ Exclude:
+ - 'bin/restclient'
+
+# TODO: configure
+# Offense count: 12
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SignalException:
+ Enabled: false
+
+# TODO
+# Offense count: 2
+# Cop supports --auto-correct.
+Style/SpaceAfterNot:
+ Enabled: false
+
+# Offense count: 19
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceAroundEqualsInParameterDefault:
+ Enabled: false
+
+# Offense count: 20
+# Cop supports --auto-correct.
+Style/SpaceAroundOperators:
+ Enabled: false
+
+# Offense count: 9
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/SpaceBeforeBlockBraces:
+ Enabled: false
+
+# Offense count: 37
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
+Style/SpaceInsideBlockBraces:
+ Enabled: false
+
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/SpaceInsideBrackets:
+ Enabled: false
+
+# Offense count: 181
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SupportedStyles.
+Style/SpaceInsideHashLiteralBraces:
+ Enabled: false
+
+# TODO
+# Offense count: 9
+# Cop supports --auto-correct.
+Style/SpaceInsideParens:
+ Enabled: false
+
+# Offense count: 414
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/StringLiterals:
+ Enabled: false
+
+Style/TrailingCommaInLiteral:
+ EnforcedStyleForMultiline: comma
+Style/TrailingCommaInArguments:
+ Enabled: false
+
+# TODO: configure
+# Offense count: 1
+# Cop supports --auto-correct.
+# Configuration parameters: ExactNameMatch, AllowPredicates, AllowDSLWriters, Whitelist.
+Style/TrivialAccessors:
+ Enabled: false
+ Exclude: ['lib/restclient/payload.rb']
+
+# TODO?
+# Offense count: 3
+Style/UnlessElse:
+ Enabled: false
+
+# TODO?
+# Offense count: 6
+# Cop supports --auto-correct.
+Style/UnneededPercentQ:
+ Enabled: false
+
+# Offense count: 5
+# Cop supports --auto-correct.
+Style/WordArray:
+ MinSize: 4
+
+# TODO?
+# Offense count: 5
+# Cop supports --auto-correct.
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+Style/BarePercentLiterals:
+ Enabled: false
diff --git a/.rubocop.yml b/.rubocop.yml
new file mode 100644
index 0000000..4a11243
--- /dev/null
+++ b/.rubocop.yml
@@ -0,0 +1,3 @@
+---
+inherit_from:
+- .rubocop-disables.yml
diff --git a/.travis.yml b/.travis.yml
index ca00399..182000c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,14 +1,48 @@
+# Available ruby versions: http://rubies.travis-ci.org/
+
language: ruby
+
+os:
+ - linux
+ - osx
+
rvm:
- - "1.9.2"
- - "1.9.3"
- "2.0.0"
- # Forgo 2.1.0 until Travis has a satisfactory fix for
- # https://github.com/travis-ci/travis-ci/issues/2220
- - "2.1" # always the latest 2.1.x
- - "jruby-19mode"
+ - "2.1" # latest 2.1.x
+ - "2.2.5"
+ - "2.3.3"
+ - "2.4.0"
+ - "ruby-head"
+ - "jruby-9.0.5.0"
+ - "jruby-9.1.5.0"
+ - "jruby-head"
+
+cache: bundler
+
script:
bundle exec rake test
+
branches:
except:
- "readme-edits"
+
+before_install:
+ - gem update --system
+ # bundler installation needed for jruby-head
+ # https://github.com/travis-ci/travis-ci/issues/5861
+ - gem install bundler
+
+# Travis OS X support is pretty janky. These are some hacks to include tests
+# only on versions that actually work.
+# (last tested: 2016-11)
+matrix:
+ # exclude: {}
+ # include: {}
+
+ allow_failures:
+ - rvm: 'ruby-head'
+
+ # return results as soon as mandatory versions are done
+ fast_finish: true
+
+sudo: false
diff --git a/AUTHORS b/AUTHORS
index 9fce0a4..163ce9e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -3,6 +3,8 @@ the following kind souls:
Adam Jacob
Adam Wiggins
+Adrian Rangel
+Alex Tomlins
Aman Gupta
Andy Brody
Blake Mizerany
@@ -17,12 +19,15 @@ Coda Hale
Crawford
Cyril Rohr
Dan Mayer
+Dario Hamidi
+Darren Coxall
David Backeus
David Perkowski
Dmitri Dolguikh
Dusty Doris
Dylan Egan
El Draper
+Evan Broder
Evan Smith
François Beausoleil
Gabriele Cirulli
@@ -40,11 +45,14 @@ Jari Bakken
Jeff Remer
Jeffrey Hardy
Jeremy Kemper
+Joe Rafaniello
John Barnette
Jon Rowe
Jordi Massaguer Pla
+Joshua J. Campoverde
Juan Alvarez
Julien Kirch
+Jun Aruga
Justin Coyne
Justin Lambert
Keith Rarick
@@ -56,26 +64,35 @@ Lars Gierth
Lawrence Leonard Gilbert
Lee Jarvis
Lennon Day-Reynolds
+Lin Jen-Shin
Marc-André Cournoyer
+Marius Butuc
Matthew Manning
Michael Klett
+Michael Rykov
+Michael Westbom
Mike Fletcher
+Nelson Elhage
Nicholas Wieland
+Nick Hammond
Nick Plante
Niko Dittmann
Oscar Del Ben
Pablo Astigarraga
Paul Dlug
Pedro Belo
+Pedro Chambino
Philip Corliss
Pierre-Louis Gottfrois
Rafael Ssouza
-Rick "technoweenie"
+Rick Olson
Robert Eanes
Rodrigo Panachi
+Samuel Cochran
Syl Turner
T. Watanabe
Tekin
W. Andrew Loe III
Waynn Lue
+Xavier Shay
tpresa
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..8cebe84
--- /dev/null
+++ b/README.md
@@ -0,0 +1,784 @@
+# REST Client -- simple DSL for accessing HTTP and REST resources
+
+[](https://rubygems.org/gems/rest-client)
+[](https://travis-ci.org/rest-client/rest-client)
+[](https://codeclimate.com/github/rest-client/rest-client)
+[](http://www.rubydoc.info/github/rest-client/rest-client/master)
+
+A simple HTTP and REST client for Ruby, inspired by the Sinatra's microframework style
+of specifying actions: get, put, post, delete.
+
+* Main page: https://github.com/rest-client/rest-client
+* Mailing list: https://groups.io/g/rest-client
+
+### New mailing list
+
+We have a new email list for announcements, hosted by Groups.io.
+
+* Subscribe on the web: https://groups.io/g/rest-client
+
+* Subscribe by sending an email: mailto:rest-client+subscribe at groups.io
+
+* Open discussion subgroup: https://groups.io/g/rest-client+discuss
+
+The old Librelist mailing list is *defunct*, as Librelist appears to be broken
+and not accepting new mail. The old archives are still up, but have been
+imported into the new list archives as well.
+http://librelist.com/browser/rest.client
+
+## Requirements
+
+MRI Ruby 2.0 and newer are supported. Alternative interpreters compatible with
+2.0+ should work as well.
+
+Earlier Ruby versions such as 1.8.7, 1.9.2, and 1.9.3 are no longer supported. These
+versions no longer have any official support, and do not receive security
+updates.
+
+The rest-client gem depends on these other gems for usage at runtime:
+
+* [mime-types](http://rubygems.org/gems/mime-types)
+* [netrc](http://rubygems.org/gems/netrc)
+* [http-cookie](https://rubygems.org/gems/http-cookie)
+
+There are also several development dependencies. It's recommended to use
+[bundler](http://bundler.io/) to manage these dependencies for hacking on
+rest-client.
+
+### Upgrading to rest-client 2.0 from 1.x
+
+Users are encouraged to upgrade to rest-client 2.0, which cleans up a number of
+API warts and wrinkles, making rest-client generally more useful. Usage is
+largely compatible, so many applications will be able to upgrade with no
+changes.
+
+Overview of significant changes:
+
+* requires Ruby >= 2.0
+* `RestClient::Response` objects are a subclass of `String` rather than a
+ Frankenstein monster. And `#body` or `#to_s` return a true `String` object.
+* cleanup of exception classes, including new `RestClient::Exceptions::Timeout`
+* improvements to handling of redirects: responses and history are properly
+ exposed
+* major changes to cookie support: cookie jars are used for browser-like
+ behavior throughout
+* encoding: Content-Type charset response headers are used to automatically set
+ the encoding of the response string
+* HTTP params: handling of GET/POST params is more consistent and sophisticated
+ for deeply nested hash objects, and `ParamsArray` can be used to pass ordered
+ params
+* improved proxy support with per-request proxy configuration, plus the ability
+ to disable proxies set by environment variables
+* default request headers: rest-client sets `Accept: */*` and
+ `User-Agent: rest-client/...`
+
+See [history.md](./history.md) for a more complete description of changes.
+
+## Usage: Raw URL
+
+Basic usage:
+
+```ruby
+require 'rest-client'
+
+RestClient.get(url, headers={})
+
+RestClient.post(url, payload, headers={})
+```
+
+In the high level helpers, only POST, PATCH, and PUT take a payload argument.
+To pass a payload with other HTTP verbs or to pass more advanced options, use
+`RestClient::Request.execute` instead.
+
+More detailed examples:
+
+```ruby
+require 'rest-client'
+
+RestClient.get 'http://example.com/resource'
+
+RestClient.get 'http://example.com/resource', {params: {id: 50, 'foo' => 'bar'}}
+
+RestClient.get 'https://user:password@example.com/private/resource', {accept: :json}
+
+RestClient.post 'http://example.com/resource', {param1: 'one', nested: {param2: 'two'}}
+
+RestClient.post "http://example.com/resource", {'x' => 1}.to_json, {content_type: :json, accept: :json}
+
+RestClient.delete 'http://example.com/resource'
+
+>> response = RestClient.get 'http://example.com/resource'
+=> <RestClient::Response 200 "<!doctype h...">
+>> response.code
+=> 200
+>> response.cookies
+=> {"Foo"=>"BAR", "QUUX"=>"QUUUUX"}
+>> response.headers
+=> {:content_type=>"text/html; charset=utf-8", :cache_control=>"private" ... }
+>> response.body
+=> "<!doctype html>\n<html>\n<head>\n <title>Example Domain</title>\n\n ..."
+
+RestClient.post( url,
+ {
+ :transfer => {
+ :path => '/foo/bar',
+ :owner => 'that_guy',
+ :group => 'those_guys'
+ },
+ :upload => {
+ :file => File.new(path, 'rb')
+ }
+ })
+```
+## Passing advanced options
+
+The top level helper methods like RestClient.get accept a headers hash as
+their last argument and don't allow passing more complex options. But these
+helpers are just thin wrappers around `RestClient::Request.execute`.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+ timeout: 10)
+
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+ ssl_ca_file: 'myca.pem',
+ ssl_ciphers: 'AESGCM:!aNULL')
+```
+You can also use this to pass a payload for HTTP verbs like DELETE, where the
+`RestClient.delete` helper doesn't accept a payload.
+
+```ruby
+RestClient::Request.execute(method: :delete, url: 'http://example.com/resource',
+ payload: 'foo', headers: {myheader: 'bar'})
+```
+
+Due to unfortunate choices in the original API, the params used to populate the
+query string are actually taken out of the headers hash. So if you want to pass
+both the params hash and more complex options, use the special key
+`:params` in the headers hash. This design may change in a future major
+release.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+ timeout: 10, headers: {params: {foo: 'bar'}})
+
+➔ GET http://example.com/resource?foo=bar
+```
+
+## Multipart
+
+Yeah, that's right! This does multipart sends for you!
+
+```ruby
+RestClient.post '/data', :myfile => File.new("/path/to/image.jpg", 'rb')
+```
+
+This does two things for you:
+
+- Auto-detects that you have a File value sends it as multipart
+- Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
+
+If you are sending params that do not contain a File object but the payload needs to be multipart then:
+
+```ruby
+RestClient.post '/data', {:foo => 'bar', :multipart => true}
+```
+
+## Usage: ActiveResource-Style
+
+```ruby
+resource = RestClient::Resource.new 'http://example.com/resource'
+resource.get
+
+private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
+private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
+```
+
+See RestClient::Resource module docs for details.
+
+## Usage: Resource Nesting
+
+```ruby
+site = RestClient::Resource.new('http://example.com')
+site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
+```
+See `RestClient::Resource` docs for details.
+
+## Exceptions (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
+
+- for result codes between `200` and `207`, a `RestClient::Response` will be returned
+- for result codes `301`, `302` or `307`, the redirection will be followed if the request is a `GET` or a `HEAD`
+- for result code `303`, the redirection will be followed and the request transformed into a `GET`
+- for other cases, a `RestClient::ExceptionWithResponse` holding the Response will be raised; a specific exception class will be thrown for known error codes
+- call `.response` on the exception to get the server's response
+
+```ruby
+>> RestClient.get 'http://example.com/nonexistent'
+Exception: RestClient::NotFound: 404 Not Found
+
+>> begin
+ RestClient.get 'http://example.com/nonexistent'
+ rescue RestClient::ExceptionWithResponse => e
+ e.response
+ end
+=> <RestClient::Response 404 "<!doctype h...">
+```
+
+### Other exceptions
+
+While most exceptions have been collected under `RestClient::RequestFailed` aka
+`RestClient::ExceptionWithResponse`, there are a few quirky exceptions that
+have been kept for backwards compatibility.
+
+RestClient will propagate up exceptions like socket errors without modification:
+
+```ruby
+>> RestClient.get 'http://localhost:12345'
+Exception: Errno::ECONNREFUSED: Connection refused - connect(2) for "localhost" port 12345
+```
+
+RestClient handles a few specific error cases separately in order to give
+better error messages. These will hopefully be cleaned up in a future major
+release.
+
+`RestClient::ServerBrokeConnection` is translated from `EOFError` to give a
+better error message.
+
+`RestClient::SSLCertificateNotVerified` is raised when HTTPS validation fails.
+Other `OpenSSL::SSL::SSLError` errors are raised as is.
+
+### Redirection
+
+By default, rest-client will follow HTTP 30x redirection requests.
+
+__New in 2.0:__ `RestClient::Response` exposes a `#history` method that returns
+a list of each response received in a redirection chain.
+
+```ruby
+>> r = RestClient.get('http://httpbin.org/redirect/2')
+=> <RestClient::Response 200 "{\n \"args\":...">
+
+# see each response in the redirect chain
+>> r.history
+=> [<RestClient::Response 302 "<!DOCTYPE H...">, <RestClient::Response 302 "">]
+
+# see each requested URL
+>> r.request.url
+=> "http://httpbin.org/get"
+>> r.history.map {|x| x.request.url}
+=> ["http://httpbin.org/redirect/2", "http://httpbin.org/relative-redirect/1"]
+```
+
+#### Manually following redirection
+
+To disable automatic redirection, set `:max_redirects => 0`.
+
+__New in 2.0:__ Prior versions of rest-client would raise
+`RestClient::MaxRedirectsReached`, with no easy way to access the server's
+response. In 2.0, rest-client raises the normal
+`RestClient::ExceptionWithResponse` as it would with any other non-HTTP-20x
+response.
+
+```ruby
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1')
+=> RestClient::Response 200 "{\n "args":..."
+
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+RestClient::Found: 302 Found
+```
+
+To manually follow redirection, you can call `Response#follow_redirection`. Or
+you could of course inspect the result and choose custom behavior.
+
+```ruby
+>> RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+RestClient::Found: 302 Found
+>> begin
+ RestClient::Request.execute(method: :get, url: 'http://httpbin.org/redirect/1', max_redirects: 0)
+ rescue RestClient::ExceptionWithResponse => err
+ end
+>> err
+=> #<RestClient::Found: 302 Found>
+>> err.response
+=> RestClient::Response 302 "<!DOCTYPE H..."
+>> err.response.headers[:location]
+=> "/get"
+>> err.response.follow_redirection
+=> RestClient::Response 200 "{\n "args":..."
+```
+
+## Result handling
+
+The result of a `RestClient::Request` is a `RestClient::Response` object.
+
+__New in 2.0:__ `RestClient::Response` objects are now a subclass of `String`.
+Previously, they were a real String object with response functionality mixed
+in, which was very confusing to work with.
+
+Response objects have several useful methods. (See the class rdoc for more details.)
+
+- `Response#code`: The HTTP response code
+- `Response#body`: The response body as a string. (AKA .to_s)
+- `Response#headers`: A hash of HTTP response headers
+- `Response#raw_headers`: A hash of HTTP response headers as unprocessed arrays
+- `Response#cookies`: A hash of HTTP cookies set by the server
+- `Response#cookie_jar`: <em>New in 1.8</em> An HTTP::CookieJar of cookies
+- `Response#request`: The RestClient::Request object used to make the request
+- `Response#history`: <em>New in 2.0</em> If redirection was followed, a list of prior Response objects
+
+```ruby
+RestClient.get('http://example.com')
+➔ <RestClient::Response 200 "<!doctype h...">
+
+begin
+ RestClient.get('http://example.com/notfound')
+rescue RestClient::ExceptionWithResponse => err
+ err.response
+end
+➔ <RestClient::Response 404 "<!doctype h...">
+```
+
+### Response callbacks, error handling
+
+A block can be passed to the RestClient method. This block will then be called with the Response.
+Response.return! can be called to invoke the default response's behavior.
+
+```ruby
+# Don't raise exceptions but return the response
+>> RestClient.get('http://example.com/nonexistent') {|response, request, result| response }
+=> <RestClient::Response 404 "<!doctype h...">
+```
+
+```ruby
+# Manage a specific error code
+RestClient.get('http://example.com/resource') { |response, request, result, &block|
+ case response.code
+ when 200
+ p "It worked !"
+ response
+ when 423
+ raise SomeCustomExceptionIfYouWant
+ else
+ response.return!(request, result, &block)
+ end
+}
+```
+
+But note that it may be more straightforward to use exceptions to handle
+different HTTP error response cases:
+
+```ruby
+begin
+ resp = RestClient.get('http://example.com/resource')
+rescue RestClient::Unauthorized, RestClient::Forbidden => err
+ puts 'Access denied'
+ return err.response
+rescue RestClient::ImATeapot => err
+ puts 'The server is a teapot! # RFC 2324'
+ return err.response
+else
+ puts 'It worked!'
+ return resp
+end
+```
+
+For GET and HEAD requests, rest-client automatically follows redirection. For
+other HTTP verbs, call `.follow_redirection` on the response object (works both
+in block form and in exception form).
+
+```ruby
+# Follow redirections for all request types and not only for get and head
+# RFC : "If the 301, 302 or 307 status code is received in response to a request other than GET or HEAD,
+# the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user,
+# since this might change the conditions under which the request was issued."
+
+# block style
+RestClient.post('http://example.com/redirect', 'body') { |response, request, result|
+ case response.code
+ when 301, 302, 307
+ response.follow_redirection
+ else
+ response.return!
+ end
+}
+
+# exception style by explicit classes
+begin
+ RestClient.post('http://example.com/redirect', 'body')
+rescue RestClient::MovedPermanently,
+ RestClient::Found,
+ RestClient::TemporaryRedirect => err
+ err.response.follow_redirection
+end
+
+# exception style by response code
+begin
+ RestClient.post('http://example.com/redirect', 'body')
+rescue RestClient::ExceptionWithResponse => err
+ case err.http_code
+ when 301, 302, 307
+ err.response.follow_redirection
+ else
+ raise
+ end
+end
+```
+
+## Non-normalized URIs
+
+If you need to normalize URIs, e.g. to work with International Resource Identifiers (IRIs),
+use the addressable gem (http://addressable.rubyforge.org/api/) in your code:
+
+```ruby
+ require 'addressable/uri'
+ RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str)
+```
+
+## Lower-level access
+
+For cases not covered by the general API, you can use the `RestClient::Request` class, which provides a lower-level API.
+
+You can:
+
+- specify ssl parameters
+- override cookies
+- manually handle the response (e.g. to operate on it as a stream rather than reading it all into memory)
+
+See `RestClient::Request`'s documentation for more information.
+
+## Shell
+
+The restclient shell command gives an IRB session with RestClient already loaded:
+
+```ruby
+$ restclient
+>> RestClient.get 'http://example.com'
+```
+
+Specify a URL argument for get/post/put/delete on that resource:
+
+```ruby
+$ restclient http://example.com
+>> put '/resource', 'data'
+```
+
+Add a user and password for authenticated resources:
+
+```ruby
+$ restclient https://example.com user pass
+>> delete '/private/resource'
+```
+
+Create ~/.restclient for named sessions:
+
+```ruby
+ sinatra:
+ url: http://localhost:4567
+ rack:
+ url: http://localhost:9292
+ private_site:
+ url: http://example.com
+ username: user
+ password: pass
+```
+
+Then invoke:
+
+```ruby
+$ restclient private_site
+```
+
+Use as a one-off, curl-style:
+
+```ruby
+$ restclient get http://example.com/resource > output_body
+
+$ restclient put http://example.com/resource < input_body
+```
+
+## Logging
+
+To enable logging you can:
+
+- set RestClient.log with a Ruby Logger, or
+- set an environment variable to avoid modifying the code (in this case you can use a file name, "stdout" or "stderr"):
+
+```ruby
+$ RESTCLIENT_LOG=stdout path/to/my/program
+```
+Either produces logs like this:
+
+```ruby
+RestClient.get "http://some/resource"
+# => 200 OK | text/html 250 bytes
+RestClient.put "http://some/resource", "payload"
+# => 401 Unauthorized | application/xml 340 bytes
+```
+
+Note that these logs are valid Ruby, so you can paste them into the `restclient`
+shell or a script to replay your sequence of rest calls.
+
+## Proxy
+
+All calls to RestClient, including Resources, will use the proxy specified by
+`RestClient.proxy`:
+
+```ruby
+RestClient.proxy = "http://proxy.example.com/"
+RestClient.get "http://some/resource"
+# => response from some/resource as proxied through proxy.example.com
+```
+
+Often the proxy URL is set in an environment variable, so you can do this to
+use whatever proxy the system is configured to use:
+
+```ruby
+ RestClient.proxy = ENV['http_proxy']
+```
+
+__New in 2.0:__ Specify a per-request proxy by passing the :proxy option to
+RestClient::Request. This will override any proxies set by environment variable
+or by the global `RestClient.proxy` value.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com',
+ proxy: 'http://proxy.example.com')
+# => single request proxied through the proxy
+```
+
+This can be used to disable the use of a proxy for a particular request.
+
+```ruby
+RestClient.proxy = "http://proxy.example.com/"
+RestClient::Request.execute(method: :get, url: 'http://example.com', proxy: nil)
+# => single request sent without a proxy
+```
+
+## Query parameters
+
+Rest-client can render a hash as HTTP query parameters for GET/HEAD/DELETE
+requests or as HTTP post data in `x-www-form-urlencoded` format for POST
+requests.
+
+__New in 2.0:__ Even though there is no standard specifying how this should
+work, rest-client follows a similar convention to the one used by Rack / Rails
+servers for handling arrays, nested hashes, and null values.
+
+The implementation in
+[./lib/rest-client/utils.rb](RestClient::Utils.encode_query_string)
+closely follows
+[Rack::Utils.build_nested_query](http://www.rubydoc.info/gems/rack/Rack/Utils#build_nested_query-class_method),
+but treats empty arrays and hashes as `nil`. (Rack drops them entirely, which
+is confusing behavior.)
+
+If you don't like this behavior and want more control, just serialize params
+yourself (e.g. with `URI.encode_www_form`) and add the query string to the URL
+directly for GET parameters or pass the payload as a string for POST requests.
+
+Basic GET params:
+```ruby
+RestClient.get('https://httpbin.org/get', params: {foo: 'bar', baz: 'qux'})
+# GET "https://httpbin.org/get?foo=bar&baz=qux"
+```
+
+Basic `x-www-form-urlencoded` POST params:
+```ruby
+>> r = RestClient.post('https://httpbin.org/post', {foo: 'bar', baz: 'qux'})
+# POST "https://httpbin.org/post", data: "foo=bar&baz=qux"
+=> <RestClient::Response 200 "{\n \"args\":...">
+>> JSON.parse(r.body)
+=> {"args"=>{},
+ "data"=>"",
+ "files"=>{},
+ "form"=>{"baz"=>"qux", "foo"=>"bar"},
+ "headers"=>
+ {"Accept"=>"*/*",
+ "Accept-Encoding"=>"gzip, deflate",
+ "Content-Length"=>"15",
+ "Content-Type"=>"application/x-www-form-urlencoded",
+ "Host"=>"httpbin.org"},
+ "json"=>nil,
+ "url"=>"https://httpbin.org/post"}
+```
+
+JSON payload: rest-client does not speak JSON natively, so serialize your
+payload to a string before passing it to rest-client.
+```ruby
+>> payload = {'name' => 'newrepo', 'description': 'A new repo'}
+>> RestClient.post('https://api.github.com/user/repos', payload.to_json, content_type: :json)
+=> <RestClient::Response 201 "{\"id\":75149...">
+```
+
+Advanced GET params (arrays):
+```ruby
+>> r = RestClient.get('https://http-params.herokuapp.com/get', params: {foo: [1,2,3]})
+# GET "https://http-params.herokuapp.com/get?foo[]=1&foo[]=2&foo[]=3"
+=> <RestClient::Response 200 "Method: GET...">
+>> puts r.body
+query_string: "foo[]=1&foo[]=2&foo[]=3"
+decoded: "foo[]=1&foo[]=2&foo[]=3"
+
+GET:
+ {"foo"=>["1", "2", "3"]}
+```
+
+Advanced GET params (nested hashes):
+```ruby
+>> r = RestClient.get('https://http-params.herokuapp.com/get', params: {outer: {foo: 123, bar: 456}})
+# GET "https://http-params.herokuapp.com/get?outer[foo]=123&outer[bar]=456"
+=> <RestClient::Response 200 "Method: GET...">
+>> puts r.body
+...
+query_string: "outer[foo]=123&outer[bar]=456"
+decoded: "outer[foo]=123&outer[bar]=456"
+
+GET:
+ {"outer"=>{"foo"=>"123", "bar"=>"456"}}
+```
+
+__New in 2.0:__ The new `RestClient::ParamsArray` class allows callers to
+provide ordering even to structured parameters. This is useful for unusual
+cases where the server treats the order of parameters as significant or you
+want to pass a particular key multiple times.
+
+Multiple fields with the same name using ParamsArray:
+```ruby
+>> RestClient.get('https://httpbin.org/get', params:
+ RestClient::ParamsArray.new([[:foo, 1], [:foo, 2]]))
+# GET "https://httpbin.org/get?foo=1&foo=2"
+```
+
+Nested ParamsArray:
+```ruby
+>> RestClient.get('https://httpbin.org/get', params:
+ {foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])})
+# GET "https://httpbin.org/get?foo[a]=1&foo[a]=2"
+```
+
+## Headers
+
+Request headers can be set by passing a ruby hash containing keys and values
+representing header names and values:
+
+```ruby
+# GET request with modified headers
+RestClient.get 'http://example.com/resource', {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+
+# POST request with modified headers
+RestClient.post 'http://example.com/resource', {:foo => 'bar', :baz => 'qux'}, {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+
+# DELETE request with modified headers
+RestClient.delete 'http://example.com/resource', {:Authorization => 'Bearer cT0febFoD5lxAlNAXHo6g'}
+```
+
+## Timeouts
+
+By default the timeout for a request is 60 seconds. Timeouts for your request can
+be adjusted by setting the `timeout:` to the number of seconds that you would like
+the request to wait. Setting `timeout:` will override both `read_timeout:` and `open_timeout:`.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+ timeout: 120)
+```
+
+Additionally, you can set `read_timeout:` and `open_timeout:` separately.
+
+```ruby
+RestClient::Request.execute(method: :get, url: 'http://example.com/resource',
+ read_timeout: 120, open_timeout: 240)
+```
+
+## Cookies
+
+Request and Response objects know about HTTP cookies, and will automatically
+extract and set headers for them as needed:
+
+```ruby
+response = RestClient.get 'http://example.com/action_which_sets_session_id'
+response.cookies
+# => {"_applicatioN_session_id" => "1234"}
+
+response2 = RestClient.post(
+ 'http://localhost:3000/',
+ {:param1 => "foo"},
+ {:cookies => {:session_id => "1234"}}
+)
+# ...response body
+```
+### Full cookie jar support (new in 1.8)
+
+The original cookie implementation was very naive and ignored most of the
+cookie RFC standards.
+__New in 1.8__: An HTTP::CookieJar of cookies
+
+Response objects now carry a cookie_jar method that exposes an HTTP::CookieJar
+of cookies, which supports full standards compliant behavior.
+
+## SSL/TLS support
+
+Various options are supported for configuring rest-client's TLS settings. By
+default, rest-client will verify certificates using the system's CA store on
+all platforms. (This is intended to be similar to how browsers behave.) You can
+specify an :ssl_ca_file, :ssl_ca_path, or :ssl_cert_store to customize the
+certificate authorities accepted.
+
+### SSL Client Certificates
+
+```ruby
+RestClient::Resource.new(
+ 'https://example.com',
+ :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")),
+ :ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
+ :ssl_ca_file => "ca_certificate.pem",
+ :verify_ssl => OpenSSL::SSL::VERIFY_PEER
+).get
+```
+Self-signed certificates can be generated with the openssl command-line tool.
+
+## Hook
+
+RestClient.add_before_execution_proc add a Proc to be called before each execution.
+It's handy if you need direct access to the HTTP request.
+
+Example:
+
+```ruby
+# Add oauth support using the oauth gem
+require 'oauth'
+access_token = ...
+
+RestClient.add_before_execution_proc do |req, params|
+ access_token.sign! req
+end
+
+RestClient.get 'http://example.com'
+```
+
+## More
+
+Need caching, more advanced logging or any ability provided by Rack middleware?
+
+Have a look at rest-client-components: http://github.com/crohr/rest-client-components
+
+## Credits
+| | |
+|-------------------------|---------------------------------------------------------|
+| **REST Client Team** | Andy Brody |
+| **Creator** | Adam Wiggins |
+| **Maintainers Emeriti** | Lawrence Leonard Gilbert, Matthew Manning, Julien Kirch |
+| **Major contributions** | Blake Mizerany, Julien Kirch |
+
+A great many generous folks have contributed features and patches.
+See AUTHORS for the full list.
+
+## Legal
+
+Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"Master Shake" photo (http://www.flickr.com/photos/solgrundy/924205581/) by
+"SolGrundy"; used under terms of the Creative Commons Attribution-ShareAlike 2.0
+Generic license (http://creativecommons.org/licenses/by-sa/2.0/)
+
+Code for reading Windows root certificate store derived from work by Puppet;
+used under terms of the Apache License, Version 2.0.
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index 6d330fa..0000000
--- a/README.rdoc
+++ /dev/null
@@ -1,324 +0,0 @@
-= REST Client -- simple DSL for accessing HTTP and REST resources
-
-Build status: {<img src="https://travis-ci.org/rest-client/rest-client.svg?branch=master" alt="Build Status" />}[https://travis-ci.org/rest-client/rest-client]
-
-A simple HTTP and REST client for Ruby, inspired by the Sinatra's microframework style
-of specifying actions: get, put, post, delete.
-
-* Main page: https://github.com/rest-client/rest-client
-* Mailing list: rest.client at librelist.com (send a mail to subscribe).
-
-== Requirements
-
-MRI Ruby 1.9.2 and newer are supported. Alternative interpreters compatible with
-1.9.1+ should work as well.
-
-Ruby 1.8.7 is no longer supported. That's because the Ruby 1.8.7 interpreter
-itself no longer has official support, _not_ _even_ _security_ _patches!_ If you
-have been putting off upgrading your servers, now is the time.
-({More info is on the Ruby developers'
-blog.}[http://www.ruby-lang.org/en/news/2013/06/30/we-retire-1-8-7/])
-
-The rest-client gem depends on these other gems for installation and usage:
-
-* {mime-types}[http://rubygems.org/gems/mime-types]
-* {netrc}[http://rubygems.org/gems/netrc]
-* {rdoc}[http://rubygems.org/gems/rdoc]
-
-If you want to hack on the code, you should also have {the Bundler
-gem}[http://bundler.io/] installed so it can manage all necessary development
-dependencies for you.
-
-== Usage: Raw URL
-
- require 'rest_client'
-
- RestClient.get 'http://example.com/resource'
-
- RestClient.get 'http://example.com/resource', {:params => {:id => 50, 'foo' => 'bar'}}
-
- RestClient.get 'https://user:password@example.com/private/resource', {:accept => :json}
-
- RestClient.post 'http://example.com/resource', :param1 => 'one', :nested => { :param2 => 'two' }
-
- RestClient.post "http://example.com/resource", { 'x' => 1 }.to_json, :content_type => :json, :accept => :json
-
- RestClient.delete 'http://example.com/resource'
-
- response = RestClient.get 'http://example.com/resource'
- response.code
- ➔ 200
- response.cookies
- ➔ {"Foo"=>"BAR", "QUUX"=>"QUUUUX"}
- response.headers
- ➔ {:content_type=>"text/html; charset=utf-8", :cache_control=>"private" ...
- response.to_str
- ➔ \n<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.01//EN\"\n \"http://www.w3.org/TR/html4/strict.dtd\">\n\n<html ....
-
- RestClient.post( url,
- {
- :transfer => {
- :path => '/foo/bar',
- :owner => 'that_guy',
- :group => 'those_guys'
- },
- :upload => {
- :file => File.new(path, 'rb')
- }
- })
-
-== Multipart
-
-Yeah, that's right! This does multipart sends for you!
-
- RestClient.post '/data', :myfile => File.new("/path/to/image.jpg", 'rb')
-
-This does two things for you:
-
-* Auto-detects that you have a File value sends it as multipart
-* Auto-detects the mime of the file and sets it in the HEAD of the payload for each entry
-
-If you are sending params that do not contain a File object but the payload needs to be multipart then:
-
- RestClient.post '/data', {:foo => 'bar', :multipart => true}
-
-== Usage: ActiveResource-Style
-
- resource = RestClient::Resource.new 'http://example.com/resource'
- resource.get
-
- private_resource = RestClient::Resource.new 'https://example.com/private/resource', 'user', 'pass'
- private_resource.put File.read('pic.jpg'), :content_type => 'image/jpg'
-
-See RestClient::Resource module docs for details.
-
-== Usage: Resource Nesting
-
- site = RestClient::Resource.new('http://example.com')
- site['posts/1/comments'].post 'Good article.', :content_type => 'text/plain'
-
-See RestClient::Resource docs for details.
-
-== Exceptions (see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html)
-
-* for result codes between 200 and 207, a RestClient::Response will be returned
-* for result codes 301, 302 or 307, the redirection will be followed if the request is a GET or a HEAD
-* for result code 303, the redirection will be followed and the request transformed into a GET
-* for other cases, a RestClient::Exception holding the Response will be raised; a specific exception class will be thrown for known error codes
-
- RestClient.get 'http://example.com/resource'
- ➔ RestClient::ResourceNotFound: RestClient::ResourceNotFound
-
- begin
- RestClient.get 'http://example.com/resource'
- rescue => e
- e.response
- end
- ➔ 404 Resource Not Found | text/html 282 bytes
-
-== Result handling
-
-A block can be passed to the RestClient method. This block will then be called with the Response.
-Response.return! can be called to invoke the default response's behavior.
-
- # Don't raise exceptions but return the response
- RestClient.get('http://example.com/resource'){|response, request, result| response }
- ➔ 404 Resource Not Found | text/html 282 bytes
-
- # Manage a specific error code
- RestClient.get('http://my-rest-service.com/resource'){ |response, request, result, &block|
- case response.code
- when 200
- p "It worked !"
- response
- when 423
- raise SomeCustomExceptionIfYouWant
- else
- response.return!(request, result, &block)
- end
- }
-
- # Follow redirections for all request types and not only for get and head
- # RFC : "If the 301, 302 or 307 status code is received in response to a request other than GET or HEAD,
- # the user agent MUST NOT automatically redirect the request unless it can be confirmed by the user,
- # since this might change the conditions under which the request was issued."
- RestClient.get('http://my-rest-service.com/resource'){ |response, request, result, &block|
- if [301, 302, 307].include? response.code
- response.follow_redirection(request, result, &block)
- else
- response.return!(request, result, &block)
- end
- }
-
-== Non-normalized URIs
-
-If you need to normalize URIs, e.g. to work with International Resource Identifiers (IRIs),
-use the addressable gem (http://addressable.rubyforge.org/api/) in your code:
-
- require 'addressable/uri'
- RestClient.get(Addressable::URI.parse("http://www.詹姆斯.com/").normalize.to_str)
-
-== Lower-level access
-
-For cases not covered by the general API, you can use the RestClient::Request class, which provides a lower-level API.
-
-You can:
-
-* specify ssl parameters
-* override cookies
-* manually handle the response (e.g. to operate on it as a stream rather than reading it all into memory)
-
-See RestClient::Request's documentation for more information.
-
-== Shell
-
-The restclient shell command gives an IRB session with RestClient already loaded:
-
- $ restclient
- >> RestClient.get 'http://example.com'
-
-Specify a URL argument for get/post/put/delete on that resource:
-
- $ restclient http://example.com
- >> put '/resource', 'data'
-
-Add a user and password for authenticated resources:
-
- $ restclient https://example.com user pass
- >> delete '/private/resource'
-
-Create ~/.restclient for named sessions:
-
- sinatra:
- url: http://localhost:4567
- rack:
- url: http://localhost:9292
- private_site:
- url: http://example.com
- username: user
- password: pass
-
-Then invoke:
-
- $ restclient private_site
-
-Use as a one-off, curl-style:
-
- $ restclient get http://example.com/resource > output_body
-
- $ restclient put http://example.com/resource < input_body
-
-== Logging
-
-To enable logging you can:
-
-* set RestClient.log with a Ruby Logger, or
-* set an environment variable to avoid modifying the code (in this case you can use a file name, "stdout" or "stderr"):
-
- $ RESTCLIENT_LOG=stdout path/to/my/program
-
-Either produces logs like this:
-
- RestClient.get "http://some/resource"
- # => 200 OK | text/html 250 bytes
- RestClient.put "http://some/resource", "payload"
- # => 401 Unauthorized | application/xml 340 bytes
-
-Note that these logs are valid Ruby, so you can paste them into the restclient
-shell or a script to replay your sequence of rest calls.
-
-== Proxy
-
-All calls to RestClient, including Resources, will use the proxy specified by
-RestClient.proxy:
-
- RestClient.proxy = "http://proxy.example.com/"
- RestClient.get "http://some/resource"
- # => response from some/resource as proxied through proxy.example.com
-
-Often the proxy URL is set in an environment variable, so you can do this to
-use whatever proxy the system is configured to use:
-
- RestClient.proxy = ENV['http_proxy']
-
-== Query parameters
-
-Request objects know about query parameters and will automatically add them to
-the URL for GET, HEAD and DELETE requests, escaping the keys and values as needed:
-
- RestClient.get 'http://example.com/resource', :params => {:foo => 'bar', :baz => 'qux'}
- # will GET http://example.com/resource?foo=bar&baz=qux
-
-== Cookies
-
-Request and Response objects know about HTTP cookies, and will automatically
-extract and set headers for them as needed:
-
- response = RestClient.get 'http://example.com/action_which_sets_session_id'
- response.cookies
- # => {"_applicatioN_session_id" => "1234"}
-
- response2 = RestClient.post(
- 'http://localhost:3000/',
- {:param1 => "foo"},
- {:cookies => {:session_id => "1234"}}
- )
- # ...response body
-
-== SSL Client Certificates
-
- RestClient::Resource.new(
- 'https://example.com',
- :ssl_client_cert => OpenSSL::X509::Certificate.new(File.read("cert.pem")),
- :ssl_client_key => OpenSSL::PKey::RSA.new(File.read("key.pem"), "passphrase, if any"),
- :ssl_ca_file => "ca_certificate.pem",
- :verify_ssl => OpenSSL::SSL::VERIFY_PEER
- ).get
-
-Self-signed certificates can be generated with the openssl command-line tool.
-
-== Hook
-
-RestClient.add_before_execution_proc add a Proc to be called before each execution.
-It's handy if you need direct access to the HTTP request.
-
-Example:
-
- # Add oauth support using the oauth gem
- require 'oauth'
- access_token = ...
-
- RestClient.add_before_execution_proc do |req, params|
- access_token.sign! req
- end
-
- RestClient.get 'http://example.com'
-
-== More
-
-Need caching, more advanced logging or any ability provided by Rack middleware?
-
-Have a look at rest-client-components: http://github.com/crohr/rest-client-components
-
-== Credits
-
-REST Client Team:: Matthew Manning, Lawrence Leonard Gilbert, Andy Brody
-
-Creator:: Adam Wiggins
-
-Maintainer Emeritus:: Julien Kirch
-
-Major contributions:: Blake Mizerany, Julien Kirch
-
-Patches contributed by many, including Chris Anderson, Greg Borenstein, Ardekantur, Pedro Belo, Rafael Souza, Rick Olson, Aman Gupta, François Beausoleil and Nick Plante.
-
-== Legal
-
-Released under the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"Master Shake" photo (http://www.flickr.com/photos/solgrundy/924205581/) by
-"SolGrundy"; used under terms of the Creative Commons Attribution-ShareAlike 2.0
-Generic license (http://creativecommons.org/licenses/by-sa/2.0/)
-
-Code for reading Windows root certificate store derived from work by Puppet;
-used under terms of the Apache License, Version 2.0.
diff --git a/Rakefile b/Rakefile
index c83b1c7..f804fdb 100644
--- a/Rakefile
+++ b/Rakefile
@@ -34,6 +34,22 @@ RSpec::Core::RakeTask.new('rcov') do |t|
t.rcov_opts = ['--exclude', 'examples']
end
+desc 'Regenerate authors file'
+task :authors do
+ Dir.chdir(File.dirname(__FILE__)) do
+ File.open('AUTHORS', 'w') do |f|
+ f.write( <<-EOM
+The Ruby REST Client would not be what it is today without the help of
+the following kind souls:
+
+ EOM
+ )
+ end
+
+ sh 'git shortlog -s | cut -f 2 >> AUTHORS'
+ end
+end
+
task :default do
sh 'rake -T'
end
@@ -111,6 +127,6 @@ Rake::RDocTask.new do |t|
t.title = "rest-client, fetch RESTful resources effortlessly"
t.options << '--line-numbers' << '--inline-source' << '-A cattr_accessor=object'
t.options << '--charset' << 'utf-8'
- t.rdoc_files.include('README.rdoc')
+ t.rdoc_files.include('README.md')
t.rdoc_files.include('lib/*.rb')
end
diff --git a/bin/restclient b/bin/restclient
index 9a585da..7876503 100755
--- a/bin/restclient
+++ b/bin/restclient
@@ -56,11 +56,9 @@ if @verb
end
POSSIBLE_VERBS.each do |m|
- eval <<-end_eval
-def #{m}(path, *args, &b)
- r[path].#{m}(*args, &b)
-end
- end_eval
+ define_method(m.to_sym) do |path, *args, &b|
+ r[path].public_send(m.to_sym, *args, &b)
+ end
end
def method_missing(s, * args, & b)
diff --git a/history.md b/history.md
index 56d9e87..6c00abd 100644
--- a/history.md
+++ b/history.md
@@ -1,3 +1,131 @@
+# 2.0.2
+
+- Suppress the header override warning introduced in 2.0.1 if the value is the
+ same. There's no conflict if the value is unchanged. (#578)
+
+# 2.0.1
+
+- Warn if auto-generated headers from the payload, such as Content-Type,
+ override headers set by the user. This is usually not what the user wants to
+ happen, and can be surprising. (#554)
+- Drop the old check for weak default TLS ciphers, and use the built-in Ruby
+ defaults. Ruby versions from Oct. 2014 onward use sane defaults, so this is
+ no longer needed. (#573)
+
+# 2.0.0
+
+This release is largely API compatible, but makes several breaking changes.
+
+- Drop support for Ruby 1.9
+- Allow mime-types as new as 3.x (requires ruby 2.0)
+- Respect Content-Type charset header provided by server. Previously,
+ rest-client would not override the string encoding chosen by Net::HTTP. Now
+ responses that specify a charset will yield a body string in that encoding.
+ For example, `Content-Type: text/plain; charset=EUC-JP` will return a String
+ encoded with `Encoding::EUC_JP`. (#361)
+- Change exceptions raised on request timeout. Instead of
+ `RestClient::RequestTimeout` (which is still used for HTTP 408), network
+ timeouts will now raise either `RestClient::Exceptions::ReadTimeout` or
+ `RestClient::Exceptions::OpenTimeout`, both of which inherit from
+ `RestClient::Exceptions::Timeout`. For backwards compatibility, this still
+ inherits from `RestClient::RequestTimeout` so existing uses will still work.
+ This may change in a future major release. These new timeout classes also
+ make the original wrapped exception available as `#original_exception`.
+- Unify request exceptions under `RestClient::RequestFailed`, which still
+ inherits from `ExceptionWithResponse`. Previously, HTTP 304, 401, and 404
+ inherited directly from `ExceptionWithResponse` rather than from
+ `RequestFailed`. Now _all_ HTTP status code exceptions inherit from both.
+- Rename the `:timeout` request option to `:read_timeout`. When `:timeout` is
+ passed, now set both `:read_timeout` and `:open_timeout`.
+- Change default HTTP Accept header to `*/*`
+- Use a more descriptive User-Agent header by default
+- Drop RC4-MD5 from default cipher list
+- Only prepend http:// to URIs without a scheme
+- Fix some support for using IPv6 addresses in URLs (still affected by Ruby
+ 2.0+ bug https://bugs.ruby-lang.org/issues/9129, with the fix expected to be
+ backported to 2.0 and 2.1)
+- `Response` objects are now a subclass of `String` rather than a `String` that
+ mixes in the response functionality. Most of the methods remain unchanged,
+ but this makes it much easier to understand what is happening when you look
+ at a RestClient response object. There are a few additional changes:
+ - Response objects now implement `.inspect` to make this distinction clearer.
+ - `Response#to_i` will now behave like `String#to_i` instead of returning the
+ HTTP response code, which was very surprising behavior.
+ - `Response#body` and `#to_s` will now return a true `String` object rather
+ than self. Previously there was no easy way to get the true `String`
+ response instead of the Frankenstein response string object with
+ AbstractResponse mixed in.
+ - Response objects no longer accept an extra request args hash, but instead
+ access request args directly from the request object, which reduces
+ confusion and duplication.
+- Handle multiple HTTP response headers with the same name (except for
+ Set-Cookie, which is special) by joining the values with a comma space,
+ compliant with RFC 7230
+- Rewrite cookie support to be much smarter and to use cookie jars consistently
+ for requests, responses, and redirection in order to resolve long-standing
+ complaints about the previously broken behavior: (#498)
+ - The `:cookies` option may now be a Hash of Strings, an Array of
+ HTTP::Cookie objects, or a full HTTP::CookieJar.
+ - Add `RestClient::Request#cookie_jar` and reimplement `Request#cookies` to
+ be a wrapper around the cookie jar.
+ - Still support passing the `:cookies` option in the headers hash, but now
+ raise ArgumentError if that option is also passed to `Request#initialize`.
+ - Warn if both `:cookies` and a `Cookie` header are supplied.
+ - Use the `Request#cookie_jar` as the basis for `Response#cookie_jar`,
+ creating a copy of the jar and adding any newly received cookies.
+ - When following redirection, also use this same strategy so that cookies
+ from the original request are carried through in a standards-compliant way
+ by the cookie jar.
+- Don't set basic auth header if explicit `Authorization` header is specified
+- Add `:proxy` option to requests, which can be used for thread-safe
+ per-request proxy configuration, overriding `RestClient.proxy`
+- Allow overriding `ENV['http_proxy']` to disable proxies by setting
+ `RestClient.proxy` to a falsey value. Previously there was no way in Ruby 2.x
+ to turn off a proxy specified in the environment without changing `ENV`.
+- Add actual support for streaming request payloads. Previously rest-client
+ would call `.to_s` even on RestClient::Payload::Streamed objects. Instead,
+ treat any object that responds to `.read` as a streaming payload and pass it
+ through to `.body_stream=` on the Net:HTTP object. This massively reduces the
+ memory required for large file uploads.
+- Changes to redirection behavior: (#381, #484)
+ - Remove `RestClient::MaxRedirectsReached` in favor of the normal
+ `ExceptionWithResponse` subclasses. This makes the response accessible on
+ the exception object as `.response`, making it possible for callers to tell
+ what has actually happened when the redirect limit is reached.
+ - When following HTTP redirection, store a list of each previous response on
+ the response object as `.history`. This makes it possible to access the
+ original response headers and body before the redirection was followed.
+ - Follow redirection consistently, regardless of whether the HTTP method was
+ passed as a symbol or string. Under the hood rest-client now normalizes the
+ HTTP request method to a lowercase string.
+- Add `:before_execution_proc` option to `RestClient::Request`. This makes it
+ possible to add procs like `RestClient.add_before_execution_proc` to a single
+ request without global state.
+- Run tests on Travis's beta OS X support.
+- Make `Request#transmit` a private method, along with a few others.
+- Refactor URI parsing to happen earlier, in Request initialization.
+- Improve consistency and functionality of complex URL parameter handling:
+ - When adding URL params, handle URLs that already contain params.
+ - Add new convention for handling URL params containing deeply nested arrays
+ and hashes, unify handling of null/empty values, and use the same code for
+ GET and POST params. (#437)
+ - Add the RestClient::ParamsArray class, a simple array-like container that
+ can be used to pass multiple keys with same name or keys where the ordering
+ is significant.
+- Add a few more exception classes for obscure HTTP status codes.
+- Multipart: use a much more robust multipart boundary with greater entropy.
+- Make `RestClient::Payload::Base#inspect` stop pretending to be a String.
+- Add `Request#redacted_uri` and `Request#redacted_url` to display the URI
+ with any password redacted.
+
+# 2.0.0.rc1
+
+Changes in the release candidate that did not persist through the final 2.0.0
+release:
+- RestClient::Exceptions::Timeout was originally going to be a direct subclass
+ of RestClient::Exception in the release candidate. This exception tree was
+ made a subclass of RestClient::RequestTimeout prior to the final release.
+
# 1.8.0
- Security: implement standards compliant cookie handling by adding a
diff --git a/lib/restclient.rb b/lib/restclient.rb
index 530fc59..bdb53f0 100644
--- a/lib/restclient.rb
+++ b/lib/restclient.rb
@@ -7,11 +7,13 @@ require 'zlib'
require File.dirname(__FILE__) + '/restclient/version'
require File.dirname(__FILE__) + '/restclient/platform'
require File.dirname(__FILE__) + '/restclient/exceptions'
+require File.dirname(__FILE__) + '/restclient/utils'
require File.dirname(__FILE__) + '/restclient/request'
require File.dirname(__FILE__) + '/restclient/abstract_response'
require File.dirname(__FILE__) + '/restclient/response'
require File.dirname(__FILE__) + '/restclient/raw_response'
require File.dirname(__FILE__) + '/restclient/resource'
+require File.dirname(__FILE__) + '/restclient/params_array'
require File.dirname(__FILE__) + '/restclient/payload'
require File.dirname(__FILE__) + '/restclient/windows'
@@ -89,8 +91,24 @@ module RestClient
Request.execute(:method => :options, :url => url, :headers => headers, &block)
end
- class << self
- attr_accessor :proxy
+ # A global proxy URL to use for all requests. This can be overridden on a
+ # per-request basis by passing `:proxy` to RestClient::Request.
+ def self.proxy
+ @proxy ||= nil
+ end
+
+ def self.proxy=(value)
+ @proxy = value
+ @proxy_set = true
+ end
+
+ # Return whether RestClient.proxy was set explicitly. We use this to
+ # differentiate between no value being set and a value explicitly set to nil.
+ #
+ # @return [Boolean]
+ #
+ def self.proxy_set?
+ @proxy_set ||= false
end
# Setup the log for RestClient calls.
@@ -150,6 +168,7 @@ module RestClient
# Add a Proc to be called before each request in executed.
# The proc parameters will be the http request and the request params.
def self.add_before_execution_proc &proc
+ raise ArgumentError.new('block is required') unless proc
@@before_execution_procs << proc
end
diff --git a/lib/restclient/abstract_response.rb b/lib/restclient/abstract_response.rb
index 1cc5657..7cd5b82 100644
--- a/lib/restclient/abstract_response.rb
+++ b/lib/restclient/abstract_response.rb
@@ -5,13 +5,21 @@ module RestClient
module AbstractResponse
- attr_reader :net_http_res, :args, :request
+ attr_reader :net_http_res, :request
+
+ def inspect
+ raise NotImplementedError.new('must override in subclass')
+ end
# HTTP status code
def code
@code ||= @net_http_res.code.to_i
end
+ def history
+ @history ||= request.redirection_history || []
+ end
+
# A hash of the headers, beautified with symbols and underscores.
# e.g. "Content-type" will become :content_type.
def headers
@@ -23,17 +31,28 @@ module RestClient
@raw_headers ||= @net_http_res.to_hash
end
- def response_set_vars(net_http_res, args, request)
+ def response_set_vars(net_http_res, request)
@net_http_res = net_http_res
- @args = args
@request = request
+
+ # prime redirection history
+ history
end
- # Hash of cookies extracted from response headers
+ # Hash of cookies extracted from response headers.
+ #
+ # NB: This will return only cookies whose domain matches this request, and
+ # may not even return all of those cookies if there are duplicate names.
+ # Use the full cookie_jar for more nuanced access.
+ #
+ # @see #cookie_jar
+ #
+ # @return [Hash]
+ #
def cookies
hash = {}
- cookie_jar.cookies.each do |cookie|
+ cookie_jar.cookies(@request.uri).each do |cookie|
hash[cookie.name] = cookie.value
end
@@ -45,91 +64,163 @@ module RestClient
# @return [HTTP::CookieJar]
#
def cookie_jar
- return @cookie_jar if @cookie_jar
+ return @cookie_jar if defined?(@cookie_jar) && @cookie_jar
- jar = HTTP::CookieJar.new
+ jar = @request.cookie_jar.dup
headers.fetch(:set_cookie, []).each do |cookie|
- jar.parse(cookie, @request.url)
+ jar.parse(cookie, @request.uri)
end
@cookie_jar = jar
end
# Return the default behavior corresponding to the response code:
- # the response itself for code in 200..206, redirection for 301, 302 and 307 in get and head cases, redirection for 303 and an exception in other cases
- def return! request = nil, result = nil, & block
- if (200..207).include? code
+ #
+ # For 20x status codes: return the response itself
+ #
+ # For 30x status codes:
+ # 301, 302, 307: redirect GET / HEAD if there is a Location header
+ # 303: redirect, changing method to GET, if there is a Location header
+ #
+ # For all other responses, raise a response exception
+ #
+ def return!(&block)
+ case code
+ when 200..207
self
- elsif [301, 302, 307].include? code
- unless [:get, :head].include? args[:method]
- raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
+ when 301, 302, 307
+ case request.method
+ when 'get', 'head'
+ check_max_redirects
+ follow_redirection(&block)
else
- follow_redirection(request, result, & block)
+ raise exception_with_response
end
- elsif code == 303
- args[:method] = :get
- args.delete :payload
- follow_redirection(request, result, & block)
- elsif Exceptions::EXCEPTIONS_MAP[code]
- raise Exceptions::EXCEPTIONS_MAP[code].new(self, code)
+ when 303
+ check_max_redirects
+ follow_get_redirection(&block)
else
- raise RequestFailed.new(self, code)
+ raise exception_with_response
end
end
def to_i
- code
+ warn('warning: calling Response#to_i is not recommended')
+ super
end
def description
"#{code} #{STATUSES[code]} | #{(headers[:content_type] || '').gsub(/;.*$/, '')} #{size} bytes\n"
end
- # Follow a redirection
- def follow_redirection request = nil, result = nil, & block
- new_args = @args.dup
+ # Follow a redirection response by making a new HTTP request to the
+ # redirection target.
+ def follow_redirection(&block)
+ _follow_redirection(request.args.dup, &block)
+ end
- url = headers[:location]
- if url !~ /^http/
- url = URI.parse(request.url).merge(url).to_s
- end
- new_args[:url] = url
- if request
- if request.max_redirects == 0
- raise MaxRedirectsReached
- end
- new_args[:password] = request.password
- new_args[:user] = request.user
- new_args[:headers] = request.headers
- new_args[:max_redirects] = request.max_redirects - 1
-
- # TODO: figure out what to do with original :cookie, :cookies values
- new_args[:headers]['Cookie'] = HTTP::Cookie.cookie_value(
- cookie_jar.cookies(new_args.fetch(:url)))
- end
+ # Follow a redirection response, but change the HTTP method to GET and drop
+ # the payload from the original request.
+ def follow_get_redirection(&block)
+ new_args = request.args.dup
+ new_args[:method] = :get
+ new_args.delete(:payload)
- Request.execute(new_args, &block)
+ _follow_redirection(new_args, &block)
end
+ # Convert headers hash into canonical form.
+ #
+ # Header names will be converted to lowercase symbols with underscores
+ # instead of hyphens.
+ #
+ # Headers specified multiple times will be joined by comma and space,
+ # except for Set-Cookie, which will always be an array.
+ #
+ # Per RFC 2616, if a server sends multiple headers with the same key, they
+ # MUST be able to be joined into a single header by a comma. However,
+ # Set-Cookie (RFC 6265) cannot because commas are valid within cookie
+ # definitions. The newer RFC 7230 notes (3.2.2) that Set-Cookie should be
+ # handled as a special case.
+ #
+ # http://tools.ietf.org/html/rfc2616#section-4.2
+ # http://tools.ietf.org/html/rfc7230#section-3.2.2
+ # http://tools.ietf.org/html/rfc6265
+ #
+ # @param headers [Hash]
+ # @return [Hash]
+ #
def self.beautify_headers(headers)
headers.inject({}) do |out, (key, value)|
- out[key.gsub(/-/, '_').downcase.to_sym] = %w{ set-cookie }.include?(key.downcase) ? value : value.first
+ key_sym = key.tr('-', '_').downcase.to_sym
+
+ # Handle Set-Cookie specially since it cannot be joined by comma.
+ if key.downcase == 'set-cookie'
+ out[key_sym] = value
+ else
+ out[key_sym] = value.join(', ')
+ end
+
out
end
end
private
- # Parse a cookie value and return its content in an Hash
- def parse_cookie cookie_content
- out = {}
- CGI::Cookie::parse(cookie_content).each do |key, cookie|
- unless ['expires', 'path'].include? key
- out[CGI::escape(key)] = cookie.value[0] ? (CGI::escape(cookie.value[0]) || '') : ''
- end
+ # Follow a redirection
+ #
+ # @param new_args [Hash] Start with this hash of arguments for the
+ # redirection request. The hash will be mutated, so be sure to dup any
+ # existing hash that should not be modified.
+ #
+ def _follow_redirection(new_args, &block)
+
+ # parse location header and merge into existing URL
+ url = headers[:location]
+
+ # cannot follow redirection if there is no location header
+ unless url
+ raise exception_with_response
+ end
+
+ # handle relative redirects
+ unless url.start_with?('http')
+ url = URI.parse(request.url).merge(url).to_s
+ end
+ new_args[:url] = url
+
+ new_args[:password] = request.password
+ new_args[:user] = request.user
+ new_args[:headers] = request.headers
+ new_args[:max_redirects] = request.max_redirects - 1
+
+ # pass through our new cookie jar
+ new_args[:cookies] = cookie_jar
+
+ # prepare new request
+ new_req = Request.new(new_args)
+
+ # append self to redirection history
+ new_req.redirection_history = history + [self]
+
+ # execute redirected request
+ new_req.execute(&block)
+ end
+
+ def check_max_redirects
+ if request.max_redirects <= 0
+ raise exception_with_response
end
- out
end
- end
+ def exception_with_response
+ begin
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
+ rescue KeyError
+ raise RequestFailed.new(self, code)
+ end
+
+ raise klass.new(self, code)
+ end
+ end
end
diff --git a/lib/restclient/exceptions.rb b/lib/restclient/exceptions.rb
index 4632444..5703bff 100644
--- a/lib/restclient/exceptions.rb
+++ b/lib/restclient/exceptions.rb
@@ -1,5 +1,19 @@
module RestClient
+ # Hash of HTTP status code => message.
+ #
+ # 1xx: Informational - Request received, continuing process
+ # 2xx: Success - The action was successfully received, understood, and
+ # accepted
+ # 3xx: Redirection - Further action must be taken in order to complete the
+ # request
+ # 4xx: Client Error - The request contains bad syntax or cannot be fulfilled
+ # 5xx: Server Error - The server failed to fulfill an apparently valid
+ # request
+ #
+ # @see
+ # http://www.iana.org/assignments/http-status-codes/http-status-codes.xhtml
+ #
STATUSES = {100 => 'Continue',
101 => 'Switching Protocols',
102 => 'Processing', #WebDAV
@@ -12,6 +26,8 @@ module RestClient
205 => 'Reset Content',
206 => 'Partial Content',
207 => 'Multi-Status', #WebDAV
+ 208 => 'Already Reported', # RFC5842
+ 226 => 'IM Used', # RFC3229
300 => 'Multiple Choices',
301 => 'Moved Permanently',
@@ -21,12 +37,13 @@ module RestClient
305 => 'Use Proxy', # http/1.1
306 => 'Switch Proxy', # no longer used
307 => 'Temporary Redirect', # http/1.1
+ 308 => 'Permanent Redirect', # RFC7538
400 => 'Bad Request',
401 => 'Unauthorized',
402 => 'Payment Required',
403 => 'Forbidden',
- 404 => 'Resource Not Found',
+ 404 => 'Not Found',
405 => 'Method Not Allowed',
406 => 'Not Acceptable',
407 => 'Proxy Authentication Required',
@@ -35,10 +52,10 @@ module RestClient
410 => 'Gone',
411 => 'Length Required',
412 => 'Precondition Failed',
- 413 => 'Request Entity Too Large',
- 414 => 'Request-URI Too Long',
+ 413 => 'Payload Too Large', # RFC7231 (renamed, see below)
+ 414 => 'URI Too Long', # RFC7231 (renamed, see below)
415 => 'Unsupported Media Type',
- 416 => 'Requested Range Not Satisfiable',
+ 416 => 'Range Not Satisfiable', # RFC7233 (renamed, see below)
417 => 'Expectation Failed',
418 => 'I\'m A Teapot', #RFC2324
421 => 'Too Many Connections From This IP',
@@ -61,22 +78,27 @@ module RestClient
505 => 'HTTP Version Not Supported',
506 => 'Variant Also Negotiates',
507 => 'Insufficient Storage', #WebDAV
+ 508 => 'Loop Detected', # RFC5842
509 => 'Bandwidth Limit Exceeded', #Apache
510 => 'Not Extended',
511 => 'Network Authentication Required', # RFC6585
}
- # Compatibility : make the Response act like a Net::HTTPResponse when needed
- module ResponseForException
- def method_missing symbol, *args
- if net_http_res.respond_to? symbol
- warn "[warning] The response contained in an RestClient::Exception is now a RestClient::Response instead of a Net::HTTPResponse, please update your code"
- net_http_res.send symbol, *args
- else
- super
- end
- end
- end
+ STATUSES_COMPATIBILITY = {
+ # The RFCs all specify "Not Found", but "Resource Not Found" was used in
+ # earlier RestClient releases.
+ 404 => ['ResourceNotFound'],
+
+ # HTTP 413 was renamed to "Payload Too Large" in RFC7231.
+ 413 => ['RequestEntityTooLarge'],
+
+ # HTTP 414 was renamed to "URI Too Long" in RFC7231.
+ 414 => ['RequestURITooLong'],
+
+ # HTTP 416 was renamed to "Range Not Satisfiable" in RFC7233.
+ 416 => ['RequestedRangeNotSatisfiable'],
+ }
+
# This is the base RestClient exception class. Rescue it if you want to
# catch any exception that your request might raise
@@ -86,15 +108,13 @@ module RestClient
# probably an HTML error page) is e.response.
class Exception < RuntimeError
attr_accessor :response
+ attr_accessor :original_exception
attr_writer :message
def initialize response = nil, initial_response_code = nil
@response = response
@message = nil
@initial_response_code = initial_response_code
-
- # compatibility: this make the exception behave like a Net::HTTPResponse
- response.extend ResponseForException if response
end
def http_code
@@ -106,22 +126,25 @@ module RestClient
end
end
- def http_body
- @response.body if @response
+ def http_headers
+ @response.headers if @response
end
- def inspect
- "#{message}: #{http_body}"
+ def http_body
+ @response.body if @response
end
def to_s
- inspect
+ message
end
def message
- @message || self.class.name
+ @message || default_message
end
+ def default_message
+ self.class.name
+ end
end
# Compatibility
@@ -131,7 +154,7 @@ module RestClient
# The request failed with an error code not managed by the code
class RequestFailed < ExceptionWithResponse
- def message
+ def default_message
"HTTP status code #{http_code}"
end
@@ -140,43 +163,68 @@ module RestClient
end
end
- # We will a create an exception for each status code, see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ # RestClient exception classes. TODO: move all exceptions into this module.
+ #
+ # We will a create an exception for each status code, see
+ # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
+ #
module Exceptions
# Map http status codes to the corresponding exception class
EXCEPTIONS_MAP = {}
end
+ # Create HTTP status exception classes
STATUSES.each_pair do |code, message|
-
- # Compatibility
- superclass = ([304, 401, 404].include? code) ? ExceptionWithResponse : RequestFailed
- klass = Class.new(superclass) do
- send(:define_method, :message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
+ klass = Class.new(RequestFailed) do
+ send(:define_method, :default_message) {"#{http_code ? "#{http_code} " : ''}#{message}"}
end
- klass_constant = const_set message.delete(' \-\''), klass
+ klass_constant = const_set(message.delete(' \-\''), klass)
Exceptions::EXCEPTIONS_MAP[code] = klass_constant
end
- # A redirect was encountered; caught by execute to retry with the new url.
- class Redirect < Exception
-
- def message
- 'Redirect'
+ # Create HTTP status exception classes used for backwards compatibility
+ STATUSES_COMPATIBILITY.each_pair do |code, compat_list|
+ klass = Exceptions::EXCEPTIONS_MAP.fetch(code)
+ compat_list.each do |old_name|
+ const_set(old_name, klass)
end
+ end
- attr_accessor :url
+ module Exceptions
+ # We have to split the Exceptions module like we do here because the
+ # EXCEPTIONS_MAP is under Exceptions, but we depend on
+ # RestClient::RequestTimeout below.
+
+ # Base class for request timeouts.
+ #
+ # NB: Previous releases of rest-client would raise RequestTimeout both for
+ # HTTP 408 responses and for actual connection timeouts.
+ class Timeout < RestClient::RequestTimeout
+ def initialize(message=nil, original_exception=nil)
+ super(nil, nil)
+ self.message = message if message
+ self.original_exception = original_exception if original_exception
+ end
+ end
- def initialize(url)
- @url = url
+ # Timeout when connecting to a server. Typically wraps Net::OpenTimeout (in
+ # ruby 2.0 or greater).
+ class OpenTimeout < Timeout
+ def default_message
+ 'Timed out connecting to server'
+ end
end
- end
- class MaxRedirectsReached < Exception
- def message
- 'Maximum number of redirect reached'
+ # Timeout when reading from a server. Typically wraps Net::ReadTimeout (in
+ # ruby 2.0 or greater).
+ class ReadTimeout < Timeout
+ def default_message
+ 'Timed out reading data from server'
+ end
end
end
+
# The server broke the connection prior to the request completing. Usually
# this means it crashed, or sometimes that your network connection was
# severed before it could complete.
@@ -188,16 +236,9 @@ module RestClient
end
class SSLCertificateNotVerified < Exception
- def initialize(message)
+ def initialize(message = 'SSL certificate not verified')
super nil, nil
self.message = message
end
end
end
-
-class RestClient::Request
- # backwards compatibility
- Redirect = RestClient::Redirect
- Unauthorized = RestClient::Unauthorized
- RequestFailed = RestClient::RequestFailed
-end
diff --git a/lib/restclient/params_array.rb b/lib/restclient/params_array.rb
new file mode 100644
index 0000000..359b687
--- /dev/null
+++ b/lib/restclient/params_array.rb
@@ -0,0 +1,72 @@
+module RestClient
+
+ # The ParamsArray class is used to represent an ordered list of [key, value]
+ # pairs. Use this when you need to include a key multiple times or want
+ # explicit control over parameter ordering.
+ #
+ # Most of the request payload & parameter functions normally accept a Hash of
+ # keys => values, which does not allow for duplicated keys.
+ #
+ # @see RestClient::Utils.encode_query_string
+ # @see RestClient::Utils.flatten_params
+ #
+ class ParamsArray
+ include Enumerable
+
+ # @param array [Array<Array>] An array of parameter key,value pairs. These
+ # pairs may be 2 element arrays [key, value] or single element hashes
+ # {key => value}. They may also be single element arrays to represent a
+ # key with no value.
+ #
+ # @example
+ # >> ParamsArray.new([[:foo, 123], [:foo, 456], [:bar, 789]])
+ # This will be encoded as "foo=123&foo=456&bar=789"
+ #
+ # @example
+ # >> ParamsArray.new({foo: 123, bar: 456})
+ # This is valid, but there's no reason not to just use the Hash directly
+ # instead of a ParamsArray.
+ #
+ #
+ def initialize(array)
+ @array = process_input(array)
+ end
+
+ def each(*args, &blk)
+ @array.each(*args, &blk)
+ end
+
+ def empty?
+ @array.empty?
+ end
+
+ private
+
+ def process_input(array)
+ array.map {|v| process_pair(v) }
+ end
+
+ # A pair may be:
+ # - A single element hash, e.g. {foo: 'bar'}
+ # - A two element array, e.g. ['foo', 'bar']
+ # - A one element array, e.g. ['foo']
+ #
+ def process_pair(pair)
+ case pair
+ when Hash
+ if pair.length != 1
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
+ end
+ pair.to_a.fetch(0)
+ when Array
+ if pair.length > 2
+ raise ArgumentError.new("Bad # of fields for pair: #{pair.inspect}")
+ end
+ [pair.fetch(0), pair[1]]
+ else
+ # recurse, converting any non-array to an array
+ process_pair(pair.to_a)
+ end
+ end
+ end
+end
diff --git a/lib/restclient/payload.rb b/lib/restclient/payload.rb
index b550598..56563fa 100644
--- a/lib/restclient/payload.rb
+++ b/lib/restclient/payload.rb
@@ -1,5 +1,7 @@
require 'tempfile'
+require 'securerandom'
require 'stringio'
+
require 'mime/types'
module RestClient
@@ -23,28 +25,20 @@ module RestClient
end
def has_file?(params)
- params.any? do |_, v|
- case v
- when Hash
- has_file?(v)
- when Array
- has_file_array?(v)
- else
- v.respond_to?(:path) && v.respond_to?(:read)
- end
+ unless params.is_a?(Hash)
+ raise ArgumentError.new("Must pass Hash, not #{params.inspect}")
end
+ _has_file?(params)
end
- def has_file_array?(params)
- params.any? do |v|
- case v
- when Hash
- has_file?(v)
- when Array
- has_file_array?(v)
- else
- v.respond_to?(:path) && v.respond_to?(:read)
- end
+ def _has_file?(obj)
+ case obj
+ when Hash, ParamsArray
+ obj.any? {|_, v| _has_file?(v) }
+ when Array
+ obj.any? {|v| _has_file?(v) }
+ else
+ obj.respond_to?(:path) && obj.respond_to?(:read)
end
end
@@ -58,40 +52,13 @@ module RestClient
@stream.seek(0)
end
- def read(bytes=nil)
- @stream.read(bytes)
- end
-
- alias :to_s :read
-
- # Flatten parameters by converting hashes of hashes to flat hashes
- # {keys1 => {keys2 => value}} will be transformed into [keys1[key2], value]
- def flatten_params(params, parent_key = nil)
- result = []
- params.each do |key, value|
- calculated_key = parent_key ? "#{parent_key}[#{handle_key(key)}]" : handle_key(key)
- if value.is_a? Hash
- result += flatten_params(value, calculated_key)
- elsif value.is_a? Array
- result += flatten_params_array(value, calculated_key)
- else
- result << [calculated_key, value]
- end
- end
- result
+ def read(*args)
+ @stream.read(*args)
end
- def flatten_params_array value, calculated_key
- result = []
- value.each do |elem|
- if elem.is_a? Hash
- result += flatten_params(elem, calculated_key)
- elsif elem.is_a? Array
- result += flatten_params_array(elem, calculated_key)
- else
- result << ["#{calculated_key}[]", elem]
- end
- end
+ def to_s
+ result = read
+ @stream.seek(0)
result
end
@@ -109,14 +76,12 @@ module RestClient
@stream.close unless @stream.closed?
end
- def inspect
- result = to_s.inspect
- @stream.seek(0)
- result
+ def to_s_inspect
+ to_s.inspect
end
def short_inspect
- (size > 500 ? "#{size} byte(s) length" : inspect)
+ (size > 500 ? "#{size} byte(s) length" : to_s_inspect)
end
end
@@ -139,37 +104,28 @@ module RestClient
class UrlEncoded < Base
def build_stream(params = nil)
- @stream = StringIO.new(flatten_params(params).collect do |entry|
- "#{entry[0]}=#{handle_key(entry[1])}"
- end.join("&"))
+ @stream = StringIO.new(Utils.encode_query_string(params))
@stream.seek(0)
end
- # for UrlEncoded escape the keys
- def handle_key key
- Parser.escape(key.to_s, Escape)
- end
-
def headers
super.merge({'Content-Type' => 'application/x-www-form-urlencoded'})
end
-
- Parser = URI.const_defined?(:Parser) ? URI::Parser.new : URI
- Escape = Regexp.new("[^#{URI::PATTERN::UNRESERVED}]")
end
class Multipart < Base
EOL = "\r\n"
def build_stream(params)
- b = "--#{boundary}"
+ b = '--' + boundary
@stream = Tempfile.new("RESTClient.Stream.#{rand(1000)}")
@stream.binmode
@stream.write(b + EOL)
- if params.is_a? Hash
- x = flatten_params(params)
+ case params
+ when Hash, ParamsArray
+ x = Utils.flatten_params(params)
else
x = params
end
@@ -218,10 +174,25 @@ module RestClient
end
def boundary
- @boundary ||= rand(1_000_000).to_s
+ return @boundary if defined?(@boundary) && @boundary
+
+ # Use the same algorithm used by WebKit: generate 16 random
+ # alphanumeric characters, replacing `+` `/` with `A` `B` (included in
+ # the list twice) to round out the set of 64.
+ s = SecureRandom.base64(12)
+ s.tr!('+/', 'AB')
+
+ @boundary = '----RubyFormBoundary' + s
end
# for Multipart do not escape the keys
+ #
+ # Ostensibly multipart keys MAY be percent encoded per RFC 7578, but in
+ # practice no major browser that I'm aware of uses percent encoding.
+ #
+ # Further discussion of multipart encoding:
+ # https://github.com/rest-client/rest-client/pull/403#issuecomment-156976930
+ #
def handle_key key
key
end
diff --git a/lib/restclient/platform.rb b/lib/restclient/platform.rb
index c3f321b..87df973 100644
--- a/lib/restclient/platform.rb
+++ b/lib/restclient/platform.rb
@@ -1,3 +1,5 @@
+require 'rbconfig'
+
module RestClient
module Platform
# Return true if we are running on a darwin-based Ruby platform. This will
@@ -26,5 +28,22 @@ module RestClient
# defined on mri >= 1.9
RUBY_ENGINE == 'jruby'
end
+
+ def self.architecture
+ "#{RbConfig::CONFIG['host_os']} #{RbConfig::CONFIG['host_cpu']}"
+ end
+
+ def self.ruby_agent_version
+ case RUBY_ENGINE
+ when 'jruby'
+ "jruby/#{JRUBY_VERSION} (#{RUBY_VERSION}p#{RUBY_PATCHLEVEL})"
+ else
+ "#{RUBY_ENGINE}/#{RUBY_VERSION}p#{RUBY_PATCHLEVEL}"
+ end
+ end
+
+ def self.default_user_agent
+ "rest-client/#{VERSION} (#{architecture}) #{ruby_agent_version}"
+ end
end
end
diff --git a/lib/restclient/raw_response.rb b/lib/restclient/raw_response.rb
index 69fc939..59ecc5c 100644
--- a/lib/restclient/raw_response.rb
+++ b/lib/restclient/raw_response.rb
@@ -15,9 +15,12 @@ module RestClient
attr_reader :file, :request
- def initialize(tempfile, net_http_res, args, request)
+ def inspect
+ "<RestClient::RawResponse @code=#{code.inspect}, @file=#{file.inspect}, @request=#{request.inspect}>"
+ end
+
+ def initialize(tempfile, net_http_res, request)
@net_http_res = net_http_res
- @args = args
@file = tempfile
@request = request
end
diff --git a/lib/restclient/request.rb b/lib/restclient/request.rb
index a9a0e60..a875dc0 100644
--- a/lib/restclient/request.rb
+++ b/lib/restclient/request.rb
@@ -16,106 +16,75 @@ module RestClient
# * :url
# Optional parameters (have a look at ssl and/or uri for some explanations):
# * :headers a hash containing the request headers
- # * :cookies will replace possible cookies in the :headers
+ # * :cookies may be a Hash{String/Symbol => String} of cookie values, an
+ # Array<HTTP::Cookie>, or an HTTP::CookieJar containing cookies. These
+ # will be added to a cookie jar before the request is sent.
# * :user and :password for basic auth, will be replaced by a user/password available in the :url
# * :block_response call the provided block with the HTTPResponse as parameter
# * :raw_response return a low-level RawResponse instead of a Response
# * :max_redirects maximum number of redirections (default to 10)
+ # * :proxy An HTTP proxy URI to use for this request. Any value here
+ # (including nil) will override RestClient.proxy.
# * :verify_ssl enable ssl verification, possible values are constants from
# OpenSSL::SSL::VERIFY_*, defaults to OpenSSL::SSL::VERIFY_PEER
- # * :timeout and :open_timeout are how long to wait for a response and to
- # open a connection, in seconds. Pass nil to disable the timeout.
+ # * :read_timeout and :open_timeout are how long to wait for a response and
+ # to open a connection, in seconds. Pass nil to disable the timeout.
+ # * :timeout can be used to set both timeouts
# * :ssl_client_cert, :ssl_client_key, :ssl_ca_file, :ssl_ca_path,
# :ssl_cert_store, :ssl_verify_callback, :ssl_verify_callback_warnings
# * :ssl_version specifies the SSL version for the underlying Net::HTTP connection
# * :ssl_ciphers sets SSL ciphers for the connection. See
# OpenSSL::SSL::SSLContext#ciphers=
+ # * :before_execution_proc a Proc to call before executing the request. This
+ # proc, like procs from RestClient.before_execution_procs, will be
+ # called with the HTTP request and request params.
class Request
- attr_reader :method, :url, :headers, :cookies,
- :payload, :user, :password, :timeout, :max_redirects,
+ attr_reader :method, :uri, :url, :headers, :payload, :proxy,
+ :user, :password, :read_timeout, :max_redirects,
:open_timeout, :raw_response, :processed_headers, :args,
:ssl_opts
+ # An array of previous redirection responses
+ attr_accessor :redirection_history
+
def self.execute(args, & block)
new(args).execute(& block)
end
- # This is similar to the list now in ruby core, but adds HIGH and RC4-MD5
- # for better compatibility (similar to Firefox) and moves AES-GCM cipher
- # suites above DHE/ECDHE CBC suites (similar to Chromium).
- # https://github.com/ruby/ruby/commit/699b209cf8cf11809620e12985ad33ae33b119ee
- #
- # This list will be used by default if the Ruby global OpenSSL default
- # ciphers appear to be a weak list.
- DefaultCiphers = %w{
- !aNULL
- !eNULL
- !EXPORT
- !SSLV2
- !LOW
-
- ECDHE-ECDSA-AES128-GCM-SHA256
- ECDHE-RSA-AES128-GCM-SHA256
- ECDHE-ECDSA-AES256-GCM-SHA384
- ECDHE-RSA-AES256-GCM-SHA384
- DHE-RSA-AES128-GCM-SHA256
- DHE-DSS-AES128-GCM-SHA256
- DHE-RSA-AES256-GCM-SHA384
- DHE-DSS-AES256-GCM-SHA384
- AES128-GCM-SHA256
- AES256-GCM-SHA384
- ECDHE-ECDSA-AES128-SHA256
- ECDHE-RSA-AES128-SHA256
- ECDHE-ECDSA-AES128-SHA
- ECDHE-RSA-AES128-SHA
- ECDHE-ECDSA-AES256-SHA384
- ECDHE-RSA-AES256-SHA384
- ECDHE-ECDSA-AES256-SHA
- ECDHE-RSA-AES256-SHA
- DHE-RSA-AES128-SHA256
- DHE-RSA-AES256-SHA256
- DHE-RSA-AES128-SHA
- DHE-RSA-AES256-SHA
- DHE-DSS-AES128-SHA256
- DHE-DSS-AES256-SHA256
- DHE-DSS-AES128-SHA
- DHE-DSS-AES256-SHA
- AES128-SHA256
- AES256-SHA256
- AES128-SHA
- AES256-SHA
- ECDHE-ECDSA-RC4-SHA
- ECDHE-RSA-RC4-SHA
- RC4-SHA
-
- HIGH
- +RC4
- RC4-MD5
- }.join(":")
-
- # A set of weak default ciphers that we will override by default.
- WeakDefaultCiphers = Set.new([
- "ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW",
- ])
-
SSLOptionList = %w{client_cert client_key ca_file ca_path cert_store
version ciphers verify_callback verify_callback_warnings}
+ def inspect
+ "<RestClient::Request @method=#{@method.inspect}, @url=#{@url.inspect}>"
+ end
+
def initialize args
- @method = args[:method] or raise ArgumentError, "must pass :method"
- @headers = args[:headers] || {}
+ @method = normalize_method(args[:method])
+ @headers = (args[:headers] || {}).dup
if args[:url]
- @url = process_url_params(args[:url], headers)
+ @url = process_url_params(normalize_url(args[:url]), headers)
else
raise ArgumentError, "must pass :url"
end
- @cookies = @headers.delete(:cookies) || args[:cookies] || {}
+
+ @user = @password = nil
+ parse_url_with_auth!(url)
+
+ # process cookie arguments found in headers or args
+ @cookie_jar = process_cookie_args!(@uri, @headers, args)
+
@payload = Payload.generate(args[:payload])
- @user = args[:user]
- @password = args[:password]
+
+ @user = args[:user] if args.include?(:user)
+ @password = args[:password] if args.include?(:password)
+
if args.include?(:timeout)
- @timeout = args[:timeout]
+ @read_timeout = args[:timeout]
+ @open_timeout = args[:timeout]
+ end
+ if args.include?(:read_timeout)
+ @read_timeout = args[:read_timeout]
end
if args.include?(:open_timeout)
@open_timeout = args[:open_timeout]
@@ -123,6 +92,8 @@ module RestClient
@block_response = args[:block_response]
@raw_response = args[:raw_response] || false
+ @proxy = args.fetch(:proxy) if args.include?(:proxy)
+
@ssl_opts = {}
if args.include?(:verify_ssl)
@@ -151,17 +122,12 @@ module RestClient
end
end
- # If there's no CA file, CA path, or cert store provided, use default
- if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
- @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
- end
+ # Set some other default SSL options, but only if we have an HTTPS URI.
+ if use_ssl?
- unless @ssl_opts.include?(:ciphers)
- # If we're on a Ruby version that has insecure default ciphers,
- # override it with our default list.
- if WeakDefaultCiphers.include?(
- OpenSSL::SSL::SSLContext::DEFAULT_PARAMS.fetch(:ciphers))
- @ssl_opts[:ciphers] = DefaultCiphers
+ # If there's no CA file, CA path, or cert store provided, use default
+ if !ssl_ca_file && !ssl_ca_path && !@ssl_opts.include?(:cert_store)
+ @ssl_opts[:cert_store] = self.class.default_ssl_cert_store
end
end
@@ -169,11 +135,14 @@ module RestClient
@max_redirects = args[:max_redirects] || 10
@processed_headers = make_headers headers
@args = args
+
+ @before_execution_proc = args[:before_execution_proc]
end
def execute & block
- uri = parse_url_with_auth(url)
- transmit uri, net_http_request_class(method).new(uri.request_uri, processed_headers), payload, & block
+ # With 2.0.0+, net/http accepts URI objects in requests and handles wrapping
+ # IPv6 addresses in [] for use in the Host request header.
+ transmit uri, net_http_request_class(method).new(uri, processed_headers), payload, & block
ensure
payload.close if payload
end
@@ -188,82 +157,291 @@ module RestClient
end
end
+ # Return true if the request URI will use HTTPS.
+ #
+ # @return [Boolean]
+ #
+ def use_ssl?
+ uri.is_a?(URI::HTTPS)
+ end
+
# Extract the query parameters and append them to the url
- def process_url_params url, headers
- url_params = {}
+ #
+ # Look through the headers hash for a :params option (case-insensitive,
+ # may be string or symbol). If present and the value is a Hash or
+ # RestClient::ParamsArray, *delete* the key/value pair from the headers
+ # hash and encode the value into a query string. Append this query string
+ # to the URL and return the resulting URL.
+ #
+ # @param [String] url
+ # @param [Hash] headers An options/headers hash to process. Mutation
+ # warning: the params key may be removed if present!
+ #
+ # @return [String] resulting url with query string
+ #
+ def process_url_params(url, headers)
+ url_params = nil
+
+ # find and extract/remove "params" key if the value is a Hash/ParamsArray
headers.delete_if do |key, value|
- if 'params' == key.to_s.downcase && value.is_a?(Hash)
- url_params.merge! value
+ if key.to_s.downcase == 'params' &&
+ (value.is_a?(Hash) || value.is_a?(RestClient::ParamsArray))
+ if url_params
+ raise ArgumentError.new("Multiple 'params' options passed")
+ end
+ url_params = value
true
else
false
end
end
- unless url_params.empty?
- query_string = url_params.collect { |k, v| "#{k.to_s}=#{CGI::escape(v.to_s)}" }.join('&')
- url + "?#{query_string}"
+
+ # build resulting URL with query string
+ if url_params && !url_params.empty?
+ query_string = RestClient::Utils.encode_query_string(url_params)
+
+ if url.include?('?')
+ url + '&' + query_string
+ else
+ url + '?' + query_string
+ end
else
url
end
end
- def make_headers user_headers
- unless @cookies.empty?
+ # Render a hash of key => value pairs for cookies in the Request#cookie_jar
+ # that are valid for the Request#uri. This will not necessarily include all
+ # cookies if there are duplicate keys. It's safer to use the cookie_jar
+ # directly if that's a concern.
+ #
+ # @see Request#cookie_jar
+ #
+ # @return [Hash]
+ #
+ def cookies
+ hash = {}
- # Validate that the cookie names and values look sane. If you really
- # want to pass scary characters, just set the Cookie header directly.
- # RFC6265 is actually much more restrictive than we are.
- @cookies.each do |key, val|
- unless valid_cookie_key?(key)
- raise ArgumentError.new("Invalid cookie name: #{key.inspect}")
- end
- unless valid_cookie_value?(val)
- raise ArgumentError.new("Invalid cookie value: #{val.inspect}")
+ @cookie_jar.cookies(uri).each do |c|
+ hash[c.name] = c.value
+ end
+
+ hash
+ end
+
+ # @return [HTTP::CookieJar]
+ def cookie_jar
+ @cookie_jar
+ end
+
+ # Render a Cookie HTTP request header from the contents of the @cookie_jar,
+ # or nil if the jar is empty.
+ #
+ # @see Request#cookie_jar
+ #
+ # @return [String, nil]
+ #
+ def make_cookie_header
+ return nil if cookie_jar.nil?
+
+ arr = cookie_jar.cookies(url)
+ return nil if arr.empty?
+
+ return HTTP::Cookie.cookie_value(arr)
+ end
+
+ # Process cookies passed as hash or as HTTP::CookieJar. For backwards
+ # compatibility, these may be passed as a :cookies option masquerading
+ # inside the headers hash. To avoid confusion, if :cookies is passed in
+ # both headers and Request#initialize, raise an error.
+ #
+ # :cookies may be a:
+ # - Hash{String/Symbol => String}
+ # - Array<HTTP::Cookie>
+ # - HTTP::CookieJar
+ #
+ # Passing as a hash:
+ # Keys may be symbols or strings. Values must be strings.
+ # Infer the domain name from the request URI and allow subdomains (as
+ # though '.example.com' had been set in a Set-Cookie header). Assume a
+ # path of '/'.
+ #
+ # RestClient::Request.new(url: 'http://example.com', method: :get,
+ # :cookies => {:foo => 'Value', 'bar' => '123'}
+ # )
+ #
+ # results in cookies as though set from the server by:
+ # Set-Cookie: foo=Value; Domain=.example.com; Path=/
+ # Set-Cookie: bar=123; Domain=.example.com; Path=/
+ #
+ # which yields a client cookie header of:
+ # Cookie: foo=Value; bar=123
+ #
+ # Passing as HTTP::CookieJar, which will be passed through directly:
+ #
+ # jar = HTTP::CookieJar.new
+ # jar.add(HTTP::Cookie.new('foo', 'Value', domain: 'example.com',
+ # path: '/', for_domain: false))
+ #
+ # RestClient::Request.new(..., :cookies => jar)
+ #
+ # @param [URI::HTTP] uri The URI for the request. This will be used to
+ # infer the domain name for cookies passed as strings in a hash. To avoid
+ # this implicit behavior, pass a full cookie jar or use HTTP::Cookie hash
+ # values.
+ # @param [Hash] headers The headers hash from which to pull the :cookies
+ # option. MUTATION NOTE: This key will be deleted from the hash if
+ # present.
+ # @param [Hash] args The options passed to Request#initialize. This hash
+ # will be used as another potential source for the :cookies key.
+ # These args will not be mutated.
+ #
+ # @return [HTTP::CookieJar] A cookie jar containing the parsed cookies.
+ #
+ def process_cookie_args!(uri, headers, args)
+
+ # Avoid ambiguity in whether options from headers or options from
+ # Request#initialize should take precedence by raising ArgumentError when
+ # both are present. Prior versions of rest-client claimed to give
+ # precedence to init options, but actually gave precedence to headers.
+ # Avoid that mess by erroring out instead.
+ if headers[:cookies] && args[:cookies]
+ raise ArgumentError.new(
+ "Cannot pass :cookies in Request.new() and in headers hash")
+ end
+
+ cookies_data = headers.delete(:cookies) || args[:cookies]
+
+ # return copy of cookie jar as is
+ if cookies_data.is_a?(HTTP::CookieJar)
+ return cookies_data.dup
+ end
+
+ # convert cookies hash into a CookieJar
+ jar = HTTP::CookieJar.new
+
+ (cookies_data || []).each do |key, val|
+
+ # Support for Array<HTTP::Cookie> mode:
+ # If key is a cookie object, add it to the jar directly and assert that
+ # there is no separate val.
+ if key.is_a?(HTTP::Cookie)
+ if val
+ raise ArgumentError.new("extra cookie val: #{val.inspect}")
end
+
+ jar.add(key)
+ next
+ end
+
+ if key.is_a?(Symbol)
+ key = key.to_s
end
- user_headers[:cookie] = @cookies.map { |key, val| "#{key}=#{val}" }.sort.join('; ')
+ # assume implicit domain from the request URI, and set for_domain to
+ # permit subdomains
+ jar.add(HTTP::Cookie.new(key, val, domain: uri.hostname.downcase,
+ path: '/', for_domain: true))
end
- headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
- headers.merge!(@payload.headers) if @payload
- headers
+
+ jar
end
- # Do some sanity checks on cookie keys.
+ # Generate headers for use by a request. Header keys will be stringified
+ # using `#stringify_headers` to normalize them as capitalized strings.
#
- # Properly it should be a valid TOKEN per RFC 2616, but lots of servers are
- # more liberal.
+ # The final headers consist of:
+ # - default headers from #default_headers
+ # - user_headers provided here
+ # - headers from the payload object (e.g. Content-Type, Content-Lenth)
+ # - cookie headers from #make_cookie_header
#
- # Disallow the empty string as well as keys containing control characters,
- # equals sign, semicolon, comma, or space.
+ # @param [Hash] user_headers User-provided headers to include
#
- def valid_cookie_key?(string)
- return false if string.empty?
+ # @return [Hash<String, String>] A hash of HTTP headers => values
+ #
+ def make_headers(user_headers)
+ headers = stringify_headers(default_headers).merge(stringify_headers(user_headers))
- ! Regexp.new('[\x0-\x1f\x7f=;, ]').match(string)
+ # override headers from the payload (e.g. Content-Type, Content-Length)
+ if @payload
+ payload_headers = @payload.headers
+
+ # Warn the user if we override any headers that were previously
+ # present. This usually indicates that rest-client was passed
+ # conflicting information, e.g. if it was asked to render a payload as
+ # x-www-form-urlencoded but a Content-Type application/json was
+ # also supplied by the user.
+ payload_headers.each_pair do |key, val|
+ if headers.include?(key) && headers[key] != val
+ warn("warning: Overriding #{key.inspect} header " +
+ "#{headers.fetch(key).inspect} with #{val.inspect} " +
+ "due to payload")
+ end
+ end
+
+ headers.merge!(payload_headers)
+ end
+
+ # merge in cookies
+ cookies = make_cookie_header
+ if cookies && !cookies.empty?
+ if headers['Cookie']
+ warn('warning: overriding "Cookie" header with :cookies option')
+ end
+ headers['Cookie'] = cookies
+ end
+
+ headers
end
- # Validate cookie values. Rather than following RFC 6265, allow anything
- # but control characters, comma, and semicolon.
- def valid_cookie_value?(value)
- ! Regexp.new('[\x0-\x1f\x7f,;]').match(value)
+ # The proxy URI for this request. If `:proxy` was provided on this request,
+ # use it over `RestClient.proxy`.
+ #
+ # Return false if a proxy was explicitly set and is falsy.
+ #
+ # @return [URI, false, nil]
+ #
+ def proxy_uri
+ if defined?(@proxy)
+ if @proxy
+ URI.parse(@proxy)
+ else
+ false
+ end
+ elsif RestClient.proxy_set?
+ if RestClient.proxy
+ URI.parse(RestClient.proxy)
+ else
+ false
+ end
+ else
+ nil
+ end
end
- def net_http_class
- if RestClient.proxy
- proxy_uri = URI.parse(RestClient.proxy)
- Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password)
+ def net_http_object(hostname, port)
+ p_uri = proxy_uri
+
+ if p_uri.nil?
+ # no proxy set
+ Net::HTTP.new(hostname, port)
+ elsif !p_uri
+ # proxy explicitly set to none
+ Net::HTTP.new(hostname, port, nil, nil, nil, nil)
else
- Net::HTTP
+ Net::HTTP.new(hostname, port,
+ p_uri.hostname, p_uri.port, p_uri.user, p_uri.password)
+
end
end
def net_http_request_class(method)
- Net::HTTP.const_get(method.to_s.capitalize)
+ Net::HTTP.const_get(method.capitalize, false)
end
def net_http_do_request(http, req, body=nil, &block)
- if body != nil && body.respond_to?(:read)
+ if body && body.respond_to?(:read)
req.body_stream = body
return http.request(req, nil, &block)
else
@@ -271,36 +449,19 @@ module RestClient
end
end
- def parse_url(url)
- url = "http://#{url}" unless url.match(/^http/)
- URI.parse(url)
- end
-
- def parse_url_with_auth(url)
- uri = parse_url(url)
- @user = CGI.unescape(uri.user) if uri.user
- @password = CGI.unescape(uri.password) if uri.password
- if !@user && !@password
- @user, @password = Netrc.read[uri.host]
- end
- uri
- end
-
- def process_payload(p=nil, parent_key=nil)
- unless p.is_a?(Hash)
- p
- else
- @headers[:content_type] ||= 'application/x-www-form-urlencoded'
- p.keys.map do |k|
- key = parent_key ? "#{parent_key}[#{k}]" : k
- if p[k].is_a? Hash
- process_payload(p[k], key)
- else
- value = parser.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
- "#{key}=#{value}"
- end
- end.join("&")
- end
+ # Normalize a URL by adding a protocol if none is present.
+ #
+ # If the string has no HTTP-like scheme (i.e. scheme followed by '//'), a
+ # scheme of 'http' will be added. This mimics the behavior of browsers and
+ # user agents like cURL.
+ #
+ # @param [String] url A URL string.
+ #
+ # @return [String]
+ #
+ def normalize_url(url)
+ url = 'http://' + url unless url.match(%r{\A[a-z][a-z0-9+.-]*://}i)
+ url
end
# Return a certificate store that can be used to validate certificates with
@@ -332,6 +493,122 @@ module RestClient
cert_store
end
+ def self.decode content_encoding, body
+ if (!body) || body.empty?
+ body
+ elsif content_encoding == 'gzip'
+ Zlib::GzipReader.new(StringIO.new(body)).read
+ elsif content_encoding == 'deflate'
+ begin
+ Zlib::Inflate.new.inflate body
+ rescue Zlib::DataError
+ # No luck with Zlib decompression. Let's try with raw deflate,
+ # like some broken web servers do.
+ Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
+ end
+ else
+ body
+ end
+ end
+
+ def redacted_uri
+ if uri.password
+ sanitized_uri = uri.dup
+ sanitized_uri.password = 'REDACTED'
+ sanitized_uri
+ else
+ uri
+ end
+ end
+
+ def redacted_url
+ redacted_uri.to_s
+ end
+
+ def log_request
+ return unless RestClient.log
+
+ out = []
+
+ out << "RestClient.#{method} #{redacted_url.inspect}"
+ out << payload.short_inspect if payload
+ out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
+ RestClient.log << out.join(', ') + "\n"
+ end
+
+ def log_response res
+ return unless RestClient.log
+
+ size = if @raw_response
+ File.size(@tf.path)
+ else
+ res.body.nil? ? 0 : res.body.size
+ end
+
+ RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
+ end
+
+ # Return a hash of headers whose keys are capitalized strings
+ def stringify_headers headers
+ headers.inject({}) do |result, (key, value)|
+ if key.is_a? Symbol
+ key = key.to_s.split(/_/).map(&:capitalize).join('-')
+ end
+ if 'CONTENT-TYPE' == key.upcase
+ result[key] = maybe_convert_extension(value.to_s)
+ elsif 'ACCEPT' == key.upcase
+ # Accept can be composed of several comma-separated values
+ if value.is_a? Array
+ target_values = value
+ else
+ target_values = value.to_s.split ','
+ end
+ result[key] = target_values.map { |ext|
+ maybe_convert_extension(ext.to_s.strip)
+ }.join(', ')
+ else
+ result[key] = value.to_s
+ end
+ result
+ end
+ end
+
+ def default_headers
+ {
+ :accept => '*/*',
+ :accept_encoding => 'gzip, deflate',
+ :user_agent => RestClient::Platform.default_user_agent,
+ }
+ end
+
+ private
+
+ # Parse the `@url` string into a URI object and save it as
+ # `@uri`. Also save any basic auth user or password as @user and @password.
+ # If no auth info was passed, check for credentials in a Netrc file.
+ #
+ # @param [String] url A URL string.
+ #
+ # @return [URI]
+ #
+ # @raise URI::InvalidURIError on invalid URIs
+ #
+ def parse_url_with_auth!(url)
+ uri = URI.parse(url)
+
+ if uri.hostname.nil?
+ raise URI::InvalidURIError.new("bad URI(no host provided): #{url}")
+ end
+
+ @user = CGI.unescape(uri.user) if uri.user
+ @password = CGI.unescape(uri.password) if uri.password
+ if !@user && !@password
+ @user, @password = Netrc.read[uri.hostname]
+ end
+
+ @uri = uri
+ end
+
def print_verify_callback_warnings
warned = false
if RestClient::Platform.mac_mri?
@@ -346,10 +623,32 @@ module RestClient
warned
end
+ # Parse a method and return a normalized string version.
+ #
+ # Raise ArgumentError if the method is falsy, but otherwise do no
+ # validation.
+ #
+ # @param method [String, Symbol]
+ #
+ # @return [String]
+ #
+ # @see net_http_request_class
+ #
+ def normalize_method(method)
+ raise ArgumentError.new('must pass :method') unless method
+ method.to_s.downcase
+ end
+
def transmit uri, req, payload, & block
+
+ # We set this to true in the net/http block so that we can distinguish
+ # read_timeout from open_timeout. Now that we only support Ruby 2.0+,
+ # this is only needed for Timeout exceptions thrown outside of Net::HTTP.
+ established_connection = false
+
setup_credentials req
- net = net_http_class.new(uri.host, uri.port)
+ net = net_http_object(uri.hostname, uri.port)
net.use_ssl = uri.is_a?(URI::HTTPS)
net.ssl_version = ssl_version if ssl_version
net.ciphers = ssl_ciphers if ssl_ciphers
@@ -388,16 +687,16 @@ module RestClient
warn('Try passing :verify_ssl => false instead.')
end
- if defined? @timeout
- if @timeout == -1
- warn 'To disable read timeouts, please set timeout to nil instead of -1'
- @timeout = nil
+ if defined? @read_timeout
+ if @read_timeout == -1
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
+ @read_timeout = nil
end
- net.read_timeout = @timeout
+ net.read_timeout = @read_timeout
end
if defined? @open_timeout
if @open_timeout == -1
- warn 'To disable open timeouts, please set open_timeout to nil instead of -1'
+ warn 'Deprecated: to disable timeouts, please use nil instead of -1'
@open_timeout = nil
end
net.open_timeout = @open_timeout
@@ -407,24 +706,38 @@ module RestClient
before_proc.call(req, args)
end
- log_request
+ if @before_execution_proc
+ @before_execution_proc.call(req, args)
+ end
+ log_request
net.start do |http|
+ established_connection = true
+
if @block_response
- net_http_do_request(http, req, payload ? payload.to_s : nil,
- &@block_response)
+ net_http_do_request(http, req, payload, &@block_response)
else
- res = net_http_do_request(http, req, payload ? payload.to_s : nil) \
- { |http_response| fetch_body(http_response) }
+ res = net_http_do_request(http, req, payload) { |http_response|
+ fetch_body(http_response)
+ }
log_response res
process_result res, & block
end
end
rescue EOFError
raise RestClient::ServerBrokeConnection
- rescue Timeout::Error, Errno::ETIMEDOUT
- raise RestClient::RequestTimeout
+ rescue Net::OpenTimeout => err
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
+ rescue Net::ReadTimeout => err
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
+ rescue Timeout::Error, Errno::ETIMEDOUT => err
+ # handling for non-Net::HTTP timeouts
+ if established_connection
+ raise RestClient::Exceptions::ReadTimeout.new(nil, err)
+ else
+ raise RestClient::Exceptions::OpenTimeout.new(nil, err)
+ end
rescue OpenSSL::SSL::SSLError => error
# TODO: deprecate and remove RestClient::SSLCertificateNotVerified and just
@@ -449,7 +762,7 @@ module RestClient
end
def setup_credentials(req)
- req.basic_auth(user, password) if user
+ req.basic_auth(user, password) if user && !headers.has_key?("Authorization")
end
def fetch_body(http_response)
@@ -457,9 +770,9 @@ module RestClient
# Taken from Chef, which as in turn...
# Stolen from http://www.ruby-forum.com/topic/166423
# Kudos to _why!
- @tf = Tempfile.new("rest-client")
+ @tf = Tempfile.new('rest-client.')
@tf.binmode
- size, total = 0, http_response.header['Content-Length'].to_i
+ size, total = 0, http_response['Content-Length'].to_i
http_response.read_body do |chunk|
@tf.write chunk
size += chunk.size
@@ -484,101 +797,20 @@ module RestClient
def process_result res, & block
if @raw_response
# We don't decode raw requests
- response = RawResponse.new(@tf, res, args, self)
+ response = RawResponse.new(@tf, res, self)
else
- response = Response.create(Request.decode(res['content-encoding'], res.body), res, args, self)
+ decoded = Request.decode(res['content-encoding'], res.body)
+ response = Response.create(decoded, res, self)
end
if block_given?
block.call(response, self, res, & block)
else
- response.return!(self, res, & block)
- end
-
- end
-
- def self.decode content_encoding, body
- if (!body) || body.empty?
- body
- elsif content_encoding == 'gzip'
- Zlib::GzipReader.new(StringIO.new(body)).read
- elsif content_encoding == 'deflate'
- begin
- Zlib::Inflate.new.inflate body
- rescue Zlib::DataError
- # No luck with Zlib decompression. Let's try with raw deflate,
- # like some broken web servers do.
- Zlib::Inflate.new(-Zlib::MAX_WBITS).inflate body
- end
- else
- body
- end
- end
-
- def log_request
- return unless RestClient.log
-
- out = []
- sanitized_url = begin
- uri = URI.parse(url)
- uri.password = "REDACTED" if uri.password
- uri.to_s
- rescue URI::InvalidURIError
- # An attacker may be able to manipulate the URL to be
- # invalid, which could force discloure of a password if
- # we show any of the un-parsed URL here.
- "[invalid uri]"
- end
-
- out << "RestClient.#{method} #{sanitized_url.inspect}"
- out << payload.short_inspect if payload
- out << processed_headers.to_a.sort.map { |(k, v)| [k.inspect, v.inspect].join("=>") }.join(", ")
- RestClient.log << out.join(', ') + "\n"
- end
-
- def log_response res
- return unless RestClient.log
-
- size = if @raw_response
- File.size(@tf.path)
- else
- res.body.nil? ? 0 : res.body.size
- end
-
- RestClient.log << "# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes\n"
- end
-
- # Return a hash of headers whose keys are capitalized strings
- def stringify_headers headers
- headers.inject({}) do |result, (key, value)|
- if key.is_a? Symbol
- key = key.to_s.split(/_/).map { |w| w.capitalize }.join('-')
- end
- if 'CONTENT-TYPE' == key.upcase
- result[key] = maybe_convert_extension(value.to_s)
- elsif 'ACCEPT' == key.upcase
- # Accept can be composed of several comma-separated values
- if value.is_a? Array
- target_values = value
- else
- target_values = value.to_s.split ','
- end
- result[key] = target_values.map { |ext|
- maybe_convert_extension(ext.to_s.strip)
- }.join(', ')
- else
- result[key] = value.to_s
- end
- result
+ response.return!(&block)
end
- end
- def default_headers
- {:accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate'}
end
- private
-
def parser
URI.const_defined?(:Parser) ? URI::Parser.new : URI
end
diff --git a/lib/restclient/resource.rb b/lib/restclient/resource.rb
index 0df636a..955f682 100644
--- a/lib/restclient/resource.rb
+++ b/lib/restclient/resource.rb
@@ -14,7 +14,7 @@ module RestClient
#
# With a timeout (seconds):
#
- # RestClient::Resource.new('http://slow', :timeout => 10)
+ # RestClient::Resource.new('http://slow', :read_timeout => 10)
#
# With an open timeout (seconds):
#
@@ -113,8 +113,8 @@ module RestClient
options[:headers] || {}
end
- def timeout
- options[:timeout]
+ def read_timeout
+ options[:read_timeout]
end
def open_timeout
diff --git a/lib/restclient/response.rb b/lib/restclient/response.rb
index 47d8af5..1f0ba89 100644
--- a/lib/restclient/response.rb
+++ b/lib/restclient/response.rb
@@ -2,20 +2,79 @@ module RestClient
# A Response from RestClient, you can access the response body, the code or the headers.
#
- module Response
+ class Response < String
include AbstractResponse
+ # Return the HTTP response body.
+ #
+ # Future versions of RestClient will deprecate treating response objects
+ # directly as strings, so it will be necessary to call `.body`.
+ #
+ # @return [String]
+ #
def body
- self
+ # Benchmarking suggests that "#{self}" is fastest, and that caching the
+ # body string in an instance variable doesn't make it enough faster to be
+ # worth the extra memory storage.
+ String.new(self)
end
- def self.create body, net_http_res, args, request
- result = body || ''
- result.extend Response
- result.response_set_vars(net_http_res, args, request)
+ # Convert the HTTP response body to a pure String object.
+ #
+ # @return [String]
+ def to_s
+ body
+ end
+
+ # Convert the HTTP response body to a pure String object.
+ #
+ # @return [String]
+ def to_str
+ body
+ end
+
+ def inspect
+ "<RestClient::Response #{code.inspect} #{body_truncated(10).inspect}>"
+ end
+
+ def self.create(body, net_http_res, request)
+ result = self.new(body || '')
+
+ result.response_set_vars(net_http_res, request)
+ fix_encoding(result)
+
result
end
+ def self.fix_encoding(response)
+ charset = RestClient::Utils.get_encoding_from_headers(response.headers)
+ encoding = nil
+
+ begin
+ encoding = Encoding.find(charset) if charset
+ rescue ArgumentError
+ if RestClient.log
+ RestClient.log << "No such encoding: #{charset.inspect}"
+ end
+ end
+
+ return unless encoding
+
+ response.force_encoding(encoding)
+
+ response
+ end
+
+ private
+
+ def body_truncated(length)
+ b = body
+ if b.length > length
+ b[0..length] + '...'
+ else
+ b
+ end
+ end
end
end
diff --git a/lib/restclient/utils.rb b/lib/restclient/utils.rb
new file mode 100644
index 0000000..d41eefa
--- /dev/null
+++ b/lib/restclient/utils.rb
@@ -0,0 +1,235 @@
+module RestClient
+ # Various utility methods
+ module Utils
+
+ # Return encoding from an HTTP header hash.
+ #
+ # We use the RFC 7231 specification and do not impose a default encoding on
+ # text. This differs from the older RFC 2616 behavior, which specifies
+ # using ISO-8859-1 for text/* content types without a charset.
+ #
+ # Strings will use the default encoding when this method returns nil. This
+ # default is likely to be UTF-8 for Ruby >= 2.0
+ #
+ # @param headers [Hash<Symbol,String>]
+ #
+ # @return [String, nil] encoding Return the string encoding or nil if no
+ # header is found.
+ #
+ # @example
+ # >> get_encoding_from_headers({:content_type => 'text/plain; charset=UTF-8'})
+ # => "UTF-8"
+ #
+ def self.get_encoding_from_headers(headers)
+ type_header = headers[:content_type]
+ return nil unless type_header
+
+ _content_type, params = cgi_parse_header(type_header)
+
+ if params.include?('charset')
+ return params.fetch('charset').gsub(/(\A["']*)|(["']*\z)/, '')
+ end
+
+ nil
+ end
+
+ # Parse semi-colon separated, potentially quoted header string iteratively.
+ #
+ # @private
+ #
+ def self._cgi_parseparam(s)
+ return enum_for(__method__, s) unless block_given?
+
+ while s[0] == ';'
+ s = s[1..-1]
+ ends = s.index(';')
+ while ends && ends > 0 \
+ && (s[0...ends].count('"') -
+ s[0...ends].scan('\"').count) % 2 != 0
+ ends = s.index(';', ends + 1)
+ end
+ if ends.nil?
+ ends = s.length
+ end
+ f = s[0...ends]
+ yield f.strip
+ s = s[ends..-1]
+ end
+ nil
+ end
+
+ # Parse a Content-Type like header.
+ #
+ # Return the main content-type and a hash of options.
+ #
+ # This method was ported directly from Python's cgi.parse_header(). It
+ # probably doesn't read or perform particularly well in ruby.
+ # https://github.com/python/cpython/blob/3.4/Lib/cgi.py#L301-L331
+ #
+ #
+ # @param [String] line
+ # @return [Array(String, Hash)]
+ #
+ def self.cgi_parse_header(line)
+ parts = _cgi_parseparam(';' + line)
+ key = parts.next
+ pdict = {}
+
+ begin
+ while (p = parts.next)
+ i = p.index('=')
+ if i
+ name = p[0...i].strip.downcase
+ value = p[i+1..-1].strip
+ if value.length >= 2 && value[0] == '"' && value[-1] == '"'
+ value = value[1...-1]
+ value = value.gsub('\\\\', '\\').gsub('\\"', '"')
+ end
+ pdict[name] = value
+ end
+ end
+ rescue StopIteration
+ end
+
+ [key, pdict]
+ end
+
+ # Serialize a ruby object into HTTP query string parameters.
+ #
+ # There is no standard for doing this, so we choose our own slightly
+ # idiosyncratic format. The output closely matches the format understood by
+ # Rails, Rack, and PHP.
+ #
+ # If you don't want handling of complex objects and only want to handle
+ # simple flat hashes, you may want to use `URI.encode_www_form` instead,
+ # which implements HTML5-compliant URL encoded form data.
+ #
+ # @param [Hash,ParamsArray] object The object to serialize
+ #
+ # @return [String] A string appropriate for use as an HTTP query string
+ #
+ # @see {flatten_params}
+ #
+ # @see URI.encode_www_form
+ #
+ # @see See also Object#to_query in ActiveSupport
+ # @see http://php.net/manual/en/function.http-build-query.php
+ # http_build_query in PHP
+ # @see See also Rack::Utils.build_nested_query in Rack
+ #
+ # Notable differences from the ActiveSupport implementation:
+ #
+ # - Empty hash and empty array are treated the same as nil instead of being
+ # omitted entirely from the output. Rather than disappearing, they will
+ # appear to be nil instead.
+ #
+ # It's most common to pass a Hash as the object to serialize, but you can
+ # also use a ParamsArray if you want to be able to pass the same key with
+ # multiple values and not use the rack/rails array convention.
+ #
+ # @since 2.0.0
+ #
+ # @example Simple hashes
+ # >> encode_query_string({foo: 123, bar: 456})
+ # => 'foo=123&bar=456'
+ #
+ # @example Simple arrays
+ # >> encode_query_string({foo: [1,2,3]})
+ # => 'foo[]=1&foo[]=2&foo[]=3'
+ #
+ # @example Nested hashes
+ # >> encode_query_string({outer: {foo: 123, bar: 456}})
+ # => 'outer[foo]=123&outer[bar]=456'
+ #
+ # @example Deeply nesting
+ # >> encode_query_string({coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]})
+ # => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3'
+ #
+ # @example Null and empty values
+ # >> encode_query_string({string: '', empty: nil, list: [], hash: {}})
+ # => 'string=&empty&list&hash'
+ #
+ # @example Nested nulls
+ # >> encode_query_string({foo: {string: '', empty: nil}})
+ # => 'foo[string]=&foo[empty]'
+ #
+ # @example Multiple fields with the same name using ParamsArray
+ # >> encode_query_string(RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]))
+ # => 'foo=1&foo=2&foo=3'
+ #
+ # @example Nested ParamsArray
+ # >> encode_query_string({foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])})
+ # => 'foo[a]=1&foo[a]=2'
+ #
+ # >> encode_query_string(RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]))
+ # => 'foo[a]=1&foo[a]=2'
+ #
+ def self.encode_query_string(object)
+ flatten_params(object, true).map {|k, v| v.nil? ? k : "#{k}=#{v}" }.join('&')
+ end
+
+ # Transform deeply nested param containers into a flat array of [key,
+ # value] pairs.
+ #
+ # @example
+ # >> flatten_params({key1: {key2: 123}})
+ # => [["key1[key2]", 123]]
+ #
+ # @example
+ # >> flatten_params({key1: {key2: 123, arr: [1,2,3]}})
+ # => [["key1[key2]", 123], ["key1[arr][]", 1], ["key1[arr][]", 2], ["key1[arr][]", 3]]
+ #
+ # @param object [Hash, ParamsArray] The container to flatten
+ # @param uri_escape [Boolean] Whether to URI escape keys and values
+ # @param parent_key [String] Should not be passed (used for recursion)
+ #
+ def self.flatten_params(object, uri_escape=false, parent_key=nil)
+ unless object.is_a?(Hash) || object.is_a?(ParamsArray) ||
+ (parent_key && object.is_a?(Array))
+ raise ArgumentError.new('expected Hash or ParamsArray, got: ' + object.inspect)
+ end
+
+ # transform empty collections into nil, where possible
+ if object.empty? && parent_key
+ return [[parent_key, nil]]
+ end
+
+ # This is essentially .map(), but we need to do += for nested containers
+ object.reduce([]) { |result, item|
+ if object.is_a?(Array)
+ # item is already the value
+ k = nil
+ v = item
+ else
+ # item is a key, value pair
+ k, v = item
+ k = escape(k.to_s) if uri_escape
+ end
+
+ processed_key = parent_key ? "#{parent_key}[#{k}]" : k
+
+ case v
+ when Array, Hash, ParamsArray
+ result.concat flatten_params(v, uri_escape, processed_key)
+ else
+ v = escape(v.to_s) if uri_escape && v
+ result << [processed_key, v]
+ end
+ }
+ end
+
+ # Encode string for safe transport by URI or form encoding. This uses a CGI
+ # style escape, which transforms ` ` into `+` and various special
+ # characters into percent encoded forms.
+ #
+ # This calls URI.encode_www_form_component for the implementation. The only
+ # difference between this and CGI.escape is that it does not escape `*`.
+ # http://stackoverflow.com/questions/25085992/
+ #
+ # @see URI.encode_www_form_component
+ #
+ def self.escape(string)
+ URI.encode_www_form_component(string)
+ end
+ end
+end
diff --git a/lib/restclient/version.rb b/lib/restclient/version.rb
index 5f71842..d7367a2 100644
--- a/lib/restclient/version.rb
+++ b/lib/restclient/version.rb
@@ -1,5 +1,6 @@
module RestClient
- VERSION = '1.8.0' unless defined?(self::VERSION)
+ VERSION_INFO = [2, 0, 2] unless defined?(self::VERSION_INFO)
+ VERSION = VERSION_INFO.map(&:to_s).join('.') unless defined?(self::VERSION)
def self.version
VERSION
diff --git a/metadata.yml b/metadata.yml
deleted file mode 100644
index 2668e98..0000000
--- a/metadata.yml
+++ /dev/null
@@ -1,253 +0,0 @@
---- !ruby/object:Gem::Specification
-name: rest-client
-version: !ruby/object:Gem::Version
- version: 1.8.0
-platform: ruby
-authors:
-- REST Client Team
-autorequire:
-bindir: bin
-cert_chain: []
-date: 2015-03-24 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
- name: webmock
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '1.4'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '1.4'
-- !ruby/object:Gem::Dependency
- name: rspec
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '2.4'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '2.4'
-- !ruby/object:Gem::Dependency
- name: pry
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: pry-doc
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-- !ruby/object:Gem::Dependency
- name: rdoc
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 2.4.2
- - - "<"
- - !ruby/object:Gem::Version
- version: '5.0'
- type: :development
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 2.4.2
- - - "<"
- - !ruby/object:Gem::Version
- version: '5.0'
-- !ruby/object:Gem::Dependency
- name: http-cookie
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 1.0.2
- - - "<"
- - !ruby/object:Gem::Version
- version: '2.0'
- type: :runtime
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 1.0.2
- - - "<"
- - !ruby/object:Gem::Version
- version: '2.0'
-- !ruby/object:Gem::Dependency
- name: mime-types
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '1.16'
- - - "<"
- - !ruby/object:Gem::Version
- version: '3.0'
- type: :runtime
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '1.16'
- - - "<"
- - !ruby/object:Gem::Version
- version: '3.0'
-- !ruby/object:Gem::Dependency
- name: netrc
- requirement: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '0.7'
- type: :runtime
- prerelease: false
- version_requirements: !ruby/object:Gem::Requirement
- requirements:
- - - "~>"
- - !ruby/object:Gem::Version
- version: '0.7'
-description: 'A simple HTTP and REST client for Ruby, inspired by the Sinatra microframework
- style of specifying actions: get, put, post, delete.'
-email: rest.client at librelist.com
-executables:
-- restclient
-extensions: []
-extra_rdoc_files:
-- README.rdoc
-- history.md
-files:
-- ".gitignore"
-- ".rspec"
-- ".travis.yml"
-- AUTHORS
-- Gemfile
-- LICENSE
-- README.rdoc
-- Rakefile
-- bin/restclient
-- history.md
-- lib/rest-client.rb
-- lib/rest_client.rb
-- lib/restclient.rb
-- lib/restclient/abstract_response.rb
-- lib/restclient/exceptions.rb
-- lib/restclient/payload.rb
-- lib/restclient/platform.rb
-- lib/restclient/raw_response.rb
-- lib/restclient/request.rb
-- lib/restclient/resource.rb
-- lib/restclient/response.rb
-- lib/restclient/version.rb
-- lib/restclient/windows.rb
-- lib/restclient/windows/root_certs.rb
-- rest-client.gemspec
-- rest-client.windows.gemspec
-- spec/integration/capath_digicert/244b5494.0
-- spec/integration/capath_digicert/81b9768f.0
-- spec/integration/capath_digicert/README
-- spec/integration/capath_digicert/digicert.crt
-- spec/integration/capath_verisign/415660c1.0
-- spec/integration/capath_verisign/7651b327.0
-- spec/integration/capath_verisign/README
-- spec/integration/capath_verisign/verisign.crt
-- spec/integration/certs/digicert.crt
-- spec/integration/certs/verisign.crt
-- spec/integration/integration_spec.rb
-- spec/integration/request_spec.rb
-- spec/spec_helper.rb
-- spec/unit/abstract_response_spec.rb
-- spec/unit/exceptions_spec.rb
-- spec/unit/master_shake.jpg
-- spec/unit/payload_spec.rb
-- spec/unit/raw_response_spec.rb
-- spec/unit/request2_spec.rb
-- spec/unit/request_spec.rb
-- spec/unit/resource_spec.rb
-- spec/unit/response_spec.rb
-- spec/unit/restclient_spec.rb
-- spec/unit/windows/root_certs_spec.rb
-homepage: https://github.com/rest-client/rest-client
-licenses:
-- MIT
-metadata: {}
-post_install_message:
-rdoc_options: []
-require_paths:
-- lib
-required_ruby_version: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: 1.9.2
-required_rubygems_version: !ruby/object:Gem::Requirement
- requirements:
- - - ">="
- - !ruby/object:Gem::Version
- version: '0'
-requirements: []
-rubyforge_project:
-rubygems_version: 2.2.2
-signing_key:
-specification_version: 4
-summary: Simple HTTP and REST client for Ruby, inspired by microframework syntax for
- specifying actions.
-test_files:
-- spec/integration/capath_digicert/244b5494.0
-- spec/integration/capath_digicert/81b9768f.0
-- spec/integration/capath_digicert/README
-- spec/integration/capath_digicert/digicert.crt
-- spec/integration/capath_verisign/415660c1.0
-- spec/integration/capath_verisign/7651b327.0
-- spec/integration/capath_verisign/README
-- spec/integration/capath_verisign/verisign.crt
-- spec/integration/certs/digicert.crt
-- spec/integration/certs/verisign.crt
-- spec/integration/integration_spec.rb
-- spec/integration/request_spec.rb
-- spec/spec_helper.rb
-- spec/unit/abstract_response_spec.rb
-- spec/unit/exceptions_spec.rb
-- spec/unit/master_shake.jpg
-- spec/unit/payload_spec.rb
-- spec/unit/raw_response_spec.rb
-- spec/unit/request2_spec.rb
-- spec/unit/request_spec.rb
-- spec/unit/resource_spec.rb
-- spec/unit/response_spec.rb
-- spec/unit/restclient_spec.rb
-- spec/unit/windows/root_certs_spec.rb
-has_rdoc:
diff --git a/rest-client.gemspec b/rest-client.gemspec
index 967457c..e780a12 100644
--- a/rest-client.gemspec
+++ b/rest-client.gemspec
@@ -10,21 +10,22 @@ Gem::Specification.new do |s|
s.license = 'MIT'
s.email = 'rest.client at librelist.com'
s.executables = ['restclient']
- s.extra_rdoc_files = ['README.rdoc', 'history.md']
+ s.extra_rdoc_files = ['README.md', 'history.md']
s.files = `git ls-files -z`.split("\0")
s.test_files = `git ls-files -z spec/`.split("\0")
s.homepage = 'https://github.com/rest-client/rest-client'
s.summary = 'Simple HTTP and REST client for Ruby, inspired by microframework syntax for specifying actions.'
- s.add_development_dependency('webmock', '~> 1.4')
- s.add_development_dependency('rspec', '~> 2.4')
- s.add_development_dependency('pry')
- s.add_development_dependency('pry-doc')
- s.add_development_dependency('rdoc', '>= 2.4.2', '< 5.0')
+ s.add_development_dependency('webmock', '~> 2.0')
+ s.add_development_dependency('rspec', '~> 3.0')
+ s.add_development_dependency('pry', '~> 0')
+ s.add_development_dependency('pry-doc', '~> 0')
+ s.add_development_dependency('rdoc', '>= 2.4.2', '< 6.0')
+ s.add_development_dependency('rubocop', '~> 0')
s.add_dependency('http-cookie', '>= 1.0.2', '< 2.0')
- s.add_dependency('mime-types', '>= 1.16', '< 3.0')
- s.add_dependency('netrc', '~> 0.7')
+ s.add_dependency('mime-types', '>= 1.16', '< 4.0')
+ s.add_dependency('netrc', '~> 0.8')
- s.required_ruby_version = '>= 1.9.2'
+ s.required_ruby_version = '>= 2.0.0'
end
diff --git a/spec/helpers.rb b/spec/helpers.rb
new file mode 100644
index 0000000..1de717d
--- /dev/null
+++ b/spec/helpers.rb
@@ -0,0 +1,22 @@
+require 'uri'
+
+module Helpers
+ def response_double(opts={})
+ double('response', {:to_hash => {}}.merge(opts))
+ end
+
+ def fake_stderr
+ original_stderr = $stderr
+ $stderr = StringIO.new
+ yield
+ $stderr.string
+ ensure
+ $stderr = original_stderr
+ end
+
+ def request_double(url: 'http://example.com', method: 'get')
+ double('request', url: url, uri: URI.parse(url), method: method,
+ user: nil, password: nil, cookie_jar: HTTP::CookieJar.new,
+ redirection_history: nil, args: {url: url, method: method})
+ end
+end
diff --git a/spec/integration/_lib.rb b/spec/integration/_lib.rb
new file mode 100644
index 0000000..935238d
--- /dev/null
+++ b/spec/integration/_lib.rb
@@ -0,0 +1 @@
+require_relative '../spec_helper'
diff --git a/spec/integration/httpbin_spec.rb b/spec/integration/httpbin_spec.rb
new file mode 100644
index 0000000..8c83b37
--- /dev/null
+++ b/spec/integration/httpbin_spec.rb
@@ -0,0 +1,87 @@
+require_relative '_lib'
+require 'json'
+
+describe RestClient::Request do
+ before(:all) do
+ WebMock.disable!
+ end
+
+ after(:all) do
+ WebMock.enable!
+ end
+
+ def default_httpbin_url
+ # add a hack to work around java/jruby bug
+ # java.lang.RuntimeException: Could not generate DH keypair with backtrace
+ # Also (2017-04-09) Travis Jruby versions have a broken CA keystore
+ if ENV['TRAVIS_RUBY_VERSION'] =~ /\Ajruby-/
+ 'http://httpbin.org/'
+ else
+ 'https://httpbin.org/'
+ end
+ end
+
+ def httpbin(suffix='')
+ url = ENV.fetch('HTTPBIN_URL', default_httpbin_url)
+ unless url.end_with?('/')
+ url += '/'
+ end
+
+ url + suffix
+ end
+
+ def execute_httpbin(suffix, opts={})
+ opts = {url: httpbin(suffix)}.merge(opts)
+ RestClient::Request.execute(opts)
+ end
+
+ def execute_httpbin_json(suffix, opts={})
+ JSON.parse(execute_httpbin(suffix, opts))
+ end
+
+ describe '.execute' do
+ it 'sends a user agent' do
+ data = execute_httpbin_json('user-agent', method: :get)
+ expect(data['user-agent']).to match(/rest-client/)
+ end
+
+ it 'receives cookies on 302' do
+ expect {
+ execute_httpbin('cookies/set?foo=bar', method: :get, max_redirects: 0)
+ }.to raise_error(RestClient::Found) { |ex|
+ expect(ex.http_code).to eq 302
+ expect(ex.response.cookies['foo']).to eq 'bar'
+ }
+ end
+
+ it 'passes along cookies through 302' do
+ data = execute_httpbin_json('cookies/set?foo=bar', method: :get)
+ expect(data).to have_key('cookies')
+ expect(data['cookies']['foo']).to eq 'bar'
+ end
+
+ it 'handles quote wrapped cookies' do
+ expect {
+ execute_httpbin('cookies/set?foo=' + CGI.escape('"bar:baz"'),
+ method: :get, max_redirects: 0)
+ }.to raise_error(RestClient::Found) { |ex|
+ expect(ex.http_code).to eq 302
+ expect(ex.response.cookies['foo']).to eq '"bar:baz"'
+ }
+ end
+
+ it 'sends basic auth' do
+ user = 'user'
+ pass = 'pass'
+
+ data = execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: pass)
+ expect(data).to eq({'authenticated' => true, 'user' => user})
+
+ expect {
+ execute_httpbin_json("basic-auth/#{user}/#{pass}", method: :get, user: user, password: 'badpass')
+ }.to raise_error(RestClient::Unauthorized) { |ex|
+ expect(ex.http_code).to eq 401
+ }
+ end
+ end
+end
diff --git a/spec/integration/integration_spec.rb b/spec/integration/integration_spec.rb
index a0f32ce..963bad0 100644
--- a/spec/integration/integration_spec.rb
+++ b/spec/integration/integration_spec.rb
@@ -1,4 +1,6 @@
-require 'spec_helper'
+# -*- coding: utf-8 -*-
+require_relative '_lib'
+require 'base64'
describe RestClient do
@@ -6,15 +8,15 @@ describe RestClient do
body = 'abc'
stub_request(:get, "www.example.com").to_return(:body => body, :status => 200)
response = RestClient.get "www.example.com"
- response.code.should eq 200
- response.body.should eq body
+ expect(response.code).to eq 200
+ expect(response.body).to eq body
end
it "a simple request with gzipped content" do
stub_request(:get, "www.example.com").with(:headers => { 'Accept-Encoding' => 'gzip, deflate' }).to_return(:body => "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000", :status => 200, :headers => { 'Content-Encoding' => 'gzip' } )
response = RestClient.get "www.example.com"
- response.code.should eq 200
- response.body.should eq "i'm gziped\n"
+ expect(response.code).to eq 200
+ expect(response.body).to eq "i'm gziped\n"
end
it "a 404" do
@@ -24,12 +26,100 @@ describe RestClient do
RestClient.get "www.example.com"
raise
rescue RestClient::ResourceNotFound => e
- e.http_code.should eq 404
- e.response.code.should eq 404
- e.response.body.should eq body
- e.http_body.should eq body
+ expect(e.http_code).to eq 404
+ expect(e.response.code).to eq 404
+ expect(e.response.body).to eq body
+ expect(e.http_body).to eq body
end
end
+ describe 'charset parsing' do
+ it 'handles utf-8' do
+ body = "λ".force_encoding('ASCII-8BIT')
+ stub_request(:get, "www.example.com").to_return(
+ :body => body, :status => 200, :headers => {
+ 'Content-Type' => 'text/plain; charset=UTF-8'
+ })
+ response = RestClient.get "www.example.com"
+ expect(response.encoding).to eq Encoding::UTF_8
+ expect(response.valid_encoding?).to eq true
+ end
+
+ it 'handles windows-1252' do
+ body = "\xff".force_encoding('ASCII-8BIT')
+ stub_request(:get, "www.example.com").to_return(
+ :body => body, :status => 200, :headers => {
+ 'Content-Type' => 'text/plain; charset=windows-1252'
+ })
+ response = RestClient.get "www.example.com"
+ expect(response.encoding).to eq Encoding::WINDOWS_1252
+ expect(response.encode('utf-8')).to eq "ÿ"
+ expect(response.valid_encoding?).to eq true
+ end
+
+ it 'handles binary' do
+ body = "\xfe".force_encoding('ASCII-8BIT')
+ stub_request(:get, "www.example.com").to_return(
+ :body => body, :status => 200, :headers => {
+ 'Content-Type' => 'application/octet-stream; charset=binary'
+ })
+ response = RestClient.get "www.example.com"
+ expect(response.encoding).to eq Encoding::BINARY
+ expect {
+ response.encode('utf-8')
+ }.to raise_error(Encoding::UndefinedConversionError)
+ expect(response.valid_encoding?).to eq true
+ end
+
+ it 'handles euc-jp' do
+ body = "\xA4\xA2\xA4\xA4\xA4\xA6\xA4\xA8\xA4\xAA".
+ force_encoding(Encoding::BINARY)
+ body_utf8 = 'あいうえお'
+ expect(body_utf8.encoding).to eq Encoding::UTF_8
+
+ stub_request(:get, 'www.example.com').to_return(
+ :body => body, :status => 200, :headers => {
+ 'Content-Type' => 'text/plain; charset=EUC-JP'
+ })
+ response = RestClient.get 'www.example.com'
+ expect(response.encoding).to eq Encoding::EUC_JP
+ expect(response.valid_encoding?).to eq true
+ expect(response.length).to eq 5
+ expect(response.encode('utf-8')).to eq body_utf8
+ end
+
+ it 'defaults to the default encoding' do
+ stub_request(:get, 'www.example.com').to_return(
+ body: 'abc', status: 200, headers: {
+ 'Content-Type' => 'text/plain'
+ })
+ response = RestClient.get 'www.example.com'
+ # expect(response.encoding).to eq Encoding.default_external
+ expect(response.encoding).to eq Encoding::UTF_8
+ end
+
+ it 'handles invalid encoding' do
+ stub_request(:get, 'www.example.com').to_return(
+ body: 'abc', status: 200, headers: {
+ 'Content-Type' => 'text; charset=plain'
+ })
+
+ response = RestClient.get 'www.example.com'
+ # expect(response.encoding).to eq Encoding.default_external
+ expect(response.encoding).to eq Encoding::UTF_8
+ end
+
+ it 'leaves images as binary' do
+ gif = Base64.strict_decode64('R0lGODlhAQABAAAAADs=')
+
+ stub_request(:get, 'www.example.com').to_return(
+ body: gif, status: 200, headers: {
+ 'Content-Type' => 'image/gif'
+ })
+
+ response = RestClient.get 'www.example.com'
+ expect(response.encoding).to eq Encoding::BINARY
+ end
+ end
end
diff --git a/spec/integration/request_spec.rb b/spec/integration/request_spec.rb
index 5b0011d..99bfefa 100644
--- a/spec/integration/request_spec.rb
+++ b/spec/integration/request_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::Request do
before(:all) do
@@ -75,7 +75,7 @@ describe RestClient::Request do
},
)
expect {request.execute }.to_not raise_error
- ran_callback.should eq(true)
+ expect(ran_callback).to eq(true)
end
it "fails verification when the callback returns false",
@@ -101,4 +101,27 @@ describe RestClient::Request do
expect { request.execute }.to_not raise_error
end
end
+
+ describe "timeouts" do
+ it "raises OpenTimeout when it hits an open timeout" do
+ request = RestClient::Request.new(
+ :method => :get,
+ :url => 'http://www.mozilla.org',
+ :open_timeout => 1e-10,
+ )
+ expect { request.execute }.to(
+ raise_error(RestClient::Exceptions::OpenTimeout))
+ end
+
+ it "raises ReadTimeout when it hits a read timeout via :read_timeout" do
+ request = RestClient::Request.new(
+ :method => :get,
+ :url => 'https://www.mozilla.org',
+ :read_timeout => 1e-10,
+ )
+ expect { request.execute }.to(
+ raise_error(RestClient::Exceptions::ReadTimeout))
+ end
+ end
+
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 60cf27e..bca8e28 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -1,2 +1,29 @@
require 'webmock/rspec'
-require 'restclient'
+require 'rest-client'
+
+require_relative './helpers'
+
+# See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
+RSpec.configure do |config|
+ config.raise_errors_for_deprecations!
+
+ # 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'
+
+ # always run with ruby warnings enabled
+ # TODO: figure out why this is so obscenely noisy (rspec bug?)
+ # config.warnings = true
+
+ # add helpers
+ config.include Helpers, :include_helpers
+
+ config.mock_with :rspec do |mocks|
+ mocks.yield_receiver_to_any_instance_implementation_blocks = true
+ end
+end
+
+# always run with ruby warnings enabled (see above)
+$VERBOSE = true
diff --git a/spec/unit/_lib.rb b/spec/unit/_lib.rb
new file mode 100644
index 0000000..935238d
--- /dev/null
+++ b/spec/unit/_lib.rb
@@ -0,0 +1 @@
+require_relative '../spec_helper'
diff --git a/spec/unit/abstract_response_spec.rb b/spec/unit/abstract_response_spec.rb
index 7e259a1..0407165 100644
--- a/spec/unit/abstract_response_spec.rb
+++ b/spec/unit/abstract_response_spec.rb
@@ -1,6 +1,6 @@
-require 'spec_helper'
+require_relative '_lib'
-describe RestClient::AbstractResponse do
+describe RestClient::AbstractResponse, :include_helpers do
class MyAbstractResponse
@@ -8,9 +8,8 @@ describe RestClient::AbstractResponse do
attr_accessor :size
- def initialize net_http_res, args, request
+ def initialize net_http_res, request
@net_http_res = net_http_res
- @args = args
@request = request
end
@@ -18,71 +17,129 @@ describe RestClient::AbstractResponse do
before do
@net_http_res = double('net http response')
- @request = double('restclient request', :url => 'http://example.com')
- @response = MyAbstractResponse.new(@net_http_res, {}, @request)
+ @request = request_double(url: 'http://example.com', method: 'get')
+ @response = MyAbstractResponse.new(@net_http_res, @request)
end
it "fetches the numeric response code" do
- @net_http_res.should_receive(:code).and_return('200')
- @response.code.should eq 200
+ expect(@net_http_res).to receive(:code).and_return('200')
+ expect(@response.code).to eq 200
end
it "has a nice description" do
- @net_http_res.should_receive(:to_hash).and_return({'Content-Type' => ['application/pdf']})
- @net_http_res.should_receive(:code).and_return('200')
- @response.description.should eq "200 OK | application/pdf bytes\n"
+ expect(@net_http_res).to receive(:to_hash).and_return({'Content-Type' => ['application/pdf']})
+ expect(@net_http_res).to receive(:code).and_return('200')
+ expect(@response.description).to eq "200 OK | application/pdf bytes\n"
end
- it "beautifies the headers by turning the keys to symbols" do
- h = RestClient::AbstractResponse.beautify_headers('content-type' => [ 'x' ])
- h.keys.first.should eq :content_type
- end
+ describe '.beautify_headers' do
+ it "beautifies the headers by turning the keys to symbols" do
+ h = RestClient::AbstractResponse.beautify_headers('content-type' => [ 'x' ])
+ expect(h.keys.first).to eq :content_type
+ end
- it "beautifies the headers by turning the values to strings instead of one-element arrays" do
- h = RestClient::AbstractResponse.beautify_headers('x' => [ 'text/html' ] )
- h.values.first.should eq 'text/html'
+ it "beautifies the headers by turning the values to strings instead of one-element arrays" do
+ h = RestClient::AbstractResponse.beautify_headers('x' => [ 'text/html' ] )
+ expect(h.values.first).to eq 'text/html'
+ end
+
+ it 'joins multiple header values by comma' do
+ expect(RestClient::AbstractResponse.beautify_headers(
+ {'My-Header' => ['one', 'two']}
+ )).to eq({:my_header => 'one, two'})
+ end
+
+ it 'leaves set-cookie headers as array' do
+ expect(RestClient::AbstractResponse.beautify_headers(
+ {'Set-Cookie' => ['cookie1=foo', 'cookie2=bar']}
+ )).to eq({:set_cookie => ['cookie1=foo', 'cookie2=bar']})
+ end
end
it "fetches the headers" do
- @net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
- @response.headers.should eq({ :content_type => 'text/html' })
+ expect(@net_http_res).to receive(:to_hash).and_return('content-type' => [ 'text/html' ])
+ expect(@response.headers).to eq({ :content_type => 'text/html' })
end
it "extracts cookies from response headers" do
- @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
- @response.cookies.should eq({ 'session_id' => '1' })
+ expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+ expect(@response.cookies).to eq({ 'session_id' => '1' })
end
it "extract strange cookies" do
- @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/'])
- @response.headers.should eq({:set_cookie => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']})
- @response.cookies.should eq({ 'session_id' => 'ZJ/HQVH6YE+rVkTpn0zvTQ==' })
+ expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/'])
+ expect(@response.headers).to eq({:set_cookie => ['session_id=ZJ/HQVH6YE+rVkTpn0zvTQ==; path=/']})
+ expect(@response.cookies).to eq({ 'session_id' => 'ZJ/HQVH6YE+rVkTpn0zvTQ==' })
end
it "doesn't escape cookies" do
- @net_http_res.should_receive(:to_hash).and_return('set-cookie' => ['session_id=BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca; path=/'])
- @response.cookies.should eq({ 'session_id' => 'BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca' })
+ expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca; path=/'])
+ expect(@response.cookies).to eq({ 'session_id' => 'BAh7BzoNYXBwX25hbWUiEGFwcGxpY2F0aW9uOgpsb2dpbiIKYWRtaW4%3D%0A--08114ba654f17c04d20dcc5228ec672508f738ca' })
+ end
+
+ describe '.cookie_jar' do
+ it 'extracts cookies into cookie jar' do
+ expect(@net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+ expect(@response.cookie_jar).to be_a HTTP::CookieJar
+
+ cookie = @response.cookie_jar.cookies.first
+ expect(cookie.domain).to eq 'example.com'
+ expect(cookie.name).to eq 'session_id'
+ expect(cookie.value).to eq '1'
+ expect(cookie.path).to eq '/'
+ end
+
+ it 'handles cookies when URI scheme is implicit' do
+ net_http_res = double('net http response')
+ expect(net_http_res).to receive(:to_hash).and_return('set-cookie' => ['session_id=1; path=/'])
+ request = double(url: 'example.com', uri: URI.parse('http://example.com'),
+ method: 'get', cookie_jar: HTTP::CookieJar.new)
+ response = MyAbstractResponse.new(net_http_res, request)
+ expect(response.cookie_jar).to be_a HTTP::CookieJar
+
+ cookie = response.cookie_jar.cookies.first
+ expect(cookie.domain).to eq 'example.com'
+ expect(cookie.name).to eq 'session_id'
+ expect(cookie.value).to eq '1'
+ expect(cookie.path).to eq '/'
+ end
end
it "can access the net http result directly" do
- @response.net_http_res.should eq @net_http_res
+ expect(@response.net_http_res).to eq @net_http_res
end
describe "#return!" do
it "should return the response itself on 200-codes" do
- @net_http_res.should_receive(:code).and_return('200')
- @response.return!.should be_equal(@response)
+ expect(@net_http_res).to receive(:code).and_return('200')
+ expect(@response.return!).to be_equal(@response)
end
it "should raise RequestFailed on unknown codes" do
- @net_http_res.should_receive(:code).and_return('1000')
- lambda { @response.return! }.should raise_error RestClient::RequestFailed
+ expect(@net_http_res).to receive(:code).and_return('1000')
+ expect { @response.return! }.to raise_error RestClient::RequestFailed
end
it "should raise an error on a redirection after non-GET/HEAD requests" do
- @net_http_res.should_receive(:code).and_return('301')
- @response.args.merge(:method => :put)
- lambda { @response.return! }.should raise_error RestClient::RequestFailed
+ expect(@net_http_res).to receive(:code).and_return('301')
+ expect(@request).to receive(:method).and_return('put')
+ expect(@response).not_to receive(:follow_redirection)
+ expect { @response.return! }.to raise_error RestClient::RequestFailed
+ end
+
+ it "should follow 302 redirect" do
+ expect(@net_http_res).to receive(:code).and_return('302')
+ expect(@response).to receive(:check_max_redirects).and_return('fake-check')
+ expect(@response).to receive(:follow_redirection).and_return('fake-redirection')
+ expect(@response.return!).to eq 'fake-redirection'
+ end
+
+ it "should gracefully handle 302 redirect with no location header" do
+ @net_http_res = response_double(code: 302, location: nil)
+ @request = request_double()
+ @response = MyAbstractResponse.new(@net_http_res, @request)
+ expect(@response).to receive(:check_max_redirects).and_return('fake-check')
+ expect { @response.return! }.to raise_error RestClient::Found
end
end
end
diff --git a/spec/unit/exceptions_spec.rb b/spec/unit/exceptions_spec.rb
index 5d86373..8675584 100644
--- a/spec/unit/exceptions_spec.rb
+++ b/spec/unit/exceptions_spec.rb
@@ -1,32 +1,32 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::Exception do
it "returns a 'message' equal to the class name if the message is not set, because 'message' should not be nil" do
e = RestClient::Exception.new
- e.message.should eq "RestClient::Exception"
+ expect(e.message).to eq "RestClient::Exception"
end
it "returns the 'message' that was set" do
e = RestClient::Exception.new
message = "An explicitly set message"
e.message = message
- e.message.should eq message
+ expect(e.message).to eq message
end
it "sets the exception message to ErrorMessage" do
- RestClient::ResourceNotFound.new.message.should eq 'Resource Not Found'
+ expect(RestClient::ResourceNotFound.new.message).to eq 'Not Found'
end
it "contains exceptions in RestClient" do
- RestClient::Unauthorized.new.should be_a_kind_of(RestClient::Exception)
- RestClient::ServerBrokeConnection.new.should be_a_kind_of(RestClient::Exception)
+ expect(RestClient::Unauthorized.new).to be_a_kind_of(RestClient::Exception)
+ expect(RestClient::ServerBrokeConnection.new).to be_a_kind_of(RestClient::Exception)
end
end
describe RestClient::ServerBrokeConnection do
it "should have a default message of 'Server broke connection'" do
e = RestClient::ServerBrokeConnection.new
- e.message.should eq 'Server broke connection'
+ expect(e.message).to eq 'Server broke connection'
end
end
@@ -40,21 +40,21 @@ describe RestClient::RequestFailed do
begin
raise RestClient::RequestFailed, response
rescue RestClient::RequestFailed => e
- e.response.should eq response
+ expect(e.response).to eq response
end
end
it "http_code convenience method for fetching the code as an integer" do
- RestClient::RequestFailed.new(@response).http_code.should eq 502
+ expect(RestClient::RequestFailed.new(@response).http_code).to eq 502
end
it "http_body convenience method for fetching the body (decoding when necessary)" do
- RestClient::RequestFailed.new(@response).http_code.should eq 502
- RestClient::RequestFailed.new(@response).message.should eq 'HTTP status code 502'
+ expect(RestClient::RequestFailed.new(@response).http_code).to eq 502
+ expect(RestClient::RequestFailed.new(@response).message).to eq 'HTTP status code 502'
end
it "shows the status code in the message" do
- RestClient::RequestFailed.new(@response).to_s.should match(/502/)
+ expect(RestClient::RequestFailed.new(@response).to_s).to match(/502/)
end
end
@@ -64,32 +64,45 @@ describe RestClient::ResourceNotFound do
begin
raise RestClient::ResourceNotFound, response
rescue RestClient::ResourceNotFound => e
- e.response.should eq response
+ expect(e.response).to eq response
+ end
+ end
+
+ it 'stores the body on the response of the exception' do
+ body = "body"
+ stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
+ begin
+ RestClient.get "www.example.com"
+ raise
+ rescue RestClient::ResourceNotFound => e
+ expect(e.response.body).to eq body
end
end
end
describe "backwards compatibility" do
- it "alias RestClient::Request::Redirect to RestClient::Redirect" do
- RestClient::Request::Redirect.should eq RestClient::Redirect
+ it 'aliases RestClient::NotFound as ResourceNotFound' do
+ expect(RestClient::ResourceNotFound).to eq RestClient::NotFound
end
- it "alias RestClient::Request::Unauthorized to RestClient::Unauthorized" do
- RestClient::Request::Unauthorized.should eq RestClient::Unauthorized
+ it 'aliases old names for HTTP 413, 414, 416' do
+ expect(RestClient::RequestEntityTooLarge).to eq RestClient::PayloadTooLarge
+ expect(RestClient::RequestURITooLong).to eq RestClient::URITooLong
+ expect(RestClient::RequestedRangeNotSatisfiable).to eq RestClient::RangeNotSatisfiable
end
- it "alias RestClient::Request::RequestFailed to RestClient::RequestFailed" do
- RestClient::Request::RequestFailed.should eq RestClient::RequestFailed
+ it 'subclasses NotFound from RequestFailed, ExceptionWithResponse' do
+ expect(RestClient::NotFound).to be < RestClient::RequestFailed
+ expect(RestClient::NotFound).to be < RestClient::ExceptionWithResponse
end
- it "make the exception's response act like an Net::HTTPResponse" do
- body = "body"
- stub_request(:get, "www.example.com").to_return(:body => body, :status => 404)
- begin
- RestClient.get "www.example.com"
- raise
- rescue RestClient::ResourceNotFound => e
- e.response.body.should eq body
- end
+ it 'subclasses timeout from RestClient::RequestTimeout, RequestFailed, EWR' do
+ expect(RestClient::Exceptions::OpenTimeout).to be < RestClient::Exceptions::Timeout
+ expect(RestClient::Exceptions::ReadTimeout).to be < RestClient::Exceptions::Timeout
+
+ expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestTimeout
+ expect(RestClient::Exceptions::Timeout).to be < RestClient::RequestFailed
+ expect(RestClient::Exceptions::Timeout).to be < RestClient::ExceptionWithResponse
end
+
end
diff --git a/spec/unit/params_array_spec.rb b/spec/unit/params_array_spec.rb
new file mode 100644
index 0000000..926f088
--- /dev/null
+++ b/spec/unit/params_array_spec.rb
@@ -0,0 +1,36 @@
+require_relative '_lib'
+
+describe RestClient::ParamsArray do
+
+ describe '.new' do
+ it 'accepts various types of containers' do
+ as_array = [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]]
+ [
+ [[:foo, 123], [:foo, 456], [:bar, 789], [:empty, nil]],
+ [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}],
+ [{foo: 123}, {foo: 456}, {bar: 789}, {empty: nil}],
+ [{foo: 123}, [:foo, 456], {bar: 789}, {empty: nil}],
+ [{foo: 123}, [:foo, 456], {bar: 789}, [:empty]],
+ ].each do |input|
+ expect(RestClient::ParamsArray.new(input).to_a).to eq as_array
+ end
+
+ expect(RestClient::ParamsArray.new([]).to_a).to eq []
+ expect(RestClient::ParamsArray.new([]).empty?).to eq true
+ end
+
+ it 'rejects various invalid input' do
+ expect {
+ RestClient::ParamsArray.new([[]])
+ }.to raise_error(IndexError)
+
+ expect {
+ RestClient::ParamsArray.new([[1,2,3]])
+ }.to raise_error(ArgumentError)
+
+ expect {
+ RestClient::ParamsArray.new([1,2,3])
+ }.to raise_error(NoMethodError)
+ end
+ end
+end
diff --git a/spec/unit/payload_spec.rb b/spec/unit/payload_spec.rb
index 5fc7457..d34634a 100644
--- a/spec/unit/payload_spec.rb
+++ b/spec/unit/payload_spec.rb
@@ -1,60 +1,68 @@
# encoding: binary
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::Payload do
+ context "Base Payload" do
+ it "should reset stream after to_s" do
+ payload = RestClient::Payload::Base.new('foobar')
+ expect(payload.to_s).to eq 'foobar'
+ expect(payload.to_s).to eq 'foobar'
+ end
+ end
+
context "A regular Payload" do
it "should use standard enctype as default content-type" do
- RestClient::Payload::UrlEncoded.new({}).headers['Content-Type'].
- should eq 'application/x-www-form-urlencoded'
+ expect(RestClient::Payload::UrlEncoded.new({}).headers['Content-Type']).
+ to eq 'application/x-www-form-urlencoded'
end
it "should form properly encoded params" do
- RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s.
- should eq "foo=bar"
- ["foo=bar&baz=qux", "baz=qux&foo=bar"].should include(
+ expect(RestClient::Payload::UrlEncoded.new({:foo => 'bar'}).to_s).
+ to eq "foo=bar"
+ expect(["foo=bar&baz=qux", "baz=qux&foo=bar"]).to include(
RestClient::Payload::UrlEncoded.new({:foo => 'bar', :baz => 'qux'}).to_s)
end
it "should escape parameters" do
- RestClient::Payload::UrlEncoded.new({'foo ' => 'bar'}).to_s.
- should eq "foo%20=bar"
+ expect(RestClient::Payload::UrlEncoded.new({'foo + bar' => 'baz'}).to_s).
+ to eq "foo+%2B+bar=baz"
end
it "should properly handle hashes as parameter" do
- RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz'}}).to_s.
- should eq "foo[bar]=baz"
- RestClient::Payload::UrlEncoded.new({:foo => {:bar => {:baz => 'qux'}}}).to_s.
- should eq "foo[bar][baz]=qux"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz'}}).to_s).
+ to eq "foo[bar]=baz"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => {:baz => 'qux'}}}).to_s).
+ to eq "foo[bar][baz]=qux"
end
it "should handle many attributes inside a hash" do
parameters = RestClient::Payload::UrlEncoded.new({:foo => {:bar => 'baz', :baz => 'qux'}}).to_s
- parameters.should include("foo[bar]=baz", "foo[baz]=qux")
+ expect(parameters).to eq 'foo[bar]=baz&foo[baz]=qux'
end
- it "should handle attributes inside a an array inside an hash" do
+ it "should handle attributes inside an array inside an hash" do
parameters = RestClient::Payload::UrlEncoded.new({"foo" => [{"bar" => 'baz'}, {"bar" => 'qux'}]}).to_s
- parameters.should include("foo[bar]=baz", "foo[bar]=qux")
+ expect(parameters).to eq 'foo[][bar]=baz&foo[][bar]=qux'
end
- it "should handle attributes inside a an array inside an array inside an hash" do
- parameters = RestClient::Payload::UrlEncoded.new({"foo" => [[{"bar" => 'baz'}, {"bar" => 'qux'}]]}).to_s
- parameters.should include("foo[bar]=baz", "foo[bar]=qux")
+ it "should handle arrays inside a hash inside a hash" do
+ parameters = RestClient::Payload::UrlEncoded.new({"foo" => {'even' => [0, 2], 'odd' => [1, 3]}}).to_s
+ expect(parameters).to eq 'foo[even][]=0&foo[even][]=2&foo[odd][]=1&foo[odd][]=3'
end
it "should form properly use symbols as parameters" do
- RestClient::Payload::UrlEncoded.new({:foo => :bar}).to_s.
- should eq "foo=bar"
- RestClient::Payload::UrlEncoded.new({:foo => {:bar => :baz}}).to_s.
- should eq "foo[bar]=baz"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => :bar}).to_s).
+ to eq "foo=bar"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => {:bar => :baz}}).to_s).
+ to eq "foo[bar]=baz"
end
it "should properly handle arrays as repeated parameters" do
- RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s.
- should eq "foo[]=bar"
- RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s.
- should eq "foo[]=bar&foo[]=baz"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar']}).to_s).
+ to eq "foo[]=bar"
+ expect(RestClient::Payload::UrlEncoded.new({:foo => ['bar', 'baz']}).to_s).
+ to eq "foo[]=bar&foo[]=baz"
end
it 'should not close if stream already closed' do
@@ -67,8 +75,8 @@ describe RestClient::Payload do
context "A multipart Payload" do
it "should use standard enctype as default content-type" do
m = RestClient::Payload::Multipart.new({})
- m.stub(:boundary).and_return(123)
- m.headers['Content-Type'].should eq 'multipart/form-data; boundary=123'
+ allow(m).to receive(:boundary).and_return(123)
+ expect(m.headers['Content-Type']).to eq 'multipart/form-data; boundary=123'
end
it 'should not error on close if stream already closed' do
@@ -78,7 +86,7 @@ describe RestClient::Payload do
it "should form properly separated multipart data" do
m = RestClient::Payload::Multipart.new([[:bar, "baz"], [:foo, "bar"]])
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="bar"\r
\r
@@ -93,7 +101,7 @@ bar\r
it "should not escape parameters names" do
m = RestClient::Payload::Multipart.new([["bar ", "baz"]])
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="bar "\r
\r
@@ -105,7 +113,7 @@ baz\r
it "should form properly separated multipart data" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
m = RestClient::Payload::Multipart.new({:foo => f})
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="foo"; filename="master_shake.jpg"\r
Content-Type: image/jpeg\r
@@ -118,7 +126,7 @@ Content-Type: image/jpeg\r
it "should ignore the name attribute when it's not set" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
m = RestClient::Payload::Multipart.new({nil => f})
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; filename="master_shake.jpg"\r
Content-Type: image/jpeg\r
@@ -133,7 +141,7 @@ Content-Type: image/jpeg\r
f.instance_eval "def content_type; 'text/plain'; end"
f.instance_eval "def original_filename; 'foo.txt'; end"
m = RestClient::Payload::Multipart.new({:foo => f})
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="foo"; filename="foo.txt"\r
Content-Type: text/plain\r
@@ -145,7 +153,7 @@ Content-Type: text/plain\r
it "should handle hash in hash parameters" do
m = RestClient::Payload::Multipart.new({:bar => {:baz => "foo"}})
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="bar[baz]"\r
\r
@@ -157,7 +165,7 @@ foo\r
f.instance_eval "def content_type; 'text/plain'; end"
f.instance_eval "def original_filename; 'foo.txt'; end"
m = RestClient::Payload::Multipart.new({:foo => {:bar => f}})
- m.to_s.should eq <<-EOS
+ expect(m.to_s).to eq <<-EOS
--#{m.boundary}\r
Content-Disposition: form-data; name="foo[bar]"; filename="foo.txt"\r
Content-Type: text/plain\r
@@ -167,29 +175,36 @@ Content-Type: text/plain\r
EOS
end
+ it 'should correctly format hex boundary' do
+ allow(SecureRandom).to receive(:base64).with(12).and_return('TGs89+ttw/xna6TV')
+ f = File.new(File.dirname(__FILE__) + '/master_shake.jpg')
+ m = RestClient::Payload::Multipart.new({:foo => f})
+ expect(m.boundary).to eq('-' * 4 + 'RubyFormBoundary' + 'TGs89AttwBxna6TV')
+ end
+
end
context "streamed payloads" do
it "should properly determine the size of file payloads" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
payload = RestClient::Payload.generate(f)
- payload.size.should eq 76_988
- payload.length.should eq 76_988
+ expect(payload.size).to eq 76_988
+ expect(payload.length).to eq 76_988
end
it "should properly determine the size of other kinds of streaming payloads" do
s = StringIO.new 'foo'
payload = RestClient::Payload.generate(s)
- payload.size.should eq 3
- payload.length.should eq 3
+ expect(payload.size).to eq 3
+ expect(payload.length).to eq 3
begin
f = Tempfile.new "rest-client"
f.write 'foo bar'
payload = RestClient::Payload.generate(f)
- payload.size.should eq 7
- payload.length.should eq 7
+ expect(payload.size).to eq 7
+ expect(payload.length).to eq 7
ensure
f.close
end
@@ -198,48 +213,51 @@ Content-Type: text/plain\r
context "Payload generation" do
it "should recognize standard urlencoded params" do
- RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded)
+ expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded)
end
it "should recognize multipart params" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
- RestClient::Payload.generate({"foo" => f}).should be_kind_of(RestClient::Payload::Multipart)
+ expect(RestClient::Payload.generate({"foo" => f})).to be_kind_of(RestClient::Payload::Multipart)
end
it "should be multipart if forced" do
- RestClient::Payload.generate({"foo" => "bar", :multipart => true}).should be_kind_of(RestClient::Payload::Multipart)
+ expect(RestClient::Payload.generate({"foo" => "bar", :multipart => true})).to be_kind_of(RestClient::Payload::Multipart)
end
+ it "should handle deeply nested multipart" do
+ f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
+ params = {foo: RestClient::ParamsArray.new({nested: f})}
+ expect(RestClient::Payload.generate(params)).to be_kind_of(RestClient::Payload::Multipart)
+ end
+
+
it "should return data if no of the above" do
- RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base)
+ expect(RestClient::Payload.generate("data")).to be_kind_of(RestClient::Payload::Base)
end
it "should recognize nested multipart payloads in hashes" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
- RestClient::Payload.generate({"foo" => {"file" => f}}).should be_kind_of(RestClient::Payload::Multipart)
+ expect(RestClient::Payload.generate({"foo" => {"file" => f}})).to be_kind_of(RestClient::Payload::Multipart)
end
it "should recognize nested multipart payloads in arrays" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
- RestClient::Payload.generate({"foo" => [f]}).should be_kind_of(RestClient::Payload::Multipart)
+ expect(RestClient::Payload.generate({"foo" => [f]})).to be_kind_of(RestClient::Payload::Multipart)
end
it "should recognize file payloads that can be streamed" do
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
- RestClient::Payload.generate(f).should be_kind_of(RestClient::Payload::Streamed)
+ expect(RestClient::Payload.generate(f)).to be_kind_of(RestClient::Payload::Streamed)
end
it "should recognize other payloads that can be streamed" do
- RestClient::Payload.generate(StringIO.new('foo')).should be_kind_of(RestClient::Payload::Streamed)
+ expect(RestClient::Payload.generate(StringIO.new('foo'))).to be_kind_of(RestClient::Payload::Streamed)
end
# hashery gem introduces Hash#read convenience method. Existence of #read method used to determine of content is streameable :/
it "shouldn't treat hashes as streameable" do
- RestClient::Payload.generate({"foo" => 'bar'}).should be_kind_of(RestClient::Payload::UrlEncoded)
+ expect(RestClient::Payload.generate({"foo" => 'bar'})).to be_kind_of(RestClient::Payload::UrlEncoded)
end
end
-
- class HashMapForTesting < Hash
- alias :read :[]
- end
end
diff --git a/spec/unit/raw_response_spec.rb b/spec/unit/raw_response_spec.rb
index 0bc5fa5..13f859d 100644
--- a/spec/unit/raw_response_spec.rb
+++ b/spec/unit/raw_response_spec.rb
@@ -1,18 +1,18 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::RawResponse do
before do
@tf = double("Tempfile", :read => "the answer is 42", :open => true)
@net_http_res = double('net http response')
@request = double('http request')
- @response = RestClient::RawResponse.new(@tf, @net_http_res, {}, @request)
+ @response = RestClient::RawResponse.new(@tf, @net_http_res, @request)
end
it "behaves like string" do
- @response.to_s.should eq 'the answer is 42'
+ expect(@response.to_s).to eq 'the answer is 42'
end
it "exposes a Tempfile" do
- @response.file.should eq @tf
+ expect(@response.file).to eq @tf
end
end
diff --git a/spec/unit/request2_spec.rb b/spec/unit/request2_spec.rb
index 5088ee8..71e5d6e 100644
--- a/spec/unit/request2_spec.rb
+++ b/spec/unit/request2_spec.rb
@@ -1,13 +1,35 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::Request do
- it "manage params for get requests" do
- stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}).body.should eq 'foo'
+ context 'params for GET requests' do
+ it "manage params for get requests" do
+ stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}).body).to eq 'foo'
+
+ stub_request(:get, 'http://some/resource').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => :a}).body).to eq 'foo'
+ end
+
+ it 'adds GET params when params are present in URL' do
+ stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+ expect(RestClient::Request.execute(:url => 'http://some/resource?a=b', :method => :get, :headers => {:foo => :bar, :params => {:c => 'd'}}).body).to eq 'foo'
+ end
+
+ it 'encodes nested GET params' do
+ stub_request(:get, 'http://some/resource?a[foo][]=1&a[foo][]=2&a[bar]&b=foo+bar&math=2+%2B+2+%3D%3D+4').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
+ expect(RestClient::Request.execute(url: 'http://some/resource', method: :get, headers: {
+ params: {
+ a: {
+ foo: [1,2],
+ bar: nil,
+ },
+ b: 'foo bar',
+ math: '2 + 2 == 4',
+ }
+ }).body).to eq 'foo'
+ end
- stub_request(:get, 'http://some/resource').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar', 'params' => 'a'}).to_return(:body => 'foo', :status => 200)
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => :a}).body.should eq 'foo'
end
it "can use a block to process response" do
@@ -15,18 +37,18 @@ describe RestClient::Request do
block = proc do |http_response|
response_value = http_response.body
end
- stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
+ stub_request(:get, 'http://some/resource?a=b&c=d').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate', 'Foo'=>'bar'}).to_return(:body => 'foo', :status => 200)
RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :headers => {:foo => :bar, :params => {:a => :b, 'c' => 'd'}}, :block_response => block)
- response_value.should eq "foo"
+ expect(response_value).to eq "foo"
end
it 'closes payload if not nil' do
test_file = File.new(File.join( File.dirname(File.expand_path(__FILE__)), 'master_shake.jpg'))
- stub_request(:post, 'http://some/resource').with(:headers => {'Accept'=>'*/*; q=0.5, application/xml', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
+ stub_request(:post, 'http://some/resource').with(:headers => {'Accept'=>'*/*', 'Accept-Encoding'=>'gzip, deflate'}).to_return(:body => 'foo', :status => 200)
RestClient::Request.execute(:url => 'http://some/resource', :method => :post, :payload => {:file => test_file})
- test_file.closed?.should be_true
+ expect(test_file.closed?).to be true
end
end
diff --git a/spec/unit/request_spec.rb b/spec/unit/request_spec.rb
index 7c00873..bdffd66 100644
--- a/spec/unit/request_spec.rb
+++ b/spec/unit/request_spec.rb
@@ -1,120 +1,230 @@
-require 'spec_helper'
+require_relative './_lib'
-describe RestClient::Request do
+describe RestClient::Request, :include_helpers do
before do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
@uri = double("uri")
- @uri.stub(:request_uri).and_return('/resource')
- @uri.stub(:host).and_return('some')
- @uri.stub(:port).and_return(80)
+ allow(@uri).to receive(:request_uri).and_return('/resource')
+ allow(@uri).to receive(:hostname).and_return('some')
+ allow(@uri).to receive(:port).and_return(80)
@net = double("net::http base")
@http = double("net::http connection")
- Net::HTTP.stub(:new).and_return(@net)
- @net.stub(:start).and_yield(@http)
- @net.stub(:use_ssl=)
- @net.stub(:verify_mode=)
- @net.stub(:verify_callback=)
+
+ allow(Net::HTTP).to receive(:new).and_return(@net)
+
+ allow(@net).to receive(:start).and_yield(@http)
+ allow(@net).to receive(:use_ssl=)
+ allow(@net).to receive(:verify_mode=)
+ allow(@net).to receive(:verify_callback=)
allow(@net).to receive(:ciphers=)
allow(@net).to receive(:cert_store=)
RestClient.log = nil
end
- it "accept */* mimetype, preferring xml" do
- @request.default_headers[:accept].should eq '*/*; q=0.5, application/xml'
+ it "accept */* mimetype" do
+ expect(@request.default_headers[:accept]).to eq '*/*'
end
describe "compression" do
it "decodes an uncompressed result body by passing it straight through" do
- RestClient::Request.decode(nil, 'xyz').should eq 'xyz'
+ expect(RestClient::Request.decode(nil, 'xyz')).to eq 'xyz'
end
it "doesn't fail for nil bodies" do
- RestClient::Request.decode('gzip', nil).should be_nil
+ expect(RestClient::Request.decode('gzip', nil)).to be_nil
end
it "decodes a gzip body" do
- RestClient::Request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000").should eq "i'm gziped\n"
+ expect(RestClient::Request.decode('gzip', "\037\213\b\b\006'\252H\000\003t\000\313T\317UH\257\312,HM\341\002\000G\242(\r\v\000\000\000")).to eq "i'm gziped\n"
end
it "ingores gzip for empty bodies" do
- RestClient::Request.decode('gzip', '').should be_empty
+ expect(RestClient::Request.decode('gzip', '')).to be_empty
end
it "decodes a deflated body" do
- RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should eq "some deflated text"
+ expect(RestClient::Request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363")).to eq "some deflated text"
end
end
it "processes a successful result" do
- res = double("result")
- res.stub(:code).and_return("200")
- res.stub(:body).and_return('body')
- res.stub(:[]).with('content-encoding').and_return(nil)
- @request.process_result(res).body.should eq 'body'
- @request.process_result(res).to_s.should eq 'body'
+ res = response_double
+ allow(res).to receive(:code).and_return("200")
+ allow(res).to receive(:body).and_return('body')
+ allow(res).to receive(:[]).with('content-encoding').and_return(nil)
+ expect(@request.send(:process_result, res).body).to eq 'body'
+ expect(@request.send(:process_result, res).to_s).to eq 'body'
end
it "doesn't classify successful requests as failed" do
203.upto(207) do |code|
- res = double("result")
- res.stub(:code).and_return(code.to_s)
- res.stub(:body).and_return("")
- res.stub(:[]).with('content-encoding').and_return(nil)
- @request.process_result(res).should be_empty
+ res = response_double
+ allow(res).to receive(:code).and_return(code.to_s)
+ allow(res).to receive(:body).and_return("")
+ allow(res).to receive(:[]).with('content-encoding').and_return(nil)
+ expect(@request.send(:process_result, res)).to be_empty
end
end
- it "parses a url into a URI object" do
- URI.should_receive(:parse).with('http://example.com/resource')
- @request.parse_url('http://example.com/resource')
- end
+ describe '.normalize_url' do
+ it "adds http:// to the front of resources specified in the syntax example.com/resource" do
+ expect(@request.normalize_url('example.com/resource')).to eq 'http://example.com/resource'
+ end
+
+ it 'adds http:// to resources containing a colon' do
+ expect(@request.normalize_url('example.com:1234')).to eq 'http://example.com:1234'
+ end
+
+ it 'does not add http:// to the front of https resources' do
+ expect(@request.normalize_url('https://example.com/resource')).to eq 'https://example.com/resource'
+ end
+
+ it 'does not add http:// to the front of capital HTTP resources' do
+ expect(@request.normalize_url('HTTP://example.com/resource')).to eq 'HTTP://example.com/resource'
+ end
+
+ it 'does not add http:// to the front of capital HTTPS resources' do
+ expect(@request.normalize_url('HTTPS://example.com/resource')).to eq 'HTTPS://example.com/resource'
+ end
- it "adds http:// to the front of resources specified in the syntax example.com/resource" do
- URI.should_receive(:parse).with('http://example.com/resource')
- @request.parse_url('example.com/resource')
+ it 'raises with invalid URI' do
+ expect {
+ RestClient::Request.new(method: :get, url: 'http://a@b:c')
+ }.to raise_error(URI::InvalidURIError)
+ expect {
+ RestClient::Request.new(method: :get, url: 'http://::')
+ }.to raise_error(URI::InvalidURIError)
+ end
end
describe "user - password" do
it "extracts the username and password when parsing http://user:password@example.com/" do
- URI.stub(:parse).and_return(double('uri', :user => 'joe', :password => 'pass1'))
- @request.parse_url_with_auth('http://joe:pass1@example.com/resource')
- @request.user.should eq 'joe'
- @request.password.should eq 'pass1'
+ @request.send(:parse_url_with_auth!, 'http://joe:pass1@example.com/resource')
+ expect(@request.user).to eq 'joe'
+ expect(@request.password).to eq 'pass1'
end
it "extracts with escaping the username and password when parsing http://user:password@example.com/" do
- URI.stub(:parse).and_return(double('uri', :user => 'joe%20', :password => 'pass1'))
- @request.parse_url_with_auth('http://joe%20:pass1@example.com/resource')
- @request.user.should eq 'joe '
- @request.password.should eq 'pass1'
+ @request.send(:parse_url_with_auth!, 'http://joe%20:pass1@example.com/resource')
+ expect(@request.user).to eq 'joe '
+ expect(@request.password).to eq 'pass1'
end
it "doesn't overwrite user and password (which may have already been set by the Resource constructor) if there is no user/password in the url" do
- URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil))
- @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :user => 'beth', :password => 'pass2')
- @request.parse_url_with_auth('http://example.com/resource')
- @request.user.should eq 'beth'
- @request.password.should eq 'pass2'
+ request = RestClient::Request.new(method: :get, url: 'http://example.com/resource', user: 'beth', password: 'pass2')
+ expect(request.user).to eq 'beth'
+ expect(request.password).to eq 'pass2'
+ end
+
+ it 'uses the username and password from the URL' do
+ request = RestClient::Request.new(method: :get, url: 'http://person:secret@example.com/resource')
+ expect(request.user).to eq 'person'
+ expect(request.password).to eq 'secret'
+ end
+
+ it 'overrides URL user/pass with explicit options' do
+ request = RestClient::Request.new(method: :get, url: 'http://person:secret@example.com/resource', user: 'beth', password: 'pass2')
+ expect(request.user).to eq 'beth'
+ expect(request.password).to eq 'pass2'
end
end
it "correctly formats cookies provided to the constructor" do
- URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil))
- @request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1', :user_id => "someone" })
- @request.should_receive(:default_headers).and_return({'Foo' => 'bar'})
- @request.make_headers({}).should eq({ 'Foo' => 'bar', 'Cookie' => 'session_id=1; user_id=someone'})
+ cookies_arr = [
+ HTTP::Cookie.new('session_id', '1', domain: 'example.com', path: '/'),
+ HTTP::Cookie.new('user_id', 'someone', domain: 'example.com', path: '/'),
+ ]
+
+ jar = HTTP::CookieJar.new
+ cookies_arr.each {|c| jar << c }
+
+ # test Hash, HTTP::CookieJar, and Array<HTTP::Cookie> modes
+ [
+ {session_id: '1', user_id: 'someone'},
+ jar,
+ cookies_arr
+ ].each do |cookies|
+ [true, false].each do |in_headers|
+ if in_headers
+ opts = {headers: {cookies: cookies}}
+ else
+ opts = {cookies: cookies}
+ end
+
+ request = RestClient::Request.new(method: :get, url: 'example.com', **opts)
+ expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+ expect(request.make_headers({})).to eq({'Foo' => 'bar', 'Cookie' => 'session_id=1; user_id=someone'})
+ expect(request.make_cookie_header).to eq 'session_id=1; user_id=someone'
+ expect(request.cookies).to eq({'session_id' => '1', 'user_id' => 'someone'})
+ expect(request.cookie_jar.cookies.length).to eq 2
+ expect(request.cookie_jar.object_id).not_to eq jar.object_id # make sure we dup it
+ end
+ end
+
+ # test with no cookies
+ request = RestClient::Request.new(method: :get, url: 'example.com')
+ expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+ expect(request.make_headers({})).to eq({'Foo' => 'bar'})
+ expect(request.make_cookie_header).to be_nil
+ expect(request.cookies).to eq({})
+ expect(request.cookie_jar.cookies.length).to eq 0
+ end
+
+ it 'strips out cookies set for a different domain name' do
+ jar = HTTP::CookieJar.new
+ jar << HTTP::Cookie.new('session_id', '1', domain: 'other.example.com', path: '/')
+ jar << HTTP::Cookie.new('user_id', 'someone', domain: 'other.example.com', path: '/')
+
+ request = RestClient::Request.new(method: :get, url: 'www.example.com', cookies: jar)
+ expect(request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+ expect(request.make_headers({})).to eq({'Foo' => 'bar'})
+ expect(request.make_cookie_header).to eq nil
+ expect(request.cookies).to eq({})
+ expect(request.cookie_jar.cookies.length).to eq 2
+ end
+
+ it 'assumes default domain and path for cookies set by hash' do
+ request = RestClient::Request.new(method: :get, url: 'www.example.com', cookies: {'session_id' => '1'})
+ expect(request.cookie_jar.cookies.length).to eq 1
+
+ cookie = request.cookie_jar.cookies.first
+ expect(cookie).to be_a(HTTP::Cookie)
+ expect(cookie.domain).to eq('www.example.com')
+ expect(cookie.for_domain?).to be_truthy
+ expect(cookie.path).to eq('/')
+ end
+
+ it 'rejects or warns with contradictory cookie options' do
+ # same opt in two different places
+ expect {
+ RestClient::Request.new(method: :get, url: 'example.com',
+ cookies: {bar: '456'},
+ headers: {cookies: {foo: '123'}})
+ }.to raise_error(ArgumentError, /Cannot pass :cookies in Request.*headers/)
+
+ # :cookies opt and Cookie header
+ [
+ {cookies: {foo: '123'}, headers: {cookie: 'foo'}},
+ {cookies: {foo: '123'}, headers: {'Cookie' => 'foo'}},
+ {headers: {cookies: {foo: '123'}, cookie: 'foo'}},
+ {headers: {cookies: {foo: '123'}, 'Cookie' => 'foo'}},
+ ].each do |opts|
+ expect(fake_stderr {
+ RestClient::Request.new(method: :get, url: 'example.com', **opts)
+ }).to match(/warning: overriding "Cookie" header with :cookies option/)
+ end
end
it "does not escape or unescape cookies" do
cookie = 'Foo%20:Bar%0A~'
@request = RestClient::Request.new(:method => 'get', :url => 'example.com',
:cookies => {:test => cookie})
- @request.should_receive(:default_headers).and_return({'Foo' => 'bar'})
- @request.make_headers({}).should eq({
+ expect(@request).to receive(:default_headers).and_return({'Foo' => 'bar'})
+ expect(@request.make_headers({})).to eq({
'Foo' => 'bar',
'Cookie' => "test=#{cookie}"
})
@@ -124,244 +234,408 @@ describe RestClient::Request do
# Cookie validity is something of a mess, but we should reject the worst of
# the RFC 6265 (4.1.1) prohibited characters such as control characters.
- ['', 'foo=bar', 'foo;bar', "foo\nbar"].each do |cookie_name|
- lambda {
+ ['foo=bar', 'foo;bar', "foo\nbar"].each do |cookie_name|
+ expect {
RestClient::Request.new(:method => 'get', :url => 'example.com',
:cookies => {cookie_name => 'value'})
- }.should raise_error(ArgumentError, /\AInvalid cookie name/)
+ }.to raise_error(ArgumentError, /\AInvalid cookie name/i)
end
+
+ cookie_name = ''
+ expect {
+ RestClient::Request.new(:method => 'get', :url => 'example.com',
+ :cookies => {cookie_name => 'value'})
+ }.to raise_error(ArgumentError, /cookie name cannot be empty/i)
end
it "rejects cookie values containing invalid characters" do
# Cookie validity is something of a mess, but we should reject the worst of
# the RFC 6265 (4.1.1) prohibited characters such as control characters.
- ['foo,bar', 'foo;bar', "foo\nbar"].each do |cookie_value|
- lambda {
+ ["foo\tbar", "foo\nbar"].each do |cookie_value|
+ expect {
RestClient::Request.new(:method => 'get', :url => 'example.com',
:cookies => {'test' => cookie_value})
- }.should raise_error(ArgumentError, /\AInvalid cookie value/)
+ }.to raise_error(ArgumentError, /\AInvalid cookie value/i)
end
end
+ it 'warns when overriding existing headers via payload' do
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com',
+ payload: {'foo' => 1}, headers: {content_type: :json})
+ }).to match(/warning: Overriding "Content-Type" header/i)
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com',
+ payload: {'foo' => 1}, headers: {'Content-Type' => 'application/json'})
+ }).to match(/warning: Overriding "Content-Type" header/i)
+
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com',
+ payload: '123456', headers: {content_length: '20'})
+ }).to match(/warning: Overriding "Content-Length" header/i)
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com',
+ payload: '123456', headers: {'Content-Length' => '20'})
+ }).to match(/warning: Overriding "Content-Length" header/i)
+ end
+
+ it "does not warn when overriding user header with header derived from payload if those header values were identical" do
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com',
+ payload: {'foo' => '123456'}, headers: { 'Content-Type' => 'application/x-www-form-urlencoded' })
+ }).not_to match(/warning: Overriding "Content-Type" header/i)
+ end
+
+ it 'does not warn for a normal looking payload' do
+ expect(fake_stderr {
+ RestClient::Request.new(method: :post, url: 'example.com', payload: 'payload')
+ RestClient::Request.new(method: :post, url: 'example.com', payload: 'payload', headers: {content_type: :json})
+ RestClient::Request.new(method: :post, url: 'example.com', payload: {'foo' => 'bar'})
+ }).to eq ''
+ end
+
it "uses netrc credentials" do
- URI.stub(:parse).and_return(double('uri', :user => nil, :password => nil, :host => 'example.com'))
- Netrc.stub(:read).and_return('example.com' => ['a', 'b'])
- @request.parse_url_with_auth('http://example.com/resource')
- @request.user.should eq 'a'
- @request.password.should eq 'b'
+ expect(Netrc).to receive(:read).and_return('example.com' => ['a', 'b'])
+ request = RestClient::Request.new(:method => :put, :url => 'http://example.com/', :payload => 'payload')
+ expect(request.user).to eq 'a'
+ expect(request.password).to eq 'b'
end
it "uses credentials in the url in preference to netrc" do
- URI.stub(:parse).and_return(double('uri', :user => 'joe%20', :password => 'pass1', :host => 'example.com'))
- Netrc.stub(:read).and_return('example.com' => ['a', 'b'])
- @request.parse_url_with_auth('http://joe%20:pass1@example.com/resource')
- @request.user.should eq 'joe '
- @request.password.should eq 'pass1'
+ allow(Netrc).to receive(:read).and_return('example.com' => ['a', 'b'])
+ request = RestClient::Request.new(:method => :put, :url => 'http://joe%20:pass1@example.com/', :payload => 'payload')
+ expect(request.user).to eq 'joe '
+ expect(request.password).to eq 'pass1'
end
it "determines the Net::HTTP class to instantiate by the method name" do
- @request.net_http_request_class(:put).should eq Net::HTTP::Put
+ expect(@request.net_http_request_class(:put)).to eq Net::HTTP::Put
end
describe "user headers" do
it "merges user headers with the default headers" do
- @request.should_receive(:default_headers).and_return({ :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' })
+ expect(@request).to receive(:default_headers).and_return({ :accept => '*/*', :accept_encoding => 'gzip, deflate' })
headers = @request.make_headers("Accept" => "application/json", :accept_encoding => 'gzip')
- headers.should have_key "Accept-Encoding"
- headers["Accept-Encoding"].should eq "gzip"
- headers.should have_key "Accept"
- headers["Accept"].should eq "application/json"
+ expect(headers).to have_key "Accept-Encoding"
+ expect(headers["Accept-Encoding"]).to eq "gzip"
+ expect(headers).to have_key "Accept"
+ expect(headers["Accept"]).to eq "application/json"
end
it "prefers the user header when the same header exists in the defaults" do
- @request.should_receive(:default_headers).and_return({ '1' => '2' })
+ expect(@request).to receive(:default_headers).and_return({ '1' => '2' })
headers = @request.make_headers('1' => '3')
- headers.should have_key('1')
- headers['1'].should eq '3'
+ expect(headers).to have_key('1')
+ expect(headers['1']).to eq '3'
end
it "converts user headers to string before calling CGI::unescape which fails on non string values" do
- @request.should_receive(:default_headers).and_return({ '1' => '2' })
+ expect(@request).to receive(:default_headers).and_return({ '1' => '2' })
headers = @request.make_headers('1' => 3)
- headers.should have_key('1')
- headers['1'].should eq '3'
+ expect(headers).to have_key('1')
+ expect(headers['1']).to eq '3'
end
end
describe "header symbols" do
it "converts header symbols from :content_type to 'Content-Type'" do
- @request.should_receive(:default_headers).and_return({})
+ expect(@request).to receive(:default_headers).and_return({})
headers = @request.make_headers(:content_type => 'abc')
- headers.should have_key('Content-Type')
- headers['Content-Type'].should eq 'abc'
+ expect(headers).to have_key('Content-Type')
+ expect(headers['Content-Type']).to eq 'abc'
end
it "converts content-type from extension to real content-type" do
- @request.should_receive(:default_headers).and_return({})
+ expect(@request).to receive(:default_headers).and_return({})
headers = @request.make_headers(:content_type => 'json')
- headers.should have_key('Content-Type')
- headers['Content-Type'].should eq 'application/json'
+ expect(headers).to have_key('Content-Type')
+ expect(headers['Content-Type']).to eq 'application/json'
end
it "converts accept from extension(s) to real content-type(s)" do
- @request.should_receive(:default_headers).and_return({})
+ expect(@request).to receive(:default_headers).and_return({})
headers = @request.make_headers(:accept => 'json, mp3')
- headers.should have_key('Accept')
- headers['Accept'].should eq 'application/json, audio/mpeg'
+ expect(headers).to have_key('Accept')
+ expect(headers['Accept']).to eq 'application/json, audio/mpeg'
- @request.should_receive(:default_headers).and_return({})
+ expect(@request).to receive(:default_headers).and_return({})
headers = @request.make_headers(:accept => :json)
- headers.should have_key('Accept')
- headers['Accept'].should eq 'application/json'
+ expect(headers).to have_key('Accept')
+ expect(headers['Accept']).to eq 'application/json'
end
it "only convert symbols in header" do
- @request.should_receive(:default_headers).and_return({})
+ expect(@request).to receive(:default_headers).and_return({})
headers = @request.make_headers({:foo_bar => 'value', "bar_bar" => 'value'})
- headers['Foo-Bar'].should eq 'value'
- headers['bar_bar'].should eq 'value'
+ expect(headers['Foo-Bar']).to eq 'value'
+ expect(headers['bar_bar']).to eq 'value'
end
it "converts header values to strings" do
- @request.make_headers('A' => 1)['A'].should eq '1'
+ expect(@request.make_headers('A' => 1)['A']).to eq '1'
end
end
it "executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
- @request.should_receive(:parse_url_with_auth).with('http://some/resource').and_return(@uri)
klass = double("net:http class")
- @request.should_receive(:net_http_request_class).with(:put).and_return(klass)
- klass.should_receive(:new).and_return('result')
- @request.should_receive(:transmit).with(@uri, 'result', kind_of(RestClient::Payload::Base))
+ expect(@request).to receive(:net_http_request_class).with('put').and_return(klass)
+ expect(klass).to receive(:new).and_return('result')
+ expect(@request).to receive(:transmit).with(@request.uri, 'result', kind_of(RestClient::Payload::Base))
@request.execute
end
+ it "IPv6: executes by constructing the Net::HTTP object, headers, and payload and calling transmit" do
+ @request = RestClient::Request.new(:method => :put, :url => 'http://[::1]/some/resource', :payload => 'payload')
+ klass = double("net:http class")
+ expect(@request).to receive(:net_http_request_class).with('put').and_return(klass)
+
+ if RUBY_VERSION >= "2.0.0"
+ expect(klass).to receive(:new).with(kind_of(URI), kind_of(Hash)).and_return('result')
+ else
+ expect(klass).to receive(:new).with(kind_of(String), kind_of(Hash)).and_return('result')
+ end
+
+ expect(@request).to receive(:transmit)
+ @request.execute
+ end
+
+ # TODO: almost none of these tests should actually call transmit, which is
+ # part of the private API
+
it "transmits the request with Net::HTTP" do
- @http.should_receive(:request).with('req', 'payload')
- @request.should_receive(:process_result)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@http).to receive(:request).with('req', 'payload')
+ expect(@request).to receive(:process_result)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
+ # TODO: most of these payload tests are historical relics that actually
+ # belong in payload_spec.rb. Or we need new tests that actually cover the way
+ # that Request#initialize or Request#execute uses the payload.
describe "payload" do
it "sends nil payloads" do
- @http.should_receive(:request).with('req', nil)
- @request.should_receive(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', nil)
+ expect(@http).to receive(:request).with('req', nil)
+ expect(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', nil)
end
it "passes non-hash payloads straight through" do
- @request.process_payload("x").should eq "x"
+ expect(RestClient::Payload.generate("x").to_s).to eq "x"
end
it "converts a hash payload to urlencoded data" do
- @request.process_payload(:a => 'b c+d').should eq "a=b%20c%2Bd"
+ expect(RestClient::Payload.generate(:a => 'b c+d').to_s).to eq "a=b+c%2Bd"
end
it "accepts nested hashes in payload" do
- payload = @request.process_payload(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }})
- payload.should include('user[name]=joe')
- payload.should include('user[location][country]=USA')
- payload.should include('user[location][state]=CA')
+ payload = RestClient::Payload.generate(:user => { :name => 'joe', :location => { :country => 'USA', :state => 'CA' }}).to_s
+ expect(payload).to include('user[name]=joe')
+ expect(payload).to include('user[location][country]=USA')
+ expect(payload).to include('user[location][state]=CA')
end
end
it "set urlencoded content_type header on hash payloads" do
- @request.process_payload(:a => 1)
- @request.headers[:content_type].should eq 'application/x-www-form-urlencoded'
+ req = RestClient::Request.new(method: :post, url: 'http://some/resource', payload: {a: 1})
+ expect(req.processed_headers.fetch('Content-Type')).to eq 'application/x-www-form-urlencoded'
end
describe "credentials" do
it "sets up the credentials prior to the request" do
- @http.stub(:request)
+ allow(@http).to receive(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @request.stub(:user).and_return('joe')
- @request.stub(:password).and_return('mypass')
- @request.should_receive(:setup_credentials).with('req')
+ allow(@request).to receive(:user).and_return('joe')
+ allow(@request).to receive(:password).and_return('mypass')
+ expect(@request).to receive(:setup_credentials).with('req')
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
end
it "does not attempt to send any credentials if user is nil" do
- @request.stub(:user).and_return(nil)
+ allow(@request).to receive(:user).and_return(nil)
req = double("request")
- req.should_not_receive(:basic_auth)
- @request.setup_credentials(req)
+ expect(req).not_to receive(:basic_auth)
+ @request.send(:setup_credentials, req)
end
it "setup credentials when there's a user" do
- @request.stub(:user).and_return('joe')
- @request.stub(:password).and_return('mypass')
+ allow(@request).to receive(:user).and_return('joe')
+ allow(@request).to receive(:password).and_return('mypass')
req = double("request")
- req.should_receive(:basic_auth).with('joe', 'mypass')
- @request.setup_credentials(req)
+ expect(req).to receive(:basic_auth).with('joe', 'mypass')
+ @request.send(:setup_credentials, req)
+ end
+
+ it "does not attempt to send credentials if Authorization header is set" do
+ @request.headers['Authorization'] = 'Token abc123'
+ allow(@request).to receive(:user).and_return('joe')
+ allow(@request).to receive(:password).and_return('mypass')
+ req = double("request")
+ expect(req).not_to receive(:basic_auth)
+ @request.send(:setup_credentials, req)
end
end
it "catches EOFError and shows the more informative ServerBrokeConnection" do
- @http.stub(:request).and_raise(EOFError)
- lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::ServerBrokeConnection)
+ allow(@http).to receive(:request).and_raise(EOFError)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::ServerBrokeConnection)
end
it "catches OpenSSL::SSL::SSLError and raise it back without more informative message" do
- @http.stub(:request).and_raise(OpenSSL::SSL::SSLError)
- lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(OpenSSL::SSL::SSLError)
+ allow(@http).to receive(:request).and_raise(OpenSSL::SSL::SSLError)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(OpenSSL::SSL::SSLError)
+ end
+
+ it "catches Timeout::Error and raise the more informative ReadTimeout" do
+ allow(@http).to receive(:request).and_raise(Timeout::Error)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
+ end
+
+ it "catches Errno::ETIMEDOUT and raise the more informative ReadTimeout" do
+ allow(@http).to receive(:request).and_raise(Errno::ETIMEDOUT)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
+ end
+
+ it "catches Net::ReadTimeout and raises RestClient's ReadTimeout",
+ :if => defined?(Net::ReadTimeout) do
+ allow(@http).to receive(:request).and_raise(Net::ReadTimeout)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout)
end
- it "catches Timeout::Error and raise the more informative RequestTimeout" do
- @http.stub(:request).and_raise(Timeout::Error)
- lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::RequestTimeout)
+ it "catches Net::OpenTimeout and raises RestClient's OpenTimeout",
+ :if => defined?(Net::OpenTimeout) do
+ allow(@http).to receive(:request).and_raise(Net::OpenTimeout)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::OpenTimeout)
end
- it "catches Timeout::Error and raise the more informative RequestTimeout" do
- @http.stub(:request).and_raise(Errno::ETIMEDOUT)
- lambda { @request.transmit(@uri, 'req', nil) }.should raise_error(RestClient::RequestTimeout)
+ it "uses correct error message for ReadTimeout",
+ :if => defined?(Net::ReadTimeout) do
+ allow(@http).to receive(:request).and_raise(Net::ReadTimeout)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::ReadTimeout, 'Timed out reading data from server')
end
+ it "uses correct error message for OpenTimeout",
+ :if => defined?(Net::OpenTimeout) do
+ allow(@http).to receive(:request).and_raise(Net::OpenTimeout)
+ expect { @request.send(:transmit, @uri, 'req', nil) }.to raise_error(RestClient::Exceptions::OpenTimeout, 'Timed out connecting to server')
+ end
+
+
it "class method execute wraps constructor" do
req = double("rest request")
- RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
- req.should_receive(:execute)
+ expect(RestClient::Request).to receive(:new).with(1 => 2).and_return(req)
+ expect(req).to receive(:execute)
RestClient::Request.execute(1 => 2)
end
describe "exception" do
it "raises Unauthorized when the response is 401" do
- res = double('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' )
- lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
+ res = response_double(:code => '401', :[] => ['content-encoding' => ''], :body => '' )
+ expect { @request.send(:process_result, res) }.to raise_error(RestClient::Unauthorized)
end
it "raises ResourceNotFound when the response is 404" do
- res = double('response', :code => '404', :[] => ['content-encoding' => ''], :body => '' )
- lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
+ res = response_double(:code => '404', :[] => ['content-encoding' => ''], :body => '' )
+ expect { @request.send(:process_result, res) }.to raise_error(RestClient::ResourceNotFound)
end
it "raises RequestFailed otherwise" do
- res = double('response', :code => '500', :[] => ['content-encoding' => ''], :body => '' )
- lambda { @request.process_result(res) }.should raise_error(RestClient::InternalServerError)
+ res = response_double(:code => '500', :[] => ['content-encoding' => ''], :body => '' )
+ expect { @request.send(:process_result, res) }.to raise_error(RestClient::InternalServerError)
end
end
describe "block usage" do
it "returns what asked to" do
- res = double('response', :code => '401', :[] => ['content-encoding' => ''], :body => '' )
- @request.process_result(res){|response, request| "foo"}.should eq "foo"
+ res = response_double(:code => '401', :[] => ['content-encoding' => ''], :body => '' )
+ expect(@request.send(:process_result, res){|response, request| "foo"}).to eq "foo"
end
end
describe "proxy" do
+ before do
+ # unstub Net::HTTP creation since we need to test it
+ allow(Net::HTTP).to receive(:new).and_call_original
+
+ @proxy_req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+ end
+
it "creates a proxy class if a proxy url is given" do
- RestClient.stub(:proxy).and_return("http://example.com/")
- @request.net_http_class.proxy_class?.should be_true
+ allow(RestClient).to receive(:proxy).and_return("http://example.com/")
+ allow(RestClient).to receive(:proxy_set?).and_return(true)
+ expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+ end
+
+ it "creates a proxy class with the correct address if a IPv6 proxy url is given" do
+ allow(RestClient).to receive(:proxy).and_return("http://[::1]/")
+ allow(RestClient).to receive(:proxy_set?).and_return(true)
+ expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+ expect(@proxy_req.net_http_object('host', 80).proxy_address).to eq('::1')
end
it "creates a non-proxy class if a proxy url is not given" do
- @request.net_http_class.proxy_class?.should be_false
+ expect(@proxy_req.net_http_object('host', 80).proxy?).to be_falsey
+ end
+
+ it "disables proxy on a per-request basis" do
+ allow(RestClient).to receive(:proxy).and_return('http://example.com')
+ allow(RestClient).to receive(:proxy_set?).and_return(true)
+ expect(@proxy_req.net_http_object('host', 80).proxy?).to be true
+
+ disabled_req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+ expect(disabled_req.net_http_object('host', 80).proxy?).to be_falsey
+ end
+
+ it "sets proxy on a per-request basis" do
+ expect(@proxy_req.net_http_object('some', 80).proxy?).to be_falsey
+
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => 'http://example.com')
+ expect(req.net_http_object('host', 80).proxy?).to be true
+ end
+
+ it "overrides proxy from environment", if: RUBY_VERSION >= '2.0' do
+ allow(ENV).to receive(:[]).with("http_proxy").and_return("http://127.0.0.1")
+ allow(ENV).to receive(:[]).with("no_proxy").and_return(nil)
+ allow(ENV).to receive(:[]).with("NO_PROXY").and_return(nil)
+ allow(Netrc).to receive(:read).and_return({})
+
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+ obj = req.net_http_object('host', 80)
+ expect(obj.proxy?).to be true
+ expect(obj.proxy_address).to eq '127.0.0.1'
+
+ # test original method .proxy?
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+ obj = req.net_http_object('host', 80)
+ expect(obj.proxy?).to be_falsey
+
+ # stub RestClient.proxy_set? to peek into implementation
+ allow(RestClient).to receive(:proxy_set?).and_return(true)
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
+ obj = req.net_http_object('host', 80)
+ expect(obj.proxy?).to be_falsey
+
+ # test stubbed Net::HTTP.new
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => nil)
+ expect(Net::HTTP).to receive(:new).with('host', 80, nil, nil, nil, nil)
+ req.net_http_object('host', 80)
+ end
+
+ it "overrides global proxy with per-request proxy" do
+ allow(RestClient).to receive(:proxy).and_return('http://example.com')
+ allow(RestClient).to receive(:proxy_set?).and_return(true)
+ obj = @proxy_req.net_http_object('host', 80)
+ expect(obj.proxy?).to be true
+ expect(obj.proxy_address).to eq 'example.com'
+
+ req = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :proxy => 'http://127.0.0.1/')
+ expect(req.net_http_object('host', 80).proxy?).to be true
+ expect(req.net_http_object('host', 80).proxy_address).to eq('127.0.0.1')
end
end
@@ -369,189 +643,221 @@ describe RestClient::Request do
describe "logging" do
it "logs a get request" do
log = RestClient.log = []
- RestClient::Request.new(:method => :get, :url => 'http://url').log_request
- log[0].should eq %Q{RestClient.get "http://url", "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate"\n}
+ RestClient::Request.new(:method => :get, :url => 'http://url', :headers => {:user_agent => 'rest-client'}).log_request
+ expect(log[0]).to eq %Q{RestClient.get "http://url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
end
it "logs a post request with a small payload" do
log = RestClient.log = []
- RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').log_request
- log[0].should eq %Q{RestClient.post "http://url", "foo", "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"3"\n}
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo', :headers => {:user_agent => 'rest-client'}).log_request
+ expect(log[0]).to eq %Q{RestClient.post "http://url", "foo", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"3", "User-Agent"=>"rest-client"\n}
end
it "logs a post request with a large payload" do
log = RestClient.log = []
- RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).log_request
- log[0].should eq %Q{RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*; q=0.5, application/xml", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"1000"\n}
+ RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000), :headers => {:user_agent => 'rest-client'}).log_request
+ expect(log[0]).to eq %Q{RestClient.post "http://url", 1000 byte(s) length, "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "Content-Length"=>"1000", "User-Agent"=>"rest-client"\n}
end
it "logs input headers as a hash" do
log = RestClient.log = []
- RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).log_request
- log[0].should eq %Q{RestClient.get "http://url", "Accept"=>"text/plain", "Accept-Encoding"=>"gzip, deflate"\n}
+ RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain', :user_agent => 'rest-client' }).log_request
+ expect(log[0]).to eq %Q{RestClient.get "http://url", "Accept"=>"text/plain", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
end
it "logs a response including the status code, content type, and result body size in bytes" do
log = RestClient.log = []
res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
- res.stub(:[]).with('Content-type').and_return('text/html')
+ allow(res).to receive(:[]).with('Content-type').and_return('text/html')
@request.log_response res
- log[0].should eq "# => 200 OK | text/html 4 bytes\n"
+ expect(log[0]).to eq "# => 200 OK | text/html 4 bytes\n"
end
it "logs a response with a nil Content-type" do
log = RestClient.log = []
res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
- res.stub(:[]).with('Content-type').and_return(nil)
+ allow(res).to receive(:[]).with('Content-type').and_return(nil)
@request.log_response res
- log[0].should eq "# => 200 OK | 4 bytes\n"
+ expect(log[0]).to eq "# => 200 OK | 4 bytes\n"
end
it "logs a response with a nil body" do
log = RestClient.log = []
res = double('result', :code => '200', :class => Net::HTTPOK, :body => nil)
- res.stub(:[]).with('Content-type').and_return('text/html; charset=utf-8')
+ allow(res).to receive(:[]).with('Content-type').and_return('text/html; charset=utf-8')
@request.log_response res
- log[0].should eq "# => 200 OK | text/html 0 bytes\n"
+ expect(log[0]).to eq "# => 200 OK | text/html 0 bytes\n"
end
it 'does not log request password' do
log = RestClient.log = []
- RestClient::Request.new(:method => :get, :url => 'http://user:password@url', :headers => {:user_agent => 'rest-client', :accept => '*/*'}).log_request
- log[0].should eq %Q{RestClient.get "http://user:REDACTED@url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
- end
-
- it 'logs invalid URIs, even though they will fail elsewhere' do
- log = RestClient.log = []
- RestClient::Request.new(:method => :get, :url => 'http://a@b:c', :headers => {:user_agent => 'rest-client', :accept => '*/*'}).log_request
- log[0].should eq %Q{RestClient.get "[invalid uri]", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
+ RestClient::Request.new(:method => :get, :url => 'http://user:password@url', :headers => {:user_agent => 'rest-client'}).log_request
+ expect(log[0]).to eq %Q{RestClient.get "http://user:REDACTED@url", "Accept"=>"*/*", "Accept-Encoding"=>"gzip, deflate", "User-Agent"=>"rest-client"\n}
end
end
it "strips the charset from the response content type" do
log = RestClient.log = []
res = double('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
- res.stub(:[]).with('Content-type').and_return('text/html; charset=utf-8')
+ allow(res).to receive(:[]).with('Content-type').and_return('text/html; charset=utf-8')
@request.log_response res
- log[0].should eq "# => 200 OK | text/html 4 bytes\n"
+ expect(log[0]).to eq "# => 200 OK | text/html 4 bytes\n"
end
describe "timeout" do
it "does not set timeouts if not specified" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @net.should_not_receive(:read_timeout=)
- @net.should_not_receive(:open_timeout=)
+ expect(@net).not_to receive(:read_timeout=)
+ expect(@net).not_to receive(:open_timeout=)
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
end
- it "set read_timeout" do
- @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ it 'sets read_timeout' do
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => 123)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @net.should_receive(:read_timeout=).with(123)
+ expect(@net).to receive(:read_timeout=).with(123)
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
end
- it "set open_timeout" do
+ it "sets open_timeout" do
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :open_timeout => 123)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @net.should_receive(:open_timeout=).with(123)
+ expect(@net).to receive(:open_timeout=).with(123)
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
end
+ it 'sets both timeouts with :timeout' do
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+
+ expect(@net).to receive(:open_timeout=).with(123)
+ expect(@net).to receive(:read_timeout=).with(123)
+
+ @request.send(:transmit, @uri, 'req', nil)
+ end
+
+ it 'supersedes :timeout with open/read_timeout' do
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => 123, :open_timeout => 34, :read_timeout => 56)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+
+ expect(@net).to receive(:open_timeout=).with(34)
+ expect(@net).to receive(:read_timeout=).with(56)
+
+ @request.send(:transmit, @uri, 'req', nil)
+ end
+
+
it "disable timeout by setting it to nil" do
- @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => nil, :open_timeout => nil)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => nil, :open_timeout => nil)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @net.should_receive(:read_timeout=).with(nil)
- @net.should_receive(:open_timeout=).with(nil)
+ expect(@net).to receive(:read_timeout=).with(nil)
+ expect(@net).to receive(:open_timeout=).with(nil)
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
+ end
+
+ it 'deprecated: warns when disabling timeout by setting it to -1' do
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => -1)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+
+ expect(@net).to receive(:read_timeout=).with(nil)
+
+ expect(fake_stderr {
+ @request.send(:transmit, @uri, 'req', nil)
+ }).to match(/^Deprecated: .*timeout.* nil instead of -1$/)
end
it "deprecated: disable timeout by setting it to -1" do
- @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :timeout => -1, :open_timeout => -1)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
+ @request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload', :read_timeout => -1, :open_timeout => -1)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
- @request.should_receive(:warn)
- @net.should_receive(:read_timeout=).with(nil)
+ expect(@request).to receive(:warn)
+ expect(@net).to receive(:read_timeout=).with(nil)
- @request.should_receive(:warn)
- @net.should_receive(:open_timeout=).with(nil)
+ expect(@request).to receive(:warn)
+ expect(@net).to receive(:open_timeout=).with(nil)
- @request.transmit(@uri, 'req', nil)
+ @request.send(:transmit, @uri, 'req', nil)
end
end
describe "ssl" do
it "uses SSL when the URI refers to a https address" do
- @uri.stub(:is_a?).with(URI::HTTPS).and_return(true)
- @net.should_receive(:use_ssl=).with(true)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ allow(@uri).to receive(:is_a?).with(URI::HTTPS).and_return(true)
+ expect(@net).to receive(:use_ssl=).with(true)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should default to verifying ssl certificates" do
- @request.verify_ssl.should eq OpenSSL::SSL::VERIFY_PEER
+ expect(@request.verify_ssl).to eq OpenSSL::SSL::VERIFY_PEER
end
it "should have expected values for VERIFY_PEER and VERIFY_NONE" do
- OpenSSL::SSL::VERIFY_NONE.should eq(0)
- OpenSSL::SSL::VERIFY_PEER.should eq(1)
+ expect(OpenSSL::SSL::VERIFY_NONE).to eq(0)
+ expect(OpenSSL::SSL::VERIFY_PEER).to eq(1)
end
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
@request = RestClient::Request.new(:method => :put, :verify_ssl => false, :url => 'http://some/resource', :payload => 'payload')
- @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is true" do
@request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
- @net.should_not_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_NONE)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_PEER if verify_ssl is true" do
@request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload', :verify_ssl => true)
- @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_PEER if verify_ssl is not given" do
@request = RestClient::Request.new(:method => :put, :url => 'https://some/resource', :payload => 'payload')
- @net.should_receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
@@ -560,15 +866,15 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload',
:verify_ssl => mode )
- @net.should_receive(:verify_mode=).with(mode)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:verify_mode=).with(mode)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should default to not having an ssl_client_cert" do
- @request.ssl_client_cert.should be(nil)
+ expect(@request.ssl_client_cert).to be(nil)
end
it "should set the ssl_version if provided" do
@@ -578,11 +884,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_version => "TLSv1"
)
- @net.should_receive(:ssl_version=).with("TLSv1")
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:ssl_version=).with("TLSv1")
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_version if not provided" do
@@ -591,11 +897,11 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_not_receive(:ssl_version=).with("TLSv1")
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:ssl_version=).with("TLSv1")
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set the ssl_ciphers if provided" do
@@ -606,11 +912,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_ciphers => ciphers
)
- @net.should_receive(:ciphers=).with(ciphers)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:ciphers=).with(ciphers)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_ciphers if set to nil" do
@@ -620,61 +926,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_ciphers => nil,
)
- @net.should_not_receive(:ciphers=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
- end
-
- it "should override ssl_ciphers with better defaults with weak default ciphers" do
- stub_const(
- '::OpenSSL::SSL::SSLContext::DEFAULT_PARAMS',
- {
- :ssl_version=>"SSLv23",
- :verify_mode=>1,
- :ciphers=>"ALL:!ADH:!EXPORT:!SSLv2:RC4+RSA:+HIGH:+MEDIUM:+LOW",
- :options=>-2147480577,
- }
- )
-
- @request = RestClient::Request.new(
- :method => :put,
- :url => 'https://some/resource',
- :payload => 'payload',
- )
-
- @net.should_receive(:ciphers=).with(RestClient::Request::DefaultCiphers)
-
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
- end
-
- it "should not override ssl_ciphers with better defaults with different default ciphers" do
- stub_const(
- '::OpenSSL::SSL::SSLContext::DEFAULT_PARAMS',
- {
- :ssl_version=>"SSLv23",
- :verify_mode=>1,
- :ciphers=>"HIGH:!aNULL:!eNULL:!EXPORT:!LOW:!MEDIUM:!SSLv2",
- :options=>-2147480577,
- }
- )
-
- @request = RestClient::Request.new(
- :method => :put,
- :url => 'https://some/resource',
- :payload => 'payload',
- )
-
- @net.should_not_receive(:ciphers=)
-
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:ciphers=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set the ssl_client_cert if provided" do
@@ -684,11 +940,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_client_cert => "whatsupdoc!"
)
- @net.should_receive(:cert=).with("whatsupdoc!")
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:cert=).with("whatsupdoc!")
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_client_cert if it is not provided" do
@@ -697,15 +953,15 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_not_receive(:cert=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:cert=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should default to not having an ssl_client_key" do
- @request.ssl_client_key.should be(nil)
+ expect(@request.ssl_client_key).to be(nil)
end
it "should set the ssl_client_key if provided" do
@@ -715,11 +971,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_client_key => "whatsupdoc!"
)
- @net.should_receive(:key=).with("whatsupdoc!")
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:key=).with("whatsupdoc!")
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_client_key if it is not provided" do
@@ -728,15 +984,15 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_not_receive(:key=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:key=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should default to not having an ssl_ca_file" do
- @request.ssl_ca_file.should be(nil)
+ expect(@request.ssl_ca_file).to be(nil)
end
it "should set the ssl_ca_file if provided" do
@@ -746,12 +1002,12 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_ca_file => "Certificate Authority File"
)
- @net.should_receive(:ca_file=).with("Certificate Authority File")
- @net.should_not_receive(:cert_store=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:ca_file=).with("Certificate Authority File")
+ expect(@net).not_to receive(:cert_store=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_ca_file if it is not provided" do
@@ -760,15 +1016,15 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_not_receive(:ca_file=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:ca_file=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should default to not having an ssl_ca_path" do
- @request.ssl_ca_path.should be(nil)
+ expect(@request.ssl_ca_path).to be(nil)
end
it "should set the ssl_ca_path if provided" do
@@ -778,12 +1034,12 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_ca_path => "Certificate Authority Path"
)
- @net.should_receive(:ca_path=).with("Certificate Authority Path")
- @net.should_not_receive(:cert_store=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:ca_path=).with("Certificate Authority Path")
+ expect(@net).not_to receive(:cert_store=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_ca_path if it is not provided" do
@@ -792,11 +1048,11 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_not_receive(:ca_path=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:ca_path=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set the ssl_cert_store if provided" do
@@ -809,13 +1065,13 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_cert_store => store
)
- @net.should_receive(:cert_store=).with(store)
- @net.should_not_receive(:ca_path=)
- @net.should_not_receive(:ca_file=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:cert_store=).with(store)
+ expect(@net).not_to receive(:ca_path=)
+ expect(@net).not_to receive(:ca_file=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should by default set the ssl_cert_store if no CA info is provided" do
@@ -824,13 +1080,13 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload'
)
- @net.should_receive(:cert_store=)
- @net.should_not_receive(:ca_path=)
- @net.should_not_receive(:ca_file=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).to receive(:cert_store=)
+ expect(@net).not_to receive(:ca_path=)
+ expect(@net).not_to receive(:ca_file=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_cert_store if it is set falsy" do
@@ -840,11 +1096,11 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_cert_store => nil,
)
- @net.should_not_receive(:cert_store=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:cert_store=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should not set the ssl_verify_callback by default" do
@@ -853,11 +1109,11 @@ describe RestClient::Request do
:url => 'https://some/resource',
:payload => 'payload',
)
- @net.should_not_receive(:verify_callback=)
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ expect(@net).not_to receive(:verify_callback=)
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
it "should set the ssl_verify_callback if passed" do
@@ -868,7 +1124,7 @@ describe RestClient::Request do
:payload => 'payload',
:ssl_verify_callback => callback,
)
- @net.should_receive(:verify_callback=).with(callback)
+ expect(@net).to receive(:verify_callback=).with(callback)
# we'll read cert_store on jruby
# https://github.com/jruby/jruby/issues/597
@@ -876,10 +1132,10 @@ describe RestClient::Request do
allow(@net).to receive(:cert_store)
end
- @http.stub(:request)
- @request.stub(:process_result)
- @request.stub(:response_log)
- @request.transmit(@uri, 'req', 'payload')
+ allow(@http).to receive(:request)
+ allow(@request).to receive(:process_result)
+ allow(@request).to receive(:response_log)
+ @request.send(:transmit, @uri, 'req', 'payload')
end
# </ssl>
@@ -892,11 +1148,11 @@ describe RestClient::Request do
:payload => 'payload'
)
net_http_res = Net::HTTPNoContent.new("", "204", "No Content")
- net_http_res.stub(:read_body).and_return(nil)
- @http.should_receive(:request).and_return(@request.fetch_body(net_http_res))
- response = @request.transmit(@uri, 'req', 'payload')
- response.should_not be_nil
- response.code.should eq 204
+ allow(net_http_res).to receive(:read_body).and_return(nil)
+ expect(@http).to receive(:request).and_return(@request.send(:fetch_body, net_http_res))
+ response = @request.send(:transmit, @uri, 'req', 'payload')
+ expect(response).not_to be_nil
+ expect(response.code).to eq 204
end
describe "raw response" do
@@ -904,14 +1160,91 @@ describe RestClient::Request do
@request = RestClient::Request.new(:method => "get", :url => "example.com", :raw_response => true)
tempfile = double("tempfile")
- tempfile.should_receive(:binmode)
- tempfile.stub(:open)
- tempfile.stub(:close)
- Tempfile.should_receive(:new).with("rest-client").and_return(tempfile)
+ expect(tempfile).to receive(:binmode)
+ allow(tempfile).to receive(:open)
+ allow(tempfile).to receive(:close)
+ expect(Tempfile).to receive(:new).with("rest-client.").and_return(tempfile)
net_http_res = Net::HTTPOK.new(nil, "200", "body")
- net_http_res.stub(:read_body).and_return("body")
- @request.fetch_body(net_http_res)
+ allow(net_http_res).to receive(:read_body).and_return("body")
+ @request.send(:fetch_body, net_http_res)
+ end
+ end
+
+ describe 'payloads' do
+ it 'should accept string payloads' do
+ payload = 'Foo'
+ @request = RestClient::Request.new(method: :get, url: 'example.com', :payload => payload)
+ expect(@request).to receive(:process_result)
+ expect(@http).to receive(:request).with('req', payload)
+ @request.send(:transmit, @uri, 'req', payload)
+ end
+
+ it 'should accept streaming IO payloads' do
+ payload = StringIO.new('streamed')
+
+ @request = RestClient::Request.new(method: :get, url: 'example.com', :payload => payload)
+ expect(@request).to receive(:process_result)
+
+ @get = double('net::http::get')
+ expect(@get).to receive(:body_stream=).with(instance_of(RestClient::Payload::Streamed))
+
+ allow(@request.net_http_request_class(:GET)).to receive(:new).and_return(@get)
+ expect(@http).to receive(:request).with(@get, nil)
+ @request.execute
+ end
+ end
+
+ describe 'constructor' do
+ it 'should reject valid URIs with no hostname' do
+ expect(URI.parse('http:///').hostname).to be_nil
+
+ expect {
+ RestClient::Request.new(method: :get, url: 'http:///')
+ }.to raise_error(URI::InvalidURIError, /\Abad URI/)
+ end
+
+ it 'should reject invalid URIs' do
+ expect {
+ RestClient::Request.new(method: :get, url: 'http://::')
+ }.to raise_error(URI::InvalidURIError)
+ end
+ end
+
+ describe 'process_url_params' do
+ it 'should handle basic URL params' do
+ expect(@request.process_url_params('https://example.com/foo', params: {key1: 123, key2: 'abc'})).
+ to eq 'https://example.com/foo?key1=123&key2=abc'
+
+ expect(@request.process_url_params('https://example.com/foo', params: {'key1' => 123})).
+ to eq 'https://example.com/foo?key1=123'
+
+ expect(@request.process_url_params('https://example.com/path',
+ params: {foo: 'one two', bar: 'three + four == seven'})).
+ to eq 'https://example.com/path?foo=one+two&bar=three+%2B+four+%3D%3D+seven'
+ end
+
+ it 'should combine with & when URL params already exist' do
+ expect(@request.process_url_params('https://example.com/path?foo=1', params: {bar: 2})).
+ to eq 'https://example.com/path?foo=1&bar=2'
+ end
+
+ it 'should handle complex nested URL params per Rack / Rails conventions' do
+ expect(@request.process_url_params('https://example.com/', params: {
+ foo: [1,2,3],
+ null: nil,
+ false: false,
+ math: '2+2=4',
+ nested: {'key + escaped' => 'value + escaped', other: [], arr: [1,2]},
+ })).to eq 'https://example.com/?foo[]=1&foo[]=2&foo[]=3&null&false=false&math=2%2B2%3D4' \
+ '&nested[key+%2B+escaped]=value+%2B+escaped&nested[other]' \
+ '&nested[arr][]=1&nested[arr][]=2'
+ end
+
+ it 'should handle ParamsArray objects' do
+ expect(@request.process_url_params('https://example.com/',
+ params: RestClient::ParamsArray.new([[:foo, 1], [:foo, 2]])
+ )).to eq 'https://example.com/?foo=1&foo=2'
end
end
end
diff --git a/spec/unit/resource_spec.rb b/spec/unit/resource_spec.rb
index 1657ee8..3777767 100644
--- a/spec/unit/resource_spec.rb
+++ b/spec/unit/resource_spec.rb
@@ -1,4 +1,4 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient::Resource do
before do
@@ -7,37 +7,37 @@ describe RestClient::Resource do
context "Resource delegation" do
it "GET" do
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.get
end
it "HEAD" do
- RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.head
end
it "POST" do
- RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.post 'abc', :content_type => 'image/jpg'
end
it "PUT" do
- RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.put 'abc', :content_type => 'image/jpg'
end
it "PATCH" do
- RestClient::Request.should_receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'abc', :headers => {:content_type => 'image/jpg', 'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.patch 'abc', :content_type => 'image/jpg'
end
it "DELETE" do
- RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {'X-Something' => '1'}, :user => 'jane', :password => 'mypass')
@resource.delete
end
it "overrides resource headers" do
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '2'}, :user => 'jane', :password => 'mypass')
+ expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {'X-Something' => '2'}, :user => 'jane', :password => 'mypass')
@resource.get 'X-Something' => '2'
end
end
@@ -48,41 +48,41 @@ describe RestClient::Resource do
it "is backwards compatible with previous constructor" do
@resource = RestClient::Resource.new('http://some/resource', 'user', 'pass')
- @resource.user.should eq 'user'
- @resource.password.should eq 'pass'
+ expect(@resource.user).to eq 'user'
+ expect(@resource.password).to eq 'pass'
end
it "concatenates urls, inserting a slash when it needs one" do
- @resource.concat_urls('http://example.com', 'resource').should eq 'http://example.com/resource'
+ expect(@resource.concat_urls('http://example.com', 'resource')).to eq 'http://example.com/resource'
end
it "concatenates urls, using no slash if the first url ends with a slash" do
- @resource.concat_urls('http://example.com/', 'resource').should eq 'http://example.com/resource'
+ expect(@resource.concat_urls('http://example.com/', 'resource')).to eq 'http://example.com/resource'
end
it "concatenates urls, using no slash if the second url starts with a slash" do
- @resource.concat_urls('http://example.com', '/resource').should eq 'http://example.com/resource'
+ expect(@resource.concat_urls('http://example.com', '/resource')).to eq 'http://example.com/resource'
end
it "concatenates even non-string urls, :posts + 1 => 'posts/1'" do
- @resource.concat_urls(:posts, 1).should eq 'posts/1'
+ expect(@resource.concat_urls(:posts, 1)).to eq 'posts/1'
end
it "offers subresources via []" do
parent = RestClient::Resource.new('http://example.com')
- parent['posts'].url.should eq 'http://example.com/posts'
+ expect(parent['posts'].url).to eq 'http://example.com/posts'
end
it "transports options to subresources" do
parent = RestClient::Resource.new('http://example.com', :user => 'user', :password => 'password')
- parent['posts'].user.should eq 'user'
- parent['posts'].password.should eq 'password'
+ expect(parent['posts'].user).to eq 'user'
+ expect(parent['posts'].password).to eq 'password'
end
it "passes a given block to subresources" do
block = proc {|r| r}
parent = RestClient::Resource.new('http://example.com', &block)
- parent['posts'].block.should eq block
+ expect(parent['posts'].block).to eq block
end
it "the block should be overrideable" do
@@ -90,40 +90,44 @@ describe RestClient::Resource do
block2 = proc {|r| }
parent = RestClient::Resource.new('http://example.com', &block1)
# parent['posts', &block2].block.should eq block2 # ruby 1.9 syntax
- parent.send(:[], 'posts', &block2).block.should eq block2
- parent.send(:[], 'posts', &block2).block.should_not eq block1
+ expect(parent.send(:[], 'posts', &block2).block).to eq block2
+ expect(parent.send(:[], 'posts', &block2).block).not_to eq block1
end
- it "the block should be overrideable in ruby 1.9 syntax" do
+ # Test fails on jruby 9.1.[0-5].* due to
+ # https://github.com/jruby/jruby/issues/4217
+ it "the block should be overrideable in ruby 1.9 syntax",
+ :unless => (RUBY_ENGINE == 'jruby' && JRUBY_VERSION =~ /\A9\.1\.[0-5]\./) \
+ do
block1 = proc {|r| r}
block2 = ->(r) {}
parent = RestClient::Resource.new('http://example.com', &block1)
- parent['posts', &block2].block.should eq block2
- parent['posts', &block2].block.should_not eq block1
+ expect(parent['posts', &block2].block).to eq block2
+ expect(parent['posts', &block2].block).not_to eq block1
end
it "prints its url with to_s" do
- RestClient::Resource.new('x').to_s.should eq 'x'
+ expect(RestClient::Resource.new('x').to_s).to eq 'x'
end
describe 'block' do
it 'can use block when creating the resource' do
stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' }
- resource.get.should eq 'foo'
+ expect(resource.get).to eq 'foo'
end
it 'can use block when executing the resource' do
stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
resource = RestClient::Resource.new('www.example.com')
- resource.get { |response, request| 'foo' }.should eq 'foo'
+ expect(resource.get { |response, request| 'foo' }).to eq 'foo'
end
it 'execution block override resource block' do
stub_request(:get, 'www.example.com').to_return(:body => '', :status => 404)
resource = RestClient::Resource.new('www.example.com') { |response, request| 'foo' }
- resource.get { |response, request| 'bar' }.should eq 'bar'
+ expect(resource.get { |response, request| 'bar' }).to eq 'bar'
end
end
diff --git a/spec/unit/response_spec.rb b/spec/unit/response_spec.rb
index 2507676..096c2ab 100644
--- a/spec/unit/response_spec.rb
+++ b/spec/unit/response_spec.rb
@@ -1,26 +1,40 @@
-require 'spec_helper'
+require_relative '_lib'
-describe RestClient::Response do
+describe RestClient::Response, :include_helpers do
before do
@net_http_res = double('net http response', :to_hash => {"Status" => ["200 OK"]}, :code => 200)
@example_url = 'http://example.com'
- @request = double('http request', :user => nil, :password => nil, :url => @example_url)
- @response = RestClient::Response.create('abc', @net_http_res, {}, @request)
+ @request = request_double(url: @example_url, method: 'get')
+ @response = RestClient::Response.create('abc', @net_http_res, @request)
end
it "behaves like string" do
- @response.to_s.should eq 'abc'
- @response.to_str.should eq 'abc'
- @response.to_i.should eq 200
+ expect(@response.to_s).to eq 'abc'
+ expect(@response.to_str).to eq 'abc'
+
+ expect(@response).to receive(:warn)
+ expect(@response.to_i).to eq 0
end
it "accepts nil strings and sets it to empty for the case of HEAD" do
- RestClient::Response.create(nil, @net_http_res, {}, @request).to_s.should eq ""
+ expect(RestClient::Response.create(nil, @net_http_res, @request).to_s).to eq ""
end
- it "test headers and raw headers" do
- @response.raw_headers["Status"][0].should eq "200 OK"
- @response.headers[:status].should eq "200 OK"
+ describe 'header processing' do
+ it "test headers and raw headers" do
+ expect(@response.raw_headers["Status"][0]).to eq "200 OK"
+ expect(@response.headers[:status]).to eq "200 OK"
+ end
+
+ it 'handles multiple headers by joining with comma' do
+ @net_http_res = double('net http response', :to_hash => {'My-Header' => ['foo', 'bar']}, :code => 200)
+ @example_url = 'http://example.com'
+ @request = request_double(url: @example_url, method: 'get')
+ @response = RestClient::Response.create('abc', @net_http_res, @request)
+
+ expect(@response.raw_headers['My-Header']).to eq ['foo', 'bar']
+ expect(@response.headers[:my_header]).to eq 'foo, bar'
+ end
end
describe "cookie processing" do
@@ -28,16 +42,16 @@ describe RestClient::Response do
header_val = "main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT".freeze
net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => [header_val]})
- response = RestClient::Response.create('abc', net_http_res, {}, @request)
- response.headers[:set_cookie].should eq [header_val]
- response.cookies.should eq({ "main_page" => "main_page_no_rewrite" })
+ response = RestClient::Response.create('abc', net_http_res, @request)
+ expect(response.headers[:set_cookie]).to eq [header_val]
+ expect(response.cookies).to eq({ "main_page" => "main_page_no_rewrite" })
end
it "should correctly deal with multiple cookies [multiple Set-Cookie headers]" do
net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]})
- response = RestClient::Response.create('abc', net_http_res, {}, @request)
- response.headers[:set_cookie].should eq ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]
- response.cookies.should eq({
+ response = RestClient::Response.create('abc', net_http_res, @request)
+ expect(response.headers[:set_cookie]).to eq ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT", "remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT", "user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]
+ expect(response.cookies).to eq({
"main_page" => "main_page_no_rewrite",
"remember_me" => "",
"user" => "somebody"
@@ -46,8 +60,8 @@ describe RestClient::Response do
it "should correctly deal with multiple cookies [one Set-Cookie header with multiple cookies]" do
net_http_res = double('net http response', :to_hash => {"etag" => ["\"e1ac1a2df945942ef4cac8116366baad\""], "set-cookie" => ["main_page=main_page_no_rewrite; path=/; expires=Sat, 10-Jan-2037 15:03:14 GMT, remember_me=; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT, user=somebody; path=/; expires=Sat, 10-Jan-2037 00:00:00 GMT"]})
- response = RestClient::Response.create('abc', net_http_res, {}, @request)
- response.cookies.should eq({
+ response = RestClient::Response.create('abc', net_http_res, @request)
+ expect(response.cookies).to eq({
"main_page" => "main_page_no_rewrite",
"remember_me" => "",
"user" => "somebody"
@@ -58,18 +72,19 @@ describe RestClient::Response do
describe "exceptions processing" do
it "should return itself for normal codes" do
(200..206).each do |code|
- net_http_res = double('net http response', :code => '200')
- response = RestClient::Response.create('abc', net_http_res, {}, @request)
- response.return! @request
+ net_http_res = response_double(:code => '200')
+ resp = RestClient::Response.create('abc', net_http_res, @request)
+ resp.return!
end
end
it "should throw an exception for other codes" do
- RestClient::Exceptions::EXCEPTIONS_MAP.each_key do |code|
+ RestClient::Exceptions::EXCEPTIONS_MAP.each_pair do |code, exc|
unless (200..207).include? code
- net_http_res = double('net http response', :code => code.to_i)
- response = RestClient::Response.create('abc', net_http_res, {}, @request)
- lambda { response.return!}.should raise_error
+ net_http_res = response_double(:code => code.to_i)
+ resp = RestClient::Response.create('abc', net_http_res, @request)
+ allow(@request).to receive(:max_redirects).and_return(5)
+ expect { resp.return! }.to raise_error(exc)
end
end
end
@@ -81,95 +96,146 @@ describe RestClient::Response do
it "follows a redirection when the request is a get" do
stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
+ end
+
+ it "keeps redirection history" do
+ stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
+ stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
+ r = RestClient::Request.execute(url: 'http://some/resource', method: :get)
+ expect(r.body).to eq 'Foo'
+ expect(r.history.length).to eq 1
+ expect(r.history.fetch(0)).to be_a(RestClient::Response)
+ expect(r.history.fetch(0).code).to be 301
end
it "follows a redirection and keep the parameters" do
- stub_request(:get, 'http://foo:bar@some/resource').with(:headers => {'Accept' => 'application/json'}).to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
- stub_request(:get, 'http://foo:bar@new/resource').with(:headers => {'Accept' => 'application/json'}).to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :user => 'foo', :password => 'bar', :headers => {:accept => :json}).body.should eq 'Foo'
+ stub_request(:get, 'http://some/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
+ stub_request(:get, 'http://new/resource').with(:headers => {'Accept' => 'application/json'}, :basic_auth => ['foo', 'bar']).to_return(:body => 'Foo')
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get, :user => 'foo', :password => 'bar', :headers => {:accept => :json}).body).to eq 'Foo'
end
it "follows a redirection and keep the cookies" do
stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://some/new_resource', })
stub_request(:get, 'http://some/new_resource').with(:headers => {'Cookie' => 'Foo=Bar'}).to_return(:body => 'Qux')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Qux'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Qux'
end
- it 'does not keep cookies across domains' do
- stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://new/resource', })
- stub_request(:get, 'http://new/resource').with(:headers => {'Cookie' => ''}).to_return(:body => 'Qux')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Qux'
+ it 'respects cookie domains on redirect' do
+ stub_request(:get, 'http://some.example.com/').to_return(:body => '', :status => 301,
+ :headers => {'Set-Cookie' => 'Foo=Bar', 'Location' => 'http://new.example.com/', })
+ stub_request(:get, 'http://new.example.com/').with(
+ :headers => {'Cookie' => 'passedthrough=1'}).to_return(:body => 'Qux')
+
+ expect(RestClient::Request.execute(:url => 'http://some.example.com/', :method => :get, cookies: [HTTP::Cookie.new('passedthrough', '1', domain: 'new.example.com', path: '/')]).body).to eq 'Qux'
end
it "doesn't follow a 301 when the request is a post" do
- net_http_res = double('net http response', :code => 301)
- response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
- lambda { response.return!(@request)}.should raise_error(RestClient::MovedPermanently)
+ net_http_res = response_double(:code => 301)
+
+ response = RestClient::Response.create('abc', net_http_res,
+ request_double(method: 'post'))
+ expect {
+ response.return!
+ }.to raise_error(RestClient::MovedPermanently)
end
it "doesn't follow a 302 when the request is a post" do
- net_http_res = double('net http response', :code => 302)
- response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
- lambda { response.return!(@request)}.should raise_error(RestClient::Found)
+ net_http_res = response_double(:code => 302)
+ response = RestClient::Response.create('abc', net_http_res,
+ request_double(method: 'post'))
+ expect {
+ response.return!
+ }.to raise_error(RestClient::Found)
end
it "doesn't follow a 307 when the request is a post" do
- net_http_res = double('net http response', :code => 307)
- response = RestClient::Response.create('abc', net_http_res, {:method => :post}, @request)
- lambda { response.return!(@request)}.should raise_error(RestClient::TemporaryRedirect)
+ net_http_res = response_double(:code => 307)
+ response = RestClient::Response.create('abc', net_http_res,
+ request_double(method: 'post'))
+ expect(response).not_to receive(:follow_redirection)
+ expect {
+ response.return!
+ }.to raise_error(RestClient::TemporaryRedirect)
end
it "doesn't follow a redirection when the request is a put" do
- net_http_res = double('net http response', :code => 301)
- response = RestClient::Response.create('abc', net_http_res, {:method => :put}, @request)
- lambda { response.return!(@request)}.should raise_error(RestClient::MovedPermanently)
+ net_http_res = response_double(:code => 301)
+ response = RestClient::Response.create('abc', net_http_res,
+ request_double(method: 'put'))
+ expect {
+ response.return!
+ }.to raise_error(RestClient::MovedPermanently)
end
it "follows a redirection when the request is a post and result is a 303" do
stub_request(:put, 'http://some/resource').to_return(:body => '', :status => 303, :headers => {'Location' => 'http://new/resource'})
stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :put).body).to eq 'Foo'
end
it "follows a redirection when the request is a head" do
stub_request(:head, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
stub_request(:head, 'http://new/resource').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :head).body).to eq 'Foo'
end
it "handles redirects with relative paths" do
stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index'})
stub_request(:get, 'http://some/index').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
end
it "handles redirects with relative path and query string" do
stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'index?q=1'})
stub_request(:get, 'http://some/index?q=1').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
end
it "follow a redirection when the request is a get and the response is in the 30x range" do
stub_request(:get, 'http://some/resource').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://new/resource'})
stub_request(:get, 'http://new/resource').to_return(:body => 'Foo')
- RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body.should eq 'Foo'
+ expect(RestClient::Request.execute(:url => 'http://some/resource', :method => :get).body).to eq 'Foo'
end
it "follows no more than 10 redirections before raising error" do
stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
- lambda { RestClient::Request.execute(:url => 'http://some/redirect-1', :method => :get) }.should raise_error(RestClient::MaxRedirectsReached)
- WebMock.should have_requested(:get, 'http://some/redirect-2').times(10)
+ expect {
+ RestClient::Request.execute(url: 'http://some/redirect-1', method: :get)
+ }.to raise_error(RestClient::MovedPermanently) { |ex|
+ ex.response.history.each {|r| expect(r).to be_a(RestClient::Response) }
+ expect(ex.response.history.length).to eq 10
+ }
+ expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(10)
end
it "follows no more than max_redirects redirections, if specified" do
stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
stub_request(:get, 'http://some/redirect-2').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/redirect-2'})
- lambda { RestClient::Request.execute(:url => 'http://some/redirect-1', :method => :get, :max_redirects => 5) }.should raise_error(RestClient::MaxRedirectsReached)
- WebMock.should have_requested(:get, 'http://some/redirect-2').times(5)
+ expect {
+ RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 5)
+ }.to raise_error(RestClient::MovedPermanently) { |ex|
+ expect(ex.response.history.length).to eq 5
+ }
+ expect(WebMock).to have_requested(:get, 'http://some/redirect-2').times(5)
+ end
+
+ it "allows for manual following of redirects" do
+ stub_request(:get, 'http://some/redirect-1').to_return(:body => '', :status => 301, :headers => {'Location' => 'http://some/resource'})
+ stub_request(:get, 'http://some/resource').to_return(:body => 'Qux', :status => 200)
+
+ begin
+ RestClient::Request.execute(url: 'http://some/redirect-1', method: :get, max_redirects: 0)
+ rescue RestClient::MovedPermanently => err
+ resp = err.response.follow_redirection
+ else
+ raise 'notreached'
+ end
+
+ expect(resp.code).to eq 200
+ expect(resp.body).to eq 'Qux'
end
end
-
end
diff --git a/spec/unit/restclient_spec.rb b/spec/unit/restclient_spec.rb
index 190d1bf..cb4dfe0 100644
--- a/spec/unit/restclient_spec.rb
+++ b/spec/unit/restclient_spec.rb
@@ -1,39 +1,39 @@
-require 'spec_helper'
+require_relative '_lib'
describe RestClient do
describe "API" do
it "GET" do
- RestClient::Request.should_receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :get, :url => 'http://some/resource', :headers => {})
RestClient.get('http://some/resource')
end
it "POST" do
- RestClient::Request.should_receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :post, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.post('http://some/resource', 'payload')
end
it "PUT" do
- RestClient::Request.should_receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :put, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.put('http://some/resource', 'payload')
end
it "PATCH" do
- RestClient::Request.should_receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'payload', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :patch, :url => 'http://some/resource', :payload => 'payload', :headers => {})
RestClient.patch('http://some/resource', 'payload')
end
it "DELETE" do
- RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
RestClient.delete('http://some/resource')
end
it "HEAD" do
- RestClient::Request.should_receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :head, :url => 'http://some/resource', :headers => {})
RestClient.head('http://some/resource')
end
it "OPTIONS" do
- RestClient::Request.should_receive(:execute).with(:method => :options, :url => 'http://some/resource', :headers => {})
+ expect(RestClient::Request).to receive(:execute).with(:method => :options, :url => 'http://some/resource', :headers => {})
RestClient.options('http://some/resource')
end
end
@@ -45,35 +45,35 @@ describe RestClient do
it "uses << if the log is not a string" do
log = RestClient.log = []
- log.should_receive(:<<).with('xyz')
+ expect(log).to receive(:<<).with('xyz')
RestClient.log << 'xyz'
end
it "displays the log to stdout" do
RestClient.log = 'stdout'
- STDOUT.should_receive(:puts).with('xyz')
+ expect(STDOUT).to receive(:puts).with('xyz')
RestClient.log << 'xyz'
end
it "displays the log to stderr" do
RestClient.log = 'stderr'
- STDERR.should_receive(:puts).with('xyz')
+ expect(STDERR).to receive(:puts).with('xyz')
RestClient.log << 'xyz'
end
it "append the log to the requested filename" do
RestClient.log = '/tmp/restclient.log'
f = double('file handle')
- File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
- f.should_receive(:puts).with('xyz')
+ expect(File).to receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
+ expect(f).to receive(:puts).with('xyz')
RestClient.log << 'xyz'
end
end
describe 'version' do
- it 'has a version ~> 1.8.0.alpha' do
+ it 'has a version ~> 2.0.0.alpha' do
ver = Gem::Version.new(RestClient.version)
- Gem::Requirement.new('~> 1.8.0.alpha').should be_satisfied_by(ver)
+ expect(Gem::Requirement.new('~> 2.0.0.alpha')).to be_satisfied_by(ver)
end
end
end
diff --git a/spec/unit/utils_spec.rb b/spec/unit/utils_spec.rb
new file mode 100644
index 0000000..9c90ec0
--- /dev/null
+++ b/spec/unit/utils_spec.rb
@@ -0,0 +1,147 @@
+require_relative '_lib'
+
+describe RestClient::Utils do
+ describe '.get_encoding_from_headers' do
+ it 'assumes no encoding by default for text' do
+ headers = {:content_type => 'text/plain'}
+ expect(RestClient::Utils.get_encoding_from_headers(headers)).
+ to eq nil
+ end
+
+ it 'returns nil on failures' do
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'blah'})).to eq nil
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {})).to eq nil
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'foo; bar=baz'})).to eq nil
+ end
+
+ it 'handles various charsets' do
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'text/plain; charset=UTF-8'})).to eq 'UTF-8'
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'application/json; charset=ISO-8859-1'})).
+ to eq 'ISO-8859-1'
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'text/html; charset=windows-1251'})).
+ to eq 'windows-1251'
+
+ expect(RestClient::Utils.get_encoding_from_headers(
+ {:content_type => 'text/html; charset="UTF-16"'})).
+ to eq 'UTF-16'
+ end
+ end
+
+ describe '.cgi_parse_header' do
+ it 'parses headers' do
+ expect(RestClient::Utils.cgi_parse_header('text/plain')).
+ to eq ['text/plain', {}]
+
+ expect(RestClient::Utils.cgi_parse_header('text/vnd.just.made.this.up ; ')).
+ to eq ['text/vnd.just.made.this.up', {}]
+
+ expect(RestClient::Utils.cgi_parse_header('text/plain;charset=us-ascii')).
+ to eq ['text/plain', {'charset' => 'us-ascii'}]
+
+ expect(RestClient::Utils.cgi_parse_header('text/plain ; charset="us-ascii"')).
+ to eq ['text/plain', {'charset' => 'us-ascii'}]
+
+ expect(RestClient::Utils.cgi_parse_header(
+ 'text/plain ; charset="us-ascii"; another=opt')).
+ to eq ['text/plain', {'charset' => 'us-ascii', 'another' => 'opt'}]
+
+ expect(RestClient::Utils.cgi_parse_header(
+ 'attachment; filename="silly.txt"')).
+ to eq ['attachment', {'filename' => 'silly.txt'}]
+
+ expect(RestClient::Utils.cgi_parse_header(
+ 'attachment; filename="strange;name"')).
+ to eq ['attachment', {'filename' => 'strange;name'}]
+
+ expect(RestClient::Utils.cgi_parse_header(
+ 'attachment; filename="strange;name";size=123;')).to eq \
+ ['attachment', {'filename' => 'strange;name', 'size' => '123'}]
+
+ expect(RestClient::Utils.cgi_parse_header(
+ 'form-data; name="files"; filename="fo\\"o;bar"')).to eq \
+ ['form-data', {'name' => 'files', 'filename' => 'fo"o;bar'}]
+ end
+ end
+
+ describe '.encode_query_string' do
+ it 'handles simple hashes' do
+ {
+ {foo: 123, bar: 456} => 'foo=123&bar=456',
+ {'foo' => 123, 'bar' => 456} => 'foo=123&bar=456',
+ {foo: 'abc', bar: 'one two'} => 'foo=abc&bar=one+two',
+ {escaped: '1+2=3'} => 'escaped=1%2B2%3D3',
+ {'escaped + key' => 'foo'} => 'escaped+%2B+key=foo',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles simple arrays' do
+ {
+ {foo: [1, 2, 3]} => 'foo[]=1&foo[]=2&foo[]=3',
+ {foo: %w{a b c}, bar: [1, 2, 3]} => 'foo[]=a&foo[]=b&foo[]=c&bar[]=1&bar[]=2&bar[]=3',
+ {foo: ['one two', 3]} => 'foo[]=one+two&foo[]=3',
+ {'a+b' => [1,2,3]} => 'a%2Bb[]=1&a%2Bb[]=2&a%2Bb[]=3',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles nested hashes' do
+ {
+ {outer: {foo: 123, bar: 456}} => 'outer[foo]=123&outer[bar]=456',
+ {outer: {foo: [1, 2, 3], bar: 'baz'}} => 'outer[foo][]=1&outer[foo][]=2&outer[foo][]=3&outer[bar]=baz',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles null and empty values' do
+ {
+ {string: '', empty: nil, list: [], hash: {}, falsey: false } =>
+ 'string=&empty&list&hash&falsey=false',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles nested nulls' do
+ {
+ {foo: {string: '', empty: nil}} => 'foo[string]=&foo[empty]',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles deep nesting' do
+ {
+ {coords: [{x: 1, y: 0}, {x: 2}, {x: 3}]} => 'coords[][x]=1&coords[][y]=0&coords[][x]=2&coords[][x]=3',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles multiple fields with the same name using ParamsArray' do
+ {
+ RestClient::ParamsArray.new([[:foo, 1], [:foo, 2], [:foo, 3]]) => 'foo=1&foo=2&foo=3',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+
+ it 'handles nested ParamsArrays' do
+ {
+ {foo: RestClient::ParamsArray.new([[:a, 1], [:a, 2]])} => 'foo[a]=1&foo[a]=2',
+ RestClient::ParamsArray.new([[:foo, {a: 1}], [:foo, {a: 2}]]) => 'foo[a]=1&foo[a]=2',
+ }.each_pair do |input, expected|
+ expect(RestClient::Utils.encode_query_string(input)).to eq expected
+ end
+ end
+ end
+end
diff --git a/spec/unit/windows/root_certs_spec.rb b/spec/unit/windows/root_certs_spec.rb
index 12d68ef..6229333 100644
--- a/spec/unit/windows/root_certs_spec.rb
+++ b/spec/unit/windows/root_certs_spec.rb
@@ -1,11 +1,11 @@
-require 'spec_helper'
+require_relative '../_lib'
describe 'RestClient::Windows::RootCerts',
:if => RestClient::Platform.windows? do
let(:x509_store) { RestClient::Windows::RootCerts.instance.to_a }
it 'should return at least one X509 certificate' do
- expect(x509_store.to_a).to have_at_least(1).items
+ expect(x509_store.to_a.size).to be >= 1
end
it 'should return an X509 certificate with a subject' do
@@ -16,7 +16,7 @@ describe 'RestClient::Windows::RootCerts',
it 'should return X509 certificate objects' do
x509_store.each do |cert|
- cert.should be_a(OpenSSL::X509::Certificate)
+ expect(cert).to be_a(OpenSSL::X509::Certificate)
end
end
end
--
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-rest-client.git
More information about the Pkg-ruby-extras-commits
mailing list