mirror of
https://github.com/rest-client/rest-client.git
synced 2022-11-09 13:49:40 -05:00
Merge branch 'master' into prereorg
Conflicts: lib/rest_client.rb lib/restclient/resource.rb rest-client.gemspec spec/rest_client_spec.rb
This commit is contained in:
commit
3425532660
23 changed files with 1182 additions and 754 deletions
69
Rakefile
69
Rakefile
|
@ -1,4 +1,24 @@
|
|||
require 'rake'
|
||||
|
||||
require 'jeweler'
|
||||
|
||||
Jeweler::Tasks.new do |s|
|
||||
s.name = "rest-client"
|
||||
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
||||
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
|
||||
s.author = "Adam Wiggins"
|
||||
s.email = "adam@heroku.com"
|
||||
s.homepage = "http://rest-client.heroku.com/"
|
||||
s.rubyforge_project = "rest-client"
|
||||
s.has_rdoc = true
|
||||
s.files = FileList["[A-Z]*", "{bin,lib,spec}/**/*"]
|
||||
s.executables = %w(restclient)
|
||||
end
|
||||
|
||||
Jeweler::RubyforgeTasks.new
|
||||
|
||||
############################
|
||||
|
||||
require 'spec/rake/spectask'
|
||||
|
||||
desc "Run all specs"
|
||||
|
@ -22,54 +42,9 @@ end
|
|||
|
||||
task :default => :spec
|
||||
|
||||
######################################################
|
||||
############################
|
||||
|
||||
require 'rake'
|
||||
require 'rake/testtask'
|
||||
require 'rake/clean'
|
||||
require 'rake/gempackagetask'
|
||||
require 'rake/rdoctask'
|
||||
require 'fileutils'
|
||||
|
||||
version = "0.8.2"
|
||||
name = "rest-client"
|
||||
|
||||
spec = Gem::Specification.new do |s|
|
||||
s.name = name
|
||||
s.version = version
|
||||
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
|
||||
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
||||
s.author = "Adam Wiggins"
|
||||
s.email = "adam@heroku.com"
|
||||
s.homepage = "http://rest-client.heroku.com/"
|
||||
s.rubyforge_project = "rest-client"
|
||||
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.has_rdoc = true
|
||||
|
||||
s.files = %w(Rakefile) + Dir.glob("{lib,spec}/**/*")
|
||||
s.executables = ['restclient']
|
||||
|
||||
s.require_path = "lib"
|
||||
end
|
||||
|
||||
Rake::GemPackageTask.new(spec) do |p|
|
||||
p.need_tar = true if RUBY_PLATFORM !~ /mswin/
|
||||
end
|
||||
|
||||
task :install => [ :package ] do
|
||||
sh %{sudo gem install pkg/#{name}-#{version}.gem}
|
||||
end
|
||||
|
||||
task :uninstall => [ :clean ] do
|
||||
sh %{sudo gem uninstall #{name}}
|
||||
end
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "spec"
|
||||
t.test_files = FileList['spec/*_spec.rb']
|
||||
t.verbose = true
|
||||
end
|
||||
|
||||
Rake::RDocTask.new do |t|
|
||||
t.rdoc_dir = 'rdoc'
|
||||
|
@ -80,5 +55,3 @@ Rake::RDocTask.new do |t|
|
|||
t.rdoc_files.include('lib/*.rb')
|
||||
end
|
||||
|
||||
CLEAN.include [ 'pkg', '*.gem', '.config' ]
|
||||
|
||||
|
|
1
VERSION
Normal file
1
VERSION
Normal file
|
@ -0,0 +1 @@
|
|||
1.2.1
|
|
@ -1,7 +1,7 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
$:.unshift File.dirname(__FILE__) + "/../lib"
|
||||
require 'rest_client'
|
||||
require 'restclient'
|
||||
|
||||
require "yaml"
|
||||
|
||||
|
|
|
@ -1,293 +1,2 @@
|
|||
require 'uri'
|
||||
require 'net/https'
|
||||
require 'zlib'
|
||||
require 'stringio'
|
||||
|
||||
require File.dirname(__FILE__) + '/rest_client/resource'
|
||||
require File.dirname(__FILE__) + '/rest_client/request_errors'
|
||||
require File.dirname(__FILE__) + '/rest_client/payload'
|
||||
require File.dirname(__FILE__) + '/rest_client/net_http_ext'
|
||||
|
||||
# This module's static methods are the entry point for using the REST client.
|
||||
#
|
||||
# # GET
|
||||
# xml = RestClient.get 'http://example.com/resource'
|
||||
# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
|
||||
#
|
||||
# # authentication and SSL
|
||||
# RestClient.get 'https://user:password@example.com/private/resource'
|
||||
#
|
||||
# # POST or PUT with a hash sends parameters as a urlencoded form body
|
||||
# RestClient.post 'http://example.com/resource', :param1 => 'one'
|
||||
#
|
||||
# # nest hash parameters
|
||||
# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
|
||||
#
|
||||
# # POST and PUT with raw payloads
|
||||
# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
|
||||
# RestClient.post 'http://example.com/resource.xml', xml_doc
|
||||
# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
|
||||
#
|
||||
# # DELETE
|
||||
# RestClient.delete 'http://example.com/resource'
|
||||
#
|
||||
# To use with a proxy, just set RestClient.proxy to the proper http proxy:
|
||||
#
|
||||
# RestClient.proxy = "http://proxy.example.com/"
|
||||
#
|
||||
# Or inherit the proxy from the environment:
|
||||
#
|
||||
# RestClient.proxy = ENV['http_proxy']
|
||||
#
|
||||
# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
|
||||
#
|
||||
# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
|
||||
# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
|
||||
#
|
||||
module RestClient
|
||||
def self.get(url, headers={}, &b)
|
||||
Request.execute(:method => :get,
|
||||
:url => url,
|
||||
:headers => headers, &b)
|
||||
end
|
||||
|
||||
def self.post(url, payload, headers={}, &b)
|
||||
Request.execute(:method => :post,
|
||||
:url => url,
|
||||
:payload => payload,
|
||||
:headers => headers, &b)
|
||||
end
|
||||
|
||||
def self.put(url, payload, headers={}, &b)
|
||||
Request.execute(:method => :put,
|
||||
:url => url,
|
||||
:payload => payload,
|
||||
:headers => headers, &b)
|
||||
end
|
||||
|
||||
def self.delete(url, headers={}, &b)
|
||||
Request.execute(:method => :delete,
|
||||
:url => url,
|
||||
:headers => headers, &b)
|
||||
end
|
||||
|
||||
class <<self
|
||||
attr_accessor :proxy
|
||||
end
|
||||
|
||||
# Print log of RestClient calls. Value can be stdout, stderr, or a filename.
|
||||
# You can also configure logging by the environment variable RESTCLIENT_LOG.
|
||||
def self.log=(log)
|
||||
@@log = log
|
||||
end
|
||||
|
||||
def self.log # :nodoc:
|
||||
return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
|
||||
return @@log if defined? @@log
|
||||
nil
|
||||
end
|
||||
|
||||
# Internal class used to build and execute the request.
|
||||
class Request
|
||||
attr_reader :method, :url, :payload, :headers, :user, :password, :timeout
|
||||
|
||||
def self.execute(args, &b)
|
||||
new(args).execute(&b)
|
||||
end
|
||||
|
||||
def initialize(args)
|
||||
@method = args[:method] or raise ArgumentError, "must pass :method"
|
||||
@url = args[:url] or raise ArgumentError, "must pass :url"
|
||||
@payload = Payload.generate(args[:payload] || '')
|
||||
@headers = args[:headers] || {}
|
||||
@user = args[:user]
|
||||
@password = args[:password]
|
||||
@timeout = args[:timeout]
|
||||
end
|
||||
|
||||
def execute(&b)
|
||||
execute_inner(&b)
|
||||
rescue Redirect => e
|
||||
@url = e.url
|
||||
execute(&b)
|
||||
end
|
||||
|
||||
def execute_inner(&b)
|
||||
uri = parse_url_with_auth(url)
|
||||
transmit(uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload, &b)
|
||||
end
|
||||
|
||||
def make_headers(user_headers)
|
||||
default_headers.merge(user_headers).inject({}) do |final, (key, value)|
|
||||
final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
|
||||
final
|
||||
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)
|
||||
else
|
||||
Net::HTTP
|
||||
end
|
||||
end
|
||||
|
||||
def net_http_request_class(method)
|
||||
Net::HTTP.const_get(method.to_s.capitalize)
|
||||
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 = uri.user if uri.user
|
||||
@password = uri.password if uri.password
|
||||
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 = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
||||
"#{key}=#{value}"
|
||||
end
|
||||
end.join("&")
|
||||
end
|
||||
end
|
||||
|
||||
def transmit(uri, req, payload, &b)
|
||||
setup_credentials(req)
|
||||
|
||||
net = net_http_class.new(uri.host, uri.port)
|
||||
net.use_ssl = uri.is_a?(URI::HTTPS)
|
||||
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
|
||||
display_log request_log
|
||||
|
||||
net.start do |http|
|
||||
http.read_timeout = @timeout if @timeout
|
||||
res = http.request(req, payload)
|
||||
display_log response_log(res)
|
||||
## Ok. I know this is weird but it's a hack for now
|
||||
## this lets process_result determine if it should read the body
|
||||
## into memory or not
|
||||
string = process_result(http.request(req, payload || "", &b), &b)
|
||||
if string
|
||||
Response.new(string, res)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
rescue EOFError
|
||||
raise RestClient::ServerBrokeConnection
|
||||
rescue Timeout::Error
|
||||
raise RestClient::RequestTimeout
|
||||
ensure
|
||||
payload.close
|
||||
end
|
||||
|
||||
def setup_credentials(req)
|
||||
req.basic_auth(user, password) if user
|
||||
end
|
||||
|
||||
def process_result(res, &b)
|
||||
if %w(200 201 202).include? res.code
|
||||
decode(res['content-encoding'], res.body) unless b
|
||||
elsif %w(301 302 303).include? res.code
|
||||
url = res.header['Location']
|
||||
|
||||
if url !~ /^http/
|
||||
uri = URI.parse(@url)
|
||||
uri.path = "/#{url}".squeeze('/')
|
||||
url = uri.to_s
|
||||
end
|
||||
|
||||
raise Redirect, url
|
||||
elsif res.code == "304"
|
||||
raise NotModified
|
||||
elsif res.code == "401"
|
||||
raise Unauthorized, res
|
||||
elsif res.code == "404"
|
||||
raise ResourceNotFound, res
|
||||
else
|
||||
raise RequestFailed, res
|
||||
end
|
||||
end
|
||||
|
||||
def payload
|
||||
@payload
|
||||
|
||||
def decode(content_encoding, body)
|
||||
if content_encoding == 'gzip'
|
||||
Zlib::GzipReader.new(StringIO.new(body)).read
|
||||
elsif content_encoding == 'deflate'
|
||||
Zlib::Inflate.new.inflate(body)
|
||||
else
|
||||
body
|
||||
end
|
||||
end
|
||||
|
||||
def request_log
|
||||
out = []
|
||||
out << "RestClient.#{method} #{url.inspect}"
|
||||
out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
|
||||
out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
|
||||
out.join(', ')
|
||||
end
|
||||
|
||||
def response_log(res)
|
||||
"# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{(res.body) ? res.body.size : nil} bytes"
|
||||
end
|
||||
|
||||
def display_log(msg)
|
||||
return unless log_to = RestClient.log
|
||||
|
||||
if log_to == 'stdout'
|
||||
STDOUT.puts msg
|
||||
elsif log_to == 'stderr'
|
||||
STDERR.puts msg
|
||||
else
|
||||
File.open(log_to, 'a') { |f| f.puts msg }
|
||||
end
|
||||
end
|
||||
|
||||
def default_headers
|
||||
@payload.headers.merge({ :accept => 'application/xml', :accept_encoding => 'gzip, deflate' })
|
||||
end
|
||||
end
|
||||
|
||||
class Response < String
|
||||
attr_reader :net_http_res
|
||||
|
||||
def initialize(string, net_http_res)
|
||||
@net_http_res = net_http_res
|
||||
super string
|
||||
end
|
||||
|
||||
def code
|
||||
@code ||= @net_http_res.code.to_i
|
||||
end
|
||||
|
||||
def headers
|
||||
@headers ||= self.class.beautify_headers(@net_http_res.to_hash)
|
||||
end
|
||||
|
||||
def self.beautify_headers(headers)
|
||||
headers.inject({}) do |out, (key, value)|
|
||||
out[key.gsub(/-/, '_').to_sym] = value.first
|
||||
out
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# This file exists for backward compatbility with require 'rest_client'
|
||||
require File.dirname(__FILE__) + '/restclient'
|
||||
|
|
101
lib/restclient.rb
Normal file
101
lib/restclient.rb
Normal file
|
@ -0,0 +1,101 @@
|
|||
require 'uri'
|
||||
require 'zlib'
|
||||
require 'stringio'
|
||||
|
||||
begin
|
||||
require 'net/https'
|
||||
rescue LoadError => e
|
||||
raise e unless RUBY_PLATFORM =~ /linux/
|
||||
raise LoadError, "no such file to load -- net/https. Try running apt-get install libopenssl-ruby"
|
||||
end
|
||||
|
||||
require File.dirname(__FILE__) + '/restclient/request'
|
||||
require File.dirname(__FILE__) + '/restclient/mixin/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/exceptions'
|
||||
require File.dirname(__FILE__) + '/restclient/payload'
|
||||
require File.dirname(__FILE__) + '/restclient/net_http_ext'
|
||||
|
||||
# This module's static methods are the entry point for using the REST client.
|
||||
#
|
||||
# # GET
|
||||
# xml = RestClient.get 'http://example.com/resource'
|
||||
# jpg = RestClient.get 'http://example.com/resource', :accept => 'image/jpg'
|
||||
#
|
||||
# # authentication and SSL
|
||||
# RestClient.get 'https://user:password@example.com/private/resource'
|
||||
#
|
||||
# # POST or PUT with a hash sends parameters as a urlencoded form body
|
||||
# RestClient.post 'http://example.com/resource', :param1 => 'one'
|
||||
#
|
||||
# # nest hash parameters
|
||||
# RestClient.post 'http://example.com/resource', :nested => { :param1 => 'one' }
|
||||
#
|
||||
# # POST and PUT with raw payloads
|
||||
# RestClient.post 'http://example.com/resource', 'the post body', :content_type => 'text/plain'
|
||||
# RestClient.post 'http://example.com/resource.xml', xml_doc
|
||||
# RestClient.put 'http://example.com/resource.pdf', File.read('my.pdf'), :content_type => 'application/pdf'
|
||||
#
|
||||
# # DELETE
|
||||
# RestClient.delete 'http://example.com/resource'
|
||||
#
|
||||
# # retreive the response http code and headers
|
||||
# res = RestClient.get 'http://example.com/some.jpg'
|
||||
# res.code # => 200
|
||||
# res.headers[:content_type] # => 'image/jpg'
|
||||
#
|
||||
# # HEAD
|
||||
# RestClient.head('http://example.com').headers
|
||||
#
|
||||
# To use with a proxy, just set RestClient.proxy to the proper http proxy:
|
||||
#
|
||||
# RestClient.proxy = "http://proxy.example.com/"
|
||||
#
|
||||
# Or inherit the proxy from the environment:
|
||||
#
|
||||
# RestClient.proxy = ENV['http_proxy']
|
||||
#
|
||||
# For live tests of RestClient, try using http://rest-test.heroku.com, which echoes back information about the rest call:
|
||||
#
|
||||
# >> RestClient.put 'http://rest-test.heroku.com/resource', :foo => 'baz'
|
||||
# => "PUT http://rest-test.heroku.com/resource with a 7 byte payload, content type application/x-www-form-urlencoded {\"foo\"=>\"baz\"}"
|
||||
#
|
||||
module RestClient
|
||||
def self.get(url, headers={})
|
||||
Request.execute(:method => :get, :url => url, :headers => headers)
|
||||
end
|
||||
|
||||
def self.post(url, payload, headers={})
|
||||
Request.execute(:method => :post, :url => url, :payload => payload, :headers => headers)
|
||||
end
|
||||
|
||||
def self.put(url, payload, headers={})
|
||||
Request.execute(:method => :put, :url => url, :payload => payload, :headers => headers)
|
||||
end
|
||||
|
||||
def self.delete(url, headers={})
|
||||
Request.execute(:method => :delete, :url => url, :headers => headers)
|
||||
end
|
||||
|
||||
def self.head(url, headers={})
|
||||
Request.execute(:method => :head, :url => url, :headers => headers)
|
||||
end
|
||||
|
||||
class << self
|
||||
attr_accessor :proxy
|
||||
end
|
||||
|
||||
# Print log of RestClient calls. Value can be stdout, stderr, or a filename.
|
||||
# You can also configure logging by the environment variable RESTCLIENT_LOG.
|
||||
def self.log=(log)
|
||||
@@log = log
|
||||
end
|
||||
|
||||
def self.log # :nodoc:
|
||||
return ENV['RESTCLIENT_LOG'] if ENV['RESTCLIENT_LOG']
|
||||
return @@log if defined? @@log
|
||||
nil
|
||||
end
|
||||
end
|
|
@ -18,6 +18,10 @@ module RestClient
|
|||
def http_code
|
||||
@response.code.to_i if @response
|
||||
end
|
||||
|
||||
def http_body
|
||||
RestClient::Request.decode(@response['content-encoding'], @response.body) if @response
|
||||
end
|
||||
end
|
||||
|
||||
# A redirect was encountered; caught by execute to retry with the new url.
|
||||
|
@ -30,7 +34,7 @@ module RestClient
|
|||
end
|
||||
end
|
||||
|
||||
class NotModified < Exception
|
||||
class NotModified < ExceptionWithResponse
|
||||
ErrorMessage = 'NotModified'
|
||||
end
|
||||
|
||||
|
@ -44,7 +48,9 @@ module RestClient
|
|||
ErrorMessage = 'Resource not found'
|
||||
end
|
||||
|
||||
# The server broke the connection prior to the request completing.
|
||||
# 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.
|
||||
class ServerBrokeConnection < Exception
|
||||
ErrorMessage = 'Server broke connection'
|
||||
end
|
43
lib/restclient/mixin/response.rb
Normal file
43
lib/restclient/mixin/response.rb
Normal file
|
@ -0,0 +1,43 @@
|
|||
module RestClient
|
||||
module Mixin
|
||||
module Response
|
||||
attr_reader :net_http_res
|
||||
|
||||
# HTTP status code, always 200 since RestClient throws exceptions for
|
||||
# other codes.
|
||||
def code
|
||||
@code ||= @net_http_res.code.to_i
|
||||
end
|
||||
|
||||
# A hash of the headers, beautified with symbols and underscores.
|
||||
# e.g. "Content-type" will become :content_type.
|
||||
def headers
|
||||
@headers ||= self.class.beautify_headers(@net_http_res.to_hash)
|
||||
end
|
||||
|
||||
# Hash of cookies extracted from response headers
|
||||
def cookies
|
||||
@cookies ||= (self.headers[:set_cookie] || "").split('; ').inject({}) do |out, raw_c|
|
||||
key, val = raw_c.split('=')
|
||||
unless %w(expires domain path secure).member?(key)
|
||||
out[key] = val
|
||||
end
|
||||
out
|
||||
end
|
||||
end
|
||||
|
||||
def self.included(receiver)
|
||||
receiver.extend(RestClient::Mixin::Response::ClassMethods)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def beautify_headers(headers)
|
||||
headers.inject({}) do |out, (key, value)|
|
||||
out[key.gsub(/-/, '_').to_sym] = value.first
|
||||
out
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,14 +8,24 @@ module RestClient
|
|||
def generate(params)
|
||||
if params.is_a?(String)
|
||||
Base.new(params)
|
||||
elsif params.delete(:multipart) == true ||
|
||||
params.any? { |_,v| v.respond_to?(:path) && v.respond_to?(:read) }
|
||||
elsif params.delete(:multipart) == true || has_file?(params)
|
||||
Multipart.new(params)
|
||||
else
|
||||
UrlEncoded.new(params)
|
||||
end
|
||||
end
|
||||
|
||||
def has_file?(params)
|
||||
params.any? do |_, v|
|
||||
case v
|
||||
when Hash
|
||||
has_file?(v)
|
||||
else
|
||||
v.respond_to?(:path) && v.respond_to?(:read)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class Base
|
||||
def initialize(params)
|
||||
build_stream(params)
|
||||
|
@ -92,7 +102,7 @@ module RestClient
|
|||
|
||||
def create_file_field(s, k, v)
|
||||
begin
|
||||
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{v.path}\"#{EOL}")
|
||||
s.write("Content-Disposition: multipart/form-data; name=\"#{k}\"; filename=\"#{File.basename(v.path)}\"#{EOL}")
|
||||
s.write("Content-Type: #{mime_for(v.path)}#{EOL}")
|
||||
s.write(EOL)
|
||||
while data = v.read(8124)
|
30
lib/restclient/raw_response.rb
Normal file
30
lib/restclient/raw_response.rb
Normal file
|
@ -0,0 +1,30 @@
|
|||
require File.dirname(__FILE__) + '/mixin/response'
|
||||
|
||||
module RestClient
|
||||
# The response from RestClient on a raw request looks like a string, but is
|
||||
# actually one of these. 99% of the time you're making a rest call all you
|
||||
# care about is the body, but on the occassion you want to fetch the
|
||||
# headers you can:
|
||||
#
|
||||
# RestClient.get('http://example.com').headers[:content_type]
|
||||
#
|
||||
# In addition, if you do not use the response as a string, you can access
|
||||
# a Tempfile object at res.file, which contains the path to the raw
|
||||
# downloaded request body.
|
||||
class RawResponse
|
||||
include RestClient::Mixin::Response
|
||||
|
||||
attr_reader :file
|
||||
|
||||
def initialize(tempfile, net_http_res)
|
||||
@net_http_res = net_http_res
|
||||
@file = tempfile
|
||||
end
|
||||
|
||||
def to_s
|
||||
@file.open
|
||||
@file.read
|
||||
end
|
||||
|
||||
end
|
||||
end
|
238
lib/restclient/request.rb
Normal file
238
lib/restclient/request.rb
Normal file
|
@ -0,0 +1,238 @@
|
|||
require 'tempfile'
|
||||
|
||||
module RestClient
|
||||
# This class is used internally by RestClient to send the request, but you can also
|
||||
# access it internally if you'd like to use a method not directly supported by the
|
||||
# main API. For example:
|
||||
#
|
||||
# RestClient::Request.execute(:method => :head, :url => 'http://example.com')
|
||||
#
|
||||
class Request
|
||||
attr_reader :method, :url, :payload, :headers,
|
||||
:cookies, :user, :password, :timeout, :open_timeout,
|
||||
:verify_ssl, :ssl_client_cert, :ssl_client_key, :ssl_ca_file,
|
||||
:raw_response
|
||||
|
||||
def self.execute(args)
|
||||
new(args).execute
|
||||
end
|
||||
|
||||
def initialize(args)
|
||||
@method = args[:method] or raise ArgumentError, "must pass :method"
|
||||
@url = args[:url] or raise ArgumentError, "must pass :url"
|
||||
@headers = args[:headers] || {}
|
||||
@cookies = @headers.delete(:cookies) || args[:cookies] || {}
|
||||
@payload = process_payload(args[:payload])
|
||||
@user = args[:user]
|
||||
@password = args[:password]
|
||||
@timeout = args[:timeout]
|
||||
@open_timeout = args[:open_timeout]
|
||||
@raw_response = args[:raw_response] || false
|
||||
@verify_ssl = args[:verify_ssl] || false
|
||||
@ssl_client_cert = args[:ssl_client_cert] || nil
|
||||
@ssl_client_key = args[:ssl_client_key] || nil
|
||||
@ssl_ca_file = args[:ssl_ca_file] || nil
|
||||
@tf = nil # If you are a raw request, this is your tempfile
|
||||
end
|
||||
|
||||
def execute
|
||||
execute_inner
|
||||
rescue Redirect => e
|
||||
@url = e.url
|
||||
@method = :get
|
||||
@payload = nil
|
||||
execute
|
||||
end
|
||||
|
||||
def execute_inner
|
||||
uri = parse_url_with_auth(url)
|
||||
transmit uri, net_http_request_class(method).new(uri.request_uri, make_headers(headers)), payload
|
||||
end
|
||||
|
||||
def make_headers(user_headers)
|
||||
unless @cookies.empty?
|
||||
user_headers[:cookie] = @cookies.map {|key, val| "#{key.to_s}=#{val}" }.join('; ')
|
||||
end
|
||||
|
||||
default_headers.merge(user_headers).inject({}) do |final, (key, value)|
|
||||
final[key.to_s.gsub(/_/, '-').capitalize] = value.to_s
|
||||
final
|
||||
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)
|
||||
else
|
||||
Net::HTTP
|
||||
end
|
||||
end
|
||||
|
||||
def net_http_request_class(method)
|
||||
Net::HTTP.const_get(method.to_s.capitalize)
|
||||
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 = uri.user if uri.user
|
||||
@password = uri.password if uri.password
|
||||
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 = URI.escape(p[k].to_s, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
|
||||
"#{key}=#{value}"
|
||||
end
|
||||
end.join("&")
|
||||
end
|
||||
end
|
||||
|
||||
def transmit(uri, req, payload)
|
||||
setup_credentials(req)
|
||||
|
||||
net = net_http_class.new(uri.host, uri.port)
|
||||
net.use_ssl = uri.is_a?(URI::HTTPS)
|
||||
if @verify_ssl == false
|
||||
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
||||
elsif @verify_ssl.is_a? Integer
|
||||
net.verify_mode = @verify_ssl
|
||||
end
|
||||
net.cert = @ssl_client_cert if @ssl_client_cert
|
||||
net.key = @ssl_client_key if @ssl_client_key
|
||||
net.ca_file = @ssl_ca_file if @ssl_ca_file
|
||||
net.read_timeout = @timeout if @timeout
|
||||
net.open_timeout = @open_timeout if @open_timeout
|
||||
|
||||
display_log request_log
|
||||
|
||||
net.start do |http|
|
||||
res = http.request(req, payload) { |http_response| fetch_body(http_response) }
|
||||
result = process_result(res)
|
||||
display_log response_log(res)
|
||||
|
||||
if result.kind_of?(String) or @method == :head
|
||||
Response.new(result, res)
|
||||
elsif @raw_response
|
||||
RawResponse.new(@tf, res)
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
rescue EOFError
|
||||
raise RestClient::ServerBrokeConnection
|
||||
rescue Timeout::Error
|
||||
raise RestClient::RequestTimeout
|
||||
end
|
||||
|
||||
def setup_credentials(req)
|
||||
req.basic_auth(user, password) if user
|
||||
end
|
||||
|
||||
def fetch_body(http_response)
|
||||
if @raw_response
|
||||
# Taken from Chef, which as in turn...
|
||||
# Stolen from http://www.ruby-forum.com/topic/166423
|
||||
# Kudos to _why!
|
||||
@tf = Tempfile.new("rest-client")
|
||||
size, total = 0, http_response.header['Content-Length'].to_i
|
||||
http_response.read_body do |chunk|
|
||||
@tf.write(chunk)
|
||||
size += chunk.size
|
||||
if size == 0
|
||||
display_log("#{@method} #{@url} done (0 length file)")
|
||||
elsif total == 0
|
||||
display_log("#{@method} #{@url} (zero content length)")
|
||||
else
|
||||
display_log("#{@method} #{@url} %d%% done (%d of %d)" % [(size * 100) / total, size, total])
|
||||
end
|
||||
end
|
||||
@tf.close
|
||||
@tf
|
||||
else
|
||||
http_response.read_body
|
||||
end
|
||||
http_response
|
||||
end
|
||||
|
||||
def process_result(res)
|
||||
if res.code =~ /\A2\d{2}\z/
|
||||
# We don't decode raw requests
|
||||
unless @raw_response
|
||||
self.class.decode res['content-encoding'], res.body if res.body
|
||||
end
|
||||
elsif %w(301 302 303).include? res.code
|
||||
url = res.header['Location']
|
||||
|
||||
if url !~ /^http/
|
||||
uri = URI.parse(@url)
|
||||
uri.path = "/#{url}".squeeze('/')
|
||||
url = uri.to_s
|
||||
end
|
||||
|
||||
raise Redirect, url
|
||||
elsif res.code == "304"
|
||||
raise NotModified, res
|
||||
elsif res.code == "401"
|
||||
raise Unauthorized, res
|
||||
elsif res.code == "404"
|
||||
raise ResourceNotFound, res
|
||||
else
|
||||
raise RequestFailed, res
|
||||
end
|
||||
end
|
||||
|
||||
def self.decode(content_encoding, body)
|
||||
if content_encoding == 'gzip' and not body.empty?
|
||||
Zlib::GzipReader.new(StringIO.new(body)).read
|
||||
elsif content_encoding == 'deflate'
|
||||
Zlib::Inflate.new.inflate(body)
|
||||
else
|
||||
body
|
||||
end
|
||||
end
|
||||
|
||||
def request_log
|
||||
out = []
|
||||
out << "RestClient.#{method} #{url.inspect}"
|
||||
out << (payload.size > 100 ? "(#{payload.size} byte payload)".inspect : payload.inspect) if payload
|
||||
out << headers.inspect.gsub(/^\{/, '').gsub(/\}$/, '') unless headers.empty?
|
||||
out.join(', ')
|
||||
end
|
||||
|
||||
def response_log(res)
|
||||
size = @raw_response ? File.size(@tf.path) : (res.body.nil? ? 0 : res.body.size)
|
||||
"# => #{res.code} #{res.class.to_s.gsub(/^Net::HTTP/, '')} | #{(res['Content-type'] || '').gsub(/;.*$/, '')} #{size} bytes"
|
||||
end
|
||||
|
||||
def display_log(msg)
|
||||
return unless log_to = RestClient.log
|
||||
|
||||
if log_to == 'stdout'
|
||||
STDOUT.puts msg
|
||||
elsif log_to == 'stderr'
|
||||
STDERR.puts msg
|
||||
else
|
||||
File.open(log_to, 'a') { |f| f.puts msg }
|
||||
end
|
||||
end
|
||||
|
||||
def default_headers
|
||||
{ :accept => '*/*; q=0.5, application/xml', :accept_encoding => 'gzip, deflate' }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -16,6 +16,10 @@ module RestClient
|
|||
#
|
||||
# RestClient::Resource.new('http://slow', :timeout => 10)
|
||||
#
|
||||
# With an open timeout (seconds):
|
||||
#
|
||||
# RestClient::Resource.new('http://behindfirewall', :open_timeout => 10)
|
||||
#
|
||||
# You can also use resources to share common headers. For headers keys,
|
||||
# symbols are converted to strings. Example:
|
||||
#
|
||||
|
@ -89,9 +93,10 @@ module RestClient
|
|||
|
||||
def timeout
|
||||
options[:timeout]
|
||||
:user => user,
|
||||
:password => password,
|
||||
:headers => headers, &b)
|
||||
end
|
||||
|
||||
def open_timeout
|
||||
options[:open_timeout]
|
||||
end
|
||||
|
||||
# Construct a subresource, preserving authentication.
|
20
lib/restclient/response.rb
Normal file
20
lib/restclient/response.rb
Normal file
|
@ -0,0 +1,20 @@
|
|||
require File.dirname(__FILE__) + '/mixin/response'
|
||||
|
||||
module RestClient
|
||||
# The response from RestClient looks like a string, but is actually one of
|
||||
# these. 99% of the time you're making a rest call all you care about is
|
||||
# the body, but on the occassion you want to fetch the headers you can:
|
||||
#
|
||||
# RestClient.get('http://example.com').headers[:content_type]
|
||||
#
|
||||
class Response < String
|
||||
|
||||
include RestClient::Mixin::Response
|
||||
|
||||
def initialize(string, net_http_res)
|
||||
@net_http_res = net_http_res
|
||||
super(string || "")
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -1,18 +1,70 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
|
||||
Gem::Specification.new do |s|
|
||||
s.name = "rest-client"
|
||||
s.version = "0.8.2"
|
||||
s.summary = "Simple REST client for Ruby, inspired by microframework syntax for specifying actions."
|
||||
s.description = "A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete."
|
||||
s.author = "Adam Wiggins"
|
||||
s.email = "adam@heroku.com"
|
||||
s.rubyforge_project = "rest-client"
|
||||
s.homepage = "http://rest-client.heroku.com/"
|
||||
s.has_rdoc = true
|
||||
s.platform = Gem::Platform::RUBY
|
||||
s.files = %w(Rakefile README.rdoc rest-client.gemspec
|
||||
lib/request_errors.rb lib/resource.rb lib/rest_client.rb
|
||||
spec/base.rb spec/request_errors_spec.rb spec/resource_spec.rb spec/rest_client_spec.rb
|
||||
bin/restclient)
|
||||
s.executables = ['restclient']
|
||||
s.require_path = "lib"
|
||||
s.name = %q{rest-client}
|
||||
s.version = "1.1.0"
|
||||
|
||||
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
||||
s.authors = ["Adam Wiggins"]
|
||||
s.date = %q{2009-08-12}
|
||||
s.default_executable = %q{restclient}
|
||||
s.description = %q{A simple REST client for Ruby, inspired by the Sinatra microframework style of specifying actions: get, put, post, delete.}
|
||||
s.email = %q{adam@heroku.com}
|
||||
s.executables = ["restclient"]
|
||||
s.extra_rdoc_files = [
|
||||
"README.rdoc"
|
||||
]
|
||||
s.files = [
|
||||
"README.rdoc",
|
||||
"Rakefile",
|
||||
"VERSION",
|
||||
"bin/restclient",
|
||||
"lib/rest_client.rb",
|
||||
"lib/restclient.rb",
|
||||
"lib/restclient/exceptions.rb",
|
||||
"lib/restclient/mixin/response.rb",
|
||||
"lib/restclient/net_http_ext.rb",
|
||||
"lib/restclient/payload.rb",
|
||||
"lib/restclient/raw_response.rb",
|
||||
"lib/restclient/request.rb",
|
||||
"lib/restclient/resource.rb",
|
||||
"lib/restclient/response.rb",
|
||||
"spec/base.rb",
|
||||
"spec/exceptions_spec.rb",
|
||||
"spec/master_shake.jpg",
|
||||
"spec/mixin/response_spec.rb",
|
||||
"spec/payload_spec.rb",
|
||||
"spec/raw_response_spec.rb",
|
||||
"spec/request_spec.rb",
|
||||
"spec/resource_spec.rb",
|
||||
"spec/response_spec.rb",
|
||||
"spec/restclient_spec.rb"
|
||||
]
|
||||
s.homepage = %q{http://rest-client.heroku.com/}
|
||||
s.rdoc_options = ["--charset=UTF-8"]
|
||||
s.require_paths = ["lib"]
|
||||
s.rubyforge_project = %q{rest-client}
|
||||
s.rubygems_version = %q{1.3.5}
|
||||
s.summary = %q{Simple REST client for Ruby, inspired by microframework syntax for specifying actions.}
|
||||
s.test_files = [
|
||||
"spec/base.rb",
|
||||
"spec/exceptions_spec.rb",
|
||||
"spec/mixin/response_spec.rb",
|
||||
"spec/payload_spec.rb",
|
||||
"spec/raw_response_spec.rb",
|
||||
"spec/request_spec.rb",
|
||||
"spec/resource_spec.rb",
|
||||
"spec/response_spec.rb",
|
||||
"spec/restclient_spec.rb"
|
||||
]
|
||||
|
||||
if s.respond_to? :specification_version then
|
||||
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
||||
s.specification_version = 3
|
||||
|
||||
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
||||
else
|
||||
end
|
||||
else
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
require 'rubygems'
|
||||
require 'spec'
|
||||
|
||||
require File.dirname(__FILE__) + '/../lib/rest_client'
|
||||
|
||||
require File.dirname(__FILE__) + '/../lib/restclient'
|
||||
|
|
|
@ -12,6 +12,10 @@ describe RestClient::Exception do
|
|||
end
|
||||
|
||||
describe RestClient::RequestFailed do
|
||||
before do
|
||||
@response = mock('HTTP Response', :code => '502')
|
||||
end
|
||||
|
||||
it "stores the http response on the exception" do
|
||||
begin
|
||||
raise RestClient::RequestFailed, :response
|
||||
|
@ -21,11 +25,18 @@ describe RestClient::RequestFailed do
|
|||
end
|
||||
|
||||
it "http_code convenience method for fetching the code as an integer" do
|
||||
RestClient::RequestFailed.new(mock('res', :code => '502')).http_code.should == 502
|
||||
RestClient::RequestFailed.new(@response).http_code.should == 502
|
||||
end
|
||||
|
||||
it "http_body convenience method for fetching the body (decoding when necessary)" do
|
||||
@response.stub!(:[]).with('content-encoding').and_return('gzip')
|
||||
@response.stub!(:body).and_return('compressed body')
|
||||
RestClient::Request.should_receive(:decode).with('gzip', 'compressed body').and_return('plain body')
|
||||
RestClient::RequestFailed.new(@response).http_body.should == 'plain body'
|
||||
end
|
||||
|
||||
it "shows the status code in the message" do
|
||||
RestClient::RequestFailed.new(mock('res', :code => '502')).to_s.should match(/502/)
|
||||
RestClient::RequestFailed.new(@response).to_s.should match(/502/)
|
||||
end
|
||||
end
|
||||
|
46
spec/mixin/response_spec.rb
Normal file
46
spec/mixin/response_spec.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require File.dirname(__FILE__) + '/../base'
|
||||
|
||||
class MockResponse
|
||||
include RestClient::Mixin::Response
|
||||
|
||||
def initialize(body, res)
|
||||
@net_http_res = res
|
||||
@body = @body
|
||||
end
|
||||
end
|
||||
|
||||
describe RestClient::Mixin::Response do
|
||||
before do
|
||||
@net_http_res = mock('net http response')
|
||||
@response = MockResponse.new('abc', @net_http_res)
|
||||
end
|
||||
|
||||
it "fetches the numeric response code" do
|
||||
@net_http_res.should_receive(:code).and_return('200')
|
||||
@response.code.should == 200
|
||||
end
|
||||
|
||||
it "beautifies the headers by turning the keys to symbols" do
|
||||
h = RestClient::Response.beautify_headers('content-type' => [ 'x' ])
|
||||
h.keys.first.should == :content_type
|
||||
end
|
||||
|
||||
it "beautifies the headers by turning the values to strings instead of one-element arrays" do
|
||||
h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] )
|
||||
h.values.first.should == 'text/html'
|
||||
end
|
||||
|
||||
it "fetches the headers" do
|
||||
@net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
|
||||
@response.headers.should == { :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 == { 'session_id' => '1' }
|
||||
end
|
||||
|
||||
it "can access the net http result directly" do
|
||||
@response.net_http_res.should == @net_http_res
|
||||
end
|
||||
end
|
|
@ -36,7 +36,7 @@ EOS
|
|||
m = RestClient::Payload::Multipart.new({:foo => f})
|
||||
m.to_s.should == <<-EOS
|
||||
--#{m.boundary}\r
|
||||
Content-Disposition: multipart/form-data; name="foo"; filename="./spec/master_shake.jpg"\r
|
||||
Content-Disposition: multipart/form-data; name="foo"; filename="master_shake.jpg"\r
|
||||
Content-Type: image/jpeg\r
|
||||
\r
|
||||
#{IO.read(f.path)}\r
|
||||
|
@ -63,5 +63,10 @@ EOS
|
|||
it "should return data if no of the above" do
|
||||
RestClient::Payload.generate("data").should be_kind_of(RestClient::Payload::Base)
|
||||
end
|
||||
|
||||
it "should recognize nested multipart payloads" do
|
||||
f = File.new(File.dirname(__FILE__) + "/master_shake.jpg")
|
||||
RestClient::Payload.generate({"foo" => {"file" => f}}).should be_kind_of(RestClient::Payload::Multipart)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
17
spec/raw_response_spec.rb
Normal file
17
spec/raw_response_spec.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
require File.dirname(__FILE__) + '/base'
|
||||
|
||||
describe RestClient::RawResponse do
|
||||
before do
|
||||
@tf = mock("Tempfile", :read => "the answer is 42", :open => true)
|
||||
@net_http_res = mock('net http response')
|
||||
@response = RestClient::RawResponse.new(@tf, @net_http_res)
|
||||
end
|
||||
|
||||
it "behaves like string" do
|
||||
@response.to_s.should == 'the answer is 42'
|
||||
end
|
||||
|
||||
it "exposes a Tempfile" do
|
||||
@response.file.should == @tf
|
||||
end
|
||||
end
|
476
spec/request_spec.rb
Normal file
476
spec/request_spec.rb
Normal file
|
@ -0,0 +1,476 @@
|
|||
require File.dirname(__FILE__) + '/base'
|
||||
|
||||
describe RestClient::Request do
|
||||
before do
|
||||
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
|
||||
|
||||
@uri = mock("uri")
|
||||
@uri.stub!(:request_uri).and_return('/resource')
|
||||
@uri.stub!(:host).and_return('some')
|
||||
@uri.stub!(:port).and_return(80)
|
||||
|
||||
@net = mock("net::http base")
|
||||
@http = mock("net::http connection")
|
||||
Net::HTTP.stub!(:new).and_return(@net)
|
||||
@net.stub!(:start).and_yield(@http)
|
||||
@net.stub!(:use_ssl=)
|
||||
@net.stub!(:verify_mode=)
|
||||
end
|
||||
|
||||
it "accept */* mimetype, preferring xml" do
|
||||
@request.default_headers[:accept].should == '*/*; q=0.5, application/xml'
|
||||
end
|
||||
|
||||
it "decodes an uncompressed result body by passing it straight through" do
|
||||
RestClient::Request.decode(nil, 'xyz').should == 'xyz'
|
||||
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 == "i'm gziped\n"
|
||||
end
|
||||
|
||||
it "ingores gzip for empty bodies" do
|
||||
RestClient::Request.decode('gzip', '').should 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 == "some deflated text"
|
||||
end
|
||||
|
||||
it "processes a successful result" do
|
||||
res = mock("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).should == 'body'
|
||||
end
|
||||
|
||||
it "doesn't classify successful requests as failed" do
|
||||
203.upto(206) do |code|
|
||||
res = mock("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
|
||||
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
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
it "extracts the username and password when parsing http://user:password@example.com/" do
|
||||
URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
|
||||
@request.parse_url_with_auth('http://joe:pass1@example.com/resource')
|
||||
@request.user.should == 'joe'
|
||||
@request.password.should == '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(mock('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 == 'beth'
|
||||
@request.password.should == 'pass2'
|
||||
end
|
||||
|
||||
it "correctly formats cookies provided to the constructor" do
|
||||
URI.stub!(:parse).and_return(mock('uri', :user => nil, :password => nil))
|
||||
@request = RestClient::Request.new(:method => 'get', :url => 'example.com', :cookies => {:session_id => '1' })
|
||||
@request.should_receive(:default_headers).and_return({'foo' => 'bar'})
|
||||
headers = @request.make_headers({}).should == { 'Foo' => 'bar', 'Cookie' => 'session_id=1'}
|
||||
end
|
||||
|
||||
it "determines the Net::HTTP class to instantiate by the method name" do
|
||||
@request.net_http_request_class(:put).should == Net::HTTP::Put
|
||||
end
|
||||
|
||||
it "merges user headers with the default headers" do
|
||||
@request.should_receive(:default_headers).and_return({ '1' => '2' })
|
||||
@request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
|
||||
end
|
||||
|
||||
it "prefers the user header when the same header exists in the defaults" do
|
||||
@request.should_receive(:default_headers).and_return({ '1' => '2' })
|
||||
@request.make_headers('1' => '3').should == { '1' => '3' }
|
||||
end
|
||||
|
||||
it "converts header symbols from :content_type to 'Content-type'" do
|
||||
@request.should_receive(:default_headers).and_return({})
|
||||
@request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
|
||||
end
|
||||
|
||||
it "converts header values to strings" do
|
||||
@request.make_headers('A' => 1)['A'].should == '1'
|
||||
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 = mock("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', 'payload')
|
||||
@request.execute_inner
|
||||
end
|
||||
|
||||
it "transmits the request with Net::HTTP" do
|
||||
@http.should_receive(:request).with('req', 'payload')
|
||||
@request.should_receive(:process_result)
|
||||
@request.should_receive(:response_log)
|
||||
@request.transmit(@uri, 'req', 'payload')
|
||||
end
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
it "passes non-hash payloads straight through" do
|
||||
@request.process_payload("x").should == "x"
|
||||
end
|
||||
|
||||
it "converts a hash payload to urlencoded data" do
|
||||
@request.process_payload(:a => 'b c+d').should == "a=b%20c%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')
|
||||
end
|
||||
|
||||
it "set urlencoded content_type header on hash payloads" do
|
||||
@request.process_payload(:a => 1)
|
||||
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
|
||||
end
|
||||
|
||||
it "sets up the credentials prior to the request" do
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
|
||||
@request.stub!(:user).and_return('joe')
|
||||
@request.stub!(:password).and_return('mypass')
|
||||
@request.should_receive(:setup_credentials).with('req')
|
||||
|
||||
@request.transmit(@uri, 'req', nil)
|
||||
end
|
||||
|
||||
it "does not attempt to send any credentials if user is nil" do
|
||||
@request.stub!(:user).and_return(nil)
|
||||
req = mock("request")
|
||||
req.should_not_receive(:basic_auth)
|
||||
@request.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')
|
||||
req = mock("request")
|
||||
req.should_receive(:basic_auth).with('joe', 'mypass')
|
||||
@request.setup_credentials(req)
|
||||
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)
|
||||
end
|
||||
|
||||
it "execute calls execute_inner" do
|
||||
@request.should_receive(:execute_inner)
|
||||
@request.execute
|
||||
end
|
||||
|
||||
it "class method execute wraps constructor" do
|
||||
req = mock("rest request")
|
||||
RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
|
||||
req.should_receive(:execute)
|
||||
RestClient::Request.execute(1 => 2)
|
||||
end
|
||||
|
||||
it "raises a Redirect with the new location when the response is in the 30x range" do
|
||||
res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
|
||||
end
|
||||
|
||||
it "handles redirects with relative paths" do
|
||||
res = mock('response', :code => '301', :header => { 'Location' => 'index' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
|
||||
end
|
||||
|
||||
it "handles redirects with absolute paths" do
|
||||
@request.instance_variable_set('@url', 'http://some/place/else')
|
||||
res = mock('response', :code => '301', :header => { 'Location' => '/index' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
|
||||
end
|
||||
|
||||
it "uses GET and clears payload when following 30x redirects" do
|
||||
url = "http://example.com/redirected"
|
||||
|
||||
@request.should_receive(:execute_inner).once.ordered.and_raise(RestClient::Redirect.new(url))
|
||||
|
||||
@request.should_receive(:execute_inner).once.ordered do
|
||||
@request.url.should == url
|
||||
@request.method.should == :get
|
||||
@request.payload.should be_nil
|
||||
end
|
||||
|
||||
@request.execute
|
||||
end
|
||||
|
||||
it "raises Unauthorized when the response is 401" do
|
||||
res = mock('response', :code => '401')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
|
||||
end
|
||||
|
||||
it "raises ResourceNotFound when the response is 404" do
|
||||
res = mock('response', :code => '404')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
|
||||
end
|
||||
|
||||
it "raises RequestFailed otherwise" do
|
||||
res = mock('response', :code => '500')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
|
||||
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.should include(Net::HTTP::ProxyDelta)
|
||||
end
|
||||
|
||||
it "creates a non-proxy class if a proxy url is not given" do
|
||||
@request.net_http_class.should_not include(Net::HTTP::ProxyDelta)
|
||||
end
|
||||
|
||||
it "logs a get request" do
|
||||
RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
|
||||
'RestClient.get "http://url"'
|
||||
end
|
||||
|
||||
it "logs a post request with a small payload" do
|
||||
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
|
||||
'RestClient.post "http://url", "foo"'
|
||||
end
|
||||
|
||||
it "logs a post request with a large payload" do
|
||||
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
|
||||
'RestClient.post "http://url", "(1000 byte payload)"'
|
||||
end
|
||||
|
||||
it "logs input headers as a hash" do
|
||||
RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
|
||||
'RestClient.get "http://url", :accept=>"text/plain"'
|
||||
end
|
||||
|
||||
it "logs a response including the status code, content type, and result body size in bytes" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return('text/html')
|
||||
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
|
||||
end
|
||||
|
||||
it "logs a response with a nil Content-type" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return(nil)
|
||||
@request.response_log(res).should == "# => 200 OK | 4 bytes"
|
||||
end
|
||||
|
||||
it "logs a response with a nil body" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => nil)
|
||||
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
|
||||
@request.response_log(res).should == "# => 200 OK | text/html 0 bytes"
|
||||
end
|
||||
|
||||
it "strips the charset from the response content type" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
|
||||
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
|
||||
end
|
||||
|
||||
it "displays the log to stdout" do
|
||||
RestClient.stub!(:log).and_return('stdout')
|
||||
STDOUT.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
end
|
||||
|
||||
it "displays the log to stderr" do
|
||||
RestClient.stub!(:log).and_return('stderr')
|
||||
STDERR.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
end
|
||||
|
||||
it "append the log to the requested filename" do
|
||||
RestClient.stub!(:log).and_return('/tmp/restclient.log')
|
||||
f = mock('file handle')
|
||||
File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
|
||||
f.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
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)
|
||||
|
||||
@net.should_receive(:read_timeout=).with(123)
|
||||
|
||||
@request.transmit(@uri, 'req', nil)
|
||||
end
|
||||
|
||||
it "set 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)
|
||||
|
||||
@net.should_receive(:open_timeout=).with(123)
|
||||
|
||||
@request.transmit(@uri, 'req', nil)
|
||||
end
|
||||
|
||||
it "should default to not verifying ssl certificates" do
|
||||
@request.verify_ssl.should == false
|
||||
end
|
||||
|
||||
it "should set net.verify_mode to OpenSSL::SSL::VERIFY_NONE if verify_ssl is false" do
|
||||
@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')
|
||||
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')
|
||||
end
|
||||
|
||||
it "should set net.verify_mode to the passed value if verify_ssl is an OpenSSL constant" do
|
||||
mode = OpenSSL::SSL::VERIFY_PEER |
|
||||
OpenSSL::SSL::VERIFY_FAIL_IF_NO_PEER_CERT
|
||||
@request = RestClient::Request.new( :method => :put,
|
||||
: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')
|
||||
end
|
||||
|
||||
it "should default to not having an ssl_client_cert" do
|
||||
@request.ssl_client_cert.should be(nil)
|
||||
end
|
||||
|
||||
it "should set the ssl_client_cert if provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
: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')
|
||||
end
|
||||
|
||||
it "should not set the ssl_client_cert if it is not provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
:payload => 'payload'
|
||||
)
|
||||
@net.should_not_receive(:cert=).with("whatsupdoc!")
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
@request.transmit(@uri, 'req', 'payload')
|
||||
end
|
||||
|
||||
it "should default to not having an ssl_client_key" do
|
||||
@request.ssl_client_key.should be(nil)
|
||||
end
|
||||
|
||||
it "should set the ssl_client_key if provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
: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')
|
||||
end
|
||||
|
||||
it "should not set the ssl_client_key if it is not provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
:payload => 'payload'
|
||||
)
|
||||
@net.should_not_receive(:key=).with("whatsupdoc!")
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
@request.transmit(@uri, 'req', 'payload')
|
||||
end
|
||||
|
||||
it "should default to not having an ssl_ca_file" do
|
||||
@request.ssl_ca_file.should be(nil)
|
||||
end
|
||||
|
||||
it "should set the ssl_ca_file if provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
:payload => 'payload',
|
||||
:ssl_ca_file => "Certificate Authority File"
|
||||
)
|
||||
@net.should_receive(:ca_file=).with("Certificate Authority File")
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
@request.transmit(@uri, 'req', 'payload')
|
||||
end
|
||||
|
||||
it "should not set the ssl_ca_file if it is not provided" do
|
||||
@request = RestClient::Request.new(
|
||||
:method => :put,
|
||||
:url => 'https://some/resource',
|
||||
:payload => 'payload'
|
||||
)
|
||||
@net.should_not_receive(:ca_file=).with("Certificate Authority File")
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
@request.transmit(@uri, 'req', 'payload')
|
||||
end
|
||||
end
|
16
spec/response_spec.rb
Normal file
16
spec/response_spec.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
require File.dirname(__FILE__) + '/base'
|
||||
|
||||
describe RestClient::Response do
|
||||
before do
|
||||
@net_http_res = mock('net http response')
|
||||
@response = RestClient::Response.new('abc', @net_http_res)
|
||||
end
|
||||
|
||||
it "behaves like string" do
|
||||
@response.should == 'abc'
|
||||
end
|
||||
|
||||
it "accepts nil strings and sets it to empty for the case of HEAD" do
|
||||
RestClient::Response.new(nil, @net_http_res).should == ""
|
||||
end
|
||||
end
|
|
@ -1,383 +0,0 @@
|
|||
require File.dirname(__FILE__) + '/base'
|
||||
|
||||
def generate_payload(v)
|
||||
RestClient::Payload::Base.new(v)
|
||||
end
|
||||
|
||||
describe RestClient do
|
||||
context "public API" do
|
||||
it "GET" do
|
||||
RestClient::Request.should_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 => {})
|
||||
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 => {})
|
||||
RestClient.put('http://some/resource', 'payload')
|
||||
end
|
||||
|
||||
it "DELETE" do
|
||||
RestClient::Request.should_receive(:execute).with(:method => :delete, :url => 'http://some/resource', :headers => {})
|
||||
RestClient.delete('http://some/resource')
|
||||
end
|
||||
end
|
||||
|
||||
context "logging" do
|
||||
after do
|
||||
RestClient.log = nil
|
||||
end
|
||||
|
||||
it "gets the log source from the RESTCLIENT_LOG environment variable" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return('from env')
|
||||
RestClient.log = 'from class method'
|
||||
RestClient.log.should == 'from env'
|
||||
end
|
||||
|
||||
it "sets a destination for log output, used if no environment variable is set" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
|
||||
RestClient.log = 'from class method'
|
||||
RestClient.log.should == 'from class method'
|
||||
end
|
||||
|
||||
it "returns nil (no logging) if neither are set (default)" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
|
||||
RestClient.log.should == nil
|
||||
end
|
||||
end
|
||||
|
||||
context RestClient::Request do
|
||||
before do
|
||||
@request = RestClient::Request.new(:method => :put, :url => 'http://some/resource', :payload => 'payload')
|
||||
|
||||
@uri = mock("uri")
|
||||
@uri.stub!(:request_uri).and_return('/resource')
|
||||
@uri.stub!(:host).and_return('some')
|
||||
@uri.stub!(:port).and_return(80)
|
||||
|
||||
@net = mock("net::http base")
|
||||
@http = mock("net::http connection")
|
||||
Net::HTTP.stub!(:new).and_return(@net)
|
||||
@net.stub!(:start).and_yield(@http)
|
||||
@net.stub!(:use_ssl=)
|
||||
@net.stub!(:verify_mode=)
|
||||
end
|
||||
|
||||
it "requests xml mimetype" do
|
||||
@request.default_headers[:accept].should == 'application/xml'
|
||||
end
|
||||
|
||||
it "decodes an uncompressed result body by passing it straight through" do
|
||||
@request.decode(nil, 'xyz').should == 'xyz'
|
||||
end
|
||||
|
||||
it "decodes a gzip body" do
|
||||
@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 == "i'm gziped\n"
|
||||
end
|
||||
|
||||
it "decodes a deflated body" do
|
||||
@request.decode('deflate', "x\234+\316\317MUHIM\313I,IMQ(I\255(\001\000A\223\006\363").should == "some deflated text"
|
||||
end
|
||||
|
||||
it "processes a successful result" do
|
||||
res = mock("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).should == 'body'
|
||||
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
|
||||
|
||||
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')
|
||||
end
|
||||
|
||||
it "extracts the username and password when parsing http://user:password@example.com/" do
|
||||
URI.stub!(:parse).and_return(mock('uri', :user => 'joe', :password => 'pass1'))
|
||||
@request.parse_url_with_auth('http://joe:pass1@example.com/resource')
|
||||
@request.user.should == 'joe'
|
||||
@request.password.should == '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(mock('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 == 'beth'
|
||||
@request.password.should == 'pass2'
|
||||
end
|
||||
|
||||
it "determines the Net::HTTP class to instantiate by the method name" do
|
||||
@request.net_http_request_class(:put).should == Net::HTTP::Put
|
||||
end
|
||||
|
||||
it "merges user headers with the default headers" do
|
||||
@request.should_receive(:default_headers).and_return({ '1' => '2' })
|
||||
@request.make_headers('3' => '4').should == { '1' => '2', '3' => '4' }
|
||||
end
|
||||
|
||||
it "prefers the user header when the same header exists in the defaults" do
|
||||
@request.should_receive(:default_headers).and_return({ '1' => '2' })
|
||||
@request.make_headers('1' => '3').should == { '1' => '3' }
|
||||
end
|
||||
|
||||
it "converts header symbols from :content_type to 'Content-type'" do
|
||||
@request.should_receive(:default_headers).and_return({})
|
||||
@request.make_headers(:content_type => 'abc').should == { 'Content-type' => 'abc' }
|
||||
end
|
||||
|
||||
it "converts header values to strings" do
|
||||
@request.make_headers('A' => 1)['A'].should == '1'
|
||||
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 = mock("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', be_kind_of(RestClient::Payload::Base))
|
||||
@request.execute_inner
|
||||
end
|
||||
|
||||
it "transmits the request with Net::HTTP" do
|
||||
@http.should_receive(:request).with('req', be_kind_of(RestClient::Payload::Base))
|
||||
@request.should_receive(:process_result)
|
||||
@request.should_receive(:response_log)
|
||||
@request.transmit(@uri, 'req', generate_payload('payload'))
|
||||
end
|
||||
|
||||
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', generate_payload('payload'))
|
||||
end
|
||||
|
||||
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)
|
||||
end
|
||||
|
||||
it "passes non-hash payloads straight through" do
|
||||
@request.process_payload("x").should == "x"
|
||||
end
|
||||
|
||||
it "converts a hash payload to urlencoded data" do
|
||||
@request.process_payload(:a => 'b c+d').should == "a=b%20c%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')
|
||||
end
|
||||
|
||||
it "set urlencoded content_type header on hash payloads" do
|
||||
@request.process_payload(:a => 1)
|
||||
@request.headers[:content_type].should == 'application/x-www-form-urlencoded'
|
||||
end
|
||||
|
||||
it "sets up the credentials prior to the request" do
|
||||
@http.stub!(:request)
|
||||
@request.stub!(:process_result)
|
||||
@request.stub!(:response_log)
|
||||
|
||||
@request.stub!(:user).and_return('joe')
|
||||
@request.stub!(:password).and_return('mypass')
|
||||
@request.should_receive(:setup_credentials).with('req')
|
||||
|
||||
@request.transmit(@uri, 'req', generate_payload(''))
|
||||
end
|
||||
|
||||
it "does not attempt to send any credentials if user is nil" do
|
||||
@request.stub!(:user).and_return(nil)
|
||||
req = mock("request")
|
||||
req.should_not_receive(:basic_auth)
|
||||
@request.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')
|
||||
req = mock("request")
|
||||
req.should_receive(:basic_auth).with('joe', 'mypass')
|
||||
@request.setup_credentials(req)
|
||||
end
|
||||
|
||||
it "catches EOFError and shows the more informative ServerBrokeConnection" do
|
||||
@http.stub!(:request).and_raise(EOFError)
|
||||
lambda { @request.transmit(@uri, 'req', generate_payload('')) }.
|
||||
should raise_error(RestClient::ServerBrokeConnection)
|
||||
end
|
||||
|
||||
it "execute calls execute_inner" do
|
||||
@request.should_receive(:execute_inner)
|
||||
@request.execute
|
||||
end
|
||||
|
||||
it "class method execute wraps constructor" do
|
||||
req = mock("rest request")
|
||||
RestClient::Request.should_receive(:new).with(1 => 2).and_return(req)
|
||||
req.should_receive(:execute)
|
||||
RestClient::Request.execute(1 => 2)
|
||||
end
|
||||
|
||||
it "raises a Redirect with the new location when the response is in the 30x range" do
|
||||
res = mock('response', :code => '301', :header => { 'Location' => 'http://new/resource' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://new/resource'}
|
||||
end
|
||||
|
||||
it "handles redirects with relative paths" do
|
||||
res = mock('response', :code => '301', :header => { 'Location' => 'index' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
|
||||
end
|
||||
|
||||
it "handles redirects with absolute paths" do
|
||||
@request.instance_variable_set('@url', 'http://some/place/else')
|
||||
res = mock('response', :code => '301', :header => { 'Location' => '/index' })
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Redirect) { |e| e.url.should == 'http://some/index' }
|
||||
end
|
||||
|
||||
it "raises Unauthorized when the response is 401" do
|
||||
res = mock('response', :code => '401')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::Unauthorized)
|
||||
end
|
||||
|
||||
it "raises ResourceNotFound when the response is 404" do
|
||||
res = mock('response', :code => '404')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::ResourceNotFound)
|
||||
end
|
||||
|
||||
it "raises RequestFailed otherwise" do
|
||||
res = mock('response', :code => '500')
|
||||
lambda { @request.process_result(res) }.should raise_error(RestClient::RequestFailed)
|
||||
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.should include(Net::HTTP::ProxyDelta)
|
||||
end
|
||||
|
||||
it "creates a non-proxy class if a proxy url is not given" do
|
||||
@request.net_http_class.should_not include(Net::HTTP::ProxyDelta)
|
||||
end
|
||||
|
||||
it "logs a get request" do
|
||||
RestClient::Request.new(:method => :get, :url => 'http://url').request_log.should ==
|
||||
'RestClient.get "http://url"'
|
||||
end
|
||||
|
||||
it "logs a post request with a small payload" do
|
||||
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => 'foo').request_log.should ==
|
||||
'RestClient.post "http://url", "foo"'
|
||||
end
|
||||
|
||||
it "logs a post request with a large payload" do
|
||||
RestClient::Request.new(:method => :post, :url => 'http://url', :payload => ('x' * 1000)).request_log.should ==
|
||||
'RestClient.post "http://url", "(1000 byte payload)"'
|
||||
end
|
||||
|
||||
it "logs input headers as a hash" do
|
||||
RestClient::Request.new(:method => :get, :url => 'http://url', :headers => { :accept => 'text/plain' }).request_log.should ==
|
||||
'RestClient.get "http://url", :accept=>"text/plain"'
|
||||
end
|
||||
|
||||
it "logs a response including the status code, content type, and result body size in bytes" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return('text/html')
|
||||
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
|
||||
end
|
||||
|
||||
it "logs a response with a nil Content-type" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return(nil)
|
||||
@request.response_log(res).should == "# => 200 OK | 4 bytes"
|
||||
end
|
||||
|
||||
it "strips the charset from the response content type" do
|
||||
res = mock('result', :code => '200', :class => Net::HTTPOK, :body => 'abcd')
|
||||
res.stub!(:[]).with('Content-type').and_return('text/html; charset=utf-8')
|
||||
@request.response_log(res).should == "# => 200 OK | text/html 4 bytes"
|
||||
end
|
||||
|
||||
it "displays the log to stdout" do
|
||||
RestClient.stub!(:log).and_return('stdout')
|
||||
STDOUT.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
end
|
||||
|
||||
it "displays the log to stderr" do
|
||||
RestClient.stub!(:log).and_return('stderr')
|
||||
STDERR.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
end
|
||||
|
||||
it "append the log to the requested filename" do
|
||||
RestClient.stub!(:log).and_return('/tmp/restclient.log')
|
||||
f = mock('file handle')
|
||||
File.should_receive(:open).with('/tmp/restclient.log', 'a').and_yield(f)
|
||||
f.should_receive(:puts).with('xyz')
|
||||
@request.display_log('xyz')
|
||||
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)
|
||||
|
||||
@http.should_receive(:read_timeout=).with(123)
|
||||
|
||||
@request.transmit(@uri, 'req', nil)
|
||||
end
|
||||
end
|
||||
|
||||
context RestClient::Response do
|
||||
before do
|
||||
@net_http_res = mock('net http response')
|
||||
@response = RestClient::Response.new('abc', @net_http_res)
|
||||
end
|
||||
|
||||
it "behaves like string" do
|
||||
@response.should == 'abc'
|
||||
end
|
||||
|
||||
it "fetches the numeric response code" do
|
||||
@net_http_res.should_receive(:code).and_return('200')
|
||||
@response.code.should == 200
|
||||
end
|
||||
|
||||
it "beautifies the headers by turning the keys to symbols" do
|
||||
h = RestClient::Response.beautify_headers('content-type' => [ 'x' ])
|
||||
h.keys.first.should == :content_type
|
||||
end
|
||||
|
||||
it "beautifies the headers by turning the values to strings instead of one-element arrays" do
|
||||
h = RestClient::Response.beautify_headers('x' => [ 'text/html' ] )
|
||||
h.values.first.should == 'text/html'
|
||||
end
|
||||
|
||||
it "fetches the headers" do
|
||||
@net_http_res.should_receive(:to_hash).and_return('content-type' => [ 'text/html' ])
|
||||
@response.headers.should == { :content_type => 'text/html' }
|
||||
end
|
||||
|
||||
it "can access the net http result directly" do
|
||||
@response.net_http_res.should == @net_http_res
|
||||
end
|
||||
end
|
||||
end
|
53
spec/restclient_spec.rb
Normal file
53
spec/restclient_spec.rb
Normal file
|
@ -0,0 +1,53 @@
|
|||
require File.dirname(__FILE__) + '/base'
|
||||
|
||||
describe RestClient do
|
||||
describe "API" do
|
||||
it "GET" do
|
||||
RestClient::Request.should_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 => {})
|
||||
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 => {})
|
||||
RestClient.put('http://some/resource', 'payload')
|
||||
end
|
||||
|
||||
it "DELETE" do
|
||||
RestClient::Request.should_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 => {})
|
||||
RestClient.head('http://some/resource')
|
||||
end
|
||||
end
|
||||
|
||||
describe "logging" do
|
||||
after do
|
||||
RestClient.log = nil
|
||||
end
|
||||
|
||||
it "gets the log source from the RESTCLIENT_LOG environment variable" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return('from env')
|
||||
RestClient.log = 'from class method'
|
||||
RestClient.log.should == 'from env'
|
||||
end
|
||||
|
||||
it "sets a destination for log output, used if no environment variable is set" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
|
||||
RestClient.log = 'from class method'
|
||||
RestClient.log.should == 'from class method'
|
||||
end
|
||||
|
||||
it "returns nil (no logging) if neither are set (default)" do
|
||||
ENV.stub!(:[]).with('RESTCLIENT_LOG').and_return(nil)
|
||||
RestClient.log.should == nil
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue