agcod 0.0.2

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 ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,9 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ features/support/app_root/config/agcod.yml
7
+ log
8
+ agcod_cucumber.log
9
+ features/support/certification_requests
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
@@ -0,0 +1,7 @@
1
+ = agcod
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Dan Pickett. See LICENSE for details.
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{A Wrapper for Amazon Gift Cards On Demand}
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.2
data/agcod.gemspec ADDED
@@ -0,0 +1,73 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{agcod}
8
+ s.version = "0.0.2"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Dan Pickett"]
12
+ s.date = %q{2009-10-14}
13
+ s.email = %q{dpickett@enlightsolutions.com}
14
+ s.extra_rdoc_files = [
15
+ "LICENSE",
16
+ "README.rdoc"
17
+ ]
18
+ s.files = [
19
+ ".document",
20
+ ".gitignore",
21
+ "LICENSE",
22
+ "README.rdoc",
23
+ "Rakefile",
24
+ "VERSION",
25
+ "agcod.gemspec",
26
+ "cucumber.yml",
27
+ "features/error_handling.feature",
28
+ "features/step_definitions/agcod_steps.rb",
29
+ "features/success_certification.feature",
30
+ "features/support/app_root/config/agcod.example.yml",
31
+ "features/support/env.rb",
32
+ "lib/agcod.rb",
33
+ "lib/agcod/cancel_gift_card.rb",
34
+ "lib/agcod/configuration.rb",
35
+ "lib/agcod/create_gift_card.rb",
36
+ "lib/agcod/error/configuration_error.rb",
37
+ "lib/agcod/error/invalid_parameter.rb",
38
+ "lib/agcod/health_check.rb",
39
+ "lib/agcod/option_validators.rb",
40
+ "lib/agcod/request.rb",
41
+ "lib/agcod/tasks.rb",
42
+ "lib/agcod/tasks/certification.rake",
43
+ "lib/agcod/void_gift_card_creation.rb",
44
+ "manual_features/cancel_claimed_giftcard.feature",
45
+ "manual_features/insufficient_funds.feature",
46
+ "manual_features/retry_and_http.feature",
47
+ "tasks/agcod.rake",
48
+ "test/agcod/configuration_test.rb",
49
+ "test/app_root/config/agcod.yml",
50
+ "test/macros/configuration.rb",
51
+ "test/test_helper.rb"
52
+ ]
53
+ s.homepage = %q{http://github.com/dpickett/agcod}
54
+ s.rdoc_options = ["--charset=UTF-8"]
55
+ s.require_paths = ["lib"]
56
+ s.rubygems_version = %q{1.3.5}
57
+ s.summary = %q{A Wrapper for Amazon Gift Cards On Demand}
58
+ s.test_files = [
59
+ "test/agcod/configuration_test.rb",
60
+ "test/macros/configuration.rb",
61
+ "test/test_helper.rb"
62
+ ]
63
+
64
+ if s.respond_to? :specification_version then
65
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
66
+ s.specification_version = 3
67
+
68
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
69
+ else
70
+ end
71
+ else
72
+ end
73
+ end
data/cucumber.yml ADDED
@@ -0,0 +1 @@
1
+ default: "--require features/support/env.rb --require features/step_definitions/agcod_steps.rb"
@@ -0,0 +1,31 @@
1
+ Feature: Error Handling
2
+ As a user of the agcod service
3
+ I want to correctly handle errors
4
+ So I can get certified
5
+
6
+ Background:
7
+ Given I have access to the AGCOD web service
8
+ And I am logging transactions
9
+
10
+ Scenario: #8 Error Handling E203
11
+ Given I specify response_id "AAAEPY26ZX1BSY"
12
+ And I want to cancel request "2"
13
+ When I send the request
14
+ Then I should not receive a successful response
15
+
16
+ Scenario: #9 Error Handling E204
17
+ Given I specify response_id "A3REPY26ZX1BSY"
18
+ And I want to cancel request "2"
19
+ When I send the request
20
+ Then I should not receive a successful response
21
+
22
+ Scenario: #10 Error Handling for no currency code
23
+ Given I specify the currency of ""
24
+ And I want to send request "7"
25
+ When I send the request
26
+ Then I should not receive a successful response
27
+
28
+ Scenario: #11 Error Handling with Max Limit
29
+ Given I want to send request "8"
30
+ When I send the request
31
+ Then I should not receive a successful response
@@ -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,47 @@
1
+ Feature: Successful Cases for Certification
2
+ As a user of the AGCOD api
3
+ I want to run certification tests that create successful responses
4
+ So that I can be authorized for production access
5
+
6
+ Background:
7
+ Given I have access to the AGCOD web service
8
+ And I am logging transactions
9
+
10
+ Scenario: #1 Successful Health Check
11
+ Given I want to send a health check request
12
+ When I send the request
13
+ Then I should receive a successful response
14
+
15
+ Scenario: #2a Successful Creation of a Gift Card for $12
16
+ Given I want to send request "1"
17
+ When I send the request
18
+ Then I should receive a successful response
19
+ And I should get a claim code
20
+
21
+ Scenario: #2b Successful Creation of a Gift Card for $999
22
+ Given I want to send request "2"
23
+ When I send the request
24
+ Then I should receive a successful response
25
+ And I should get a claim code
26
+
27
+ Scenario: #3 Sending the same Gift Card Request ID
28
+ Given I've sent request "3"
29
+ And the request was successful
30
+ And I want to create a gift card with the same request id
31
+ When I send the request
32
+ Then I should not receive a successful response
33
+
34
+ Scenario: #4 Cancel a Gift Card Successfully
35
+ Given I've sent request "4"
36
+ And the request was successful
37
+ And I want to cancel the gift card requested
38
+ When I send the request
39
+ Then I should receive a successful response
40
+
41
+ Scenario: #5 Void a Gift Card Successfully
42
+ Given I've sent request "5"
43
+ And the request was successful
44
+ And I want to void the gift card requested
45
+ When I send the request
46
+ Then I should receive a successful response
47
+
@@ -0,0 +1,6 @@
1
+ test:
2
+ access_key: fadsfasdgt2434
3
+ secret_key: dfasdf
4
+ partner_id: SomebodyImportant
5
+ uri: https://agcws-gamma.amazon.com/
6
+ discount_percentage: 0.04
@@ -0,0 +1,8 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__) + '/../../lib')
2
+
3
+ require 'test/unit/assertions'
4
+ require "shoulda"
5
+
6
+ require 'agcod'
7
+
8
+ World(Test::Unit::Assertions)
data/lib/agcod.rb ADDED
@@ -0,0 +1,25 @@
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 "cgi"
9
+ require "base64"
10
+ require "hmac-sha1"
11
+ require "digest/sha1"
12
+ require "cgi"
13
+ require "logger"
14
+
15
+ require "agcod/error/invalid_parameter"
16
+ require "agcod/error/configuration_error"
17
+ require "agcod/option_validators"
18
+
19
+ require "agcod/configuration"
20
+ require "agcod/request"
21
+
22
+ require "agcod/cancel_gift_card"
23
+ require "agcod/create_gift_card"
24
+ require "agcod/health_check"
25
+ require "agcod/void_gift_card_creation"
@@ -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,90 @@
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
+ else
30
+ attempt_retry unless @sent_retry
31
+ if @sent_retry
32
+ void_on_resend
33
+ end
34
+ end
35
+ end
36
+
37
+ attr_reader :claim_code, :response_id, :value
38
+
39
+ def to_yaml(name)
40
+ {"response_id" => self.response_id,
41
+ "request_id" => self.request_id,
42
+ "claim_code" => self.claim_code,
43
+ "value" => self.value,
44
+ "timestamp" => self.timestamp
45
+ }.to_yaml(name)
46
+ end
47
+
48
+ protected
49
+ def send_request
50
+ begin
51
+ super
52
+ rescue SocketError,
53
+ Timeout::Error,
54
+ ActiveResource::TimeoutError,
55
+ Errno::ECONNREFUSED,
56
+ Errno::EHOSTDOWN,
57
+ Errno::EHOSTUNREACH
58
+
59
+ attempt_to_void
60
+ end
61
+ end
62
+
63
+ def attempt_retry
64
+ #check for retry error
65
+ if self.xml_response.root.elements["Status/errorCode"] &&
66
+ self.xml_response.root.elements["Status/errorCode"].text == "E100" &&
67
+ !@sent_retry
68
+
69
+ @sent_retry = true
70
+ submit
71
+ end
72
+ end
73
+
74
+ private
75
+ def void_on_resend
76
+ if self.xml_response.root.elements["Status/errorCode"] &&
77
+ self.xml_response.root.elements["Status/errorCode"].text == "E100" &&
78
+ !@resend_void_sent
79
+
80
+ @resend_void_sent = true
81
+ attempt_to_void
82
+ end
83
+ end
84
+
85
+ def attempt_to_void
86
+ Agcod::VoidGiftCardCreation.new("request_id" => self.request_id).submit
87
+ end
88
+
89
+ end
90
+ end
@@ -0,0 +1,7 @@
1
+ module Agcod
2
+ module Error
3
+ class ConfigurationError < RuntimeError
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ module Agcod
2
+ module Error
3
+ class InvalidParameter < RuntimeError
4
+
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,8 @@
1
+ module Agcod
2
+ class HealthCheck < Agcod::Request
3
+ def initialize
4
+ @action = "HealthCheck"
5
+ super
6
+ end
7
+ end
8
+ 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,153 @@
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
+ if @response
23
+ process_response
24
+ log if Agcod::Configuration.logger
25
+ end
26
+ end
27
+
28
+ def successful?
29
+ self.sent && self.errors.size == 0 && self.status == "SUCCESS"
30
+ end
31
+
32
+ attr_reader :errors, :request_id, :sent, :action, :request, :parameters, :response, :xml_response, :status, :timestamp
33
+
34
+ def sign_string(string_to_sign)
35
+ #remove all the = and & from the serialized string
36
+ sanitized_string = string_to_sign.gsub(/=|&/, "")
37
+ # puts sanitized_string
38
+ sha1 = HMAC::SHA1::digest(Agcod::Configuration.secret_key, sanitized_string)
39
+
40
+ #Base64 encoding adds a linefeed to the end of the string so chop the last character!
41
+ CGI.escape(Base64.encode64(sha1).chomp)
42
+ end
43
+
44
+ def request_id
45
+ @options["request_id"]
46
+ end
47
+
48
+ def response_id
49
+ @response_id || @options["response_id"]
50
+ end
51
+
52
+ protected
53
+
54
+ def process_response
55
+ parse_response
56
+
57
+
58
+ @errors = []
59
+
60
+ self.xml_response.root.elements.each("Error") do |e|
61
+ @errors << e.elements["Message"].text
62
+ end
63
+
64
+ @status = self.xml_response.root.elements["Status/statusCode"].text unless xml_response.root.elements["Status/statusCode"].nil?
65
+
66
+ #something happened before it got to ACGWS (most likely a signature problem)
67
+ @status = "FAILURE" if self.errors.size > 0 && self.status.blank?
68
+
69
+ @request_id = xml_response.root.elements["RequestID"].text unless xml_response.root.elements["RequestID"].nil?
70
+ end
71
+
72
+ def parse_response
73
+ @xml_response ||= REXML::Document.new(self.response)
74
+ end
75
+
76
+ def attempt_retry
77
+ #check for retry error
78
+ if self.xml_response.root.elements["Status/errorCode"] &&
79
+ self.xml_response.root.elements["Status/errorCode"].text == "E100" &&
80
+ !@sent_retry
81
+
82
+ @sent_retry = true
83
+ submit
84
+ end
85
+ end
86
+
87
+ protected
88
+ def send_request
89
+ #send the request
90
+ uri = URI.parse(Agcod::Configuration.uri)
91
+ http = Net::HTTP.new(uri.host, uri.port)
92
+ http.read_timeout = 20
93
+ http.open_timeout = 20
94
+ http.use_ssl = true
95
+
96
+ net_response, @response = http.get(uri.path + "?" + self.request)
97
+
98
+ @sent = true
99
+ end
100
+
101
+ def default_parameters
102
+ @timestamp = Time.now.utc
103
+ {
104
+ "Action" => self.action,
105
+ "AWSAccessKeyId" => Agcod::Configuration.access_key,
106
+ "SignatureVersion" => "1",
107
+ "MessageHeader.recipientId" => "AMAZON",
108
+ "MessageHeader.sourceId" => Agcod::Configuration.partner_id,
109
+ "MessageHeader.retryCount" => "0",
110
+ "MessageHeader.contentVersion" => "2008-01-01",
111
+ "MessageHeader.messageType" => self.action + "Request",
112
+ "Timestamp" => @timestamp.strftime("%Y-%m-%dT%H:%M:%S") + ".000Z"
113
+ }
114
+ end
115
+
116
+ def build_sorted_and_signed_request_string
117
+ params_to_submit = default_parameters.merge(self.parameters)
118
+
119
+ unencoded_key_value_strings = []
120
+ encoded_key_value_strings = []
121
+ sort_parameters(params_to_submit).each do |p|
122
+ unencoded_key_value_strings << p[0].to_s + p[1].to_s
123
+
124
+ if p[0] =~ /Timestamp/i
125
+ encoded_value = p[1]
126
+ else
127
+ encoded_value = CGI.escape(p[1].to_s)
128
+ end
129
+ encoded_key_value_strings << p[0].to_s + "=" + encoded_value
130
+ end
131
+
132
+ signature = sign_string(unencoded_key_value_strings.join(""))
133
+ encoded_key_value_strings.insert(encoded_key_value_strings.index("SignatureVersion=1") + 1 , "Signature=" + signature)
134
+ encoded_key_value_strings.join("&")
135
+ end
136
+
137
+
138
+ def sort_parameters(params)
139
+ params.sort{|a, b| a[0].downcase <=> b[0].downcase }
140
+ end
141
+
142
+ def log
143
+ log_string = "[AGCOD] #{self.action} Request"
144
+ log_string << " \##{self.request_id}" if self.request_id
145
+ log_string << " received response #{self.response_id}" if self.response_id
146
+ if self.respond_to?(:claim_code) && self.claim_code
147
+ log_string << " received claim_code #{self.claim_code}"
148
+ end
149
+ Agcod::Configuration.logger.debug log_string
150
+ end
151
+
152
+ end
153
+ end
@@ -0,0 +1,3 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'tasks', '*.rake')].each do |f|
2
+ load f
3
+ 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
@@ -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
@@ -0,0 +1,13 @@
1
+ Feature: Cancel claimed Gift Card Error Handling
2
+ As a user of the agcod service
3
+ I want to handle when I attempt to cancel a claimed gift card
4
+ So I can get certified
5
+
6
+ Background:
7
+ Given I have access to the AGCOD web service
8
+ And I am logging transactions
9
+
10
+ Scenario: Claimed Gift Card
11
+ Given I want to cancel request "1"
12
+ When I send the request
13
+ Then I should not receive a successful response
@@ -0,0 +1,13 @@
1
+ Feature: Insufficient Funds
2
+ As a user of the agcod service
3
+ I want to handle when I have insufficient funds appropriately
4
+ So I can get certified
5
+
6
+ Background:
7
+ Given I have access to the AGCOD web service
8
+ And I am logging transactions
9
+
10
+ Scenario: Insufficient Funds
11
+ Given I want to send request "6"
12
+ And I send the request
13
+ Then I should not receive a successful response
@@ -0,0 +1,46 @@
1
+ Feature: Retry responses and HTTP Errors
2
+ As a user of AGCOD
3
+ I want to properly handle retry responses and HTTP errors
4
+ So that I can get certified
5
+
6
+ Background:
7
+ Given I have access to the AGCOD web service
8
+ And I am logging transactions
9
+
10
+ Scenario: #12 RESEND creation request
11
+ Given I want to send request "9"
12
+ When I send the request
13
+ Then I should not receive a successful response
14
+
15
+ Scenario: #13 RESEND cancel request
16
+ Given I want to cancel request "2"
17
+ When I send the request
18
+ Then I should not receive a successful response
19
+
20
+ Scenario: #14 HTTP error
21
+ Given I want to send request "10"
22
+ When I send the request
23
+ Then I should not receive a successful response
24
+
25
+ Scenario: #15 HTTP Void Error
26
+ Given I want to void request "10"
27
+ When I send the request
28
+ Then I should not receive a successful response
29
+
30
+ Scenario: #16 HTTP Cancel Error
31
+ Given I want to cancel request "3"
32
+ When I send the request
33
+ Then I should not receive a successful response
34
+
35
+ Scenario: #17 minimum amount
36
+ Given I want to send request "11"
37
+ When I send the request
38
+ Then I should not receive a successful response
39
+
40
+ Scenario: #17b $12 gift card
41
+ Given I want to send request "12"
42
+ When I send the request
43
+ Then I should receive a successful resposne
44
+
45
+
46
+
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,6 @@
1
+ test:
2
+ access_key: fadsfasdgt2434
3
+ secret_key: dfasdf
4
+ partner_id: SomebodyImportant
5
+ uri: https://agcws-gamma.amazon.com/
6
+ discount_percentage: 0.04
@@ -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
@@ -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,90 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: agcod
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Dan Pickett
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-10-14 00:00:00 -04: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
+ - cucumber.yml
34
+ - features/error_handling.feature
35
+ - features/step_definitions/agcod_steps.rb
36
+ - features/success_certification.feature
37
+ - features/support/app_root/config/agcod.example.yml
38
+ - features/support/env.rb
39
+ - lib/agcod.rb
40
+ - lib/agcod/cancel_gift_card.rb
41
+ - lib/agcod/configuration.rb
42
+ - lib/agcod/create_gift_card.rb
43
+ - lib/agcod/error/configuration_error.rb
44
+ - lib/agcod/error/invalid_parameter.rb
45
+ - lib/agcod/health_check.rb
46
+ - lib/agcod/option_validators.rb
47
+ - lib/agcod/request.rb
48
+ - lib/agcod/tasks.rb
49
+ - lib/agcod/tasks/certification.rake
50
+ - lib/agcod/void_gift_card_creation.rb
51
+ - manual_features/cancel_claimed_giftcard.feature
52
+ - manual_features/insufficient_funds.feature
53
+ - manual_features/retry_and_http.feature
54
+ - tasks/agcod.rake
55
+ - test/agcod/configuration_test.rb
56
+ - test/app_root/config/agcod.yml
57
+ - test/macros/configuration.rb
58
+ - test/test_helper.rb
59
+ has_rdoc: true
60
+ homepage: http://github.com/dpickett/agcod
61
+ licenses: []
62
+
63
+ post_install_message:
64
+ rdoc_options:
65
+ - --charset=UTF-8
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: "0"
73
+ version:
74
+ required_rubygems_version: !ruby/object:Gem::Requirement
75
+ requirements:
76
+ - - ">="
77
+ - !ruby/object:Gem::Version
78
+ version: "0"
79
+ version:
80
+ requirements: []
81
+
82
+ rubyforge_project:
83
+ rubygems_version: 1.3.5
84
+ signing_key:
85
+ specification_version: 3
86
+ summary: A Wrapper for Amazon Gift Cards On Demand
87
+ test_files:
88
+ - test/agcod/configuration_test.rb
89
+ - test/macros/configuration.rb
90
+ - test/test_helper.rb