connect-sdk-ruby 1.8.0 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -0
- data/connect-sdk-ruby.gemspec +2 -1
- data/lib/ingenico/connect/sdk.rb +1 -0
- data/lib/ingenico/connect/sdk/logging/stdout_communicator_logger.rb +1 -2
- data/lib/ingenico/connect/sdk/meta_data_provider.rb +1 -1
- data/lib/ingenico/connect/sdk/modules.rb +4 -0
- data/lib/ingenico/connect/sdk/webhooks.rb +11 -0
- data/lib/ingenico/connect/sdk/webhooks/api_version_mismatch_exception.rb +20 -0
- data/lib/ingenico/connect/sdk/webhooks/in_memory_secret_key_store.rb +56 -0
- data/lib/ingenico/connect/sdk/webhooks/secret_key_not_available_exception.rb +15 -0
- data/lib/ingenico/connect/sdk/webhooks/secret_key_store.rb +16 -0
- data/lib/ingenico/connect/sdk/webhooks/signature_validation_exception.rb +19 -0
- data/lib/ingenico/connect/sdk/webhooks/webhooks.rb +21 -0
- data/lib/ingenico/connect/sdk/webhooks/webhooks_event.rb +61 -0
- data/lib/ingenico/connect/sdk/webhooks/webhooks_helper.rb +98 -0
- data/lib/ingenico/connect/sdk/webhooks/webhooks_helper_builder.rb +25 -0
- data/spec/fixtures/resources/webhooks/invalid-body +31 -0
- data/spec/fixtures/resources/webhooks/valid-body +31 -0
- data/spec/lib/webhooks/webhooks_helper_spec.rb +121 -0
- data/spec/lib/webhooks/webhooks_spec.rb +17 -0
- metadata +34 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b1cc2f2e689f0848718eb4a78877c4f83506c06e
|
4
|
+
data.tar.gz: a58d8736d1ec8609b0f19d3a358dcbd898c4b8c0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
|
data/connect-sdk-ruby.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |spec|
|
2
2
|
spec.name = 'connect-sdk-ruby'
|
3
|
-
spec.version = '1.
|
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'
|
data/lib/ingenico/connect/sdk.rb
CHANGED
@@ -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
|
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
|
+
@@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
|
@@ -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.
|
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-
|
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
|