adyen-ruby-api-library 4.0.0 → 4.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +5 -5
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +27 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +7 -0
  6. data/.github/dependabot.yml +8 -0
  7. data/CODE_OF_CONDUCT.md +76 -0
  8. data/Gemfile +2 -2
  9. data/README.md +16 -2
  10. data/docs/checkout.html +17 -0
  11. data/lib/adyen-ruby-api-library.rb +4 -11
  12. data/lib/adyen/client.rb +16 -3
  13. data/lib/adyen/errors.rb +44 -2
  14. data/lib/adyen/hash_with_accessors.rb +38 -0
  15. data/lib/adyen/result.rb +2 -2
  16. data/lib/adyen/services/checkout.rb +2 -1
  17. data/lib/adyen/services/data_protection.rb +17 -0
  18. data/lib/adyen/services/marketpay.rb +18 -0
  19. data/lib/adyen/services/payments.rb +2 -1
  20. data/lib/adyen/services/postfmapi.rb +19 -0
  21. data/lib/adyen/services/service.rb +10 -1
  22. data/lib/adyen/utils/hmac_validator.rb +48 -0
  23. data/lib/adyen/version.rb +2 -2
  24. data/renovate.json +5 -0
  25. data/spec/checkout_spec.rb +13 -4
  26. data/spec/checkout_utility_spec.rb +4 -2
  27. data/spec/data_protection_spec.rb +14 -0
  28. data/spec/errors_spec.rb +33 -3
  29. data/spec/hash_with_accessors_spec.rb +127 -0
  30. data/spec/hop_spec.rb +14 -0
  31. data/spec/mocks/requests/Checkout/payment_links.json +9 -0
  32. data/spec/mocks/requests/DataProtectionService/request_subject_erasure.json +5 -0
  33. data/spec/mocks/requests/Hop/get_onboarding_url.json +4 -0
  34. data/spec/mocks/requests/Payment/donate.json +10 -0
  35. data/spec/mocks/requests/Terminal/assign_terminals.json +6 -0
  36. data/spec/mocks/requests/Terminal/find_terminal.json +3 -0
  37. data/spec/mocks/requests/Terminal/get_terminals_under_account.json +4 -0
  38. data/spec/mocks/responses/Checkout/payment_links.json +9 -0
  39. data/spec/mocks/responses/DataProtectionService/request_subject_erasure.json +3 -0
  40. data/spec/mocks/responses/Hop/get_onboarding_url.json +7 -0
  41. data/spec/mocks/responses/Payment/donate.json +4 -0
  42. data/spec/mocks/responses/Terminal/assign_terminals.json +5 -0
  43. data/spec/mocks/responses/Terminal/find_terminal.json +6 -0
  44. data/spec/mocks/responses/Terminal/get_terminals_under_account.json +11 -0
  45. data/spec/payments_spec.rb +2 -1
  46. data/spec/postfmapi_spec.rb +16 -0
  47. data/spec/service_spec.rb +45 -0
  48. data/spec/spec_helper.rb +7 -4
  49. data/spec/utils/hmac_validator_spec.rb +52 -0
  50. metadata +34 -5
  51. data/lib/adyen/util.rb +0 -21
@@ -8,7 +8,8 @@ module Adyen
8
8
  service = 'Checkout'
9
9
  method_names = [
10
10
  :payment_methods,
11
- :payment_session
11
+ :payment_session,
12
+ :payment_links
12
13
  ]
13
14
 
14
15
  super(client, version, service, method_names)
@@ -0,0 +1,17 @@
1
+ require_relative 'service'
2
+
3
+ module Adyen
4
+ class DataProtection < Service
5
+ attr_accessor :version
6
+ DEFAULT_VERSION = 1
7
+
8
+ def initialize(client, version = DEFAULT_VERSION)
9
+ service = 'DataProtectionService'
10
+ method_names = [
11
+ :request_subject_erasure
12
+ ]
13
+
14
+ super(client, version, service, method_names)
15
+ end
16
+ end
17
+ end
@@ -21,6 +21,10 @@ module Adyen
21
21
  def notification
22
22
  @notification ||= Adyen::Marketpay::Notification.new(@client)
23
23
  end
24
+
25
+ def hop
26
+ @hop ||= Adyen::Marketpay::Hop.new(@client)
27
+ end
24
28
  end
25
29
 
26
30
  class Account < Service
@@ -88,5 +92,19 @@ module Adyen
88
92
  super(client, version, service, method_names)
89
93
  end
90
94
  end
95
+
96
+ class Hop < Service
97
+ attr_accessor :version
98
+ DEFAULT_VERSION = 1
99
+
100
+ def initialize(client, version = DEFAULT_VERSION)
101
+ service = 'Hop'
102
+ method_names = [
103
+ :get_onboarding_url
104
+ ]
105
+
106
+ super(client, version, service, method_names)
107
+ end
108
+ end
91
109
  end
92
110
  end
@@ -15,7 +15,8 @@ module Adyen
15
15
  :cancel,
16
16
  :refund,
17
17
  :cancel_or_refund,
18
- :adjust_authorisation
18
+ :adjust_authorisation,
19
+ :donate
19
20
  ]
20
21
 
21
22
  super(client, version, service, method_names)
@@ -0,0 +1,19 @@
1
+ require_relative 'service'
2
+
3
+ module Adyen
4
+ class PosTerminalManagement < Service
5
+ attr_accessor :version
6
+ DEFAULT_VERSION = 1
7
+
8
+ def initialize(client, version = DEFAULT_VERSION)
9
+ service = 'Terminal'
10
+ method_names = [
11
+ :assign_terminals,
12
+ :find_terminal,
13
+ :get_terminals_under_account
14
+ ]
15
+
16
+ super(client, version, service, method_names)
17
+ end
18
+ end
19
+ end
@@ -2,6 +2,15 @@ module Adyen
2
2
  class Service
3
3
  attr_accessor :service, :version
4
4
 
5
+ # add snake case to camel case converter to String
6
+ # to convert rubinic method names to Adyen API methods
7
+ #
8
+ # i.e. snake_case -> snakeCase
9
+ # note that the first letter is not capitalized as normal
10
+ def self.action_for_method_name(method_name)
11
+ method_name.to_s.gsub(/_./) { |x| x[1].upcase }
12
+ end
13
+
5
14
  def initialize(client, version, service, method_names)
6
15
  @client = client
7
16
  @version = version
@@ -10,7 +19,7 @@ module Adyen
10
19
  # dynamically create API methods
11
20
  method_names.each do |method_name|
12
21
  define_singleton_method method_name do |request, headers = {}|
13
- action = method_name.to_s.to_camel_case
22
+ action = self.class.action_for_method_name(method_name)
14
23
  @client.call_adyen_api(@service, action, request, headers, @version)
15
24
  end
16
25
  end
@@ -0,0 +1,48 @@
1
+ module Adyen
2
+ module Utils
3
+ class HmacValidator
4
+ HMAC_ALGORITHM = 'sha256'.freeze
5
+ DATA_SEPARATOR = ':'.freeze
6
+ NOTIFICATION_VALIDATION_KEYS = %w[
7
+ pspReference originalReference merchantAccountCode merchantReference
8
+ amount.value amount.currency eventCode success
9
+ ].freeze
10
+
11
+ def valid_notification_hmac?(notification_request_item, hmac_key)
12
+ expected_sign = calculate_notification_hmac(notification_request_item, hmac_key)
13
+ merchant_sign = fetch(notification_request_item, 'additionalData.hmacSignature')
14
+
15
+ expected_sign == merchant_sign
16
+ end
17
+
18
+ def calculate_notification_hmac(notification_request_item, hmac_key)
19
+ data = data_to_sign(notification_request_item)
20
+
21
+ Base64.strict_encode64(OpenSSL::HMAC.digest(HMAC_ALGORITHM, [hmac_key].pack('H*'), data))
22
+ end
23
+
24
+ def data_to_sign(notification_request_item)
25
+ NOTIFICATION_VALIDATION_KEYS.map { |key| fetch(notification_request_item, key).to_s }
26
+ .map { |value| value.gsub('\\', '\\\\').gsub(':', '\\:') }
27
+ .join(DATA_SEPARATOR)
28
+ end
29
+
30
+ private
31
+
32
+ def fetch(hash, keys)
33
+ value = hash
34
+
35
+ keys.to_s.split('.').each do |key|
36
+ value = if key.to_i.to_s == key
37
+ value[key.to_i]
38
+ else
39
+ value[key].nil? ? value[key.to_sym] : value[key]
40
+ end
41
+ break if value.nil?
42
+ end
43
+
44
+ value
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,4 @@
1
- module Adyen
1
+ module Adyen
2
2
  NAME = "adyen-ruby-api-library"
3
- VERSION = "4.0.0".freeze
3
+ VERSION = "4.3.0".freeze
4
4
  end
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": [
3
+ "config:base"
4
+ ]
5
+ }
@@ -57,10 +57,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
57
57
  to eq(200)
58
58
  expect(response_hash).
59
59
  to eq(JSON.parse(response_body))
60
- expect(response_hash.class).
61
- to be Hash
60
+ expect(response_hash).
61
+ to be_a Adyen::HashWithAccessors
62
+ expect(response_hash).
63
+ to be_a_kind_of Hash
62
64
  expect(response_hash["resultCode"]).
63
65
  to eq("RedirectShopper")
66
+ expect(response_hash.resultCode).
67
+ to eq("RedirectShopper")
64
68
  end
65
69
 
66
70
  # must be created manually due to payments/result format
@@ -89,10 +93,14 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
89
93
  to eq(200)
90
94
  expect(response_hash).
91
95
  to eq(JSON.parse(response_body))
92
- expect(response_hash.class).
93
- to be Hash
96
+ expect(response_hash).
97
+ to be_a Adyen::HashWithAccessors
98
+ expect(response_hash).
99
+ to be_a_kind_of Hash
94
100
  expect(response_hash["resultCode"]).
95
101
  to eq("Authorised")
102
+ expect(response_hash.resultCode).
103
+ to eq("Authorised")
96
104
  end
97
105
 
98
106
  # create client for automated tests
@@ -102,6 +110,7 @@ RSpec.describe Adyen::Checkout, service: "checkout" do
102
110
  # format is defined in spec_helper
103
111
  test_sets = [
104
112
  ["payment_session", "publicKeyToken", "8115054323780109"],
113
+ ["payment_links", "url", "https://checkoutshopper-test.adyen.com"],
105
114
  ["payments", "resultCode", "Authorised"]
106
115
  ]
107
116
 
@@ -19,8 +19,10 @@ RSpec.describe Adyen::CheckoutUtility, service: "checkout utility" do
19
19
  # must be created manually because every field in the response is an array
20
20
  it "makes an origin_keys call" do
21
21
  parsed_body = create_test(@shared_values[:client], @shared_values[:service], "origin_keys", @shared_values[:client].checkout_utility)
22
- expect(parsed_body["originKeys"].class).
23
- to be Hash
22
+ expect(parsed_body["originKeys"]).
23
+ to be_a Adyen::HashWithAccessors
24
+ expect(parsed_body["originKeys"]).
25
+ to be_a_kind_of Hash
24
26
  end
25
27
  end
26
28
 
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Adyen::DataProtection, service: "Data Protection Service" do
4
+ # client instance to be used in dynamically generated tests
5
+ client = create_client(:basic)
6
+
7
+ # methods / values to test for
8
+ # format is defined in spec_helper
9
+ test_sets = [
10
+ ["request_subject_erasure", "result", "SUCCESS"],
11
+ ]
12
+
13
+ generate_tests(client, "DataProtectionService", test_sets, client.data_protection)
14
+ end
@@ -3,15 +3,45 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  RSpec.describe Adyen::AdyenError do
6
+ before(:all) do
7
+ @shared_values = {
8
+ request: {
9
+ amount: {
10
+ currency: "USD",
11
+ value: 1000
12
+ },
13
+ reference: "Your order number",
14
+ paymentMethod: {
15
+ type: "scheme",
16
+ number: "4111111111111111",
17
+ expiryMonth: "10",
18
+ expiryYear: "2020",
19
+ holderName: "John Smith",
20
+ cvc: "737"
21
+ },
22
+ returnUrl: "https://your-company.com/",
23
+ merchantAccount: "YOUR_MERCHANT_ACCOUNT"
24
+ }
25
+ }
26
+ end
27
+
6
28
  describe '#to_s' do
7
29
  it 'describes using the error properties' do
8
- expect(Adyen::AdyenError.new('request', 'response', 'message', 'code').to_s).to eq('Adyen::AdyenError code:code, request:request, response:response, msg:message')
30
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').to_s).to eq("Adyen::AdyenError code:code, msg:message, request:#{@shared_values[:request]}, response:response")
9
31
  end
10
32
  it 'skips the null properties' do
11
- expect(Adyen::AdyenError.new('request', nil, nil, 'code').to_s).to eq('Adyen::AdyenError code:code, request:request')
33
+ expect(Adyen::AdyenError.new(@shared_values[:request], nil, nil, 'code').to_s).to eq("Adyen::AdyenError code:code, request:#{@shared_values[:request]}")
12
34
  end
13
35
  it 'uses the proper error class name' do
14
- expect(Adyen::PermissionError.new('a message', 'a request').to_s).to eq('Adyen::PermissionError code:403, request:a request, msg:a message')
36
+ expect(Adyen::PermissionError.new('message', @shared_values[:request]).to_s).to eq("Adyen::PermissionError code:403, msg:message, request:#{@shared_values[:request]}")
37
+ end
38
+ end
39
+ describe '#masking' do
40
+ it 'masks card number when logging request in errors' do
41
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').request[:paymentMethod][:number]).to eq('411111******1111')
42
+ end
43
+ it 'masks CVC when logging request in errors' do
44
+ expect(Adyen::AdyenError.new(@shared_values[:request], 'response', 'message', 'code').request[:paymentMethod][:cvc]).to eq('***')
15
45
  end
16
46
  end
17
47
  end
@@ -0,0 +1,127 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.describe Adyen::HashWithAccessors do
4
+ shared_examples :hash_with_accessors do
5
+ subject do
6
+ h = described_class.new
7
+ h[key] = 1
8
+ h
9
+ end
10
+
11
+ it 'returns values of a hashes' do
12
+ expect(subject.arbitrary_accessor).to be 1
13
+ end
14
+
15
+ it 'can assign existing values' do
16
+ subject.arbitrary_accessor = 2
17
+ expect(subject.arbitrary_accessor).to be 2
18
+ expect(subject[key]).to be 2
19
+ end
20
+
21
+ it 'complains if there are arguments for the accessor' do
22
+ expect { subject.arbitrary_accessor(1) }.to raise_error(ArgumentError, 'wrong number of arguments (given 1, expected 0)')
23
+ expect { subject.arbitrary_accessor(1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 0)')
24
+ end
25
+
26
+ it 'complains if there are arguments for the accessor =' do
27
+ # using send because i'm not sure how to do this wrong with normal ruby setter calling.
28
+ # just here for completeness
29
+ expect { subject.send(:arbitrary_accessor=) }.to raise_error(ArgumentError, 'wrong number of arguments (given 0, expected 1)')
30
+ expect { subject.send(:arbitrary_accessor=, 1, 2) }.to raise_error(ArgumentError, 'wrong number of arguments (given 2, expected 1)')
31
+ end
32
+
33
+ it 'responds to the accessor' do
34
+ expect(subject).to respond_to(:arbitrary_accessor)
35
+ expect(subject).to respond_to(:arbitrary_accessor=)
36
+ expect(subject).to_not respond_to(:another_accessor)
37
+ expect(subject).to_not respond_to(:another_accessor=)
38
+ end
39
+
40
+ it "raises when the key doesn't exist" do
41
+ expect { subject.another_accessor }.to raise_error(NoMethodError)
42
+ expect { subject.another_accessor = 1 }.to raise_error(NoMethodError)
43
+ end
44
+ end
45
+
46
+ context 'with a string key' do
47
+ let(:key) { 'arbitrary_accessor' }
48
+
49
+ it_behaves_like :hash_with_accessors
50
+ end
51
+
52
+ context 'with a symbol key' do
53
+ let(:key) { :arbitrary_accessor }
54
+
55
+ it_behaves_like :hash_with_accessors
56
+ end
57
+
58
+ context 'with a conflicting key' do
59
+ subject do
60
+ h = described_class.new
61
+ h['keys'] = 'not the keys'
62
+ h
63
+ end
64
+
65
+ it "does original thing if there'd be a conflict" do
66
+ expect(subject.keys).to eq ['keys'] # the default behaviour
67
+ expect(subject['keys']).to eq 'not the keys'
68
+ end
69
+
70
+ it 'still does the writer thing even if the reader is defined' do
71
+ subject.keys = 'written keys'
72
+ expect(subject['keys']).to eq 'written keys'
73
+ expect(subject.keys).to eq ['keys']
74
+ end
75
+ end
76
+
77
+ context 'with some other method missing defined' do
78
+ # this test setup is kind of janky,
79
+ # but we want to confirm super is set up correctly
80
+ # We could do a lot more house-keeping if we weren't sure Hash doesn't
81
+ # define its own method_missing and respond_to_missing? by default,
82
+ # and there was any particular reason to clean up properly and remove our
83
+ # called_super method from Hash.
84
+
85
+ before(:all) do
86
+ class Hash
87
+ def called_super(*args)
88
+ end
89
+
90
+ def method_missing(*args)
91
+ called_super(:method_missing, *args)
92
+ super
93
+ end
94
+
95
+ def respond_to_missing?(*args)
96
+ called_super(:respond_to_missing?, *args)
97
+ super
98
+ end
99
+ end
100
+ end
101
+
102
+ subject do
103
+ h = described_class.new
104
+ h[:my_accessor] = 1
105
+ h
106
+ end
107
+
108
+ it 'can fall back to another respond_to_missing?' do
109
+ expect(subject).to_not receive(:called_super).with(:respond_to_missing?, :my_accessor, false)
110
+ expect(subject).to respond_to(:my_accessor)
111
+ expect(subject).to receive(:called_super).with(:respond_to_missing?, :literally_anything, false)
112
+ expect(subject.respond_to?(:literally_anything)).to be false
113
+ end
114
+
115
+ it 'can fall back to another method_missing' do
116
+ expect(subject).to_not receive(:called_super).with(:method_missing, :my_accessor)
117
+ expect(subject.my_accessor).to be 1
118
+ expect(subject).to receive(:called_super).with(:method_missing, :something_else)
119
+ expect { subject.something_else }.to raise_error(NoMethodError)
120
+ end
121
+
122
+ end
123
+
124
+ it "doesn't modify all hashes" do
125
+ expect { {a: 1}.a }.to raise_error(NoMethodError)
126
+ end
127
+ end
@@ -0,0 +1,14 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Adyen::Payments, service: "marketpay hop service" do
4
+ # client instance to be used in dynamically generated tests
5
+ client = create_client(:basic)
6
+
7
+ # methods / values to test for
8
+ # format is defined in spec_helper
9
+ test_sets = [
10
+ ["get_onboarding_url", "pspReference", "8815850625171183"]
11
+ ]
12
+
13
+ generate_tests(client, "Hop", test_sets, client.marketpay.hop)
14
+ end
@@ -0,0 +1,9 @@
1
+ {
2
+ "amount": {
3
+ "currency": "USD",
4
+ "value": 1000
5
+ },
6
+ "countryCode": "US",
7
+ "merchantAccount": "TestMerchant",
8
+ "reference": "Merchant Reference"
9
+ }