adyen_client 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: bed3b1c2cd6bad30f1278dedc5bbf0a6eb1192e6
4
+ data.tar.gz: eb471bf5fc009cfaa44ba2c4809602363c3bfdc5
5
+ SHA512:
6
+ metadata.gz: 3b31679659538ec245122826e0c465836b80ab6d02e3a171c5f729dfade0773191d7d570ddf90cdfd50bbc1e92dac245e10bf387b5551387d50d4f3e38a2dfb5
7
+ data.tar.gz: 2f344b0abdd6fd6ad19dfca2db359bbb769ad2c5f702fe9370a0cb9369e80d7890aa9de54eb4055c99a321f57562550ef57629fcc9054980c017a9bc11a5166c
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Lukas Rieder
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,128 @@
1
+ # A simple client that talks to the Adyen API
2
+
3
+ [![Inline docs](http://inch-ci.org/github/Overbryd/adyen_client.svg?branch=master)](http://inch-ci.org/github/Overbryd/adyen_client)
4
+
5
+ > Does not try to be smart, stays close to the documentation while adhering to Ruby conventions.
6
+
7
+ ## Setup & Configuration
8
+
9
+ `gem install adyen_client`
10
+
11
+ In your Gemfile:
12
+
13
+ `gem "adyen_client"`
14
+
15
+ Require and configure the client:
16
+
17
+ ```ruby
18
+ require "adyen_client"
19
+
20
+ # Block style
21
+ AdyenClient.configure do |c|
22
+ c.environment = :test
23
+ c.username = "ws_123456@Company.FooBar"
24
+ c.password = "correctbatteryhorsestaple"
25
+ c.cse_public_key = "10001|..."
26
+ c.default_merchant_account = "FooBar123"
27
+ c.default_currency = "EUR"
28
+ end
29
+
30
+ # Hash style works too, string or symbol keys
31
+ AdyenClient.configure(environment: :test, username: "ws_123456@Company.FooBar", ...)
32
+
33
+ # That comes in handy to configure the client from a YAML file
34
+ AdyenClient.configure(YAML.load_file(Rails.root.join("config", "adyen.yml"))[Rails.env.to_s])
35
+
36
+ # You can override all default options for each instance of a client
37
+ client = AdyenClient.new(merchant_account: "FooBarSubMerchant123")
38
+ eur_client = AdyenClient.new(currency: "EUR")
39
+ ```
40
+
41
+ ## Examples
42
+
43
+ ### Simple payment
44
+
45
+ ```ruby
46
+ client = AdyenClient.new
47
+ response = client.authorise(amount: 100, encrypted_card: "adyenjs_0_1_15$OlmG...")
48
+ if response.authorised?
49
+ puts "( ノ ゚ー゚)ノ"
50
+ else
51
+ puts "(-‸ლ)"
52
+ end
53
+ ```
54
+
55
+ ### Setup a recurring contract, charge users later
56
+
57
+ ```ruby
58
+ user = User.create(email: "john@doe.com", last_ip: request.remote_ip)
59
+
60
+ client = AdyenClient.new
61
+ response = client.create_recurring_contract(encrypted_card: "adyenjs_0_1_15$OlmG...", shopper: {
62
+ reference: user.id,
63
+ email: user.email,
64
+ ip: user.last_ip # optional but recommended
65
+ })
66
+ if response.authorised?
67
+ # now we know the users card is valid
68
+ else
69
+ # something is wrong with the users card or we got an error
70
+ end
71
+ ```
72
+
73
+ Later, we want to charge the user based on that contract.
74
+
75
+ ```ruby
76
+ user = User.find_by_email("john@doe.com")
77
+
78
+ client = AdyenClient.new
79
+ response = client.authorise_recurring_payment(amount: 1699, shopper: { reference: user.id })
80
+ if response.authorised?
81
+ # we know the payment is on its way
82
+ else
83
+ # something is wrong, maybe we got an error
84
+ end
85
+ ```
86
+
87
+ ## Documentation
88
+
89
+ All publicly usable [methods and classes are documented here](http://rdoc.info/projects/Overbryd/adyen_client).
90
+
91
+ This library does not try to be too smart, it simply provides a layer of abstraction on top of the Adyen JSON API.
92
+ Also the default `AdyenClient::Response` class basically just wraps the JSON response.
93
+ The only work it does is converting `camelCase` keys to `sneak_case`, removing unnecessary object nestings and providing you with a convenience `authorised?` method.
94
+
95
+ If you want a more sophisticated response class, you can easily hook up your own.
96
+ The only method you need to provide is `::new`. It will receive one argument, the [`HTTParty::Response`](http://www.rubydoc.info/github/jnunemaker/httparty/HTTParty/Response) for the given request.
97
+
98
+ ```ruby
99
+ class MyAdyenResponse
100
+ def self.parse(http_response)
101
+ # ... your fancy code
102
+ end
103
+ end
104
+ ```
105
+
106
+ Hook it up by initialising the client like this: `AdyenClient.new(response_class: MyAdyenResponse)`.
107
+
108
+ Similar, if you want nothing else than the bare `HTTParty::Response`, initialise the client with: `response_class: nil`.
109
+
110
+
111
+ ## Contributing
112
+
113
+ I am very happy to receive pull requests or bug reports for problems with the library.
114
+ Please make sure you are only reporting an actual issue with the library itself, I cannot help with your payment flow or advise you on anything related to the Adyen API.
115
+
116
+ ## Disclaimer
117
+
118
+ I am not associated with Adyen in any way.
119
+ If you have problems with your adyen account or your payment flow, please contact the very helpful Adyen support using `support ät adyen.com`.
120
+
121
+ Please make yourself comfortable [with the Adyen documentation](https://docs.adyen.com/) on how you want to setup your payment flow.
122
+
123
+ ## License
124
+
125
+ The MIT License (MIT), Copyright (c) 2015 Lukas Rieder
126
+
127
+ See [`LICENSE`](https://github.com/Overbryd/adyen_client/blob/master/LICENSE).
128
+
@@ -0,0 +1,27 @@
1
+ class AdyenClient
2
+
3
+ class Configuration
4
+ BASE_URI = "https://pal-%s.adyen.com/pal/servlet"
5
+ attr_accessor :environment
6
+ attr_accessor :username
7
+ attr_accessor :password
8
+ attr_accessor :cse_public_key
9
+ attr_accessor :default_merchant_account
10
+ attr_accessor :default_currency
11
+
12
+ def set(hash)
13
+ hash.each { |k, v| send("#{k}=", v) if respond_to?("#{k}=") }
14
+ end
15
+
16
+ def apply(klass)
17
+ klass.base_uri(BASE_URI % environment)
18
+ klass.basic_auth(username, password)
19
+ # prevent following redirects and raise HTTParty::RedirectionTooDeep
20
+ klass.no_follow(true)
21
+ klass.format(:json)
22
+ klass.headers("Content-Type" => "application/json; charset=utf-8")
23
+ end
24
+ end
25
+
26
+ end
27
+
@@ -0,0 +1,34 @@
1
+ class AdyenClient
2
+
3
+ class Response
4
+ def self.parse(http_response)
5
+ new(http_response.code, Utils.massage_response(http_response.parsed_response))
6
+ end
7
+
8
+ attr_reader :code, :data
9
+ alias_method :to_hash, :data
10
+
11
+ def initialize(code, data)
12
+ @code, @data = code, data
13
+ end
14
+
15
+ def success?
16
+ code == 200
17
+ end
18
+
19
+ def authorised?
20
+ success? && result_code == "Authorised"
21
+ end
22
+ alias_method :authorized?, :authorised? # for our friends abroad
23
+
24
+ def respond_to_missing?(name, include_private = false)
25
+ @data.has_key?(name.to_s) || super
26
+ end
27
+
28
+ def method_missing(name, *args, &block)
29
+ @data.fetch(name.to_s) { super(name, *args, &block) }
30
+ end
31
+ end
32
+
33
+ end
34
+
@@ -0,0 +1,33 @@
1
+ class AdyenClient
2
+
3
+ module Utils
4
+ def massage_response(value, parent = nil)
5
+ case value
6
+ when Array
7
+ value.map { |v| massage_response(v, value) }
8
+ when Hash
9
+ if parent.is_a?(Array) && value.count == 1
10
+ _, v = value.first
11
+ massage_response(v, value)
12
+ else
13
+ Hash[value.map { |k, v| [snake_caseify(k), massage_response(v, value)] }]
14
+ end
15
+ else
16
+ value
17
+ end
18
+ end
19
+
20
+ def snake_caseify(string)
21
+ string
22
+ .gsub("::", "/")
23
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
24
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
25
+ .tr("-", "_")
26
+ .downcase
27
+ end
28
+
29
+ extend self
30
+ end
31
+
32
+ end
33
+
@@ -0,0 +1,253 @@
1
+ require "httparty"
2
+ require "adyen_client/utils"
3
+ require "adyen_client/response"
4
+ require "adyen_client/configuration"
5
+
6
+ class AdyenClient
7
+ include HTTParty
8
+
9
+ # Internal: Access the configuration instance.
10
+ def self.configuration
11
+ @configuration ||= Configuration.new
12
+ end
13
+
14
+ # Public: Configure the AdyenClient class.
15
+ #
16
+ # hash - The configuration to apply. Will be evaluated before &block. (optional if &block is given)
17
+ # &block - Yields the configuration instance. (optional if hash is given)
18
+ #
19
+ # Examples
20
+ #
21
+ # # Block style
22
+ # AdyenClient.configure do |c|
23
+ # c.environment = :test
24
+ # c.username = "ws_123456@Company.FooBar"
25
+ # c.password = "correctbatteryhorsestaple"
26
+ # c.cse_public_key = "10001|..."
27
+ # c.default_merchant_account = "FooBar123"
28
+ # c.default_currency = "EUR"
29
+ # end
30
+ #
31
+ # # Hash style works too, string or symbol keys
32
+ # AdyenClient.configure(environment: :test, username: "ws_123456@Company.FooBar", ...)
33
+ #
34
+ # # That comes in handy to configure the client from a YAML file
35
+ # AdyenClient.configure(YAML.load_file(Rails.root.join("config", "adyen.yml"))[Rails.env.to_s])
36
+ #
37
+ # # You can override all default options for each instance of a client
38
+ # client = AdyenClient.new(merchant_account: "FooBarSubMerchant123")
39
+ # eur_client = AdyenClient.new(currency: "EUR")
40
+ #
41
+ # Yields the configuration singleton.
42
+ #
43
+ # Returns the configuration singleton.
44
+ def self.configure(hash = nil)
45
+ configuration.set(hash) if hash
46
+ yield configuration if block_given?
47
+ configuration.apply(self)
48
+ configuration
49
+ end
50
+
51
+ # Public: Returns an ISO8601 formatted datetime string used in Adyens generationTime.
52
+ def self.generation_time
53
+ Time.now.iso8601
54
+ end
55
+
56
+ # Public: Returns the configured CSE (client side encryption) public key.
57
+ def self.cse_public_key
58
+ configuration.cse_public_key
59
+ end
60
+
61
+ attr_reader :merchant_account
62
+
63
+ # Public: Initializes a new instance of the AdyenClient.
64
+ # You can override merchant_account and currency from the default configuration.
65
+ #
66
+ # :merchant_account - Sets the default_merchant_account for this instance. (optional)
67
+ # :currency - Sets the default_currency for this instance. (optional)
68
+ # :response_class - Use a custom class for handling responses from Adyen. (optional)
69
+ #
70
+ # Returns an AdyenClient::Response or your specific response implementation.
71
+ def initialize(merchant_account: configuration.default_merchant_account, currency: configuration.default_currency, response_class: Response)
72
+ @merchant_account = merchant_account
73
+ @currency = currency
74
+ @response_class = response_class
75
+ end
76
+
77
+ # Public: Charge a user by referencing his stored payment method.
78
+ #
79
+ # :shopper_reference - The user reference id from your side.
80
+ # :amount - The amount to charge in cents.
81
+ # :reference - Your reference id for this transaction.
82
+ # :recurring_reference - Use when referencing a specific payment method stored for the user. (default: "LATEST")
83
+ # :merchant_account - Use a specific merchant account for this transaction. (default: set by the instance or configuration default merchant account)
84
+ # :currency - Use a specific 3-letter currency code. (default: set by the instance or configuration default currency)
85
+ #
86
+ # Returns an AdyenClient::Response or your specific response implementation.
87
+ def authorise_recurring_payment(reference:, shopper_reference:, amount:, recurring_reference: "LATEST", merchant_account: @merchant_account, currency: configuration.default_currency)
88
+ postJSON("/Payment/v12/authorise",
89
+ reference: reference,
90
+ amount: { value: amount, currency: currency },
91
+ merchantAccount: merchant_account,
92
+ shopperReference: shopper_reference,
93
+ selectedRecurringDetailReference: recurring_reference,
94
+ selectedBrand: "",
95
+ recurring: { contract: "RECURRING" },
96
+ shopperInteraction: "ContAuth"
97
+ )
98
+ end
99
+ alias_method :authorize_recurring_payment, :authorise_recurring_payment
100
+
101
+ # Public: List the stored payment methods for a user.
102
+ #
103
+ # :shopper_reference - The user reference id from your side.
104
+ # :merchant_account - Use a specific merchant account for this transaction. (default: set by the instance or configuration default merchant account)
105
+ # :currency - Use a specific 3-letter currency code. (default: set by the instance or configuration default currency)
106
+ #
107
+ # Returns an AdyenClient::Response or your specific response implementation.
108
+ def list_recurring_details(shopper_reference:, merchant_account: @merchant_account, contract: "RECURRING")
109
+ postJSON("/Recurring/v12/listRecurringDetails",
110
+ shopperReference: shopper_reference,
111
+ recurring: { contract: contract },
112
+ merchantAccount: merchant_account
113
+ )
114
+ end
115
+
116
+ # Public: Store a payment method on a reference id for recurring/later use.
117
+ # Does verify the users payment method, but does not create a charge.
118
+ #
119
+ # :encrypted_card - The encrypted credit card information generated by the CSE (client side encryption) javascript integration.
120
+ # :reference - Your reference id for this transaction.
121
+ # :shopper - The hash describing the shopper for this contract:
122
+ # :reference - Your reference id for this shopper/user. (mandatory)
123
+ # :email - The shoppers email address. (optional but recommended)
124
+ # :ip - The shoppers last known ip address. (optional but recommended)
125
+ # :merchant_account - Use a specific merchant account for this transaction. (default: set by the instance or configuration default merchant account)
126
+ # :currency - Use a specific 3-letter currency code. (default: set by the instance or configuration default currency)
127
+ #
128
+ # Returns an AdyenClient::Response or your specific response implementation.
129
+ def create_recurring_contract(encrypted_card:, reference:, shopper:, merchant_account: @merchant_account, currency: @currency)
130
+ postJSON("/Payment/v12/authorise",
131
+ reference: reference,
132
+ additionalData: { "card.encrypted.json": encrypted_card },
133
+ amount: { value: 0, currency: currency },
134
+ merchantAccount: merchant_account,
135
+ shopperEmail: shopper[:email],
136
+ shopperIP: shopper[:ip],
137
+ shopperReference: shopper[:reference],
138
+ recurring: { contract: "RECURRING" }
139
+ )
140
+ end
141
+
142
+ # Public: Charge a credit card.
143
+ #
144
+ # :encrypted_card - The encrypted credit card information generated by the CSE (client side encryption) javascript integration.
145
+ # :amount - The integer amount in cents.
146
+ # :reference - Your reference id for this transaction.
147
+ # :merchant_account - Use a specific merchant account for this transaction. (default: set by the instance or configuration default merchant account)
148
+ # :currency - Use a specific 3-letter currency code. (default: set by the instance or configuration default currency)
149
+ # :shopper - The hash describing the shopper for this transaction, optional but recommended (default: {}):
150
+ # :email - The shoppers email address (optional but recommended).
151
+ # :ip - The shoppers last known ip address (optional but recommended).
152
+ # :reference - Your reference id for this shopper/user (optional).
153
+ #
154
+ # Returns an AdyenClient::Response or your specific response implementation.
155
+ def authorise(encrypted_card:, amount:, reference:, merchant_account: @merchant_account, currency: @currency, shopper: {})
156
+ postJSON("/Payment/v12/authorise",
157
+ reference: reference,
158
+ amount: { value: amount, currency: currency },
159
+ merchantAccount: merchant_account,
160
+ additionalData: { "card.encrypted.json": encrypted_card }
161
+ )
162
+ end
163
+ alias_method :authorize, :authorise
164
+
165
+ # Public: Verify a credit card (does not create a charge, but may be verified for a specified amount).
166
+ #
167
+ # :encrypted_card - The encrypted credit card information generated by the CSE (client side encryption) javascript integration.
168
+ # :reference - Your reference id for this transaction.
169
+ # :amount - The integer amount in cents. Will not be charged on the card. (default: 0)
170
+ # :merchant_account - Use a specific merchant account for this transaction (default: set by the instance or configuration default merchant account).
171
+ # :currency - Use a specific 3-letter currency code (default: set by the instance or configuration default currency).
172
+ # :shopper - The hash describing the shopper for this transaction, optional but recommended (default: {}):
173
+ # :email - The shoppers email address (optional but recommended).
174
+ # :ip - The shoppers last known ip address (optional but recommended).
175
+ # :reference - Your reference id for this shopper/user (optional).
176
+ #
177
+ # Returns an AdyenClient::Response or your specific response implementation.
178
+ def verify(encrypted_card:, reference:, amount: 0, merchant_account: @merchant_account, currency: @currency, shopper: {})
179
+ postJSON("/Payment/v12/authorise",
180
+ reference: reference,
181
+ amount: { value: 0, currency: currency },
182
+ additionalAmount: { value: amount, currency: currency },
183
+ merchantAccount: merchant_account,
184
+ additionalData: { "card.encrypted.json": encrypted_card }
185
+ )
186
+ end
187
+
188
+ # Public: Cancels a credit card transaction.
189
+ #
190
+ # :original_reference - The psp_reference from Adyen for this transaction.
191
+ # :reference - Your reference id for this transaction.
192
+ # :merchant_account - Use a specific merchant account for this transaction (default: set by the instance or configuration default merchant account).
193
+ #
194
+ # Returns an AdyenClient::Response or your specific response implementation.
195
+ def cancel(original_reference:, reference:, merchantAccount: @merchant_account)
196
+ postJSON("/Payment/v12/cancel",
197
+ reference: reference,
198
+ merchantAccount: merchant_account,
199
+ originalReference: original_reference
200
+ )
201
+ end
202
+
203
+ # Public: Refunds a credit card transaction.
204
+ #
205
+ # :original_reference - The psp_reference from Adyen for this transaction.
206
+ # :amount - The amount in cents to be refunded.
207
+ # :reference - Your reference id for this transaction.
208
+ # :merchant_account - Use a specific merchant account for this transaction (default: set by the instance or configuration default merchant account).
209
+ # :currency - Use a specific 3-letter currency code (default: set by the instance or configuration default currency).
210
+ #
211
+ # Returns an AdyenClient::Response or your specific response implementation.
212
+ def refund(original_reference:, amount:, reference:, merchantAccount: @merchant_account, currency: @currency)
213
+ postJSON("/Payment/v12/refund",
214
+ reference: reference,
215
+ merchantAccount: merchant_account,
216
+ modificationAmount: { value: amount, currency: currency },
217
+ originalReference: original_reference
218
+ )
219
+ end
220
+
221
+ # Public: Cancels or refunds a credit card transaction. Use this if you don't know the exact state of a transaction.
222
+ #
223
+ # :original_reference - The psp_reference from Adyen for this transaction.
224
+ # :reference - Your reference id for this transaction.
225
+ # :merchant_account - Use a specific merchant account for this transaction (default: set by the instance or configuration default merchant account).
226
+ #
227
+ # Returns an AdyenClient::Response or your specific response implementation.
228
+ def cancel_or_refund(original_reference:, reference:, merchantAccount: @merchant_account)
229
+ postJSON("/Payment/v12/cancelOrRefund",
230
+ reference: reference,
231
+ merchantAccount: merchant_account,
232
+ originalReference: original_reference
233
+ )
234
+ end
235
+
236
+ # Internal: Send a POST request to the Adyen API.
237
+ #
238
+ # path - The Adyen JSON API endpoint path.
239
+ # data - The Hash describing the JSON body for this request.
240
+ #
241
+ # Returns an AdyenClient::Response or your specific response implementation.
242
+ def postJSON(path, data)
243
+ response = self.class.post(path, body: data.to_json)
244
+ @response_class ? @response_class.parse(response) : response
245
+ end
246
+
247
+ # Internal: Returns the AdyenClient configuration singleton
248
+ def configuration
249
+ self.class.configuration
250
+ end
251
+
252
+ end
253
+
metadata ADDED
@@ -0,0 +1,64 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: adyen_client
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Lukas Rieder
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-12-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: httparty
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.13.5
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.13.5
27
+ description: Does not try to be smart, stays close to the documentation while adhering
28
+ to ruby conventions.
29
+ email: l.rieder@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - LICENSE
35
+ - README.md
36
+ - lib/adyen_client.rb
37
+ - lib/adyen_client/configuration.rb
38
+ - lib/adyen_client/response.rb
39
+ - lib/adyen_client/utils.rb
40
+ homepage: https://github.com/Overbryd/adyen_client
41
+ licenses:
42
+ - MIT
43
+ metadata: {}
44
+ post_install_message:
45
+ rdoc_options: []
46
+ require_paths:
47
+ - lib
48
+ required_ruby_version: !ruby/object:Gem::Requirement
49
+ requirements:
50
+ - - "~>"
51
+ - !ruby/object:Gem::Version
52
+ version: '2.0'
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: '0'
58
+ requirements: []
59
+ rubyforge_project:
60
+ rubygems_version: 2.4.5.1
61
+ signing_key:
62
+ specification_version: 4
63
+ summary: A simple client that talks to the Adyen API
64
+ test_files: []