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 +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
|