dpickett-agcod 0.0.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.
- data/.document +5 -0
- data/.gitignore +9 -0
- data/LICENSE +20 -0
- data/README.rdoc +7 -0
- data/Rakefile +67 -0
- data/VERSION +1 -0
- data/agcod.gemspec +66 -0
- data/features/create_a_giftcard.feature +11 -0
- data/features/step_definitions/agcod_steps.rb +121 -0
- data/features/support/app_root/config/agcod.example.yml +6 -0
- data/features/support/env.rb +8 -0
- data/lib/agcod/cancel_gift_card.rb +21 -0
- data/lib/agcod/configuration.rb +76 -0
- data/lib/agcod/create_gift_card.rb +43 -0
- data/lib/agcod/error/configuration_error.rb +7 -0
- data/lib/agcod/error/invalid_parameter.rb +7 -0
- data/lib/agcod/health_check.rb +8 -0
- data/lib/agcod/option_validators.rb +33 -0
- data/lib/agcod/request.rb +147 -0
- data/lib/agcod/tasks/certification.rake +45 -0
- data/lib/agcod/tasks.rb +3 -0
- data/lib/agcod/void_gift_card_creation.rb +14 -0
- data/lib/agcod.rb +24 -0
- data/tasks/agcod.rake +1 -0
- data/test/agcod/configuration_test.rb +106 -0
- data/test/app_root/config/agcod.yml +6 -0
- data/test/macros/configuration.rb +24 -0
- data/test/test_helper.rb +25 -0
- metadata +83 -0
data/.document
ADDED
data/.gitignore
ADDED
data/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright (c) 2009 Dan Pickett
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
data/Rakefile
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'rake'
|
|
3
|
+
|
|
4
|
+
begin
|
|
5
|
+
require 'jeweler'
|
|
6
|
+
Jeweler::Tasks.new do |gem|
|
|
7
|
+
gem.name = "agcod"
|
|
8
|
+
gem.summary = %Q{TODO}
|
|
9
|
+
gem.email = "dpickett@enlightsolutions.com"
|
|
10
|
+
gem.homepage = "http://github.com/dpickett/agcod"
|
|
11
|
+
gem.authors = ["Dan Pickett"]
|
|
12
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
rescue LoadError
|
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
require 'rake/testtask'
|
|
20
|
+
Rake::TestTask.new(:test) do |test|
|
|
21
|
+
test.libs << 'lib' << 'test'
|
|
22
|
+
test.pattern = 'test/**/*_test.rb'
|
|
23
|
+
test.verbose = true
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
require 'rcov/rcovtask'
|
|
28
|
+
Rcov::RcovTask.new do |test|
|
|
29
|
+
test.libs << 'test'
|
|
30
|
+
test.pattern = 'test/**/*_test.rb'
|
|
31
|
+
test.verbose = true
|
|
32
|
+
test.rcov_opts << "-x /Gems/"
|
|
33
|
+
end
|
|
34
|
+
rescue LoadError
|
|
35
|
+
task :rcov do
|
|
36
|
+
abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
begin
|
|
41
|
+
require 'cucumber/rake/task'
|
|
42
|
+
Cucumber::Rake::Task.new(:features)
|
|
43
|
+
rescue LoadError
|
|
44
|
+
task :features do
|
|
45
|
+
abort "Cucumber is not available. In order to run features, you must: sudo gem install cucumber"
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
require File.join(File.dirname(__FILE__), "lib", "agcod", "tasks")
|
|
50
|
+
|
|
51
|
+
task :default => :spec
|
|
52
|
+
|
|
53
|
+
require 'rake/rdoctask'
|
|
54
|
+
Rake::RDocTask.new do |rdoc|
|
|
55
|
+
if File.exist?('VERSION.yml')
|
|
56
|
+
config = YAML.load(File.read('VERSION.yml'))
|
|
57
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
|
58
|
+
else
|
|
59
|
+
version = ""
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
rdoc.rdoc_dir = 'rdoc'
|
|
63
|
+
rdoc.title = "agcod #{version}"
|
|
64
|
+
rdoc.rdoc_files.include('README*')
|
|
65
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
|
66
|
+
end
|
|
67
|
+
|
data/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.0.1
|
data/agcod.gemspec
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |s|
|
|
4
|
+
s.name = %q{agcod}
|
|
5
|
+
s.version = "0.0.0"
|
|
6
|
+
|
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
|
8
|
+
s.authors = ["Dan Pickett"]
|
|
9
|
+
s.date = %q{2009-07-02}
|
|
10
|
+
s.email = %q{dpickett@enlightsolutions.com}
|
|
11
|
+
s.extra_rdoc_files = [
|
|
12
|
+
"LICENSE",
|
|
13
|
+
"README.rdoc"
|
|
14
|
+
]
|
|
15
|
+
s.files = [
|
|
16
|
+
".document",
|
|
17
|
+
".gitignore",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.rdoc",
|
|
20
|
+
"Rakefile",
|
|
21
|
+
"VERSION",
|
|
22
|
+
"agcod.gemspec",
|
|
23
|
+
"features/create_a_giftcard.feature",
|
|
24
|
+
"features/step_definitions/agcod_steps.rb",
|
|
25
|
+
"features/support/app_root/config/agcod.example.yml",
|
|
26
|
+
"features/support/env.rb",
|
|
27
|
+
"lib/agcod.rb",
|
|
28
|
+
"lib/agcod/cancel_gift_card.rb",
|
|
29
|
+
"lib/agcod/configuration.rb",
|
|
30
|
+
"lib/agcod/create_gift_card.rb",
|
|
31
|
+
"lib/agcod/error/configuration_error.rb",
|
|
32
|
+
"lib/agcod/error/invalid_parameter.rb",
|
|
33
|
+
"lib/agcod/health_check.rb",
|
|
34
|
+
"lib/agcod/option_validators.rb",
|
|
35
|
+
"lib/agcod/request.rb",
|
|
36
|
+
"lib/agcod/tasks.rb",
|
|
37
|
+
"lib/agcod/tasks/certification.rake",
|
|
38
|
+
"lib/agcod/void_gift_card_creation.rb",
|
|
39
|
+
"tasks/agcod.rake",
|
|
40
|
+
"test/agcod/configuration_test.rb",
|
|
41
|
+
"test/app_root/config/agcod.yml",
|
|
42
|
+
"test/macros/configuration.rb",
|
|
43
|
+
"test/test_helper.rb"
|
|
44
|
+
]
|
|
45
|
+
s.has_rdoc = true
|
|
46
|
+
s.homepage = %q{http://github.com/dpickett/agcod}
|
|
47
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
|
48
|
+
s.require_paths = ["lib"]
|
|
49
|
+
s.rubygems_version = %q{1.3.2}
|
|
50
|
+
s.summary = %q{TODO}
|
|
51
|
+
s.test_files = [
|
|
52
|
+
"test/agcod/configuration_test.rb",
|
|
53
|
+
"test/macros/configuration.rb",
|
|
54
|
+
"test/test_helper.rb"
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
if s.respond_to? :specification_version then
|
|
58
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
|
59
|
+
s.specification_version = 3
|
|
60
|
+
|
|
61
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
|
62
|
+
else
|
|
63
|
+
end
|
|
64
|
+
else
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Feature: As a user of the AGCOD web service
|
|
2
|
+
I want create a gift card
|
|
3
|
+
So that I can remit payment to a third party
|
|
4
|
+
|
|
5
|
+
Scenario: Pay someone $35 successfully
|
|
6
|
+
Given I have access to the AGCOD web service
|
|
7
|
+
And I want to create a gift card in the amount of $35.00
|
|
8
|
+
When I send the request
|
|
9
|
+
Then it should be successful
|
|
10
|
+
And I should get a response id
|
|
11
|
+
And I should get a claim code
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
Given /^I have access to the AGCOD web service$/ do
|
|
2
|
+
Agcod::Configuration.load(File.join(File.dirname(__FILE__), "..", "support", "app_root"), "cucumber")
|
|
3
|
+
assert_not_nil Agcod::Configuration.access_key
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
Given /^I want to send request \"(.*)\"/ do |request_number|
|
|
7
|
+
req = get_request(request_number)
|
|
8
|
+
@options ||= {}
|
|
9
|
+
@request = Agcod::CreateGiftCard.new(req.merge(@options))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
Given /^I am logging transactions$/ do
|
|
13
|
+
@logger = Logger.new(File.join(FileUtils.pwd, "agcod_cucumber.log"))
|
|
14
|
+
Agcod::Configuration.logger = @logger
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
Given /^I want to send a health check request$/ do
|
|
18
|
+
@request = Agcod::HealthCheck.new
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Given /^I've sent request \"(.*)\"$/ do |request_number|
|
|
22
|
+
req = get_request(request_number)
|
|
23
|
+
@prior_request = Agcod::CreateGiftCard.new("value" => req["value"].to_f,
|
|
24
|
+
"request_id" => req["request_id"])
|
|
25
|
+
@prior_request.submit
|
|
26
|
+
dump_request(@prior_request)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
Given /^I want to cancel the gift card requested$/ do
|
|
30
|
+
@request = Agcod::CancelGiftCard.new("request_id" => @prior_request.request_id,
|
|
31
|
+
"response_id" => @prior_request.response_id)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
Given /^I want to void the gift card requested$/ do
|
|
35
|
+
@request = Agcod::VoidGiftCardCreation.new("request_id" => @prior_request.request_id)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
Given /^the request was successful$/ do
|
|
40
|
+
assert @prior_request.successful?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
Given /^I want to create a gift card with the same request id$/ do
|
|
44
|
+
@request = Agcod::CreateGiftCard.new("value" => 40, "request_id" => @prior_request.request_id)
|
|
45
|
+
@dont_dump_request = true
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
Given /^I want to cancel request "([^\"]*)"$/ do |req_num|
|
|
49
|
+
req = get_request(req_num)
|
|
50
|
+
@options ||= {}
|
|
51
|
+
@request = Agcod::CancelGiftCard.new(req.merge(@options))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
Given /^I want to void request "([^\"]*)"$/ do |req_num|
|
|
55
|
+
req = get_request(req_num)
|
|
56
|
+
@options ||= {}
|
|
57
|
+
@request = Agcod::VoidGiftCardCreation.new(req.merge(@options))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
Given /^I specify response_id "([^\"]*)"$/ do |response_id|
|
|
62
|
+
@options ||= {}
|
|
63
|
+
@options["response_id"] = response_id
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
Given /^I specify the currency of "([^\"]*)"$/ do |currency|
|
|
67
|
+
@options ||= {}
|
|
68
|
+
@options["currency_code"] = currency
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
Then /^I should not receive a successful response$/ do
|
|
74
|
+
assert !@request.successful?
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
When /^I send the request$/ do
|
|
78
|
+
@request.submit
|
|
79
|
+
dump_request(@request) if @request.is_a?(Agcod::CreateGiftCard) && !@dont_dump_request
|
|
80
|
+
@dont_dump_request = false if @dont_dump_request
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
Then /^I should receive a successful response$/ do
|
|
84
|
+
assert @request.successful?
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
Then /^I should get a response id$/ do
|
|
88
|
+
assert_not_nil @request.response_id
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
Then /^I should get a claim code$/ do
|
|
92
|
+
assert_not_nil @request.claim_code
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cert_fixture(req_num)
|
|
97
|
+
File.join(
|
|
98
|
+
File.dirname(__FILE__), "..", "support", "certification_requests", "#{req_num}.yml"
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def get_request(req_num)
|
|
103
|
+
YAML.load(File.read(cert_fixture(req_num)))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def dump_request(req)
|
|
107
|
+
req_num = req.request_id[0..0]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
req_hash = {
|
|
111
|
+
"request_id" => req.request_id,
|
|
112
|
+
"value" => req.value,
|
|
113
|
+
"response_id" => req.response_id,
|
|
114
|
+
"claim_code" => req.claim_code
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
FileUtils.rm_f(cert_fixture(req_num))
|
|
118
|
+
File.open(cert_fixture(req_num), "w") do |f|
|
|
119
|
+
f.puts req_hash.to_yaml
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
class CancelGiftCard < Agcod::Request
|
|
3
|
+
include Agcod::OptionValidators
|
|
4
|
+
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@action = "CancelGiftCard"
|
|
7
|
+
@required_options = ["request_id", "response_id"]
|
|
8
|
+
@options = options
|
|
9
|
+
@required_options.each do |r|
|
|
10
|
+
validate_length_of(r, "min" => 1, "max" => 19)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
super
|
|
14
|
+
|
|
15
|
+
@parameters["gcCreationRequestId"] = Agcod::Configuration.partner_id.to_s + options["request_id"].to_s
|
|
16
|
+
@parameters["gcCreationResponseId"] = options["response_id"]
|
|
17
|
+
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
class Configuration
|
|
3
|
+
REQUIRED_OPTIONS = ["access_key",
|
|
4
|
+
"secret_key",
|
|
5
|
+
"partner_id",
|
|
6
|
+
"uri",
|
|
7
|
+
"discount_percentage"
|
|
8
|
+
]
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
attr_reader :options
|
|
12
|
+
attr_accessor :logger
|
|
13
|
+
|
|
14
|
+
def load(app_root = nil, env = nil)
|
|
15
|
+
if app_root
|
|
16
|
+
@app_root = app_root
|
|
17
|
+
else
|
|
18
|
+
@app_root = RAILS_ROOT if defined?(RAILS_ROOT)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
if @app_root.nil? ||
|
|
22
|
+
!FileTest.exists?(config_filename = File.join(@app_root, 'config', 'agcod.yml'))
|
|
23
|
+
|
|
24
|
+
raise Error::ConfigurationError, "Configuration for AGCOD not found"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
config_file = File.read(config_filename)
|
|
28
|
+
|
|
29
|
+
environment = RAILS_ENV if defined?(RAILS_ENV)
|
|
30
|
+
environment = env if env
|
|
31
|
+
|
|
32
|
+
@options = YAML.load(config_file)[environment]
|
|
33
|
+
validate_options
|
|
34
|
+
@options
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def set(opt = {})
|
|
38
|
+
@options ||= {}
|
|
39
|
+
@options.merge!(opt)
|
|
40
|
+
|
|
41
|
+
validate_options
|
|
42
|
+
@options
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def access_key
|
|
46
|
+
@options["access_key"]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def secret_key
|
|
50
|
+
@options["secret_key"]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def partner_id
|
|
54
|
+
@options["partner_id"]
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def uri
|
|
58
|
+
@options["uri"]
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def discount_percentage
|
|
62
|
+
@options["discount_percentage"]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
def validate_options
|
|
68
|
+
REQUIRED_OPTIONS.each do |opt|
|
|
69
|
+
if options[opt].nil? || options[opt] == ""
|
|
70
|
+
raise Error::ConfigurationError, "#{opt} was not specified"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
class CreateGiftCard < Agcod::Request
|
|
3
|
+
include Agcod::OptionValidators
|
|
4
|
+
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@action = "CreateGiftCard"
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
validate_greater_than("value", 0)
|
|
10
|
+
validate_length_of("request_id", {"max" => 19, "min" => 1})
|
|
11
|
+
|
|
12
|
+
#can't have a nonexistant or 0 value for the gift card
|
|
13
|
+
@parameters["gcValue.amount"] = options["value"]
|
|
14
|
+
|
|
15
|
+
@value = options["value"]
|
|
16
|
+
|
|
17
|
+
#must have a unique identifier for the request
|
|
18
|
+
@parameters["gcCreationRequestId"] = Agcod::Configuration.partner_id + options["request_id"].to_s
|
|
19
|
+
|
|
20
|
+
@parameters["gcValue.currencyCode"] = options["currency_code"] || "USD"
|
|
21
|
+
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def process_response
|
|
25
|
+
super
|
|
26
|
+
if self.successful?
|
|
27
|
+
@claim_code = self.xml_response.root.elements["gcClaimCode"].text
|
|
28
|
+
@response_id = self.xml_response.root.elements["gcCreationResponseId"].text
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
attr_reader :claim_code, :response_id, :value
|
|
33
|
+
|
|
34
|
+
def to_yaml(name)
|
|
35
|
+
{"response_id" => self.response_id,
|
|
36
|
+
"request_id" => self.request_id,
|
|
37
|
+
"claim_code" => self.claim_code,
|
|
38
|
+
"value" => self.value,
|
|
39
|
+
"timestamp" => self.timestamp
|
|
40
|
+
}.to_yaml(name)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
module OptionValidators
|
|
3
|
+
def validate_timestamp
|
|
4
|
+
if @options["timestamp"].nil? || !@options["timestamp"].instance_of?(Time)
|
|
5
|
+
raise Agcod::Error::InvalidParameter, "Invalid Timestamp for record #{@options["record_id"]}"
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
def validate_presence_of(option_name)
|
|
11
|
+
if @options[option_name].nil? || @options[option_name].to_s.blank?
|
|
12
|
+
raise Agcod::Error::InvalidParameter, "#{option_name} not specified"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def validate_length_of(option_name, size_options)
|
|
17
|
+
size_options["min"] ||= 0
|
|
18
|
+
size_options["max"] ||= 10000
|
|
19
|
+
if @options[option_name].nil? ||
|
|
20
|
+
@options[option_name].to_s.size < size_options["min"] ||
|
|
21
|
+
@options[option_name].to_s.size > size_options["max"]
|
|
22
|
+
|
|
23
|
+
raise Agcod::Error::InvalidParameter, "#{option_name} has an invalid length"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def validate_greater_than(option_name, number)
|
|
28
|
+
if @options[option_name].nil? || @options[option_name].to_f <= number
|
|
29
|
+
raise Agcod::Error::InvalidParameter, "#{option_name} must be greater than #{number} for record #{@options["record_id"]}"
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
class Request
|
|
3
|
+
def initialize(options = {})
|
|
4
|
+
@request = ""
|
|
5
|
+
@response = ""
|
|
6
|
+
@status = ""
|
|
7
|
+
@parameters = {}
|
|
8
|
+
@options = options
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def submit
|
|
12
|
+
#action must be specified so raise an exception if it has not been populated
|
|
13
|
+
if self.action.nil?
|
|
14
|
+
raise "Action not specified"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#form the request GET parameter string
|
|
18
|
+
@request = build_sorted_and_signed_request_string
|
|
19
|
+
|
|
20
|
+
send_request
|
|
21
|
+
|
|
22
|
+
process_response
|
|
23
|
+
log if Agcod::Configuration.logger
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def successful?
|
|
27
|
+
self.sent && self.errors.size == 0 && self.status == "SUCCESS"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
attr_reader :errors, :request_id, :sent, :action, :request, :parameters, :response, :xml_response, :status, :timestamp
|
|
31
|
+
|
|
32
|
+
def sign_string(string_to_sign)
|
|
33
|
+
#remove all the = and & from the serialized string
|
|
34
|
+
sanitized_string = string_to_sign.gsub(/=|&/, "")
|
|
35
|
+
# puts sanitized_string
|
|
36
|
+
sha1 = HMAC::SHA1::digest(Agcod::Configuration.secret_key, sanitized_string)
|
|
37
|
+
|
|
38
|
+
#Base64 encoding adds a linefeed to the end of the string so chop the last character!
|
|
39
|
+
CGI.escape(Base64.encode64(sha1).chomp)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def request_id
|
|
43
|
+
@options["request_id"]
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def response_id
|
|
47
|
+
@response_id || @options["response_id"]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
protected
|
|
51
|
+
|
|
52
|
+
def process_response
|
|
53
|
+
parse_response
|
|
54
|
+
|
|
55
|
+
#check for retry error
|
|
56
|
+
if self.xml_response.root.elements["Status/errorCode"] &&
|
|
57
|
+
self.xml_response.root.elements["Status/errorCode"].text == "E100" &&
|
|
58
|
+
!@sent_retry
|
|
59
|
+
|
|
60
|
+
@sent_retry = true
|
|
61
|
+
submit
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
@errors = []
|
|
65
|
+
|
|
66
|
+
self.xml_response.root.elements.each("Error") do |e|
|
|
67
|
+
@errors << e.elements["Message"].text
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
@status = self.xml_response.root.elements["Status/statusCode"].text unless xml_response.root.elements["Status/statusCode"].nil?
|
|
71
|
+
|
|
72
|
+
#something happened before it got to ACGWS (most likely a signature problem)
|
|
73
|
+
@status = "FAILURE" if self.errors.size > 0 && self.status.blank?
|
|
74
|
+
|
|
75
|
+
@request_id = xml_response.root.elements["RequestID"].text unless xml_response.root.elements["RequestID"].nil?
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def parse_response
|
|
79
|
+
@xml_response ||= REXML::Document.new(self.response)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private
|
|
83
|
+
def send_request
|
|
84
|
+
#send the request
|
|
85
|
+
uri = URI.parse(Agcod::Configuration.uri)
|
|
86
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
87
|
+
http.use_ssl = true
|
|
88
|
+
|
|
89
|
+
net_response, @response = http.get(uri.path + "?" + self.request)
|
|
90
|
+
|
|
91
|
+
@sent = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def default_parameters
|
|
95
|
+
@timestamp = Time.now.utc
|
|
96
|
+
{
|
|
97
|
+
"Action" => self.action,
|
|
98
|
+
"AWSAccessKeyId" => Agcod::Configuration.access_key,
|
|
99
|
+
"SignatureVersion" => "1",
|
|
100
|
+
"MessageHeader.recipientId" => "AMAZON",
|
|
101
|
+
"MessageHeader.sourceId" => Agcod::Configuration.partner_id,
|
|
102
|
+
"MessageHeader.retryCount" => "0",
|
|
103
|
+
"MessageHeader.contentVersion" => "2008-01-01",
|
|
104
|
+
"MessageHeader.messageType" => self.action + "Request",
|
|
105
|
+
"Timestamp" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S") + ".000Z"
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def build_sorted_and_signed_request_string
|
|
110
|
+
params_to_submit = default_parameters.merge(self.parameters)
|
|
111
|
+
|
|
112
|
+
unencoded_key_value_strings = []
|
|
113
|
+
encoded_key_value_strings = []
|
|
114
|
+
sort_parameters(params_to_submit).each do |p|
|
|
115
|
+
unencoded_key_value_strings << p[0].to_s + p[1].to_s
|
|
116
|
+
|
|
117
|
+
if p[0] =~ /Timestamp/i
|
|
118
|
+
encoded_value = p[1]
|
|
119
|
+
else
|
|
120
|
+
encoded_value = CGI.escape(p[1].to_s)
|
|
121
|
+
end
|
|
122
|
+
encoded_key_value_strings << p[0].to_s + "=" + encoded_value
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
signature = sign_string(unencoded_key_value_strings.join(""))
|
|
126
|
+
encoded_key_value_strings.insert(encoded_key_value_strings.index("SignatureVersion=1") + 1 , "Signature=" + signature)
|
|
127
|
+
encoded_key_value_strings.join("&")
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def sort_parameters(params)
|
|
132
|
+
key_value_strings = []
|
|
133
|
+
params.sort{|a, b| a[0].downcase <=> b[0].downcase }
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def log
|
|
137
|
+
log_string = "[AGCOD] #{self.action} Request"
|
|
138
|
+
log_string << " \##{self.request_id}" if self.request_id
|
|
139
|
+
log_string << " received response #{self.response_id}" if self.response_id
|
|
140
|
+
if self.respond_to?(:claim_code) && self.claim_code
|
|
141
|
+
log_string << " received claim_code #{self.claim_code}"
|
|
142
|
+
end
|
|
143
|
+
Agcod::Configuration.logger.debug log_string
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
|
|
3
|
+
namespace :agcod do
|
|
4
|
+
namespace :certification do
|
|
5
|
+
desc "generate a request manifest for certification"
|
|
6
|
+
task :generate_manifest do
|
|
7
|
+
puts "generating manifest"
|
|
8
|
+
i = 1
|
|
9
|
+
prices = [
|
|
10
|
+
12,
|
|
11
|
+
999,
|
|
12
|
+
100,
|
|
13
|
+
50.02,
|
|
14
|
+
999.99,
|
|
15
|
+
600,
|
|
16
|
+
70,
|
|
17
|
+
100000,
|
|
18
|
+
12,
|
|
19
|
+
12,
|
|
20
|
+
1
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
path = File.join(FileUtils.pwd, "features", "support", "certification_requests")
|
|
24
|
+
FileUtils.rm_rf(path)
|
|
25
|
+
FileUtils.mkdir_p(path)
|
|
26
|
+
|
|
27
|
+
requests = []
|
|
28
|
+
prices.each do |p|
|
|
29
|
+
random_string_of_numbers = ""
|
|
30
|
+
12.times {random_string_of_numbers << rand(9).to_s}
|
|
31
|
+
|
|
32
|
+
request = {"value" => p,
|
|
33
|
+
"request_id" => i.to_s + random_string_of_numbers}
|
|
34
|
+
|
|
35
|
+
File.open(File.join(path, "#{i}.yml"), 'w') do |manifest|
|
|
36
|
+
manifest.puts request.to_yaml
|
|
37
|
+
end
|
|
38
|
+
i += 1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
puts "Manifest available at #{path}"
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
data/lib/agcod/tasks.rb
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module Agcod
|
|
2
|
+
class VoidGiftCardCreation < Agcod::Request
|
|
3
|
+
include Agcod::OptionValidators
|
|
4
|
+
|
|
5
|
+
def initialize(options = {})
|
|
6
|
+
@action = "VoidGiftCardCreation"
|
|
7
|
+
super
|
|
8
|
+
|
|
9
|
+
validate_presence_of("request_id")
|
|
10
|
+
|
|
11
|
+
@parameters["gcCreationRequestId"] = Agcod::Configuration.partner_id.to_s + self.request_id.to_s
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
data/lib/agcod.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "net/https"
|
|
5
|
+
require 'rexml/document'
|
|
6
|
+
|
|
7
|
+
require "uri"
|
|
8
|
+
require "base64"
|
|
9
|
+
require "hmac-sha1"
|
|
10
|
+
require "digest/sha1"
|
|
11
|
+
require "cgi"
|
|
12
|
+
require "logger"
|
|
13
|
+
|
|
14
|
+
require "agcod/error/invalid_parameter"
|
|
15
|
+
require "agcod/error/configuration_error"
|
|
16
|
+
require "agcod/option_validators"
|
|
17
|
+
|
|
18
|
+
require "agcod/configuration"
|
|
19
|
+
require "agcod/request"
|
|
20
|
+
|
|
21
|
+
require "agcod/cancel_gift_card"
|
|
22
|
+
require "agcod/create_gift_card"
|
|
23
|
+
require "agcod/health_check"
|
|
24
|
+
require "agcod/void_gift_card_creation"
|
data/tasks/agcod.rake
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
load File.join(File.dirname(__FILE__), "..", "lib", "agcod", "tasks.rb")
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
require "test_helper"
|
|
2
|
+
|
|
3
|
+
class Agcod::ConfigurationTest < Test::Unit::TestCase
|
|
4
|
+
context "an agcod configuration" do
|
|
5
|
+
setup do
|
|
6
|
+
@valid_app_root = File.join(File.dirname(__FILE__), "..", "app_root")
|
|
7
|
+
@valid_options = {
|
|
8
|
+
"access_key" => "45127185235",
|
|
9
|
+
"secret_key" => "4321542523453454325j",
|
|
10
|
+
"partner_id" => "SomebodySpecial",
|
|
11
|
+
"uri" => "https://agcws-gamma.amazon.com/",
|
|
12
|
+
"discount_percentage" => 0.04
|
|
13
|
+
}
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
should "raise an error if the configuration file isn't specified" do
|
|
17
|
+
assert_raise Agcod::Error::ConfigurationError do
|
|
18
|
+
Agcod::Configuration.load
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
should "raise an error if the configuration file isn't found" do
|
|
23
|
+
assert_raise Agcod::Error::ConfigurationError do
|
|
24
|
+
Agcod::Configuration.load("/foo4258fast43")
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
should "read configuration from a supplied app root" do
|
|
29
|
+
Agcod::Configuration.load(File.join(File.dirname(__FILE__), "..", "app_root"), "test")
|
|
30
|
+
Agcod::Configuration::REQUIRED_OPTIONS.each do |opt|
|
|
31
|
+
assert_not_nil Agcod::Configuration.send(opt)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
should "allow me to set config options at runtime" do
|
|
36
|
+
Agcod::Configuration.set(@valid_options)
|
|
37
|
+
assert_equal Agcod::Configuration.access_key, @valid_options["access_key"]
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
should_require_config_options [
|
|
41
|
+
"access_key",
|
|
42
|
+
"secret_key",
|
|
43
|
+
"partner_id",
|
|
44
|
+
"uri",
|
|
45
|
+
"discount_percentage"
|
|
46
|
+
]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
context "agcod logging" do
|
|
50
|
+
setup do
|
|
51
|
+
configure_with_valid_options
|
|
52
|
+
@log_path = File.join(File.dirname(__FILE__), "..", "log", "test.log")
|
|
53
|
+
FileUtils.touch(@log_path)
|
|
54
|
+
@logger = Logger.new(@log_path)
|
|
55
|
+
@logger.level = Logger::DEBUG
|
|
56
|
+
|
|
57
|
+
Agcod::Configuration.logger = @logger
|
|
58
|
+
|
|
59
|
+
FakeWeb.allow_net_connect = false
|
|
60
|
+
|
|
61
|
+
@request = Agcod::CreateGiftCard.new("request_id" => 34234, "value" => 12)
|
|
62
|
+
@request.stubs(:response_id).returns(4323535)
|
|
63
|
+
@request.stubs(:send_request)
|
|
64
|
+
@request.stubs(:process_response)
|
|
65
|
+
@request.stubs(:claim_code).returns(342145)
|
|
66
|
+
@request.submit
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
teardown do
|
|
70
|
+
FileUtils.rm_f(@log_path)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
should "allow me to define a logger for logging requests" do
|
|
74
|
+
@logger = Logger.new(STDOUT)
|
|
75
|
+
Agcod::Configuration.logger = @logger
|
|
76
|
+
assert_equal @logger, Agcod::Configuration.logger
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
should "log request operation names" do
|
|
80
|
+
assert_match /CreateGiftCard/, File.read(@log_path)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
should "log request ids when applicable" do
|
|
84
|
+
assert_match /#{@request.request_id}/, File.read(@log_path)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
should "log response ids when applicable" do
|
|
88
|
+
assert_match /#{@request.response_id}/, File.read(@log_path)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
should "og claim code when applicable" do
|
|
92
|
+
assert_match /#{@request.claim_code}/, File.read(@log_path)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def configure_with_valid_options
|
|
97
|
+
@valid_options = {
|
|
98
|
+
"access_key" => "45127185235",
|
|
99
|
+
"secret_key" => "4321542523453454325j",
|
|
100
|
+
"partner_id" => "SomebodySpecial",
|
|
101
|
+
"uri" => "https://agcws-gamma.amazon.com/",
|
|
102
|
+
"discount_percentage" => 0.04
|
|
103
|
+
}
|
|
104
|
+
Agcod::Configuration.set(@valid_options)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
class Test::Unit::TestCase
|
|
2
|
+
def self.should_require_config_options(options)
|
|
3
|
+
valid_options = {
|
|
4
|
+
"access_key" => "45127185235",
|
|
5
|
+
"secret_key" => "4321542523453454325j",
|
|
6
|
+
"partner_id" => "SomebodySpecial",
|
|
7
|
+
"uri" => "https://agcws-gamma.amazon.com/",
|
|
8
|
+
"discount_percentage" => 0.04
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
options = [options] unless options.is_a?(Array)
|
|
12
|
+
|
|
13
|
+
options.each do |option|
|
|
14
|
+
should "require #{option} as a configuration option" do
|
|
15
|
+
assert_raise Agcod::Error::ConfigurationError do
|
|
16
|
+
Agcod::Configuration.set(valid_options.merge({
|
|
17
|
+
option => ""
|
|
18
|
+
}))
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
end
|
|
24
|
+
end
|
data/test/test_helper.rb
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'rubygems'
|
|
2
|
+
require 'test/unit'
|
|
3
|
+
require 'shoulda'
|
|
4
|
+
require 'fakeweb'
|
|
5
|
+
require 'mocha'
|
|
6
|
+
|
|
7
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
|
8
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
|
9
|
+
require 'agcod'
|
|
10
|
+
|
|
11
|
+
begin require "redgreen" rescue Exception; end
|
|
12
|
+
|
|
13
|
+
FakeWeb.allow_net_connect = false
|
|
14
|
+
|
|
15
|
+
require "macros/configuration"
|
|
16
|
+
|
|
17
|
+
class Test::Unit::TestCase
|
|
18
|
+
def register_response(path, fixture)
|
|
19
|
+
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def xml_fixture_path(fixture)
|
|
23
|
+
File.join(File.dirname(__FILE__), "fixtures", "#{fixture}.xml")
|
|
24
|
+
end
|
|
25
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: dpickett-agcod
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Dan Pickett
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
|
|
12
|
+
date: 2009-07-02 00:00:00 -07:00
|
|
13
|
+
default_executable:
|
|
14
|
+
dependencies: []
|
|
15
|
+
|
|
16
|
+
description:
|
|
17
|
+
email: dpickett@enlightsolutions.com
|
|
18
|
+
executables: []
|
|
19
|
+
|
|
20
|
+
extensions: []
|
|
21
|
+
|
|
22
|
+
extra_rdoc_files:
|
|
23
|
+
- LICENSE
|
|
24
|
+
- README.rdoc
|
|
25
|
+
files:
|
|
26
|
+
- .document
|
|
27
|
+
- .gitignore
|
|
28
|
+
- LICENSE
|
|
29
|
+
- README.rdoc
|
|
30
|
+
- Rakefile
|
|
31
|
+
- VERSION
|
|
32
|
+
- agcod.gemspec
|
|
33
|
+
- features/create_a_giftcard.feature
|
|
34
|
+
- features/step_definitions/agcod_steps.rb
|
|
35
|
+
- features/support/app_root/config/agcod.example.yml
|
|
36
|
+
- features/support/env.rb
|
|
37
|
+
- lib/agcod.rb
|
|
38
|
+
- lib/agcod/cancel_gift_card.rb
|
|
39
|
+
- lib/agcod/configuration.rb
|
|
40
|
+
- lib/agcod/create_gift_card.rb
|
|
41
|
+
- lib/agcod/error/configuration_error.rb
|
|
42
|
+
- lib/agcod/error/invalid_parameter.rb
|
|
43
|
+
- lib/agcod/health_check.rb
|
|
44
|
+
- lib/agcod/option_validators.rb
|
|
45
|
+
- lib/agcod/request.rb
|
|
46
|
+
- lib/agcod/tasks.rb
|
|
47
|
+
- lib/agcod/tasks/certification.rake
|
|
48
|
+
- lib/agcod/void_gift_card_creation.rb
|
|
49
|
+
- tasks/agcod.rake
|
|
50
|
+
- test/agcod/configuration_test.rb
|
|
51
|
+
- test/app_root/config/agcod.yml
|
|
52
|
+
- test/macros/configuration.rb
|
|
53
|
+
- test/test_helper.rb
|
|
54
|
+
has_rdoc: true
|
|
55
|
+
homepage: http://github.com/dpickett/agcod
|
|
56
|
+
post_install_message:
|
|
57
|
+
rdoc_options:
|
|
58
|
+
- --charset=UTF-8
|
|
59
|
+
require_paths:
|
|
60
|
+
- lib
|
|
61
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: "0"
|
|
66
|
+
version:
|
|
67
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
|
+
requirements:
|
|
69
|
+
- - ">="
|
|
70
|
+
- !ruby/object:Gem::Version
|
|
71
|
+
version: "0"
|
|
72
|
+
version:
|
|
73
|
+
requirements: []
|
|
74
|
+
|
|
75
|
+
rubyforge_project:
|
|
76
|
+
rubygems_version: 1.2.0
|
|
77
|
+
signing_key:
|
|
78
|
+
specification_version: 3
|
|
79
|
+
summary: TODO
|
|
80
|
+
test_files:
|
|
81
|
+
- test/agcod/configuration_test.rb
|
|
82
|
+
- test/macros/configuration.rb
|
|
83
|
+
- test/test_helper.rb
|