connect-sdk-ruby 1.8.0 → 1.9.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 980877434fcc0d67e0a3b79881d54a496584dff0
4
- data.tar.gz: 84819aa760a8d93e692c0b2cbce97fa7a20a2f7c
3
+ metadata.gz: b1cc2f2e689f0848718eb4a78877c4f83506c06e
4
+ data.tar.gz: a58d8736d1ec8609b0f19d3a358dcbd898c4b8c0
5
5
  SHA512:
6
- metadata.gz: 7299829037d437656d41cee2a9331a67f35cc736d0ab7bfc7a368e771243f6ce62ebd6ba5f32b6080f40ce53f0c0a28fc976dcedf0f2013f897c17a1688f011f
7
- data.tar.gz: 015e6059dc121b9dc0c34085147c173aa854802eaeef3e47a38e8d6cad9917964cebe3cd19774ef8e3973f554b86940c1040252770bf4794de63e75b4b7a7385
6
+ metadata.gz: b0792a215969663067ac9057eac4801847ac65f216f36357d54fe2066fac1d144a5d566f9d485897d5069464cde0cacdd28194f62ce485a475e8eca8c95c2bd0
7
+ data.tar.gz: 25db60ceb4407cd84cf296aa6a918c4c52a5c67b9f42582006a48ddfb98f7cac4b1cbd1d49cb5b6614d5c90c5b94b22d8cd6082e2225fa88cc2a4338697b7666
data/README.md CHANGED
@@ -32,6 +32,7 @@ As for JRuby, version 9.0.0.0 and higher are supported.
32
32
  In addition, the following package is required:
33
33
 
34
34
  * [httpclient](https://github.com/nahi/httpclient) 2.8 or higher
35
+ * [concurrent-ruby](https://github.com/ruby-concurrency/concurrent-ruby) 1.0 or higher
35
36
 
36
37
  ## Installation
37
38
 
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |spec|
2
2
  spec.name = 'connect-sdk-ruby'
3
- spec.version = '1.8.0'
3
+ spec.version = '1.9.0'
4
4
  spec.authors = ['Ingenico ePayments']
5
5
  spec.email = ['github@epay.ingenico.com']
6
6
  spec.summary = %q{SDK to communicate with the Ingenico ePayments platform using the Ingenico Connect Server API}
@@ -18,6 +18,7 @@ Gem::Specification.new do |spec|
18
18
  spec.required_ruby_version = '>= 2.0'
19
19
 
20
20
  spec.add_dependency 'httpclient', '~> 2.8'
21
+ spec.add_dependency 'concurrent-ruby', '~>1.0'
21
22
 
22
23
  spec.add_development_dependency 'yard', '~> 0.9'
23
24
  spec.add_development_dependency 'rspec', '~> 3.5'
@@ -25,3 +25,4 @@ require prefix + 'meta_data_provider'
25
25
  require prefix + 'factory'
26
26
 
27
27
  require prefix + 'defaultimpl'
28
+ require prefix + 'webhooks'
@@ -6,14 +6,13 @@ module Ingenico::Connect::SDK
6
6
 
7
7
  # {Ingenico::Connect::SDK::Logging::CommunicatorLogger} that logs the messages to $stdout.
8
8
  class StdoutCommunicatorLogger < CommunicatorLogger
9
-
10
9
  include Singleton
11
10
 
12
11
  def initialize
13
12
  # implement the interface
14
13
  end
15
14
 
16
- # NOTE: this is needed to not break method calls based on old interface
15
+ # NOTE: this alias is needed to not break existing method calls depending on old interface
17
16
  class << self
18
17
  alias_method :INSTANCE, :instance
19
18
  end
@@ -5,7 +5,7 @@ module Ingenico::Connect::SDK
5
5
 
6
6
  # Manages metadata about the server using the SDK
7
7
  class MetaDataProvider
8
- @@SDK_VERSION = '1.8.0'
8
+ @@SDK_VERSION = '1.9.0'
9
9
  @@SERVER_META_INFO_HEADER = 'X-GCS-ServerMetaInfo'
10
10
  @@PROHIBITED_HEADERS = [@@SERVER_META_INFO_HEADER, 'X-GCS-Idempotence-Key',
11
11
  'Date', 'Content-Type', 'Authorization'].sort!.freeze
@@ -142,3 +142,7 @@ end
142
142
  # Contains data classes related to shopping cart functionality.
143
143
  module Ingenico::Connect::SDK::Domain::MetaData
144
144
  end
145
+
146
+ # Contains data classes related to webhooks functionality.
147
+ module Ingenico::Connect::SDK::Webhooks
148
+ end
@@ -0,0 +1,11 @@
1
+ prefix = 'ingenico/connect/sdk/webhooks/'
2
+
3
+ require prefix + 'api_version_mismatch_exception'
4
+ require prefix + 'signature_validation_exception'
5
+ require prefix + 'secret_key_not_available_exception'
6
+ require prefix + 'secret_key_store'
7
+ require prefix + 'in_memory_secret_key_store'
8
+ require prefix + 'webhooks_event'
9
+ require prefix + 'webhooks_helper'
10
+ require prefix + 'webhooks_helper_builder'
11
+ require prefix + 'webhooks'
@@ -0,0 +1,20 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+ # Raised when a webhooks event has an API version that is not supported by current version
4
+ # of SDK.
5
+ class ApiVersionMismatchException < RuntimeError
6
+
7
+ def initialize(event_api_version, sdk_api_version)
8
+ super("event API version #{event_api_version} is not compatible with SDK API version #{sdk_api_version}")
9
+ @event_api_version = event_api_version
10
+ @sdk_api_version = sdk_api_version
11
+ end
12
+
13
+ # The API version from the webhooks event.
14
+ attr_reader :event_api_version
15
+
16
+ # The API version that this version of the SDK supports.
17
+ attr_reader :sdk_api_version
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,56 @@
1
+ require 'concurrent'
2
+ require 'singleton'
3
+
4
+ module Ingenico::Connect::SDK
5
+ module Webhooks
6
+ # An in-memory secret key store. This implementation can be used
7
+ # in applications where secret keys are specified at application
8
+ # startup. Thread-safe.
9
+ class InMemorySecretKeyStore
10
+
11
+ include Singleton
12
+ include SecretKeyStore
13
+
14
+ # Creates new InMemorySecretKeyStore
15
+ def initialize
16
+ # NOTE: use Map instead of Hash to provide better performance
17
+ # under high concurrency.
18
+ @store = Concurrent::Map.new
19
+ end
20
+
21
+ # Retrieves the secret key corresponding to the given key id
22
+ #
23
+ # key_id:: key id of the secret key
24
+ # Raises {Ingenico::Connect::SDK::Webhooks::SecretKeyNotAvailableException} if the secret key for the given key id is not available.
25
+ def get_secret_key(key_id)
26
+ if (secret_key = @store.get(key_id)).nil?
27
+ msg = "could not find secret key for key id " + key_id
28
+ raise SecretKeyNotAvailableException.new(message: msg, key_id: key_id)
29
+ end
30
+ secret_key
31
+ end
32
+
33
+ # Stores the given secret key for the given key id.
34
+ #
35
+ # key_id:: key id of the secret key
36
+ # secret_id:: the secret key to be stored
37
+ def store_secret_key(key_id, secret_key)
38
+ raise ArgumentError if key_id.nil? or key_id.strip.empty?
39
+ raise ArgumentError if secret_key.nil? or secret_key.strip.empty?
40
+ @store.put(key_id, secret_key)
41
+ end
42
+
43
+ # Removes the secret key for the given key id.
44
+ #
45
+ # key_id:: the key id whose corresponding secret should be removed from the store
46
+ def remove_secret_key(key_id)
47
+ @store.delete(key_id)
48
+ end
49
+
50
+ # Removes all stored secret keys from the store
51
+ def clear
52
+ @store.clear
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,15 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+ # Raised when an error caused a secret to become not available.
4
+ class SecretKeyNotAvailableException < SignatureValidationException
5
+
6
+ def initialize(args)
7
+ raise ArgumentError if (key_id = args.delete(:key_id)).nil? # key_id is mandatory
8
+ super(args)
9
+ @key_id = key_id
10
+ end
11
+
12
+ attr_reader :key_id
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,16 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+ # An abstract store of secret keys. Implementation can store secret keys in a database,
4
+ # on disk, etc. Should be Thread-safe.
5
+ module SecretKeyStore
6
+
7
+ # Retrieve secret key for given key id
8
+ #
9
+ # key_id:: given key id
10
+ # Raises {Ingenico::Connect::SDK::Webhooks::SecretKeyNotAvailableException} if the secret key for the given key id is not available.
11
+ def get_secret_key(key_id)
12
+ raise NotImplementedError
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,19 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+
4
+ # Raised when an error occurred when validating Webhooks signatures
5
+ class SignatureValidationException < RuntimeError
6
+
7
+ # Creates a new SignatureValidationException
8
+ #
9
+ # @param [Hash] args the options to create the Exception with
10
+ # @option args [String] :message the error message
11
+ # @option args [RuntimeError] :cause an Error object that causes the Exception
12
+ def initialize(args)
13
+ super(args[:message]) # NOTE: can be nil
14
+ # store backtrace info if exception given
15
+ set_backtrace(args[:cause].backtrace) unless args[:cause].nil?
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,21 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+ # Ingenico ePayments platform factory for several webhooks components
4
+ module Webhooks
5
+
6
+ # Creates a WebhooksHelperBuilder that uses the
7
+ # given SecretkeyStore
8
+ def self.create_helper_builder(secret_key_store)
9
+ WebhooksHelperBuilder.new
10
+ .with_marshaller(DefaultMarshaller.INSTANCE)
11
+ .with_secret_key_store(secret_key_store)
12
+ end
13
+
14
+ # Creates a WebhooksHelper that uses the given
15
+ # SecretkeyStore.
16
+ def self.create_helper(secret_key_store)
17
+ create_helper_builder(secret_key_store).build
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,61 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+
4
+ class WebhooksEvent < Ingenico::Connect::SDK::DataObject
5
+
6
+ # String
7
+ attr_accessor :api_version
8
+
9
+ # String
10
+ attr_accessor :id
11
+
12
+ # String
13
+ attr_accessor :created
14
+
15
+ # String
16
+ attr_accessor :merchant_id
17
+
18
+ # String
19
+ attr_accessor :type
20
+
21
+ # {Ingenico::Connect::SDK::Domain::Payment::PaymentResponse}
22
+ attr_accessor :payment
23
+
24
+ # {Ingenico::Connect::SDK::Domain::Payout::PayoutResponse}
25
+ attr_accessor :refund
26
+
27
+ # {Ingenico::Connect::SDK::Domain::Refund::RefundResponse}
28
+ attr_accessor :payout
29
+
30
+ # {Ingenico::Connect::SDK::Domain::Token::TokenResponse}
31
+ attr_accessor :token
32
+
33
+ def to_h
34
+ hash = super
35
+ add_to_hash(hash, 'apiVersion', @api_version)
36
+ add_to_hash(hash, 'id', @id)
37
+ add_to_hash(hash, 'created', @created)
38
+ add_to_hash(hash, 'merchantId', @merchant_id)
39
+ add_to_hash(hash, 'type', @type)
40
+ add_to_hash(hash, 'payment', @payment)
41
+ add_to_hash(hash, 'refund', @refund)
42
+ add_to_hash(hash, 'payout', @payout)
43
+ add_to_hash(hash, 'token', @token)
44
+ hash
45
+ end
46
+
47
+ def from_hash(hash)
48
+ super
49
+ @api_version = hash['apiVersion'] if hash.has_key? 'apiVersion'
50
+ @id = hash['id'] if hash.has_key? 'id'
51
+ @created = hash['created'] if hash.has_key? 'created'
52
+ @merchant_id = hash['merchantId'] if hash.has_key? 'merchantId'
53
+ @type = hash['type'] if hash.has_key? 'type'
54
+ @payment = Ingenico::Connect::SDK::Domain::Payment::PaymentResponse.new_from_hash(hash['payment']) if hash.has_key? 'payment'
55
+ @refund = Ingenico::Connect::SDK::Domain::Refund::RefundResponse.new_from_hash(hash['refund']) if hash.has_key? 'refund'
56
+ @payout = Ingenico::Connect::SDK::Domain::Payout::PayoutResponse.new_from_hash(hash['payout']) if hash.has_key? 'payout'
57
+ @token = Ingenico::Connect::SDK::Domain::Token::TokenResponse.new_from_hash(hash['token']) if hash.has_key? 'token'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,98 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+
4
+ module Ingenico::Connect::SDK
5
+ module Webhooks
6
+
7
+ # Ingenico ePayments platform webhooks Helper, Thread-safe.
8
+ class WebhooksHelper
9
+ def initialize(marshaller, secret_key_store)
10
+ raise ArgumentError if marshaller.nil?
11
+ raise ArgumentError if secret_key_store.nil?
12
+ @marshaller = marshaller
13
+ @secret_key_store = secret_key_store
14
+ end
15
+
16
+ # Unmarshals the given body, while also validating it using the given
17
+ # request headers.
18
+ # body:: body of the request, a String
19
+ # request_headers:: headers of the request, as an Array of {Ingenico::Connect::SDK::RequestHeader}
20
+ def unmarshal(body, request_headers)
21
+ validate(body, request_headers)
22
+ event = @marshaller.unmarshal(body, WebhooksEvent)
23
+ validate_api_version(event)
24
+ event
25
+ end
26
+
27
+ # Validates incoming request using request headers
28
+ #
29
+ # body:: body of the request, a String
30
+ # request_headers:: headers of the request which is an Array of {Ingenico::Connect::SDK::RequestHeader}
31
+ def validate(body, request_headers)
32
+ validate_body(body, request_headers)
33
+ end
34
+
35
+ private
36
+
37
+ HEADER_SIGNATURE = 'X-GCS-Signature'.freeze
38
+ HEADER_KEY_ID = 'X-GCS-KeyId'.freeze
39
+ HMAC_SCHEME = 'SHA256'.freeze
40
+
41
+ # validation utility methods
42
+
43
+ # Validates the body using given request headers
44
+ #
45
+ # body:: a String converted from byte array
46
+ # request_headers:: headers of the request, as an Array of SDK::RequestHeader
47
+ def validate_body(body, request_headers)
48
+ signature = get_header_value(request_headers, HEADER_SIGNATURE)
49
+ key_id = get_header_value(request_headers, HEADER_KEY_ID)
50
+ secret_key = @secret_key_store.get_secret_key(key_id)
51
+ digest = OpenSSL::Digest.new(HMAC_SCHEME)
52
+ hmac = OpenSSL::HMAC.digest(digest, secret_key, body)
53
+ expected_signature = Base64.strict_encode64(hmac).strip
54
+
55
+ unless equal_signatures?(signature, expected_signature)
56
+ msg = "failed to validate signature '#{signature}'"
57
+ raise SignatureValidationException.new(message: msg)
58
+ end
59
+ end
60
+
61
+ # Checks two signatures
62
+ # signature:: a String
63
+ # expected_signature:: a String
64
+ def equal_signatures?(signature, expected_signature)
65
+ # NOTE: copy the signatures to avoid runtime tampering via references
66
+ signature = signature.dup.freeze
67
+ expected_signature = expected_signature.dup.freeze
68
+ # NOTE: do not use simple equality comparision to avoid side channel attack
69
+ limit = [signature.length, expected_signature.length, 256].max
70
+ limit.times.inject(true) do |flag, idx|
71
+ # NOTE: this block is constructed to take constant time to run
72
+ flag &= signature[idx] == expected_signature[idx]
73
+ # [] returns nil if idx >= the length of the String
74
+ end
75
+ end
76
+
77
+ # general utility methods
78
+
79
+ # Returns true if the client API version and the webhooks event API version matches
80
+ def validate_api_version(event)
81
+ raise ApiVersionMismatchException.new(event.api_version, Client.API_VERSION) unless Client.API_VERSION.eql?(event.api_version)
82
+ end
83
+
84
+ # Retrieves header value from the request headers whose header name matches the given parameter
85
+ def get_header_value(request_headers, header_name)
86
+ if (right_header = request_headers.select { |h| h.name.casecmp(header_name) == 0 }).size != 1
87
+ msg = if right_header.empty?
88
+ "could not find header '#{header_name}'"
89
+ else # more than 2 headers
90
+ "encountered multiple occurrences of header '#{header_name}'"
91
+ end
92
+ raise SignatureValidationException.new(message: msg)
93
+ end
94
+ right_header.first.value
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,25 @@
1
+ module Ingenico::Connect::SDK
2
+ module Webhooks
3
+
4
+ # Builder for a WebhooksHelper object.
5
+ class WebhooksHelperBuilder
6
+
7
+ # Sets the Marshaller to use.
8
+ def with_marshaller(marshaller)
9
+ @marshaller = marshaller
10
+ self
11
+ end
12
+
13
+ # Sets the SecretkeyStore to use.
14
+ def with_secret_key_store(secret_key_store)
15
+ @secret_key_store = secret_key_store
16
+ self
17
+ end
18
+
19
+ # Creates a fully initialized WebhooksHelper object
20
+ def build
21
+ WebhooksHelper.new(@marshaller, @secret_key_store)
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,31 @@
1
+ {
2
+ "apiVersion": "v1",
3
+ "id": "8ee793f6-4553-4749-85dc-f2ef095c5ab0",
4
+ "created": "2017-02-02T11:25:14.040+0100",
5
+ "merchantId": "20000",
6
+ "type": "payment.paid",
7
+ "payment": {
8
+ "id": "00000200000143570012",
9
+ "paymentOutput": {
10
+ "amountOfMoney": {
11
+ "amount": 1000,
12
+ "currencyCode": "EUR"
13
+ },
14
+ "references": {
15
+ "paymentReference": "200001681810"
16
+ },
17
+ "paymentMethod": "bankTransfer",
18
+ "bankTransferPaymentMethodSpecificOutput": {
19
+ "paymentProductId": 11
20
+ }
21
+ },
22
+ "status": "PAID",
23
+ "statusOutput": {
24
+ "isCancellable": false,
25
+ "statusCategory": "COMPLETED",
26
+ "statusCode": 1000,
27
+ "statusCodeChangeDateTime": "20170202112514",
28
+ "isAuthorized": true
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "apiVersion": "v1",
3
+ "id": "8ee793f6-4553-4749-85dc-f2ef095c5ab0",
4
+ "created": "2017-02-02T11:24:14.040+0100",
5
+ "merchantId": "20000",
6
+ "type": "payment.paid",
7
+ "payment": {
8
+ "id": "00000200000143570012",
9
+ "paymentOutput": {
10
+ "amountOfMoney": {
11
+ "amount": 1000,
12
+ "currencyCode": "EUR"
13
+ },
14
+ "references": {
15
+ "paymentReference": "200001681810"
16
+ },
17
+ "paymentMethod": "bankTransfer",
18
+ "bankTransferPaymentMethodSpecificOutput": {
19
+ "paymentProductId": 11
20
+ }
21
+ },
22
+ "status": "PAID",
23
+ "statusOutput": {
24
+ "isCancellable": false,
25
+ "statusCategory": "COMPLETED",
26
+ "statusCode": 1000,
27
+ "statusCodeChangeDateTime": "20170202112414",
28
+ "isAuthorized": true
29
+ }
30
+ }
31
+ }
@@ -0,0 +1,121 @@
1
+ require 'spec_helper'
2
+
3
+ include Ingenico::Connect::SDK
4
+ describe Webhooks::WebhooksHelper do
5
+
6
+ # define constants for the testbench
7
+ SIGNATURE_HEADER = "X-GCS-Signature"
8
+ SIGNATURE = "2S7doBj/GnJnacIjSJzr5fxGM5xmfQyFAwxv1I53ZEk="
9
+ KEY_ID_HEADER = "X-GCS-KeyId"
10
+ KEY_ID = "dummy-key-id"
11
+ SECRET_KEY = "hello+world"
12
+
13
+ def create_helper(marshaller = DefaultImpl::DefaultMarshaller.INSTANCE)
14
+ Webhooks::WebhooksHelper.new(marshaller, Webhooks::InMemorySecretKeyStore.instance)
15
+ end
16
+
17
+ def read_resource(resource_name)
18
+ prefix = 'spec/fixtures/resources/webhooks/'
19
+ IO.read(prefix + resource_name)
20
+ end
21
+
22
+ before do
23
+ Webhooks::InMemorySecretKeyStore.instance.clear
24
+ end
25
+
26
+ after do
27
+ Webhooks::InMemorySecretKeyStore.instance.clear
28
+ end
29
+
30
+ context 'unmarshal' do
31
+ let(:helper) { create_helper }
32
+ let(:body) { read_resource('valid-body') }
33
+ let(:sig_header) { RequestHeader.new(SIGNATURE_HEADER, SIGNATURE) }
34
+ let(:key_header) { RequestHeader.new(KEY_ID_HEADER, KEY_ID) }
35
+ let(:request_headers) { [sig_header, key_header] }
36
+
37
+ it 'should raise ApiVersionMismatchException when API version does not match' do
38
+ # mock marshaller once to return an event with a wrong API version number
39
+ expect(DefaultImpl::DefaultMarshaller.INSTANCE).to receive(:unmarshal) do |body, klass|
40
+ event = klass.new_from_hash(JSON.load(body))
41
+ event.api_version = 'v0' # wrong version
42
+ event
43
+ end
44
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
45
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::ApiVersionMismatchException)
46
+ end
47
+
48
+ it 'should raise SecretKeyNotAvailableException when no secret key exists' do
49
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::SecretKeyNotAvailableException)
50
+ end
51
+
52
+ it 'should raise SignatureValidationException when the signature is missing' do
53
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
54
+ expect{helper.unmarshal(body, [])}.to raise_error(Webhooks::SignatureValidationException)
55
+ end
56
+
57
+ it 'should raise SignatureValidationException when there are duplicate headers' do
58
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
59
+ request_headers = [sig_header, key_header, sig_header]
60
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::SignatureValidationException)
61
+ end
62
+
63
+ it 'should work when everything is correct' do
64
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
65
+ event = helper.unmarshal(body, request_headers)
66
+ expect(event.api_version).to eq('v1')
67
+ expect(event.id).to eq("8ee793f6-4553-4749-85dc-f2ef095c5ab0")
68
+ expect(event.created).to eq("2017-02-02T11:24:14.040+0100")
69
+ expect(event.merchant_id).to eq('20000')
70
+ expect(event.type).to eq('payment.paid')
71
+
72
+ expect(event.refund).to be_nil
73
+ expect(event.payout).to be_nil
74
+ expect(event.token).to be_nil
75
+
76
+ expect(event.payment).not_to be_nil
77
+ expect(event.payment.id).to eq("00000200000143570012")
78
+ expect(event.payment.payment_output).not_to be_nil
79
+ expect(event.payment.payment_output.amount_of_money).not_to be_nil
80
+ expect(event.payment.payment_output.amount_of_money.amount).to eq(1000)
81
+ expect(event.payment.payment_output.amount_of_money.currency_code).to eq('EUR')
82
+ expect(event.payment.payment_output.references).not_to be_nil
83
+ expect(event.payment.payment_output.references.payment_reference).to eq("200001681810")
84
+ expect(event.payment.payment_output.payment_method).to eq("bankTransfer")
85
+
86
+ expect(event.payment.payment_output.card_payment_method_specific_output).to be_nil
87
+ expect(event.payment.payment_output.cash_payment_method_specific_output).to be_nil
88
+ expect(event.payment.payment_output.direct_debit_payment_method_specific_output).to be_nil
89
+ expect(event.payment.payment_output.invoice_payment_method_specific_output).to be_nil
90
+ expect(event.payment.payment_output.redirect_payment_method_specific_output).to be_nil
91
+ expect(event.payment.payment_output.sepa_direct_debit_payment_method_specific_output).to be_nil
92
+ expect(event.payment.payment_output.bank_transfer_payment_method_specific_output).not_to be_nil
93
+ expect(event.payment.payment_output.bank_transfer_payment_method_specific_output.payment_product_id).to eq(11)
94
+
95
+ expect(event.payment.status).to eq('PAID')
96
+ expect(event.payment.status_output).not_to be_nil
97
+ expect(event.payment.status_output.is_cancellable).to be false
98
+ expect(event.payment.status_output.status_category).to eq('COMPLETED')
99
+ expect(event.payment.status_output.status_code).to eq(1000)
100
+ expect(event.payment.status_output.status_code_change_date_time).to eq("20170202112414")
101
+ expect(event.payment.status_output.is_authorized).to be true
102
+ end
103
+
104
+ it 'should raise SignatureValidationException when the body is invalid' do
105
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
106
+ body = read_resource('invalid-body')
107
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::SignatureValidationException)
108
+ end
109
+
110
+ it 'should raise SignatureValidationException when the secret key is invalid' do
111
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, '1'+SECRET_KEY) # wrong key
112
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::SignatureValidationException)
113
+ end
114
+
115
+ it 'should raise SignatureValidationException when the signature is invalid' do
116
+ Webhooks::InMemorySecretKeyStore.instance.store_secret_key(KEY_ID, SECRET_KEY)
117
+ request_headers = [RequestHeader.new(SIGNATURE_HEADER, '1'+SIGNATURE), key_header] # wrong signature
118
+ expect{helper.unmarshal(body, request_headers)}.to raise_error(Webhooks::SignatureValidationException)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,17 @@
1
+ require 'spec_helper'
2
+
3
+ include Ingenico::Connect::SDK
4
+
5
+ describe Webhooks::Webhooks do
6
+ let(:webhooks_helper) { Webhooks::Webhooks.create_helper(Webhooks::InMemorySecretKeyStore.instance) }
7
+
8
+ context 'construction' do
9
+ it 'uses the default marshaller' do
10
+ expect(webhooks_helper.instance_variable_get(:@marshaller)).to eq(DefaultImpl::DefaultMarshaller.INSTANCE)
11
+ end
12
+
13
+ it 'uses the given key store' do
14
+ expect(webhooks_helper.instance_variable_get(:@secret_key_store)).to eq(Webhooks::InMemorySecretKeyStore.instance)
15
+ end
16
+ end
17
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: connect-sdk-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ingenico ePayments
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-09-06 00:00:00.000000000 Z
11
+ date: 2017-09-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: httpclient
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.8'
27
+ - !ruby/object:Gem::Dependency
28
+ name: concurrent-ruby
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: yard
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -459,6 +473,16 @@ files:
459
473
  - lib/ingenico/connect/sdk/response_header.rb
460
474
  - lib/ingenico/connect/sdk/session.rb
461
475
  - lib/ingenico/connect/sdk/validation_exception.rb
476
+ - lib/ingenico/connect/sdk/webhooks.rb
477
+ - lib/ingenico/connect/sdk/webhooks/api_version_mismatch_exception.rb
478
+ - lib/ingenico/connect/sdk/webhooks/in_memory_secret_key_store.rb
479
+ - lib/ingenico/connect/sdk/webhooks/secret_key_not_available_exception.rb
480
+ - lib/ingenico/connect/sdk/webhooks/secret_key_store.rb
481
+ - lib/ingenico/connect/sdk/webhooks/signature_validation_exception.rb
482
+ - lib/ingenico/connect/sdk/webhooks/webhooks.rb
483
+ - lib/ingenico/connect/sdk/webhooks/webhooks_event.rb
484
+ - lib/ingenico/connect/sdk/webhooks/webhooks_helper.rb
485
+ - lib/ingenico/connect/sdk/webhooks/webhooks_helper_builder.rb
462
486
  - spec/comparable_extension.rb
463
487
  - spec/fixtures/resources/defaultimpl/convertAmount.json
464
488
  - spec/fixtures/resources/defaultimpl/createPayment.failure.invalidCardNumber.json
@@ -487,6 +511,8 @@ files:
487
511
  - spec/fixtures/resources/payment/rejected.json
488
512
  - spec/fixtures/resources/properties.proxy.yml
489
513
  - spec/fixtures/resources/properties.yml
514
+ - spec/fixtures/resources/webhooks/invalid-body
515
+ - spec/fixtures/resources/webhooks/valid-body
490
516
  - spec/integration/connection_pooling_spec.rb
491
517
  - spec/integration/convert_amount_spec.rb
492
518
  - spec/integration/idempotence_spec.rb
@@ -519,6 +545,8 @@ files:
519
545
  - spec/lib/requestparams/find_params_spec.rb
520
546
  - spec/lib/requestparams/get_params_spec.rb
521
547
  - spec/lib/requestparams/param_request_spec.rb
548
+ - spec/lib/webhooks/webhooks_helper_spec.rb
549
+ - spec/lib/webhooks/webhooks_spec.rb
522
550
  - spec/spec_helper.rb
523
551
  homepage: https://github.com/Ingenico-ePayments/connect-sdk-ruby
524
552
  licenses:
@@ -574,6 +602,8 @@ test_files:
574
602
  - spec/fixtures/resources/payment/rejected.json
575
603
  - spec/fixtures/resources/properties.proxy.yml
576
604
  - spec/fixtures/resources/properties.yml
605
+ - spec/fixtures/resources/webhooks/invalid-body
606
+ - spec/fixtures/resources/webhooks/valid-body
577
607
  - spec/integration/connection_pooling_spec.rb
578
608
  - spec/integration/convert_amount_spec.rb
579
609
  - spec/integration/idempotence_spec.rb
@@ -606,4 +636,6 @@ test_files:
606
636
  - spec/lib/requestparams/find_params_spec.rb
607
637
  - spec/lib/requestparams/get_params_spec.rb
608
638
  - spec/lib/requestparams/param_request_spec.rb
639
+ - spec/lib/webhooks/webhooks_helper_spec.rb
640
+ - spec/lib/webhooks/webhooks_spec.rb
609
641
  - spec/spec_helper.rb