pesapal 1.5.4 → 1.5.5

Sign up to get free protection for your applications and to get access to all the features.
data/lib/pesapal/oauth.rb CHANGED
@@ -1,149 +1,182 @@
1
1
  module Pesapal
2
-
2
+ # Supporting oAuth 1.0 methods. See [oAuth 1.0 spec][1] for details.
3
+ #
4
+ # [1]: http://oauth.net/core/1.0/
3
5
  module Oauth
4
-
5
- # generate query string from parameters hash
6
- def Oauth.generate_encoded_params_query_string(params = {})
7
-
8
- # 1) percent encode every key and value that will be signed
9
- # 2) sort the list of parameters alphabetically by encoded key
10
- # 3) for each key/value pair
11
- # - append the encoded key to the output string
12
- # - append the '=' character to the output string
13
- # - append the encoded value to the output string
14
- # 4) if there are more key/value pairs remaining, append a '&' character
15
- # to the output string
16
-
17
- # the oauth spec says to sort lexigraphically, which is the default
18
- # alphabetical sort for many libraries. in case of two parameters with
19
- # the same encoded key, the oauth spec says to continue sorting based on
20
- # value
21
-
6
+ # Generate query string from a Hash.
7
+ #
8
+ # 1. Percent encode every key and value that will be signed
9
+ # 2. Sort the list of parameters alphabetically by encoded key
10
+ # 3. For each key/value pair
11
+ # * append the encoded key to the output string
12
+ # * append the '=' character to the output string
13
+ # * append the encoded value to the output string
14
+ # 4. If there are more key/value pairs remaining, append a '&' character
15
+ # to the output string
16
+ #
17
+ # The oauth spec says to sort lexicographically, which is the default
18
+ # alphabetical sort for many libraries. In case of two parameters with the
19
+ # same encoded key, the oauth spec says to continue sorting based on value.
20
+ #
21
+ # @param params [Hash] Hash of parameters.
22
+ #
23
+ # @return [String] valid valid parameter query string.
24
+ def self.generate_encoded_params_query_string(params = {})
22
25
  queries = []
23
- params.each do |k,v| queries.push "#{self.parameter_encode(k.to_s)}=#{self.parameter_encode(v.to_s)}" end
24
-
25
- # parameters are sorted by name, using lexicographical byte value
26
- # ordering
26
+ params.each { |k, v| queries.push "#{parameter_encode(k.to_s)}=#{parameter_encode(v.to_s)}" }
27
27
  queries.sort!
28
-
29
28
  queries.join('&')
30
29
  end
31
30
 
32
- # generate oauth nonce
33
- def Oauth.generate_nonce(length)
34
-
35
- # the consumer shall then generate a nonce value that is unique for all
36
- # requests with that timestamp. a nonce is a random string, uniquely
37
- # generated for each request. the nonce allows the service provider to
38
- # verify that a request has never been made before and helps prevent
39
- # replay attacks when requests are made over a non- secure channel (such
40
- # as http).
41
-
31
+ # Generate an nonce
32
+ #
33
+ # > _The Consumer SHALL then generate a Nonce value that is unique for all
34
+ # > requests with that timestamp. A nonce is a random string, uniquely
35
+ # > generated for each request. The nonce allows the Service Provider to
36
+ # > verify that a request has never been made before and helps prevent
37
+ # > replay attacks when requests are made over a non-secure channel (such as
38
+ # > HTTP)._
39
+ #
40
+ # See [section 8 of the oAuth 1.0 spec][1]
41
+ #
42
+ # [1]: http://oauth.net/core/1.0/#nonce
43
+ #
44
+ # @param length [Integer] number of characters of the resulting nonce.
45
+ #
46
+ # @return [String] generated random nonce.
47
+ def self.generate_nonce(length)
42
48
  chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ0123456789'
43
49
  nonce = ''
44
50
  length.times { nonce << chars[rand(chars.size)] }
45
-
46
51
  "#{nonce}"
47
52
  end
48
53
 
49
- # generate the oauth signature using hmac-sha1 algorithm
50
- def Oauth.generate_oauth_signature(http_method, absolute_url, params, consumer_secret, token_secret = nil)
51
-
52
- # the signature is calculated by passing the signature base string and
53
- # signing key to the hmac-sha1 hashing algorithm. the output of the hmac
54
- # signing function is a binary string. this needs to be base64 encoded to
55
- # produce the signature string.
56
-
57
- # for pesapal flow we don't have a token secret to we will set as nil and
58
- # the appropriate action will be taken as per the oauth spec. see notes in
59
- # the method that creates signing keys
60
-
61
- # prepare the values we need
54
+ # Generate the oAuth signature using HMAC-SHA1 algorithm.
55
+ #
56
+ # The signature is calculated by passing the signature base string and
57
+ # signing key to the HMAC-SHA1 hashing algorithm. The output of the HMAC
58
+ # signing function is a binary string. this needs to be Base64 encoded to
59
+ # produce the signature string.
60
+ #
61
+ # For pesapal flow we don't have a token secret to we will set as nil and
62
+ # the appropriate action will be taken as per the oAuth spec. See
63
+ # {generate_signing_key} for details.
64
+ #
65
+ # @param http_method [String] the HTTP method.
66
+ #
67
+ # @param absolute_url [String] the absolute URL.
68
+ #
69
+ # @param params [Hash] URL parameters.
70
+ #
71
+ # @param consumer_secret [String] the consumer secret.
72
+ #
73
+ # @param token_secret [String] the token secret.
74
+ #
75
+ # @return [String] valid oAuth signature.
76
+ def self.generate_oauth_signature(http_method, absolute_url, params, consumer_secret, token_secret = nil)
62
77
  digest = OpenSSL::Digest::Digest.new('sha1')
63
- signature_base_string = self.generate_signature_base_string(http_method, absolute_url, params)
64
- signing_key = self.generate_signing_key(consumer_secret, token_secret)
65
-
78
+ signature_base_string = generate_signature_base_string(http_method, absolute_url, params)
79
+ signing_key = generate_signing_key(consumer_secret, token_secret)
66
80
  hmac = OpenSSL::HMAC.digest(digest, signing_key, signature_base_string)
67
81
  Base64.encode64(hmac).chomp
68
82
  end
69
83
 
70
- # generate query string from signable parameters hash
71
- def Oauth.generate_signable_encoded_params_query_string(params = {})
72
-
73
- # oauth_signature parameter MUST be excluded, assumes it was already
74
- # initialized by calling set_parameters
84
+ # Generate query string from signable parameters Hash
85
+ #
86
+ # Same as {generate_encoded_params_query_string} but without
87
+ # `:oauth_signature` included in the parameters.
88
+ #
89
+ # @param params [Hash] Hash of parameters.
90
+ #
91
+ # @return [String] valid valid parameter query string.
92
+ def self.generate_signable_encoded_params_query_string(params = {})
75
93
  params.delete(:oauth_signature)
76
-
77
- self.generate_encoded_params_query_string params
94
+ generate_encoded_params_query_string params
78
95
  end
79
96
 
80
- # generate the oauth signature
81
- def Oauth.generate_signature_base_string(http_method, absolute_url, params)
82
-
83
- # three values collected so far must be joined to make a single string,
84
- # from which the signature will be generated. This is called the
85
- # signature base string by the OAuth specification
97
+ # Generate an oAuth 1.0 signature base string.
98
+ #
99
+ # Three values collected so far must be joined to make a single string, from
100
+ # which the signature will be generated. This is called the signature base
101
+ # string. The signature base string should contain exactly 2 ampersand '&'
102
+ # characters. The percent '%' characters in the parameter string should be
103
+ # encoded as %25 in the signature base string.
104
+ #
105
+ # See [appendix A.5.1 of the oAuth 1.0 spec][1] for an example.
106
+ #
107
+ # [1]: http://oauth.net/core/1.0/#sig_base_example
108
+ #
109
+ # @param http_method [String] the HTTP method.
110
+ #
111
+ # @param absolute_url [String] the absolute URL.
112
+ #
113
+ # @param params [Hash] URL parameters.
114
+ #
115
+ # @return [String] valid signature base string.
116
+ def self.generate_signature_base_string(http_method, absolute_url, params)
86
117
 
87
118
  # step 1: convert the http method to uppercase
88
119
  http_method = http_method.upcase
89
120
 
90
121
  # step 2: percent encode the url
91
- url_encoded = self.parameter_encode(self.normalized_request_uri(absolute_url))
122
+ url_encoded = parameter_encode(normalized_request_uri(absolute_url))
92
123
 
93
124
  # step 3: percent encode the parameter string
94
- parameter_string_encoded = self.parameter_encode(self.generate_signable_encoded_params_query_string params)
95
-
96
- # the signature base string should contain exactly 2 ampersand '&'
97
- # characters. The percent '%' characters in the parameter string should be
98
- # encoded as %25 in the signature base string
125
+ parameter_string_encoded = parameter_encode(generate_signable_encoded_params_query_string params)
99
126
 
100
127
  "#{http_method}&#{url_encoded}&#{parameter_string_encoded}"
101
128
  end
102
129
 
103
- # generate signing key
104
- def Oauth.generate_signing_key(consumer_secret, token_secret = nil)
105
-
106
- # the signing key is simply the percent encoded consumer secret, followed
107
- # by an ampersand character '&', followed by the percent encoded token
108
- # secret
109
-
110
- # note that there are some flows, such as when obtaining a request token,
111
- # where the token secret is not yet known. In this case, the signing key
112
- # should consist of the percent encoded consumer secret followed by an
113
- # ampersand character '&'
114
-
115
- # "#{@credentials[:consumer_secret]}"
116
- consumer_secret_encoded = self.parameter_encode(consumer_secret)
117
-
118
- token_secret_encoded = ""
119
- unless token_secret.nil?
120
- token_secret_encoded = self.parameter_encode(token_secret)
121
- end
130
+ # Generate signing key
131
+ #
132
+ # The signing key is simply the percent encoded consumer secret, followed by
133
+ # an ampersand character '&', followed by the percent encoded token secret.
134
+ # Note that there are some flows, such as when obtaining a request token,
135
+ # where the token secret is not yet known. In this case, the signing key
136
+ # should consist of the percent encoded consumer secret followed by an
137
+ # ampersand character '&'.
138
+ #
139
+ # @param consumer_secret [String] the consumer secret.
140
+ #
141
+ # @param token_secret [String] the token secret.
142
+ #
143
+ # @return [String] valid signing key.
144
+ def self.generate_signing_key(consumer_secret, token_secret = nil)
145
+ consumer_secret_encoded = parameter_encode(consumer_secret)
146
+ token_secret_encoded = ''
147
+ token_secret_encoded = parameter_encode(token_secret) unless token_secret.nil?
122
148
 
123
149
  "#{consumer_secret_encoded}&#{token_secret_encoded}"
124
150
  end
125
151
 
126
- # normalize request absolute URL
127
- def Oauth.normalized_request_uri(absolute_url)
128
-
129
- # the signature base string includes the request absolute url, tying the
130
- # signature to a specific endpoint. the url used in the signature base
131
- # string must include the scheme, authority, and path, and must exclude
132
- # the query and fragment as defined by [rfc3986] section 3.
133
-
134
- # if the absolute request url is not available to the service provider (it
135
- # is always available to the consumer), it can be constructed by combining
136
- # the scheme being used, the http host header, and the relative http
137
- # request url. if the host header is not available, the service provider
138
- # should use the host name communicated to the consumer in the
139
- # documentation or other means.
140
-
141
- # the service provider should document the form of url used in the
142
- # signature base string to avoid ambiguity due to url normalization.
143
- # unless specified, url scheme and authority must be lowercase and include
144
- # the port number; http default port 80 and https default port 443 must be
145
- # excluded.
146
-
152
+ # Construct normalized request absolute URL.
153
+ #
154
+ # > _The Signature Base String includes the request absolute URL, tying the
155
+ # > signature to a specific endpoint. The URL used in the Signature Base
156
+ # > String MUST include the scheme, authority, and path, and MUST exclude
157
+ # > the query and fragment as defined by [RFC3986] section 3._
158
+ #
159
+ # > _If the absolute request URL is not available to the Service Provider (it
160
+ # > is always available to the Consumer), it can be constructed by combining
161
+ # > the scheme being used, the HTTP Host header, and the relative HTTP
162
+ # > request URL. If the Host header is not available, the Service Provider
163
+ # > SHOULD use the host name communicated to the Consumer in the
164
+ # > documentation or other means._
165
+ #
166
+ # > _The Service Provider SHOULD document the form of URL used in the
167
+ # > Signature Base String to avoid ambiguity due to URL normalization.
168
+ # > Unless specified, URL scheme and authority MUST be lowercase and include
169
+ # > the port number; http default port 80 and https default port 443 MUST be
170
+ # > excluded._
171
+ #
172
+ # See [section 9.1.2 of the oAuth 1.0 spec][1]
173
+ #
174
+ # [1]: http://oauth.net/core/1.0/#anchor14
175
+ #
176
+ # @param absolute_url [String] URL to be normalized.
177
+ #
178
+ # @return [String] valid constructed URL as per the spec.
179
+ def self.normalized_request_uri(absolute_url)
147
180
  u = URI.parse(absolute_url)
148
181
 
149
182
  scheme = u.scheme.downcase
@@ -157,22 +190,28 @@ module Pesapal
157
190
  "#{scheme}://#{host}#{port}#{path}"
158
191
  end
159
192
 
160
- # percentage encode value as per the oauth spec
161
- def Oauth.parameter_encode(string)
162
-
163
- # all parameter names and values are escaped using the [rfc3986] percent-
164
- # encoding (%xx) mechanism. characters not in the unreserved character set
165
- # ([rfc3986] section 2.3) must be encoded. characters in the unreserved
166
- # character set must not be encoded. hexadecimal characters in encodings
167
- # must be upper case. text names and values must be encoded as utf-8
168
- # octets before percent-encoding them per [rfc3629].
169
-
193
+ # Encodes parameter name or values.
194
+ #
195
+ # > _All parameter names and values are escaped using the [RFC3986] percent-
196
+ # > encoding (%xx) mechanism. Characters not in the unreserved character set
197
+ # > ([RFC3986] section 2.3) MUST be encoded. Characters in the unreserved
198
+ # > character set MUST NOT be encoded. Hexadecimal characters in encodings
199
+ # > MUST be upper case. Text names and values MUST be encoded as UTF-8
200
+ # > octets before percent-encoding them per [RFC3629]._
201
+ #
202
+ # See [section 5.1 of the oAuth 1.0 spec][1]
203
+ #
204
+ # [1]: http://oauth.net/core/1.0/#encoding_parameters
205
+ #
206
+ # @param str [String] parameter name or value.
207
+ #
208
+ # @return [String] valid encoded result as per the spec.
209
+ def self.parameter_encode(str)
170
210
  # reserved character regexp, per section 5.1
171
211
  reserved_characters = /[^a-zA-Z0-9\-\.\_\~]/
172
-
173
212
  # Apparently we can't force_encoding on a frozen string since that would modify it.
174
213
  # What we can do is work with a copy
175
- URI::escape(string.dup.to_s.force_encoding(Encoding::UTF_8), reserved_characters)
214
+ URI.escape(str.dup.to_s.force_encoding(Encoding::UTF_8), reserved_characters)
176
215
  end
177
216
  end
178
217
  end
@@ -1,13 +1,13 @@
1
1
  module Pesapal
2
-
2
+ # Hooks Pesapal to extend Rails and/or modify the initialization process.
3
3
  class Railtie < Rails::Railtie
4
-
4
+ # Loads pesapal credentials from initializer file depending on environment
5
+ # and fallback to default values if anything goes wrong.
5
6
  initializer 'pesapal.load_credentials' do
6
-
7
7
  path_to_yaml = "#{Rails.root}/config/pesapal.yml"
8
8
  if File.exist?(path_to_yaml)
9
9
  begin
10
- config.pesapal_credentials = YAML::load(IO.read(path_to_yaml))[Rails.env]
10
+ config.pesapal_credentials = YAML.load(IO.read(path_to_yaml))[Rails.env]
11
11
  rescue Errno::ENOENT
12
12
  logger.info('YAML configuration file couldn\'t be found.'); return
13
13
  rescue Psych::SyntaxError
@@ -1,3 +1,8 @@
1
+ # Make authenticated Pesapal API calls without the fuss! Easily post an order,
2
+ # query payment status and fetch payment details.
1
3
  module Pesapal
2
- VERSION = '1.5.4'
4
+ # This gem's version (learn about [Semantic Versioning][1]).
5
+ #
6
+ # [1]: http://semver.org/
7
+ VERSION = '1.5.5'
3
8
  end
data/pesapal.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ['j@kingori.co']
11
11
  spec.description = 'Make authenticated Pesapal API calls without the fuss!'
12
12
  spec.summary = 'Make authenticated Pesapal API calls without the fuss! Handles all the oAuth stuff abstracting any direct interaction with the API endpoints so that you can focus on what matters. Building awesome.'
13
- spec.homepage = 'http://rubydoc.info/gems/pesapal/'
13
+ spec.homepage = 'http://itsmrwave.github.io/pesapal-gem'
14
14
  spec.license = 'MIT'
15
15
 
16
16
  spec.files = `git ls-files`.split($/)
@@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ['lib']
20
20
 
21
- spec.add_development_dependency 'bundler', '~> 1.5.2'
21
+ spec.add_development_dependency 'bundler'
22
+ spec.add_development_dependency 'coveralls'
22
23
  spec.add_development_dependency 'rake'
23
- spec.add_development_dependency 'rspec', '~> 2.14.1'
24
+ spec.add_development_dependency 'rspec'
25
+ spec.add_development_dependency 'webmock'
24
26
 
25
27
  spec.add_dependency 'htmlentities'
26
28
  end
@@ -44,6 +44,90 @@ describe Pesapal::Merchant do
44
44
  })
45
45
  end
46
46
  end
47
+
48
+ describe '#generate_order_url' do
49
+
50
+ it 'generates iframe url string' do
51
+ @pesapal.order_details = { :amount => 1000,
52
+ :description => 'This is the description for the test transaction.',
53
+ :type => 'MERCHANT',
54
+ :reference => '111-222-333',
55
+ :first_name => 'Swaleh',
56
+ :last_name => 'Mdoe',
57
+ :email => 'test@example.com',
58
+ :phonenumber => '+254711000333',
59
+ :currency => 'KES'
60
+ }
61
+ expect(@pesapal.generate_order_url).to match /http:\/\/demo.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
62
+ end
63
+ end
64
+
65
+ describe '#query_payment_status' do
66
+
67
+ it 'gets pending payment status' do
68
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
69
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
70
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
71
+ end
72
+
73
+ it 'gets completed payment status' do
74
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
75
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
76
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
77
+ end
78
+
79
+ it 'gets failed payment status' do
80
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
81
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
82
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
83
+ end
84
+
85
+ it 'gets invalid payment status' do
86
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
87
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
88
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
89
+ end
90
+ end
91
+
92
+ describe '#query_payment_details' do
93
+
94
+ it 'gets pending payment details' do
95
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
96
+ to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
97
+ expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
98
+ :status => 'payment_status',
99
+ :merchant_reference => 'merchant_reference',
100
+ :transaction_tracking_id => 'transaction_tracking_id'
101
+ })
102
+ end
103
+ end
104
+
105
+ describe '#ipn_listener' do
106
+
107
+ it 'gets ipn response for pending status' do
108
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
109
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
110
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
111
+ end
112
+
113
+ it 'gets ipn response for completed status' do
114
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
115
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
116
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
117
+ end
118
+
119
+ it 'gets ipn response for failed status' do
120
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
121
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
122
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
123
+ end
124
+
125
+ it 'gets ipn response for invalid status' do
126
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
127
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
128
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
129
+ end
130
+ end
47
131
  end
48
132
 
49
133
  context 'when mode is specified as development' do
@@ -64,9 +148,9 @@ describe Pesapal::Merchant do
64
148
 
65
149
  it 'sets credentials' do
66
150
  expect(@pesapal.config).to eq({ :callback_url => 'http://0.0.0.0:3000/pesapal/callback',
67
- :consumer_key => '<YOUR_CONSUMER_KEY>',
68
- :consumer_secret => '<YOUR_CONSUMER_SECRET>'
69
- })
151
+ :consumer_key => '<YOUR_CONSUMER_KEY>',
152
+ :consumer_secret => '<YOUR_CONSUMER_SECRET>'
153
+ })
70
154
  end
71
155
 
72
156
  it 'sets order details' do
@@ -88,6 +172,90 @@ describe Pesapal::Merchant do
88
172
  })
89
173
  end
90
174
  end
175
+
176
+ describe '#generate_order_url' do
177
+
178
+ it 'generates iframe url string' do
179
+ @pesapal.order_details = { :amount => 1000,
180
+ :description => 'This is the description for the test transaction.',
181
+ :type => 'MERCHANT',
182
+ :reference => '111-222-333',
183
+ :first_name => 'Swaleh',
184
+ :last_name => 'Mdoe',
185
+ :email => 'test@example.com',
186
+ :phonenumber => '+254711000333',
187
+ :currency => 'KES'
188
+ }
189
+ expect(@pesapal.generate_order_url).to match /http:\/\/demo.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
190
+ end
191
+ end
192
+
193
+ describe '#query_payment_status' do
194
+
195
+ it 'gets pending payment status' do
196
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
197
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
198
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
199
+ end
200
+
201
+ it 'gets completed payment status' do
202
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
203
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
204
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
205
+ end
206
+
207
+ it 'gets failed payment status' do
208
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
209
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
210
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
211
+ end
212
+
213
+ it 'gets invalid payment status' do
214
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
215
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
216
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
217
+ end
218
+ end
219
+
220
+ describe '#query_payment_details' do
221
+
222
+ it 'gets pending payment details' do
223
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
224
+ to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
225
+ expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
226
+ :status => 'payment_status',
227
+ :merchant_reference => 'merchant_reference',
228
+ :transaction_tracking_id => 'transaction_tracking_id'
229
+ })
230
+ end
231
+ end
232
+
233
+ describe '#ipn_listener' do
234
+
235
+ it 'gets ipn response for pending status' do
236
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
237
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
238
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
239
+ end
240
+
241
+ it 'gets ipn response for completed status' do
242
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
243
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
244
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
245
+ end
246
+
247
+ it 'gets ipn response for failed status' do
248
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
249
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
250
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
251
+ end
252
+
253
+ it 'gets ipn response for invalid status' do
254
+ stub_request(:get, /http:\/\/demo.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
255
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
256
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
257
+ end
258
+ end
91
259
  end
92
260
 
93
261
  context 'when mode is specified as production' do
@@ -108,9 +276,9 @@ describe Pesapal::Merchant do
108
276
 
109
277
  it 'sets credentials' do
110
278
  expect(@pesapal.config).to eq({ :callback_url => 'http://0.0.0.0:3000/pesapal/callback',
111
- :consumer_key => '<YOUR_CONSUMER_KEY>',
112
- :consumer_secret => '<YOUR_CONSUMER_SECRET>'
113
- })
279
+ :consumer_key => '<YOUR_CONSUMER_KEY>',
280
+ :consumer_secret => '<YOUR_CONSUMER_SECRET>'
281
+ })
114
282
  end
115
283
 
116
284
  it 'sets order details' do
@@ -132,5 +300,89 @@ describe Pesapal::Merchant do
132
300
  })
133
301
  end
134
302
  end
303
+
304
+ describe '#generate_order_url' do
305
+
306
+ it 'generates iframe url string' do
307
+ @pesapal.order_details = { :amount => 1000,
308
+ :description => 'This is the description for the test transaction.',
309
+ :type => 'MERCHANT',
310
+ :reference => '111-222-333',
311
+ :first_name => 'Swaleh',
312
+ :last_name => 'Mdoe',
313
+ :email => 'test@example.com',
314
+ :phonenumber => '+254711000333',
315
+ :currency => 'KES'
316
+ }
317
+ expect(@pesapal.generate_order_url).to match /https:\/\/www.pesapal.com\/API\/PostPesapalDirectOrderV4\?oauth_callback=.*oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_request_data=.*/
318
+ end
319
+ end
320
+
321
+ describe '#query_payment_status' do
322
+
323
+ it 'gets pending payment status' do
324
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
325
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
326
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('PENDING')
327
+ end
328
+
329
+ it 'gets completed payment status' do
330
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
331
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
332
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('COMPLETED')
333
+ end
334
+
335
+ it 'gets failed payment status' do
336
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
337
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
338
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('FAILED')
339
+ end
340
+
341
+ it 'gets invalid payment status' do
342
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
343
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
344
+ expect(@pesapal.query_payment_status('merchant_reference', 'transaction_tracking_id')).to eq('INVALID')
345
+ end
346
+ end
347
+
348
+ describe '#query_payment_details' do
349
+
350
+ it 'gets pending payment details' do
351
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentDetails\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
352
+ to_return(:status => 200, :body => 'pesapal_response_data=transaction_tracking_id,payment_method,payment_status,merchant_reference')
353
+ expect(@pesapal.query_payment_details('merchant_reference', 'transaction_tracking_id')).to eq({ :method => 'payment_method',
354
+ :status => 'payment_status',
355
+ :merchant_reference => 'merchant_reference',
356
+ :transaction_tracking_id => 'transaction_tracking_id'
357
+ })
358
+ end
359
+ end
360
+
361
+ describe '#ipn_listener' do
362
+
363
+ it 'gets ipn response for pending status' do
364
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
365
+ to_return(:status => 200, :body => 'pesapal_response_data=PENDING')
366
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'PENDING', :response => nil})
367
+ end
368
+
369
+ it 'gets ipn response for completed status' do
370
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
371
+ to_return(:status => 200, :body => 'pesapal_response_data=COMPLETED')
372
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'COMPLETED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
373
+ end
374
+
375
+ it 'gets ipn response for failed status' do
376
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
377
+ to_return(:status => 200, :body => 'pesapal_response_data=FAILED')
378
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'FAILED', :response => 'pesapal_notification_type=CHANGE&pesapal_transaction_tracking_id=transaction_tracking_id&pesapal_merchant_reference=merchant_reference'})
379
+ end
380
+
381
+ it 'gets ipn response for invalid status' do
382
+ stub_request(:get, /https:\/\/www.pesapal.com\/API\/QueryPaymentStatus\?oauth_consumer_key=.*oauth_nonce=.*oauth_signature=.*oauth_signature_method=HMAC-SHA1&oauth_timestamp.*oauth_version=1.0&pesapal_merchant_reference=.*&pesapal_transaction_tracking_id=.*/).
383
+ to_return(:status => 200, :body => 'pesapal_response_data=INVALID')
384
+ expect(@pesapal.ipn_listener('CHANGE', 'merchant_reference', 'transaction_tracking_id')).to eq({:status => 'INVALID', :response => nil})
385
+ end
386
+ end
135
387
  end
136
388
  end