exact4r 0.5.2 → 0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/CHANGELOG +8 -0
  2. data/README +8 -8
  3. data/VERSION +1 -1
  4. data/doc/classes/EWS/Transaction/FakeResponse.html +451 -0
  5. data/doc/classes/EWS/Transaction/Request.html +54 -81
  6. data/doc/classes/EWS/Transaction/Response.html +182 -14
  7. data/doc/classes/EWS/Transaction/Validator.html +168 -0
  8. data/doc/classes/EWS/Transporter.html +271 -0
  9. data/doc/created.rid +1 -1
  10. data/doc/files/CHANGELOG.html +15 -2
  11. data/doc/files/README.html +17 -9
  12. data/doc/files/VERSION.html +2 -2
  13. data/doc/files/lib/ews/transaction/fake_response_rb.html +101 -0
  14. data/doc/files/lib/ews/transaction/mapping_rb.html +1 -1
  15. data/doc/files/lib/ews/transaction/request_rb.html +8 -1
  16. data/doc/files/lib/ews/transaction/response_rb.html +1 -1
  17. data/doc/files/lib/ews/transaction/validator_rb.html +101 -0
  18. data/doc/files/lib/ews/{transaction/transporter_rb.html → transporter_rb.html} +3 -3
  19. data/doc/files/lib/exact4r_rb.html +4 -2
  20. data/doc/fr_class_index.html +3 -1
  21. data/doc/fr_file_index.html +3 -1
  22. data/doc/fr_method_index.html +18 -7
  23. data/lib/ews/transaction/fake_response.rb +137 -0
  24. data/lib/ews/transaction/mapping.rb +38 -15
  25. data/lib/ews/transaction/request.rb +10 -58
  26. data/lib/ews/transaction/response.rb +3 -3
  27. data/lib/ews/transaction/validator.rb +230 -0
  28. data/lib/ews/transporter.rb +143 -0
  29. data/lib/exact4r.rb +4 -1
  30. data/spec/donncha_spec.rb +13 -0
  31. data/spec/mapping_spec.rb +45 -4
  32. data/spec/request_spec.rb +96 -69
  33. data/spec/spec_helper.rb +20 -8
  34. data/spec/transporter_spec.rb +26 -2
  35. data/spec/validator_spec.rb +145 -0
  36. metadata +16 -7
  37. data/doc/classes/EWS/Transaction/Transporter.html +0 -250
  38. data/lib/ews/transaction/transporter.rb +0 -120
  39. data/output.log +0 -368
@@ -0,0 +1,143 @@
1
+ require 'net/https'
2
+
3
+ module EWS # :nodoc:
4
+
5
+ # A Transporter is responsible for communicating with the E-xact Web Service in
6
+ # whichever dialect is chosen by the user. The available options are:
7
+ # :json REST with JSON payload
8
+ # :rest REST with XML payload (default)
9
+ # :soap SOAP
10
+ #
11
+ # The Transporter will connect to the service, using SSL if required, and will
12
+ # encode Reqests to send to the service, and decode Responses received from the
13
+ # service.
14
+ #
15
+ # Once configured to connect to a particular service, it can be used repeatedly
16
+ # to send as many transactions as required.
17
+ class Transporter
18
+
19
+ # Initialize a Transporter.
20
+ #
21
+ # You can specify the URL you would like the Transporter to connect to, although it defaults
22
+ # to https://api.e-xact.com, the location of our transaction processing web service.
23
+ #
24
+ # You can also specify a hash of options as follows:
25
+ # :server_cert the path to the server's certificate
26
+ # :issuer_cert the path to the certificate of the issuer of the server certificate
27
+ # :transport_type the transport_type for this transporter (defaults to :rest)
28
+ #
29
+ # The default certificates are those required to connect to https://api.e-xact.com and the
30
+ # default <tt>transport_type</tt> is <tt>:rest</tt>. The default <tt>transport_type</tt> can be overridden on a per-transaction
31
+ # basis, if you choose to do so, by specifying it as a parameter to the <tt>submit</tt> method.
32
+ def initialize(url = "https://api.e-xact.com/", options = {})
33
+ @url = URI.parse(url.gsub(/\/$/,''))
34
+ base = File.dirname(__FILE__)
35
+ @server_cert = options[:server_cert] || base+"/../../certs/exact.cer"
36
+ @issuer_cert = options[:issuer_cert] || base+"/../../certs/equifax_ca.cer"
37
+ @transport_type = options[:transport_type] || :json
38
+ end
39
+
40
+ # Submit a transaction request to the server
41
+ #
42
+ # <tt>transaction</tt>:: the Request object to encode for transmission to the server
43
+ # <tt>transport_type</tt>:: (optional) the transport type to use for this transaction only. If it is not specified, the Transporter's transport type will be used
44
+ def submit(transaction, transport_type = nil)
45
+ raise "Request not supplied" if transaction.nil?
46
+ return false unless transaction.valid?
47
+
48
+ transport_type ||= @transport_type
49
+
50
+ raise "Transport type #{transport_type} is not supported" unless @@transport_types.include? transport_type
51
+
52
+ transport_details = @@transport_types[transport_type]
53
+
54
+ request = build_http_request(transaction, transport_type, transport_details[:suffix])
55
+ request.basic_auth(transaction.gateway_id, transaction.password)
56
+ request.add_field "Accept", transport_details[:content_type]
57
+ request.add_field "User-Agent", "Ruby"
58
+ request.add_field "Content-type", "#{transport_details[:content_type]}; charset=UTF-8"
59
+
60
+ response = get_connection.request(request)
61
+
62
+ case response
63
+ when Net::HTTPSuccess then EWS::Transaction::Mapping.send "#{transport_type}_to_response", response.body
64
+ else
65
+ r = ::EWS::Transaction::Response.new
66
+ if(transport_type == :soap)
67
+ # we may have a SOAP Fault
68
+ r = EWS::Transaction::Mapping.send "#{transport_type}_to_response", response.body
69
+ end
70
+
71
+ # SOAP Fault may already have populated the error_number etc.
72
+ unless r.error_number
73
+ # populate the error number and description
74
+ r.error_number = response.code.to_i
75
+ r.error_description = response.message
76
+ end
77
+
78
+ r
79
+ end
80
+
81
+ end
82
+
83
+ private
84
+
85
+ def build_http_request(transaction, transport_type, request_suffix)
86
+ req = nil
87
+ if !transaction.is_find_transaction? or transport_type == :soap
88
+ req = Net::HTTP::Post.new(@url.path + "/transaction.#{request_suffix}")
89
+ if transport_type == :soap
90
+ # add the SOAPAction header
91
+ soapaction = (transaction.is_find_transaction?) ? "TransactionInfo" : "SendAndCommit"
92
+ req.add_field "soapaction", "http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/#{soapaction}"
93
+ end
94
+ req.body = EWS::Transaction::Mapping.send "request_to_#{transport_type.to_s}", transaction
95
+ else
96
+ req = Net::HTTP::Get.new(@url.path + "/transaction/#{transaction.transaction_tag}.#{request_suffix}")
97
+ end
98
+ req
99
+ end
100
+
101
+ def get_connection
102
+ # re-use the connection if it's available
103
+ return @connection unless @connection.nil?
104
+
105
+ @connection = Net::HTTP.new(@url.host, @url.port)
106
+ @connection.set_debug_output $stdout if $DEBUG
107
+ if @url.scheme == 'https'
108
+ @connection.use_ssl = true
109
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
110
+ @connection.ca_file = @issuer_cert
111
+ @connection.verify_callback = method(:validate_certificate)
112
+ end
113
+ @connection
114
+ end
115
+
116
+ # from OpenSSL doco:
117
+ # peer cert is always at chain[0], root CA at chain[n-1]
118
+ # OpenSSL validates signatures, and issuer attributes at each step in the chain; is_ok reflects the status of these checks
119
+ def validate_certificate(is_ok, ctx)
120
+ # if OpenSSL has flagged an issue, then fail
121
+ return false unless is_ok
122
+
123
+ peer_cert = ctx.chain[0].to_pem
124
+ current_cert = ctx.current_cert
125
+
126
+ # we're not interested in additional checking of the CA certs
127
+ return is_ok unless peer_cert == current_cert.to_pem
128
+
129
+ # check that the peer_cert is the same as the one we expect for this URL
130
+ contents = File.open(@server_cert).read
131
+ cert = OpenSSL::X509::Certificate.new(contents)
132
+ return OpenSSL::Digest::SHA1.new(current_cert.to_der) == OpenSSL::Digest::SHA1.new(cert.to_der)
133
+ end
134
+
135
+ # what transport types we support, and their corresponding suffixes
136
+ @@transport_types = {
137
+ :rest => {:suffix => "xml", :content_type => "application/xml"},
138
+ :json => {:suffix => "json", :content_type => "application/json"},
139
+ :soap => {:suffix => "xmlsoap", :content_type => "application/xml"}
140
+ }
141
+
142
+ end
143
+ end
data/lib/exact4r.rb CHANGED
@@ -3,4 +3,7 @@ require 'rubygems'
3
3
  require 'ews/transaction/mapping'
4
4
  require 'ews/transaction/request'
5
5
  require 'ews/transaction/response'
6
- require 'ews/transaction/transporter'
6
+ require 'ews/transaction/validator'
7
+ require 'ews/transporter'
8
+
9
+ require 'ews/transaction/fake_response'
@@ -0,0 +1,13 @@
1
+ require File.dirname(__FILE__) + "/spec_helper"
2
+
3
+ describe "Something" do
4
+ it "should support soap" do
5
+ txn = basic_new_transaction()
6
+ puts txn.errors.inspect unless txn.valid?
7
+
8
+ transporter = EWS::Transporter.new(LOCATION)
9
+ resp = transporter.submit(txn, :json)
10
+
11
+ puts resp.ctr
12
+ end
13
+ end
data/spec/mapping_spec.rb CHANGED
@@ -11,7 +11,7 @@ describe "Mapping TO formats" do
11
11
  res["amount"].should == "10.13"
12
12
  res["cardholder_name"].should == "Simon Brown"
13
13
  res["cc_number"].should == "4111111111111111"
14
- res["cc_expiry"].should == "1005"
14
+ res["cc_expiry"].should == "1012"
15
15
  end
16
16
 
17
17
  it "should generate a REST payload" do
@@ -24,7 +24,7 @@ describe "Mapping TO formats" do
24
24
  REXML::XPath.first(txn, "Card_Number").text.should == "4111111111111111"
25
25
  REXML::XPath.first(txn, "CardHoldersName").text.should == "Simon Brown"
26
26
  REXML::XPath.first(txn, "Transaction_Type").text.should == "00"
27
- REXML::XPath.first(txn, "Expiry_Date").text.should == "1005"
27
+ REXML::XPath.first(txn, "Expiry_Date").text.should == "1012"
28
28
  REXML::XPath.first(txn, "DollarAmount").text.should == "10.13"
29
29
  end
30
30
 
@@ -38,7 +38,7 @@ describe "Mapping TO formats" do
38
38
  REXML::XPath.first(txn, "Card_Number").text.should == "4111111111111111"
39
39
  REXML::XPath.first(txn, "CardHoldersName").text.should == "Simon Brown"
40
40
  REXML::XPath.first(txn, "Transaction_Type").text.should == "00"
41
- REXML::XPath.first(txn, "Expiry_Date").text.should == "1005"
41
+ REXML::XPath.first(txn, "Expiry_Date").text.should == "1012"
42
42
  REXML::XPath.first(txn, "DollarAmount").text.should == "10.13"
43
43
  end
44
44
  end
@@ -80,6 +80,47 @@ PAYLOAD
80
80
  r.transaction_type.should == "00"
81
81
  end
82
82
 
83
- it "should parse a SOAP payload"
83
+ it "should parse a SOAP Fault payload with <details>" do
84
+ payload = <<PAYLOAD
85
+ <?xml version="1.0" encoding="utf-8"?>
86
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
87
+ <soap:Body>
88
+ <soap:Fault>
89
+ <faultcode>soap:Server</faultcode>
90
+ <faultstring>System.Web.Services.Protocols.SoapException: Setup Error at rpc-enc.Service.SendAndCommit(Transaction Transaction) in C:\Documents and Settings\defaultuser\VSWebCache\secure2.exact.com\vplug-in\transaction\rpc-enc\Service.asmx.vb:line 99</faultstring>
91
+ <faultactor>http://secure2.e-xact.com/vplug-in/transaction/rpc-enc/Service.asmx</faultactor>
92
+ <detail>
93
+ <error number="500" description="Internal Server Error. An unknown error occurred." xmlns="http://secure2.e-xact.com/vplug-in/transaction/rpc-enc"/>
94
+ </detail>
95
+ </soap:Fault>
96
+ </soap:Body>
97
+ </soap:Envelope>
98
+ PAYLOAD
99
+
100
+ r = EWS::Transaction::Mapping.soap_to_response(payload)
101
+
102
+ r.error_number.should == 500
103
+ r.error_description.should == "Internal Server Error. An unknown error occurred."
104
+ r.should_not be_approved
105
+ end
84
106
 
107
+ it "should throw an exception when a SOAP Fault contains no details" do
108
+ payload = <<PAYLOAD
109
+ <?xml version="1.0" encoding="utf-8"?>
110
+ <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
111
+ <soap:Body>
112
+ <soap:Fault>
113
+ <faultcode>soap:Client</faultcode>
114
+ <faultstring>Server was unable to read request. --&gt; There is an error in XML document (1, 406). --&gt; &lt;SendAndCommit xmlns='http://secure2.exact.com/vplug-in/transaction/rpc-enc/'&gt; was not expected.</faultstring>
115
+ <detail />
116
+ </soap:Fault>
117
+ </soap:Body>
118
+ </soap:Envelope>
119
+ PAYLOAD
120
+
121
+ lambda {
122
+ EWS::Transaction::Mapping.soap_to_response(payload)
123
+ }.should raise_error
124
+ end
125
+
85
126
  end
data/spec/request_spec.rb CHANGED
@@ -12,56 +12,36 @@ describe "Request" do
12
12
  r.amount.should == "10.13"
13
13
  r.cardholder_name.should == "Simon Brown"
14
14
  r.cc_number.should == "4111111111111111"
15
- r.cc_expiry.should == "1005"
15
+ r.cc_expiry.should == "1012"
16
16
  end
17
17
 
18
- it "should require gateway_id and password " do
19
- EWS::Transaction::Request.new(basic_params).should be_valid
18
+ it "should raise an error with invalid tranasction_type" do
19
+ lambda {
20
+ EWS::Transaction::Request.new(basic_params(:transaction_type => "00"))
21
+ }.should_not raise_error
22
+
23
+ lambda {
24
+ EWS::Transaction::Request.new(basic_params(:transaction_type => :purchase))
25
+ }.should_not raise_error
20
26
 
21
- EWS::Transaction::Request.new(basic_params(:gateway_id => nil)).should_not be_valid
22
- EWS::Transaction::Request.new(basic_params(:gateway_id => "")).should_not be_valid
27
+ lambda {
28
+ EWS::Transaction::Request.new(basic_params(:transaction_type => nil))
29
+ }.should raise_error
23
30
 
24
- EWS::Transaction::Request.new(basic_params(:password => nil)).should_not be_valid
25
- EWS::Transaction::Request.new(basic_params(:password => "")).should_not be_valid
26
- end
27
-
28
- it "should validate amount, cc_number and cc_expiry" do
29
- # validation: amounts
30
- [:amount, :surcharge_amount, :tax1_amount, :tax2_amount].each do |amount_attr|
31
- EWS::Transaction::Request.new(basic_params(amount_attr => "10.13")).should be_valid
32
- EWS::Transaction::Request.new(basic_params(amount_attr => "10.")).should be_valid
33
- EWS::Transaction::Request.new(basic_params(amount_attr => "0.13")).should be_valid
34
- EWS::Transaction::Request.new(basic_params(amount_attr => 10.13)).should be_valid
35
- EWS::Transaction::Request.new(basic_params(amount_attr => 0.13)).should be_valid
36
- EWS::Transaction::Request.new(basic_params(amount_attr => 10)).should be_valid
37
- EWS::Transaction::Request.new(basic_params(amount_attr => "10.1s3")).should_not be_valid
38
- EWS::Transaction::Request.new(basic_params(amount_attr => "1s0.13")).should_not be_valid
39
- EWS::Transaction::Request.new(basic_params(amount_attr => "dopey")).should_not be_valid
40
- end
41
-
42
- # validation: card number
43
- EWS::Transaction::Request.new(basic_params(:cc_number => "4111111111111111")).should be_valid
44
- EWS::Transaction::Request.new(basic_params(:cc_number => "4111111111111112")).should_not be_valid
45
-
46
- # validation: card expiry (should be in YYMM format)
47
- EWS::Transaction::Request.new(basic_params(:cc_expiry => "1009")).should be_valid
48
- EWS::Transaction::Request.new(basic_params(:cc_expiry => "0708")).should_not be_valid
49
- end
50
-
51
- it "should be valid when symbol or value supplied for transaction_type" do
52
- EWS::Transaction::Request.new(basic_params(:transaction_type => "00")).should be_valid
53
- EWS::Transaction::Request.new(basic_params(:transaction_type => :purchase)).should be_valid
31
+ lambda {
32
+ EWS::Transaction::Request.new(basic_params(:transaction_type => ""))
33
+ }.should raise_error
54
34
 
55
- EWS::Transaction::Request.new(basic_params(:transaction_type => nil)).should_not be_valid
56
- EWS::Transaction::Request.new(basic_params(:transaction_type => "")).should_not be_valid
57
- EWS::Transaction::Request.new(basic_params(:transaction_type => "barry")).should_not be_valid
35
+ lambda {
36
+ EWS::Transaction::Request.new(basic_params(:transaction_type => "barry"))
37
+ }.should raise_error
58
38
  end
59
39
  end
60
-
40
+
61
41
  describe "Submitting New Requests" do
62
42
 
63
43
  before :all do
64
- @transporter = EWS::Transaction::Transporter.new
44
+ @transporter = EWS::Transporter.new(LOCATION)
65
45
  end
66
46
 
67
47
  it "should work without a specified transport_type" do
@@ -74,53 +54,100 @@ describe "Submitting New Requests" do
74
54
  resp.transaction_tag.should_not be_nil
75
55
  end
76
56
 
77
- it "should support all transport types" do
78
- [:json, :rest, :soap].each do |type|
79
- txn = basic_new_transaction
80
- resp = @transporter.submit(txn, type)
81
-
82
- resp.exact_resp_code.should == "00"
83
- resp.exact_message.should == "Transaction Normal"
84
- resp.bank_message.downcase.should match(/approved/)
85
- resp.transaction_tag.should_not be_nil
86
- end
57
+ it "should support JSON" do
58
+ txn = basic_new_transaction
59
+ resp = @transporter.submit(txn, :json)
60
+
61
+ resp.exact_resp_code.should == "00"
62
+ resp.exact_message.should == "Transaction Normal"
63
+ resp.bank_message.downcase.should match(/approved/)
64
+ resp.transaction_tag.should_not be_nil
87
65
  end
88
66
 
67
+ it "should support REST" do
68
+ txn = basic_new_transaction
69
+ resp = @transporter.submit(txn, :rest)
70
+
71
+ resp.exact_resp_code.should == "00"
72
+ resp.exact_message.should == "Transaction Normal"
73
+ resp.bank_message.downcase.should match(/approved/)
74
+ resp.transaction_tag.should_not be_nil
75
+ end
76
+
77
+ it "should support SOAP" do
78
+ txn = basic_new_transaction
79
+ resp = @transporter.submit(txn, :soap)
80
+
81
+ resp.exact_resp_code.should == "00"
82
+ resp.exact_message.should == "Transaction Normal"
83
+ resp.bank_message.downcase.should match(/approved/)
84
+ resp.transaction_tag.should_not be_nil
85
+ end
89
86
  end
90
87
 
91
88
  describe "Submitting Find Requests" do
92
89
 
93
90
  before :each do
94
- @transporter = EWS::Transaction::Transporter.new
91
+ @transporter = EWS::Transporter.new(LOCATION)
95
92
 
96
- txn = basic_new_transaction
97
- @transaction_tag = @transporter.submit(txn, :json).transaction_tag
98
- puts "Sleeping for replication: #{REPLICATION_TIME}s"
93
+ ct = basic_new_transaction
94
+ @transaction_tag = @transporter.submit(ct, :json).transaction_tag
95
+ @ft = basic_find_transaction(:transaction_tag => @transaction_tag)
96
+ putc '['
99
97
  sleep(REPLICATION_TIME) # need time for replication to take place on the host
100
- puts "Awake"
98
+ putc ']'
101
99
  end
102
100
 
103
101
  it "should work without a specified transport_type" do
104
- txn = basic_find_transaction(:transaction_tag => @transaction_tag)
105
- resp = @transporter.submit(txn)
102
+ resp = @transporter.submit(@ft)
106
103
 
107
104
  resp.exact_resp_code.should == "00"
108
105
  resp.exact_message.should == "Transaction Normal"
109
106
  resp.bank_message.downcase.should match(/approved/)
110
- resp.transaction_tag.should_not be_nil
107
+ resp.transaction_tag.should == @transaction_tag
111
108
  end
112
109
 
113
- it "should support all transport types" do
114
- [:json, :rest, :soap].each do |type|
115
- txn = basic_find_transaction(:transaction_tag => @transaction_tag)
116
- resp = @transporter.submit(txn, type)
117
-
118
- resp.transaction_tag.should == @transaction_tag
119
- resp.cc_number == "4111111111111"
120
- resp.exact_resp_code.should == "00"
121
- resp.exact_message.should == "Transaction Normal"
122
- resp.bank_message.downcase.should match(/approved/)
123
- end
110
+ it "should support JSON" do
111
+ resp = @transporter.submit(@ft, :json)
112
+
113
+ resp.cc_number == "4111111111111"
114
+ resp.exact_resp_code.should == "00"
115
+ resp.exact_message.should == "Transaction Normal"
116
+ resp.bank_message.downcase.should match(/approved/)
117
+ resp.transaction_tag.should == @transaction_tag
124
118
  end
125
119
 
120
+ it "should support REST" do
121
+ resp = @transporter.submit(@ft, :rest)
122
+
123
+ resp.cc_number == "4111111111111"
124
+ resp.exact_resp_code.should == "00"
125
+ resp.exact_message.should == "Transaction Normal"
126
+ resp.bank_message.downcase.should match(/approved/)
127
+ resp.transaction_tag.should == @transaction_tag
128
+ end
129
+
130
+ it "should support SOAP" do
131
+ resp = @transporter.submit(@ft, :soap)
132
+
133
+ resp.cc_number == "4111111111111"
134
+ resp.exact_resp_code.should == "00"
135
+ resp.exact_message.should == "Transaction Normal"
136
+ resp.bank_message.downcase.should match(/approved/)
137
+ resp.transaction_tag.should == @transaction_tag
138
+ end
139
+ end
140
+
141
+ describe "Fake requests" do
142
+ it "should stub sending" do
143
+ request = {:nonsense => "this is nonsense"}
144
+ fake_response = EWS::Transaction::FakeResponse.valid(request)
145
+ transporter = EWS::Transporter.new(LOCATION)
146
+ transporter.stubs(:submit).returns(fake_response)
147
+
148
+ response = transporter.submit(request)
149
+ response.should == fake_response
150
+ response.should be_approved
151
+ response.exact_message.should == "Transaction Normal"
152
+ end
126
153
  end