Testing HTTParty Response Handling in jnunemaker/httparty
This test suite comprehensively validates the HTTParty::Response class functionality, covering response handling, header management, and HTTP status code behaviors. The tests ensure proper initialization, method delegation, and semantic response code interpretation across various HTTP scenarios.
Test Coverage Overview
Implementation Analysis
Technical Details
Best Practices Demonstrated
jnunemaker/httparty
spec/httparty/response_spec.rb
require 'spec_helper'
RSpec.describe HTTParty::Response do
before do
@last_modified = Date.new(2010, 1, 15).to_s
@content_length = '1024'
@request_object = HTTParty::Request.new Net::HTTP::Get, '/'
@response_object = Net::HTTPOK.new('1.1', 200, 'OK')
allow(@response_object).to receive_messages(body: "{foo:'bar'}")
@response_object['last-modified'] = @last_modified
@response_object['content-length'] = @content_length
@parsed_response = lambda { {"foo" => "bar"} }
@response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
end
describe ".underscore" do
it "works with one capitalized word" do
expect(HTTParty::Response.underscore("Accepted")).to eq("accepted")
end
it "works with titlecase" do
expect(HTTParty::Response.underscore("BadGateway")).to eq("bad_gateway")
end
it "works with all caps" do
expect(HTTParty::Response.underscore("OK")).to eq("ok")
end
end
describe "initialization" do
it "should set the Net::HTTP Response" do
expect(@response.response).to eq(@response_object)
end
it "should set body" do
expect(@response.body).to eq(@response_object.body)
end
it "should set code" do
expect(@response.code).to eq(@response_object.code)
end
it "should set code as an Integer" do
expect(@response.code).to be_a(Integer)
end
it "should set http_version" do
unparseable_body = lambda { raise "Unparseable" }
unparseable_response = HTTParty::Response.new(@request_object, @response_object, unparseable_body)
expect(unparseable_response.http_version).to eq(@response_object.http_version)
end
context 'test raise_on requests' do
let(:raise_on) { [404] }
let(:request) { HTTParty::Request.new(Net::HTTP::Get, '/', raise_on: raise_on) }
let(:body) { 'Not Found' }
let(:response) { Net::HTTPNotFound.new('1.1', 404, body) }
subject { described_class.new(request, response, @parsed_response) }
before do
allow(response).to receive(:body).and_return(body)
end
context 'when raise_on is a number' do
let(:raise_on) { [404] }
context "and response's status code is in range" do
it 'throws exception' do
expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
end
end
context "and response's status code is not in range" do
let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }
it 'does not throw exception' do
expect{ subject }.not_to raise_error
end
end
end
context 'when raise_on is a regexpr' do
let(:raise_on) { ['4[0-9]*'] }
context "and response's status code is in range" do
it 'throws exception' do
expect{ subject }.to raise_error(HTTParty::ResponseError, "Code 404 - #{body}")
end
end
context "and response's status code is not in range" do
let(:response) { Net::HTTPNotFound.new('1.1', 200, body) }
it 'does not throw exception' do
expect{ subject }.not_to raise_error
end
end
end
end
end
it 'does raise an error about itself when using #method' do
expect {
HTTParty::Response.new(@request_object, @response_object, @parsed_response).method(:qux)
}.to raise_error(NameError, /HTTParty\:\:Response/)
end
it 'does raise an error about itself when invoking a method that does not exist' do
expect {
HTTParty::Response.new(@request_object, @response_object, @parsed_response).qux
}.to raise_error(NoMethodError, /HTTParty\:\:Response/)
end
it "returns response headers" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.headers).to eq({'last-modified' => [@last_modified], 'content-length' => [@content_length]})
end
it "should send missing methods to delegate" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response['foo']).to eq('bar')
end
it "responds to request" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:request)).to be_truthy
end
it "responds to response" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:response)).to be_truthy
end
it "responds to body" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:body)).to be_truthy
end
it "responds to headers" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:headers)).to be_truthy
end
it "responds to parsed_response" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:parsed_response)).to be_truthy
end
it "responds to predicates" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:success?)).to be_truthy
end
it "responds to anything parsed_response responds to" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.respond_to?(:[])).to be_truthy
end
context 'response is array' do
let(:response_value) { [{'foo' => 'bar'}, {'foo' => 'baz'}] }
let(:response) { HTTParty::Response.new(@request_object, @response_object, lambda { response_value }) }
it "should be able to iterate" do
expect(response.size).to eq(2)
expect {
response.each { |item| }
}.to_not raise_error
end
it 'should respond to array methods' do
expect(response).to respond_to(:bsearch, :compact, :cycle, :delete, :each, :flatten, :flatten!, :compact, :join)
end
it 'should equal the string response object body' do
expect(response.to_s).to eq(@response_object.body.to_s)
end
it 'should display the same as an array' do
a = StringIO.new
b = StringIO.new
response_value.display(b)
response.display(a)
expect(a.string).to eq(b.string)
end
end
it "allows headers to be accessed by mixed-case names in hash notation" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.headers['Content-LENGTH']).to eq(@content_length)
end
it "returns a comma-delimited value when multiple values exist" do
@response_object.add_field 'set-cookie', 'csrf_id=12345; path=/'
@response_object.add_field 'set-cookie', '_github_ses=A123CdE; path=/'
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
expect(response.headers['set-cookie']).to eq("csrf_id=12345; path=/, _github_ses=A123CdE; path=/")
end
# Backwards-compatibility - previously, #headers returned a Hash
it "responds to hash methods" do
response = HTTParty::Response.new(@request_object, @response_object, @parsed_response)
hash_methods = {}.methods - response.headers.methods
hash_methods.each do |method_name|
expect(response.headers.respond_to?(method_name)).to be_truthy
end
end
describe "#is_a?" do
subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }
it { is_expected.to respond_to(:is_a?).with(1).arguments }
it { expect(subject.is_a?(HTTParty::Response)).to be_truthy }
it { expect(subject.is_a?(Object)).to be_truthy }
end
describe "#kind_of?" do
subject { HTTParty::Response.new(@request_object, @response_object, @parsed_response) }
it { is_expected.to respond_to(:kind_of?).with(1).arguments }
it { expect(subject.kind_of?(HTTParty::Response)).to be_truthy }
it { expect(subject.kind_of?(Object)).to be_truthy }
end
describe "semantic methods for response codes" do
def response_mock(klass)
response = klass.new('', '', '')
allow(response).to receive(:body)
response
end
context "major codes" do
it "is information" do
net_response = response_mock(Net::HTTPInformation)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.information?).to be_truthy
end
it "is success" do
net_response = response_mock(Net::HTTPSuccess)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.success?).to be_truthy
end
it "is redirection" do
net_response = response_mock(Net::HTTPRedirection)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.redirection?).to be_truthy
end
it "is client error" do
net_response = response_mock(Net::HTTPClientError)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.client_error?).to be_truthy
end
it "is server error" do
net_response = response_mock(Net::HTTPServerError)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.server_error?).to be_truthy
end
end
context "for specific codes" do
SPECIFIC_CODES = {
accepted?: Net::HTTPAccepted,
bad_gateway?: Net::HTTPBadGateway,
bad_request?: Net::HTTPBadRequest,
conflict?: Net::HTTPConflict,
continue?: Net::HTTPContinue,
created?: Net::HTTPCreated,
expectation_failed?: Net::HTTPExpectationFailed,
forbidden?: Net::HTTPForbidden,
found?: Net::HTTPFound,
gateway_time_out?: Net::HTTPGatewayTimeOut,
gone?: Net::HTTPGone,
internal_server_error?: Net::HTTPInternalServerError,
length_required?: Net::HTTPLengthRequired,
method_not_allowed?: Net::HTTPMethodNotAllowed,
moved_permanently?: Net::HTTPMovedPermanently,
multiple_choice?: Net::HTTPMultipleChoice,
no_content?: Net::HTTPNoContent,
non_authoritative_information?: Net::HTTPNonAuthoritativeInformation,
not_acceptable?: Net::HTTPNotAcceptable,
not_found?: Net::HTTPNotFound,
not_implemented?: Net::HTTPNotImplemented,
not_modified?: Net::HTTPNotModified,
ok?: Net::HTTPOK,
partial_content?: Net::HTTPPartialContent,
payment_required?: Net::HTTPPaymentRequired,
precondition_failed?: Net::HTTPPreconditionFailed,
proxy_authentication_required?: Net::HTTPProxyAuthenticationRequired,
request_entity_too_large?: Net::HTTPRequestEntityTooLarge,
request_time_out?: Net::HTTPRequestTimeOut,
request_uri_too_long?: Net::HTTPRequestURITooLong,
requested_range_not_satisfiable?: Net::HTTPRequestedRangeNotSatisfiable,
reset_content?: Net::HTTPResetContent,
see_other?: Net::HTTPSeeOther,
service_unavailable?: Net::HTTPServiceUnavailable,
switch_protocol?: Net::HTTPSwitchProtocol,
temporary_redirect?: Net::HTTPTemporaryRedirect,
unauthorized?: Net::HTTPUnauthorized,
unsupported_media_type?: Net::HTTPUnsupportedMediaType,
use_proxy?: Net::HTTPUseProxy,
version_not_supported?: Net::HTTPVersionNotSupported
}
if ::RUBY_PLATFORM != "java"
SPECIFIC_CODES[:multiple_choices?] = Net::HTTPMultipleChoices
end
# Ruby 2.6, those status codes have been updated.
if ::RUBY_PLATFORM != "java"
SPECIFIC_CODES[:gateway_timeout?] = Net::HTTPGatewayTimeout
SPECIFIC_CODES[:payload_too_large?] = Net::HTTPPayloadTooLarge
SPECIFIC_CODES[:request_timeout?] = Net::HTTPRequestTimeout
SPECIFIC_CODES[:uri_too_long?] = Net::HTTPURITooLong
SPECIFIC_CODES[:range_not_satisfiable?] = Net::HTTPRangeNotSatisfiable
end
SPECIFIC_CODES.each do |method, klass|
it "responds to #{method}" do
net_response = response_mock(klass)
response = HTTParty::Response.new(@request_object, net_response, '')
expect(response.__send__(method)).to be_truthy
end
end
end
end
describe "headers" do
let (:empty_headers) { HTTParty::Response::Headers.new }
let (:some_headers_hash) do
{'Cookie' => 'bob',
'Content-Encoding' => 'meow'}
end
let (:some_headers) do
HTTParty::Response::Headers.new.tap do |h|
some_headers_hash.each_pair do |k,v|
h[k] = v
end
end
end
it "can initialize without headers" do
expect(empty_headers).to eq({})
end
it 'always equals itself' do
expect(empty_headers).to eq(empty_headers)
expect(some_headers).to eq(some_headers)
end
it 'does not equal itself when not equivalent' do
expect(empty_headers).to_not eq(some_headers)
end
it 'does equal a hash' do
expect(empty_headers).to eq({})
expect(some_headers).to eq(some_headers_hash)
end
end
describe "#tap" do
it "is possible to tap into a response" do
result = @response.tap(&:code)
expect(result).to eq @response
end
end
describe "#inspect" do
it "works" do
inspect = @response.inspect
expect(inspect).to include("HTTParty::Response:0x")
expect(inspect).to include("parsed_response={\"foo\"=>\"bar\"}")
expect(inspect).to include("@response=#<Net::HTTPOK 200 OK readbody=false>")
expect(inspect).to include("@headers={")
expect(inspect).to include("last-modified")
expect(inspect).to include("content-length")
end
end
describe 'marshalling' do
before { RSpec::Mocks.space.proxy_for(@response_object).remove_stub(:body) }
specify do
marshalled = Marshal.load(Marshal.dump(@response))
expect(marshalled.headers).to eq @response.headers
expect(marshalled.body).to eq @response.body
expect(marshalled.code).to eq @response.code
end
end
end