require File.expand_path('../spec_helper', __FILE__)
require 'adyen/api'
require 'rubygems'
require 'nokogiri'
require 'rexml/document'
module Net
class HTTP
class Post
attr_reader :header
attr_reader :assigned_basic_auth
alias old_basic_auth basic_auth
def basic_auth(username, password)
if Net::HTTP.stubbing_enabled
@assigned_basic_auth = [username, password]
else
old_basic_auth
end
end
def soap_action
header['soapaction'].first
end
end
class << self
attr_accessor :stubbing_enabled, :posted, :stubbed_response
def stubbing_enabled=(enabled)
reset! if @stubbing_enabled = enabled
end
def reset!
@posted = nil
@stubbed_response = nil
end
end
def host
@address
end
alias old_start start
def start
Net::HTTP.stubbing_enabled ? yield(self) : old_start
end
alias old_request request
def request(request)
if Net::HTTP.stubbing_enabled
self.class.posted = [self, request]
self.class.stubbed_response
else
old_request(request)
end
end
end
end
module Adyen
module API
class PaymentService
public :authorise_payment_request_body, :authorise_recurring_payment_request_body
end
class RecurringService
public :list_request_body, :disable_request_body
end
end
end
module APISpecHelper
def node_for_current_method(object)
node = Adyen::API::XMLQuerier.new(object.send(@method))
end
def xpath(query, &block)
node_for_current_method.xpath(query, &block)
end
def text(query)
node_for_current_method.text(query)
end
def stub_net_http(response_body)
Net::HTTP.stubbing_enabled = true
response = Net::HTTPOK.new('1.1', '200', 'OK')
response.stub!(:body).and_return(response_body)
Net::HTTP.stubbed_response = response
end
def self.included(klass)
klass.extend ClassMethods
end
module ClassMethods
def for_each_xml_backend(&block)
[:nokogiri, :rexml].each do |xml_backend|
describe "with a #{xml_backend} backend" do
before { Adyen::API::XMLQuerier.backend = xml_backend }
after { Adyen::API::XMLQuerier.backend = :nokogiri }
instance_eval(&block)
end
end
end
def it_should_have_shortcut_methods_for_params_on_the_response
it "provides shortcut methods, on the response object, for all entries in the #params hash" do
@response.params.each do |key, value|
@response.send(key).should == value
end
end
end
end
class SOAPClient < Adyen::API::SimpleSOAPClient
ENDPOINT_URI = 'https://%s.example.com/soap/Action'
end
end
shared_examples_for "payment requests" do
it "includes the merchant account handle" do
text('./payment:merchantAccount').should == 'SuperShopper'
end
it "includes the payment reference of the merchant" do
text('./payment:reference').should == 'order-id'
end
it "includes the given amount of `currency'" do
xpath('./payment:amount') do |amount|
amount.text('./common:currency').should == 'EUR'
amount.text('./common:value').should == '1234'
end
end
it "includes the shopper’s details" do
text('./payment:shopperReference').should == 'user-id'
text('./payment:shopperEmail').should == 's.hopper@example.com'
text('./payment:shopperIP').should == '61.294.12.12'
end
it "only includes shopper details for given parameters" do
@payment.params[:shopper].delete(:reference)
xpath('./payment:shopperReference').should be_empty
@payment.params[:shopper].delete(:email)
xpath('./payment:shopperEmail').should be_empty
@payment.params[:shopper].delete(:ip)
xpath('./payment:shopperIP').should be_empty
end
it "does not include any shopper details if none are given" do
@payment.params.delete(:shopper)
xpath('./payment:shopperReference').should be_empty
xpath('./payment:shopperEmail').should be_empty
xpath('./payment:shopperIP').should be_empty
end
end
describe Adyen::API do
include APISpecHelper
before :all do
Adyen::API.default_params = { :merchant_account => 'SuperShopper' }
Adyen::API.username = 'SuperShopper'
Adyen::API.password = 'secret'
end
describe Adyen::API::Response do
before do
http_response = Net::HTTPOK.new('1.1', '200', 'OK')
http_response.add_field('Content-type', 'text/xml')
http_response.stub!(:body).and_return(AUTHORISE_RESPONSE)
@response = Adyen::API::Response.new(http_response)
end
it "returns a XMLQuerier instance with the response body" do
@response.xml_querier.should be_instance_of(Adyen::API::XMLQuerier)
@response.xml_querier.to_s.should == AUTHORISE_RESPONSE
end
describe "with a successful HTTP response" do
it "returns that the (HTTP) request was a success" do
@response.should_not be_a_http_failure
@response.should be_a_success
end
end
describe "with a failed HTTP response" do
before do
http_response = Net::HTTPBadRequest.new('1.1', '400', 'Bad request')
@response = Adyen::API::Response.new(http_response)
end
it "returns that the (HTTP) request was not a success" do
@response.should be_a_http_failure
@response.should_not be_a_success
end
end
end
describe Adyen::API::SimpleSOAPClient do
before do
@client = APISpecHelper::SOAPClient.new(:reference => 'order-id')
end
it "returns the endpoint, for the current environment, from the ENDPOINT_URI constant" do
uri = APISpecHelper::SOAPClient.endpoint
uri.scheme.should == 'https'
uri.host.should == 'test.example.com'
uri.path.should == '/soap/Action'
end
it "initializes with the given parameters" do
@client.params[:reference].should == 'order-id'
end
it "merges the default parameters with the given ones" do
@client.params[:merchant_account].should == 'SuperShopper'
end
describe "call_webservice_action" do
before do
stub_net_http(AUTHORISE_RESPONSE)
@response = @client.call_webservice_action('Action', 'Yes, please', Adyen::API::Response)
@request, @post = Net::HTTP.posted
end
after do
Net::HTTP.stubbing_enabled = false
end
it "posts to the class's endpoint" do
endpoint = APISpecHelper::SOAPClient.endpoint
@request.host.should == endpoint.host
@request.port.should == endpoint.port
@post.path.should == endpoint.path
end
it "makes a request over SSL" do
@request.use_ssl.should == true
end
it "verifies certificates" do
File.should exist(Adyen::API::SimpleSOAPClient::CACERT)
@request.ca_file.should == Adyen::API::SimpleSOAPClient::CACERT
@request.verify_mode.should == OpenSSL::SSL::VERIFY_PEER
end
it "uses basic-authentication with the credentials set on the Adyen::API module" do
username, password = @post.assigned_basic_auth
username.should == 'SuperShopper'
password.should == 'secret'
end
it "sends the proper headers" do
@post.header.should == {
'accept' => ['text/xml'],
'content-type' => ['text/xml; charset=utf-8'],
'soapaction' => ['Action']
}
end
it "returns an Adyen::API::Response instance" do
@response.should be_instance_of(Adyen::API::Response)
@response.xml_querier.to_s.should == AUTHORISE_RESPONSE
end
end
end
describe "shortcut methods" do
it "performs a `authorise payment' request" do
payment = mock('PaymentService')
Adyen::API::PaymentService.should_receive(:new).with(:reference => 'order-id').and_return(payment)
payment.should_receive(:authorise_payment)
Adyen::API.authorise_payment(:reference => 'order-id')
end
it "performs a `authorise recurring payment' request" do
payment = mock('PaymentService')
Adyen::API::PaymentService.should_receive(:new).with(:reference => 'order-id').and_return(payment)
payment.should_receive(:authorise_recurring_payment)
Adyen::API.authorise_recurring_payment(:reference => 'order-id')
end
it "performs a `disable recurring contract' request" do
recurring = mock('RecurringService')
Adyen::API::RecurringService.should_receive(:new).with(:shopper => { :reference => 'user-id' }).and_return(recurring)
recurring.should_receive(:disable)
Adyen::API.disable_recurring_contract(:shopper => { :reference => 'user-id' })
end
end
describe Adyen::API::PaymentService do
describe "for a normal payment request" do
before do
@params = {
:reference => 'order-id',
:amount => {
:currency => 'EUR',
:value => '1234',
},
:shopper => {
:email => 's.hopper@example.com',
:reference => 'user-id',
:ip => '61.294.12.12',
},
:card => {
:expiry_month => 12,
:expiry_year => 2012,
:holder_name => 'Simon わくわく Hopper',
:number => '4444333322221111',
:cvc => '737',
# Maestro UK/Solo only
#:issue_number => ,
#:start_month => ,
#:start_year => ,
}
}
@payment = Adyen::API::PaymentService.new(@params)
end
describe "authorise_payment_request_body" do
before :all do
@method = :authorise_payment_request_body
end
it_should_behave_like "payment requests"
it "includes the creditcard details" do
xpath('./payment:card') do |card|
# there's no reason why Nokogiri should escape these characters, but as long as they're correct
card.text('./payment:holderName').should == 'Simon わくわく Hopper'
card.text('./payment:number').should == '4444333322221111'
card.text('./payment:cvc').should == '737'
card.text('./payment:expiryMonth').should == '12'
card.text('./payment:expiryYear').should == '2012'
end
end
it "formats the creditcard’s expiry month as a two digit number" do
@payment.params[:card][:expiry_month] = 6
text('./payment:card/payment:expiryMonth').should == '06'
end
it "includes the necessary recurring contract info if the `:recurring' param is truthful" do
xpath('./payment:recurring/payment:contract').should be_empty
@payment.params[:recurring] = true
text('./payment:recurring/payment:contract').should == 'RECURRING'
end
end
describe "authorise_payment" do
before do
stub_net_http(AUTHORISE_RESPONSE)
@response = @payment.authorise_payment
@request, @post = Net::HTTP.posted
end
after do
Net::HTTP.stubbing_enabled = false
end
it "posts the body generated for the given parameters" do
@post.body.should == @payment.authorise_payment_request_body
end
it "posts to the correct SOAP action" do
@post.soap_action.should == 'authorise'
end
for_each_xml_backend do
it "returns a hash with parsed response details" do
@payment.authorise_payment.params.should == {
:psp_reference => '9876543210987654',
:result_code => 'Authorised',
:auth_code => '1234',
:refusal_reason => ''
}
end
end
it_should_have_shortcut_methods_for_params_on_the_response
describe "with a authorized response" do
it "returns that the request was authorised" do
@response.should be_success
@response.should be_authorized
end
end
describe "with a `declined' response" do
before do
stub_net_http(AUTHORISATION_DECLINED_RESPONSE)
@response = @payment.authorise_payment
end
it "returns that the request was not authorised" do
@response.should_not be_success
@response.should_not be_authorized
end
end
describe "with a `invalid' response" do
before do
stub_net_http(AUTHORISE_REQUEST_INVALID_RESPONSE % 'validation 101 Invalid card number')
@response = @payment.authorise_payment
end
it "returns that the request was not authorised" do
@response.should_not be_success
@response.should_not be_authorized
end
it "it returns that the request was invalid" do
@response.should be_invalid_request
end
it "returns creditcard validation errors" do
[
["validation 101 Invalid card number", [:number, 'is not a valid creditcard number']],
["validation 103 CVC is not the right length", [:cvc, 'is not the right length']],
["validation 128 Card Holder Missing", [:holder_name, 'can’t be blank']],
["validation Couldn't parse expiry year", [:expiry_year, 'could not be recognized']],
["validation Expiry month should be between 1 and 12 inclusive", [:expiry_month, 'could not be recognized']],
].each do |message, error|
response_with_fault_message(message).error.should == error
end
end
it "returns any other fault messages on `base'" do
message = "validation 130 Reference Missing"
response_with_fault_message(message).error.should == [:base, message]
end
it "returns the original message corresponding to the given attribute and message" do
[
["validation 101 Invalid card number", [:number, 'is not a valid creditcard number']],
["validation 103 CVC is not the right length", [:cvc, 'is not the right length']],
["validation 128 Card Holder Missing", [:holder_name, 'can’t be blank']],
["validation Couldn't parse expiry year", [:expiry_year, 'could not be recognized']],
["validation Expiry month should be between 1 and 12 inclusive", [:expiry_month, 'could not be recognized']],
["validation 130 Reference Missing", [:base, 'validation 130 Reference Missing']],
].each do |expected, attr_and_message|
Adyen::API::PaymentService::AuthorizationResponse.original_fault_message_for(*attr_and_message).should == expected
end
end
private
def response_with_fault_message(message)
stub_net_http(AUTHORISE_REQUEST_INVALID_RESPONSE % message)
@response = @payment.authorise_payment
end
end
end
describe "authorise_recurring_payment_request_body" do
before :all do
@method = :authorise_recurring_payment_request_body
end
it_should_behave_like "payment requests"
it "does not include any creditcard details" do
xpath('./payment:card').should be_empty
end
it "includes the contract type, which is always `RECURRING'" do
text('./payment:recurring/payment:contract').should == 'RECURRING'
end
it "obviously includes the obligatory self-‘describing’ nonsense parameters" do
text('./payment:shopperInteraction').should == 'ContAuth'
end
it "uses the latest recurring detail reference, by default" do
text('./payment:selectedRecurringDetailReference').should == 'LATEST'
end
it "uses the given recurring detail reference" do
@payment.params[:recurring_detail_reference] = 'RecurringDetailReference1'
text('./payment:selectedRecurringDetailReference').should == 'RecurringDetailReference1'
end
end
describe "authorise_recurring_payment" do
before do
stub_net_http(AUTHORISE_RESPONSE)
@response = @payment.authorise_recurring_payment
@request, @post = Net::HTTP.posted
end
after do
Net::HTTP.stubbing_enabled = false
end
it "posts the body generated for the given parameters" do
@post.body.should == @payment.authorise_recurring_payment_request_body
end
it "posts to the correct SOAP action" do
@post.soap_action.should == 'authorise'
end
for_each_xml_backend do
it "returns a hash with parsed response details" do
@payment.authorise_recurring_payment.params.should == {
:psp_reference => '9876543210987654',
:result_code => 'Authorised',
:auth_code => '1234',
:refusal_reason => ''
}
end
end
it_should_have_shortcut_methods_for_params_on_the_response
end
end
private
def node_for_current_method
super(@payment).xpath('//payment:authorise/payment:paymentRequest')
end
end
describe Adyen::API::RecurringService do
before do
@params = { :shopper => { :reference => 'user-id' } }
@recurring = Adyen::API::RecurringService.new(@params)
end
describe "list_request_body" do
before :all do
@method = :list_request_body
end
it "includes the merchant account handle" do
text('./recurring:merchantAccount').should == 'SuperShopper'
end
it "includes the shopper’s reference" do
text('./recurring:shopperReference').should == 'user-id'
end
it "includes the type of contract, which is always `RECURRING'" do
text('./recurring:recurring/recurring:contract').should == 'RECURRING'
end
private
def node_for_current_method
super(@recurring).xpath('//recurring:listRecurringDetails/recurring:request')
end
end
describe "list" do
before do
stub_net_http(LIST_RESPONSE)
@response = @recurring.list
@request, @post = Net::HTTP.posted
end
after do
Net::HTTP.stubbing_enabled = false
end
it "posts the body generated for the given parameters" do
@post.body.should == @recurring.list_request_body
end
it "posts to the correct SOAP action" do
@post.soap_action.should == 'listRecurringDetails'
end
for_each_xml_backend do
it "returns a hash with parsed response details" do
@recurring.list.params.should == {
:creation_date => DateTime.parse('2009-10-27T11:26:22.203+01:00'),
:last_known_shopper_email => 's.hopper@example.com',
:shopper_reference => 'user-id',
:details => [
{
:card => {
:expiry_date => Date.new(2012, 12, 31),
:holder_name => 'S. Hopper',
:number => '1111'
},
:recurring_detail_reference => 'RecurringDetailReference1',
:variant => 'mc',
:creation_date => DateTime.parse('2009-10-27T11:50:12.178+01:00')
},
{
:bank => {
:bank_account_number => '123456789',
:bank_location_id => 'bank-location-id',
:bank_name => 'AnyBank',
:bic => 'BBBBCCLLbbb',
:country_code => 'NL',
:iban => 'NL69PSTB0001234567',
:owner_name => 'S. Hopper'
},
:recurring_detail_reference => 'RecurringDetailReference2',
:variant => 'IDEAL',
:creation_date => DateTime.parse('2009-10-27T11:26:22.216+01:00')
},
],
}
end
it_should_have_shortcut_methods_for_params_on_the_response
end
describe "disable_request_body" do
before :all do
@method = :disable_request_body
end
it "includes the merchant account handle" do
text('./recurring:merchantAccount').should == 'SuperShopper'
end
it "includes the shopper’s reference" do
text('./recurring:shopperReference').should == 'user-id'
end
it "includes the shopper’s recurring detail reference if it is given" do
xpath('./recurring:recurringDetailReference').should be_empty
@recurring.params[:recurring_detail_reference] = 'RecurringDetailReference1'
text('./recurring:recurringDetailReference').should == 'RecurringDetailReference1'
end
private
def node_for_current_method
super(@recurring).xpath('//recurring:disable/recurring:request')
end
end
describe "disable" do
before do
stub_net_http(DISABLE_RESPONSE)
@response = @recurring.disable
@request, @post = Net::HTTP.posted
end
after do
Net::HTTP.stubbing_enabled = false
end
it "posts the body generated for the given parameters" do
@post.body.should == @recurring.disable_request_body
end
it "posts to the correct SOAP action" do
@post.soap_action.should == 'disable'
end
for_each_xml_backend do
it "returns a hash with parsed response details" do
@recurring.disable.params.should == { :response => '[detail-successfully-disabled]' }
end
end
it_should_have_shortcut_methods_for_params_on_the_response
end
end
end
end
AUTHORISE_RESPONSE = <
1234
9876543210987654
Authorised
EOS
AUTHORISATION_DECLINED_RESPONSE = <
1234
9876543210987654
You need to actually own money.
Refused
EOS
AUTHORISE_REQUEST_INVALID_RESPONSE = <
soap:Server
%s
EOS
LIST_RESPONSE = <
2009-10-27T11:26:22.203+01:00
12
2012
S. Hopper
1111
2009-10-27T11:50:12.178+01:00
RecurringDetailReference1
mc
123456789
bank-location-id
AnyBank
BBBBCCLLbbb
NL
NL69PSTB0001234567
S. Hopper
2009-10-27T11:26:22.216+01:00
RecurringDetailReference2
IDEAL
s.hopper@example.com
user-id
EOS
DISABLE_RESPONSE = <
[detail-successfully-disabled]
EOS