click_and_send 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/fixtures/config.yml
15
+ spec/reports
16
+ test/tmp
17
+ test/version_tmp
18
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format progress
data/.travis.yml ADDED
@@ -0,0 +1,10 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.8.7
4
+ - 1.9.2
5
+ - 1.9.3
6
+ - jruby-18mode # JRuby in 1.8 mode
7
+ - jruby-19mode # JRuby in 1.9 mode
8
+ - rbx-18mode
9
+ - rbx-19mode
10
+ script: bundle exec rspec -t '~@acceptance' spec
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in click_and_send.gemspec
4
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Zubin Henner
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # ClickAndSend
2
+
3
+ Ruby adapter for Australia Post's [ClickAndSend](http://www.clickandsend.com.au/) API.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'click_and_send'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install click_and_send
18
+
19
+ ## Usage
20
+
21
+ Configure like this:
22
+
23
+ ClickAndSend.configure do |c|
24
+ c.account_number = '1234567'
25
+ c.api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
26
+ c.user_name = 'acme'
27
+ c.wsdl = 'https://auspost-staging.oss.neopost-id.com/modules/oss/api/ship/wsdl?sz_Client=AustraliaPost'
28
+ end
29
+
30
+ Note that the WSDL URL above is for ClickAndSend's staging environment.
31
+
32
+ You should only need to call methods on the `ClickAndSend` class.
33
+ Refer to the docs and acceptance tests for usage examples.
34
+
35
+ ## Contributing
36
+
37
+ 1. Fork it
38
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
39
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
40
+ 4. Push to the branch (`git push origin my-new-feature`)
41
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env rake
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ RSpec::Core::RakeTask.new('spec')
5
+ task :default => :spec
@@ -0,0 +1,25 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path('../lib/click_and_send/version', __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.authors = ["Zubin Henner"]
6
+ gem.email = ["zubin.henner@gmail.com"]
7
+ gem.description = %q{Ruby adapter for Australia Post's ClickAndSend API}
8
+ gem.summary = %q{Ruby adapter for Australia Post's ClickAndSend API}
9
+ gem.homepage = "https://github.com/zubin/click_and_send"
10
+
11
+ gem.files = `git ls-files`.split($\)
12
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
13
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
14
+ gem.name = "click_and_send"
15
+ gem.require_paths = ["lib"]
16
+ gem.version = ClickAndSend::VERSION
17
+
18
+ gem.add_dependency 'nori'
19
+ gem.add_dependency 'savon'
20
+
21
+ gem.add_development_dependency 'rake'
22
+ gem.add_development_dependency 'rspec'
23
+ gem.add_development_dependency 'faker'
24
+ gem.add_development_dependency 'pry'
25
+ end
@@ -0,0 +1,92 @@
1
+ require 'click_and_send/version'
2
+ require 'click_and_send/configuration'
3
+ require 'click_and_send/encryption'
4
+ require 'click_and_send/errors'
5
+ require 'click_and_send/request'
6
+ require 'click_and_send/request/check_services'
7
+ require 'click_and_send/request/create_items'
8
+ require 'click_and_send/request/delete_items'
9
+ require 'click_and_send/request/item_summary'
10
+ require 'click_and_send/request/print_item'
11
+ require 'click_and_send/xml'
12
+
13
+ module ClickAndSend
14
+ class << self
15
+ attr_accessor :configuration
16
+
17
+ # Example use:
18
+ #
19
+ # ClickAndSend.configure do |c|
20
+ # c.account_number = '1234567'
21
+ # c.api_key = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
22
+ # c.user_name = 'acme'
23
+ # c.wsdl = 'https://auspost-staging.oss.neopost-id.com/modules/oss/api/ship/wsdl?sz_Client=AustraliaPost'
24
+ # end
25
+ def configure
26
+ self.configuration ||= Configuration.new
27
+ yield(configuration)
28
+ end
29
+
30
+ def check_services(options)
31
+ ensure_keys_present(options, %w(Sender Receiver ItemDetails))
32
+ Request::CheckServices.new(options).result.fetch(:product_found)
33
+ end
34
+
35
+ def create_items(options)
36
+ ensure_keys_present(options, %w(Transactions))
37
+ Request::CreateItems.new(options).result.fetch(:item_created)
38
+ end
39
+
40
+ def delete_items_by_ref(refs)
41
+ items = find_tracking_numbers(refs).inject([]) do |carrier, (ref, tracking_numbers)|
42
+ tracking_numbers.each { |t| carrier << {'DeleteItem' => {'CustTransactionID' => ref, 'TrackingNumber' => t}} }
43
+ carrier
44
+ end
45
+ delete_items('DeleteItems' => items)
46
+ end
47
+
48
+ def delete_items(options)
49
+ ensure_keys_present(options, %w(DeleteItems))
50
+ Request::DeleteItems.new(options).result
51
+ end
52
+
53
+ def find_tracking_numbers(refs)
54
+ item_summary.inject({}) do |hash,item|
55
+ if refs.include?(item[:cust_transaction_id])
56
+ hash[item[:cust_transaction_id]] ||= []
57
+ hash[item[:cust_transaction_id]] << item[:tracking_number]
58
+ end
59
+ hash
60
+ end
61
+ end
62
+
63
+ def item_summary
64
+ Request::ItemSummary.new.result.fetch(:item_summary_transaction)
65
+ end
66
+
67
+ def pdf_url(ref)
68
+ tracking_numbers = find_tracking_numbers([ref]).fetch(ref)
69
+ case tracking_numbers.length
70
+ when 0 then raise(Errors::APIError, "Not found: '#{ref}'")
71
+ when 1 then tracking_number = tracking_numbers.first
72
+ else
73
+ raise(Errors::APIError, "Expected 1 but found #{tracking_numbers.length} tracking numbers for 'REF': #{tracking_numbers.join(', ')}")
74
+ end
75
+ options = {
76
+ 'PrintFormat' => 'LINK',
77
+ 'CustTransactionID' => ref,
78
+ 'TrackingNumber' => tracking_number
79
+ }
80
+ Request::PrintItem.new(options).result
81
+ end
82
+
83
+ private
84
+
85
+ def ensure_keys_present(hash, required_keys)
86
+ missing_keys = required_keys - hash.keys
87
+ if missing_keys.any?
88
+ raise(Errors::InvalidInput, "Missing: #{missing_keys.join(', ')}")
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,18 @@
1
+ module ClickAndSend
2
+ class Configuration
3
+ ATTRIBUTES = [:account_number, :api_version, :api_key, :user_name, :wsdl]
4
+ attr_accessor *ATTRIBUTES
5
+
6
+ def initialize
7
+ @api_version = 10
8
+ end
9
+
10
+ def errors
11
+ Hash[ATTRIBUTES.collect { |attribute| [attribute, ("is required" if send(attribute).nil?)] }]
12
+ end
13
+
14
+ def valid?
15
+ errors.values.compact.empty?
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,44 @@
1
+ require 'base64'
2
+ require 'openssl'
3
+
4
+ module ClickAndSend
5
+ class Encryption
6
+ def initialize(data)
7
+ @data = data
8
+ end
9
+
10
+ def to_s
11
+ Base64.encode64(encrypted).chomp.gsub(/\={1,2}$/, '') unless @data.nil?
12
+ end
13
+
14
+ private
15
+
16
+ def cipher
17
+ @cipher ||= begin
18
+ cipher = OpenSSL::Cipher::Cipher.new(cipher_type)
19
+ cipher.encrypt
20
+ cipher.key = key
21
+ cipher.iv = key[0..15]
22
+ cipher
23
+ end
24
+ end
25
+
26
+ def cipher_type
27
+ 'AES-256-CBC'
28
+ end
29
+
30
+ def encrypted
31
+ cipher.update(padded_data)
32
+ end
33
+
34
+ def key
35
+ ClickAndSend.configuration.api_key
36
+ end
37
+
38
+ # Data must be padded to nearest multiple of 16
39
+ def padded_data
40
+ desired_length = (@data.length.to_f / 16).ceil * 16
41
+ @data.ljust(desired_length, ' ')
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,6 @@
1
+ module ClickAndSend::Errors
2
+ class APIError < StandardError; end
3
+ class InvalidConfiguration < StandardError; end
4
+ class InvalidInput < StandardError; end
5
+ class NotImplemented < StandardError; end
6
+ end
@@ -0,0 +1,100 @@
1
+ require 'nori'
2
+ require 'savon'
3
+
4
+ module ClickAndSend
5
+ module Request
6
+ attr_accessor :data
7
+
8
+ def initialize(data = {})
9
+ self.data = data
10
+ end
11
+
12
+ def result
13
+ raise_on_error
14
+ response.fetch(key(:result))
15
+ end
16
+
17
+ private
18
+
19
+ def client
20
+ raise ClickAndSend::Errors::InvalidConfiguration unless config.valid?
21
+ @client ||= Savon.client do |wsdl, http|
22
+ wsdl.document = config.wsdl
23
+ http.auth.ssl.verify_mode = :none
24
+ end
25
+ end
26
+
27
+ def config
28
+ ClickAndSend.configuration
29
+ end
30
+
31
+ def encrypt(data)
32
+ ClickAndSend::Encryption.new(data).to_s
33
+ end
34
+
35
+ def errors
36
+ response.values_at(*key(:errors)).compact
37
+ end
38
+
39
+ def errors?
40
+ errors.any?
41
+ end
42
+
43
+ def default_keys
44
+ { :errors => [:item_errors, :errors_found] }
45
+ end
46
+
47
+ def extra_request_xml
48
+ ClickAndSend::XML.new(data).to_xml if data
49
+ end
50
+
51
+ def header
52
+ <<-XML
53
+ <Header>
54
+ <CnSUserName>#{encrypt(config.user_name)}</CnSUserName>
55
+ <APIAccessKey>#{encrypt(config.api_key)}</APIAccessKey>
56
+ <RequestDate>#{encrypt(request_date)}</RequestDate>
57
+ <APIVersion>#{config.api_version}</APIVersion>
58
+ </Header>
59
+ XML
60
+ end
61
+
62
+ def key(name)
63
+ default_keys.merge(keys).fetch(name)
64
+ end
65
+
66
+ # Hash with keys :answer, :request, :response, :result
67
+ def keys
68
+ raise(ClickAndSend::Errors::NotImplemented, "#{klass_name}#keys")
69
+ end
70
+
71
+ def klass_name
72
+ self.class.name
73
+ end
74
+
75
+ def raise_on_error
76
+ raise(ClickAndSend::Errors::APIError, errors) if errors?
77
+ end
78
+
79
+ def request_date
80
+ Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')
81
+ end
82
+
83
+ def request_xml
84
+ "<#{key(:request)}Request>%s</#{key(:request)}Request>" % CGI::escapeHTML(<<-XML.strip.gsub(/\n\s+/, "\n"))
85
+ <#{key(:request)}>
86
+ #{header}
87
+ #{extra_request_xml}
88
+ </#{key(:request)}>
89
+ XML
90
+ end
91
+
92
+ def response
93
+ @response ||= Nori.parse(request.to_hash[key(:response)][key(:response)]).fetch(key(:answer))
94
+ end
95
+
96
+ def request
97
+ client.request(:ind, key(:request), :body => request_xml)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,16 @@
1
+ module ClickAndSend
2
+ module Request
3
+ class CheckServices
4
+ include ClickAndSend::Request
5
+
6
+ private
7
+
8
+ def keys
9
+ { :answer => :ws_check_services_answer,
10
+ :request => 'WSCheckServices',
11
+ :response => :ws_check_services_response,
12
+ :result => :products_found }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ClickAndSend
2
+ module Request
3
+ class CreateItems
4
+ include ClickAndSend::Request
5
+
6
+ private
7
+
8
+ def keys
9
+ { :answer => :ws_create_items_answer,
10
+ :request => 'WSCreateItems',
11
+ :response => :ws_create_items_response,
12
+ :result => :items_created }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ClickAndSend
2
+ module Request
3
+ class DeleteItems
4
+ include ClickAndSend::Request
5
+
6
+ private
7
+
8
+ def keys
9
+ { :answer => :ws_delete_items_answer,
10
+ :request => 'WSDeleteItems',
11
+ :response => :ws_delete_items_response,
12
+ :result => :delete_items }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ClickAndSend
2
+ module Request
3
+ class ItemSummary
4
+ include ClickAndSend::Request
5
+
6
+ private
7
+
8
+ def keys
9
+ { :answer => :ws_item_summary_answer,
10
+ :request => 'WSItemSummary',
11
+ :response => :ws_item_summary_response,
12
+ :result => :item_summary_transactions }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ module ClickAndSend
2
+ module Request
3
+ class PrintItem
4
+ include ClickAndSend::Request
5
+
6
+ private
7
+
8
+ def keys
9
+ { :answer => :ws_print_item_answer,
10
+ :request => 'WSPrintItem',
11
+ :response => :ws_print_item_response,
12
+ :result => :label_http_link }
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,3 @@
1
+ module ClickAndSend
2
+ VERSION = '0.0.2'
3
+ end
@@ -0,0 +1,11 @@
1
+ module ClickAndSend
2
+ class XML
3
+ def initialize(input)
4
+ @input = input
5
+ end
6
+
7
+ def to_xml
8
+ Gyoku.xml(@input)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ require 'spec_helper'
2
+
3
+ describe ClickAndSend, :acceptance do
4
+ let(:item_details) { }
5
+ before { load_config(:acceptance) }
6
+
7
+ describe '.check_services' do
8
+ subject { ClickAndSend.check_services(attributes_for(%w(Sender Receiver ItemDetails))) }
9
+ specify { should be_a(Array) }
10
+ end
11
+
12
+ describe '.create_items' do
13
+ subject { ClickAndSend.create_items(options) }
14
+
15
+ context "when one transaction" do
16
+ let(:options) { {'Transactions' => attributes_for('Transaction')} }
17
+ specify { should be_a(Hash) }
18
+ end
19
+
20
+ context "when two transactions" do
21
+ let(:options) { attributes_for('Transactions') }
22
+ specify { should be_a(Array) }
23
+ end
24
+ end
25
+
26
+ context "when item uploaded" do
27
+ let(:create_item_attributes) { attributes_for('Transaction') }
28
+ let(:ref) { create_item_attributes.values.first.fetch('CustTransactionID') }
29
+ before { ClickAndSend.create_items('Transactions' => create_item_attributes) }
30
+
31
+ describe '.find_tracking_numbers' do
32
+ subject { ClickAndSend.find_tracking_numbers([ref]) }
33
+ specify do
34
+ should be_a(Hash)
35
+ subject.keys.should == [ref]
36
+ end
37
+ end
38
+
39
+ describe '.pdf_url' do
40
+ subject { ClickAndSend.pdf_url(ref) }
41
+ specify do
42
+ subject.should be_a(String)
43
+ open(subject).content_type.should == 'application/pdf'
44
+ end
45
+ end
46
+
47
+ describe '.delete_items_by_ref' do
48
+ specify do
49
+ expect do
50
+ ClickAndSend.delete_items_by_ref([ref])
51
+ end.to change { ClickAndSend.find_tracking_numbers([ref]).empty? }.to(true)
52
+ end
53
+ end
54
+
55
+ describe '.delete_items' do
56
+ let(:items) do
57
+ tracking_numbers.collect do |tracking_number|
58
+ {'DeleteItem' => {'CustTransactionID' => ref, 'TrackingNumber' => tracking_number}}
59
+ end
60
+ end
61
+ let(:tracking_numbers) { ClickAndSend.find_tracking_numbers([ref]).fetch(ref) }
62
+ specify do
63
+ expect do
64
+ ClickAndSend.delete_items('DeleteItems' => items)
65
+ end.to change { ClickAndSend.find_tracking_numbers(ref).empty? }.to(true)
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '.item_summary' do
71
+ subject { ClickAndSend.item_summary }
72
+ specify { should be_a(Array) }
73
+ end
74
+ end
@@ -0,0 +1,95 @@
1
+ require 'spec_helper'
2
+
3
+ describe ClickAndSend::Request do
4
+ subject { klass.new }
5
+ let(:klass) { Class.new.tap { |klass| klass.send(:include, ClickAndSend::Request) } }
6
+
7
+ describe '.new' do
8
+ it "defaults data to an empty hash" do
9
+ subject.data.should == {}
10
+ end
11
+
12
+ it "sets data" do
13
+ klass.new('foo').data.should == 'foo'
14
+ end
15
+ end
16
+
17
+ describe '#result' do
18
+ subject { klass.new(data).result }
19
+ let(:data) { stub(:data) }
20
+ before { load_config }
21
+
22
+ context "valid keys" do
23
+ let(:client) { stub(:client) }
24
+ let(:config) { ClickAndSend.configuration }
25
+ let(:expected_xml) do
26
+ "<WSSomethingRequest>%s</WSSomethingRequest>" % CGI::escapeHTML(<<-XML.strip.gsub(/\n\s+/, "\n"))
27
+ <WSSomething>
28
+ <Header>
29
+ <CnSUserName>encrypted_#{config.user_name}</CnSUserName>
30
+ <APIAccessKey>encrypted_#{config.api_key}</APIAccessKey>
31
+ <RequestDate>encrypted_#{Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ')}</RequestDate>
32
+ <APIVersion>#{config.api_version}</APIVersion>
33
+ </Header>
34
+ <ExtraRequestXml></ExtraRequestXml>
35
+ </WSSomething>
36
+ XML
37
+ end
38
+ let(:parsed_response) { {:ws_something_answer => {:something_result => 'the result'}} }
39
+ let(:request) { stub.as_null_object }
40
+ before do
41
+ klass.send(:define_method, :keys) do
42
+ { :answer => :ws_something_answer,
43
+ :request => 'WSSomething',
44
+ :response => :ws_something_response,
45
+ :result => :something_result }
46
+ end
47
+ Savon.stub(:client) { client }
48
+ ClickAndSend::Encryption.stub(:new) { |arg| stub(:to_s => "encrypted_#{arg}") }
49
+ ClickAndSend::XML.stub(:new).with(data) { stub(:to_xml => "<ExtraRequestXml></ExtraRequestXml>") }
50
+ client.stub(:request) { request }
51
+ Nori.stub(:parse).with(request.to_hash[:ws_something_response][:ws_something_response]) do
52
+ parsed_response
53
+ end
54
+
55
+ end
56
+
57
+ it "calls service with :request key" do
58
+ client.should_receive(:request).with(:ind, 'WSSomething', :body => expected_xml)
59
+ subject
60
+ end
61
+
62
+ it "parses response" do
63
+ Nori.should_receive(:parse).with(request.to_hash[:ws_something_response][:ws_something_response])
64
+ subject
65
+ end
66
+
67
+ it "uses :response and :answer keys" do
68
+ request = stub.as_null_object
69
+ client.should_receive(:request) { request }
70
+ Nori.should_receive(:parse).with(request.to_hash[:ws_something_response][:ws_something_response])
71
+ subject.should == 'the result'
72
+ end
73
+
74
+ context "when error key present in response" do
75
+ let(:parsed_response) { {:ws_something_answer => {:errors_found => "Oops!"}} }
76
+ it "raises on API error" do
77
+ expect { subject }.to raise_error(ClickAndSend::Errors::APIError)
78
+ end
79
+ end
80
+ end
81
+
82
+ context "keys undefined" do
83
+ it "raises NotImplemented" do
84
+ expect { subject }.to raise_error(ClickAndSend::Errors::NotImplemented)
85
+ end
86
+ end
87
+
88
+ context "missing key" do
89
+ before { klass.send(:define_method, :keys) { {} } }
90
+ it "raises KeyError" do
91
+ expect { subject }.to raise_error(KeyError)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,16 @@
1
+ require 'spec_helper'
2
+
3
+ describe ClickAndSend::XML do
4
+ describe '#to_xml' do
5
+ subject { ClickAndSend::XML.new(input).to_xml }
6
+ context "hash" do
7
+ let(:input) { {:a => 1, "b" => 2} }
8
+ it { should == "<a>1</a><b>2</b>" }
9
+ end
10
+
11
+ context "nested hash" do
12
+ let(:input) { {:a => {:b => 2}} }
13
+ it { should == "<a><b>2</b></a>" }
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,157 @@
1
+ require 'spec_helper'
2
+
3
+ describe ClickAndSend do
4
+ before { ClickAndSend.configuration = nil }
5
+
6
+ describe '.configuration' do
7
+ subject { ClickAndSend.configuration }
8
+
9
+ context "unconfigured" do
10
+ it { should be_nil }
11
+ end
12
+
13
+ context "configured" do
14
+ before { ClickAndSend.configure {} }
15
+ it { should be_a(ClickAndSend::Configuration) }
16
+ its(:api_version) { should == 10 }
17
+ end
18
+ end
19
+
20
+ describe '.configure' do
21
+ before { ClickAndSend.configure {} }
22
+
23
+ [:account_number, :api_key, :user_name, :wsdl].each do |attribute|
24
+ it "requires #{attribute}" do
25
+ expect do
26
+ ClickAndSend.configure { |c| c.send(:"#{attribute}=", "foo") }
27
+ end.to change { ClickAndSend.configuration.errors[attribute] }.to(nil)
28
+ end
29
+ end
30
+
31
+ context "unrecognised attribute" do
32
+ specify do
33
+ expect { ClickAndSend.configure { |c| c.foo = "bar" } }.to raise_error(NoMethodError)
34
+ end
35
+ end
36
+ end
37
+
38
+ context "requests" do
39
+ describe '.check_services' do
40
+ subject { ClickAndSend.check_services(options) }
41
+ let(:required_inputs) { %w(Sender Receiver ItemDetails) }
42
+ it_behaves_like "data required"
43
+
44
+ context "when input has valid keys" do
45
+ let(:options) { {'Sender' => stub, 'Receiver' => stub, 'ItemDetails' => stub} }
46
+ it "fetches :product_found from CheckServices result" do
47
+ ClickAndSend::Request::CheckServices.should_receive(:new) { stub(:result => {:product_found => 'services'}) }
48
+ subject.should == 'services'
49
+ end
50
+ end
51
+ end
52
+
53
+ describe '.create_items' do
54
+ subject { ClickAndSend.create_items(options) }
55
+ let(:options) { {'Transactions' => stub} }
56
+ let(:required_inputs) { %w(Transactions) }
57
+ it_behaves_like "data required"
58
+ it "fetches :item_created from CreateItems result" do
59
+ ClickAndSend::Request::CreateItems.should_receive(:new) { stub(:result => {:item_created => 'created'}) }
60
+ subject.should == 'created'
61
+ end
62
+ end
63
+
64
+ describe '.delete_items_by_ref' do
65
+ subject { ClickAndSend.delete_items_by_ref(['my_ref']) }
66
+ it "calls delete_items with ref, tracking_number" do
67
+ ClickAndSend.stub(:find_tracking_numbers).with(['my_ref']) { {'my_ref' => ['t1', 't2']} }
68
+ ClickAndSend.should_receive(:delete_items).with('DeleteItems' => [
69
+ {'DeleteItem' => {'CustTransactionID' => 'my_ref', 'TrackingNumber' => 't1'}},
70
+ {'DeleteItem' => {'CustTransactionID' => 'my_ref', 'TrackingNumber' => 't2'}}
71
+ ])
72
+ subject
73
+ end
74
+ end
75
+
76
+ describe '.delete_items' do
77
+ subject { ClickAndSend.delete_items(options) }
78
+ let(:options) { {'DeleteItems' => stub} }
79
+ let(:required_inputs) { %w(DeleteItems) }
80
+ it_behaves_like "data required"
81
+ it "calls DeleteItems result" do
82
+ ClickAndSend::Request::DeleteItems.should_receive(:new) { stub(:result => 'deleted') }
83
+ subject.should == 'deleted'
84
+ end
85
+ end
86
+
87
+ describe '.find_tracking_numbers' do
88
+ subject { ClickAndSend.find_tracking_numbers(['REF']) }
89
+ let(:item_summary) do
90
+ [ {:cust_transaction_id => 'REF', :tracking_number => 'TRACK-REF-1'},
91
+ {:cust_transaction_id => 'REF', :tracking_number => 'TRACK-REF-2'},
92
+ {:cust_transaction_id => 'OTHER_REF', :tracking_number => 'TRACK-OTHER-REF'} ]
93
+ end
94
+ before { ClickAndSend.stub(:item_summary) { item_summary } }
95
+ it { should be_a(Hash) }
96
+ its(:keys) { should == ['REF'] }
97
+ it "includes tracking numbers for REF" do
98
+ subject.values.first.should include('TRACK-REF-1')
99
+ subject.values.first.should include('TRACK-REF-2')
100
+ end
101
+ it "excludes other tracking number" do
102
+ subject.values.first.should_not include('TRACK-OTHER-REF')
103
+ end
104
+ end
105
+
106
+ describe '.item_summary' do
107
+ subject { ClickAndSend.item_summary }
108
+ it "fetches :item_summary_transaction from ItemSummary result" do
109
+ ClickAndSend::Request::ItemSummary.should_receive(:new) do
110
+ stub(:result => {:item_summary_transaction => 'items'})
111
+ end
112
+ subject.should == 'items'
113
+ end
114
+ end
115
+
116
+ describe '.pdf_url' do
117
+ subject { ClickAndSend.pdf_url('REF') }
118
+ let(:options) do
119
+ {
120
+ 'PrintFormat' => 'LINK',
121
+ 'CustTransactionID' => 'REF',
122
+ 'TrackingNumber' => tracking_numbers.first
123
+ }
124
+ end
125
+ let(:pdf_url) { 'http://example.com/paperwork.pdf' }
126
+ before do
127
+ ClickAndSend.stub(:find_tracking_numbers).with(['REF']) do
128
+ {'REF' => tracking_numbers}
129
+ end
130
+ end
131
+
132
+ context "one tracking number" do
133
+ let(:tracking_numbers) { ['xyz'] }
134
+ specify "requests URL from PrintItem" do
135
+ ClickAndSend::Request::PrintItem.should_receive(:new).with(options) do
136
+ stub(:result => pdf_url)
137
+ end
138
+ subject.should == pdf_url
139
+ end
140
+ end
141
+
142
+ context "no matching tracking numbers" do
143
+ let(:tracking_numbers) { [] }
144
+ it "raises API error" do
145
+ expect { subject }.to raise_error(ClickAndSend::Errors::APIError, "Not found: 'REF'")
146
+ end
147
+ end
148
+
149
+ context "two tracking numbers" do
150
+ let(:tracking_numbers) { ['uvw', 'xyz'] }
151
+ it "raises API error" do
152
+ expect { subject }.to raise_error(ClickAndSend::Errors::APIError, "Expected 1 but found 2 tracking numbers for 'REF': uvw, xyz")
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
@@ -0,0 +1,5 @@
1
+ # To run the acceptance test, copy this file to config.yml and change these values to your test account's credentials.
2
+ :account_number: 1234567
3
+ :api_key: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
4
+ :user_name: acme
5
+ :wsdl: https://auspost-staging.oss.neopost-id.com/modules/oss/api/ship/wsdl?sz_Client=AustraliaPost
@@ -0,0 +1,15 @@
1
+ require 'click_and_send'
2
+ Dir[File.join(File.dirname(__FILE__), 'support/**/*.rb')].each { |path| require path }
3
+
4
+ RSpec.configure do |config|
5
+ config.treat_symbols_as_metadata_keys_with_true_values = true
6
+ config.run_all_when_everything_filtered = true
7
+ config.filter_run :focus
8
+
9
+ # Run specs in random order to surface order dependencies. If you find an
10
+ # order dependency and want to debug it, you can fix the order by providing
11
+ # the seed, which is printed after each run.
12
+ # --seed 1234
13
+ # Disabled random order because some acceptance tests need to run in order.
14
+ # config.order = 'random'
15
+ end
@@ -0,0 +1,128 @@
1
+ require 'faker'
2
+
3
+ module ClickAndSend
4
+ module Test
5
+ module Factories
6
+ def attributes_for(*keys)
7
+ keys.flatten.inject({}) do |hash, key|
8
+ value = case key
9
+ when 'ItemDetails' then item_details_attributes
10
+ when 'Receiver' then us_address_attributes
11
+ when 'Sender' then au_address_attributes
12
+ when 'Transaction' then transaction_attributes
13
+ when 'Transactions'
14
+ [{'Transaction' => [transaction_attributes, transaction_attributes]}]
15
+ else raise UnknownDataKey, key
16
+ end
17
+ hash.merge(key => value)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ def address_attributes
24
+ {
25
+ 'CompanyName' => '',
26
+ 'ContactName' => name,
27
+ 'Telephone' => phone_number,
28
+ 'Email' => email,
29
+ 'AddressLine1' => street_address,
30
+ 'AddressLine2' => '',
31
+ 'AddressLine3' => '',
32
+ }
33
+ end
34
+
35
+ def au_address_attributes
36
+ address_attributes.merge(
37
+ 'Town' => "Sydney",
38
+ 'StateCode' => "NSW",
39
+ 'PostCode' => "2000",
40
+ 'CountryCode' => "AU"
41
+ )
42
+ end
43
+
44
+ def us_address_attributes
45
+ address_attributes.merge(
46
+ 'Town' => city,
47
+ 'StateCode' => "NY",
48
+ 'PostCode' => "10001",
49
+ 'CountryCode' => "US",
50
+ )
51
+ end
52
+
53
+ def city
54
+ Faker::Address.city
55
+ end
56
+
57
+ def customs_data_attributes
58
+ {
59
+ 'CustomsData' => {
60
+ # 'CustomsItemType' => '',
61
+ # 'CustomsQuantity' => '',
62
+ # 'ExportDeclarationNumber' => '',
63
+ # 'ReasonForExport' => '',
64
+ 'NameOfPersonLodging' => name,
65
+ # 'HSTariffNumber' => '',
66
+ # 'CountryOfOrigin' => '',
67
+ # 'NonDeliverySenderInstructions' => '',
68
+ # 'ReturnFollowingAddress' => '',
69
+ }
70
+ }
71
+ end
72
+
73
+ def dimensions
74
+ { 'Length' => '12.00', 'Width' => '12.00', 'Height' => '5.00' }
75
+ end
76
+
77
+ def email
78
+ Faker::Internet.email
79
+ end
80
+
81
+ def item_details_attributes
82
+ {
83
+ 'PackagingType' => 'M', # Merchandise
84
+ 'Dimensions' => dimensions,
85
+ 'ItemWeight' => '0.500', # Float Format 999.999 Kg
86
+ 'ExtraCover' => 0, # Boolean
87
+ 'ExtraCoverAmountOrDeclaredValue' => '100.00',
88
+ 'ItemDescriptionOrDescriptionOfGoods' => "stuff",
89
+ 'InternationalItemDetails' => customs_data_attributes
90
+ }
91
+ end
92
+
93
+ def name
94
+ Faker::Name.name
95
+ end
96
+
97
+ def payment_details_attributes
98
+ {
99
+ 'PaymentType' => '01',
100
+ 'PaymentData' => ClickAndSend.configuration.account_number
101
+ }
102
+ end
103
+
104
+ def phone_number
105
+ Faker::PhoneNumber.phone_number.gsub(/[^\d]/, '')
106
+ end
107
+
108
+ def street_address
109
+ Faker::Address.street_address
110
+ end
111
+
112
+ def transaction_attributes
113
+ {
114
+ 'CustTransactionID' => rand(10**10).to_s,
115
+ 'Sender' => au_address_attributes,
116
+ 'Receiver' => us_address_attributes,
117
+ 'ItemDetails' => item_details_attributes,
118
+ 'ProductCode' => '1',
119
+ 'PaymentDetails' => payment_details_attributes,
120
+ }
121
+ end
122
+
123
+ class UnknownDataKey < StandardError; end
124
+ end
125
+ end
126
+ end
127
+
128
+ RSpec.configure { |c| c.include ClickAndSend::Test::Factories }
@@ -0,0 +1,25 @@
1
+ require 'yaml'
2
+
3
+ module ClickAndSend
4
+ module Test
5
+ module Helpers
6
+ def load_config(mode = :test)
7
+ yaml_config_path = case mode
8
+ when :test
9
+ File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/config.example.yml'))
10
+ when :acceptance
11
+ File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/config.yml'))
12
+ end
13
+ if File.exists?(yaml_config_path)
14
+ ClickAndSend.configure do |config|
15
+ YAML::load_file(yaml_config_path).each { |k,v| config.send(:"#{k}=", v) }
16
+ end
17
+ else
18
+ pending "Can't run acceptance tests because #{yaml_config_path} is missing. See fixtures/config.example.yml."
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ RSpec.configure { |c| c.include ClickAndSend::Test::Helpers }
@@ -0,0 +1,2 @@
1
+ require 'open-uri'
2
+ OpenSSL::SSL::VERIFY_PEER = OpenSSL::SSL::VERIFY_NONE
@@ -0,0 +1,19 @@
1
+ shared_examples_for "data required" do
2
+ context "empty input" do
3
+ let(:options) { {} }
4
+ it "raises InvalidInput" do
5
+ required_inputs.each do |required_input|
6
+ expect { subject }.to raise_error(ClickAndSend::Errors::InvalidInput) do |e|
7
+ e.message.should include(required_input)
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ context "required keys present" do
14
+ let(:options) { required_inputs.inject({}) { |hash, key| hash.merge(key => stub) } }
15
+ it "doesn't raise InvalidInput" do
16
+ expect { subject }.not_to raise_error(ClickAndSend::Errors::InvalidInput)
17
+ end
18
+ end
19
+ end
metadata ADDED
@@ -0,0 +1,157 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: click_and_send
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Zubin Henner
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-08-14 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: nori
16
+ requirement: &70235421689060 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *70235421689060
25
+ - !ruby/object:Gem::Dependency
26
+ name: savon
27
+ requirement: &70235421676680 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *70235421676680
36
+ - !ruby/object:Gem::Dependency
37
+ name: rake
38
+ requirement: &70235421675840 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *70235421675840
47
+ - !ruby/object:Gem::Dependency
48
+ name: rspec
49
+ requirement: &70235421675300 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *70235421675300
58
+ - !ruby/object:Gem::Dependency
59
+ name: faker
60
+ requirement: &70235421674500 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *70235421674500
69
+ - !ruby/object:Gem::Dependency
70
+ name: pry
71
+ requirement: &70235421673020 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :development
78
+ prerelease: false
79
+ version_requirements: *70235421673020
80
+ description: Ruby adapter for Australia Post's ClickAndSend API
81
+ email:
82
+ - zubin.henner@gmail.com
83
+ executables: []
84
+ extensions: []
85
+ extra_rdoc_files: []
86
+ files:
87
+ - .gitignore
88
+ - .rspec
89
+ - .travis.yml
90
+ - Gemfile
91
+ - LICENSE
92
+ - README.md
93
+ - Rakefile
94
+ - click_and_send.gemspec
95
+ - lib/click_and_send.rb
96
+ - lib/click_and_send/configuration.rb
97
+ - lib/click_and_send/encryption.rb
98
+ - lib/click_and_send/errors.rb
99
+ - lib/click_and_send/request.rb
100
+ - lib/click_and_send/request/check_services.rb
101
+ - lib/click_and_send/request/create_items.rb
102
+ - lib/click_and_send/request/delete_items.rb
103
+ - lib/click_and_send/request/item_summary.rb
104
+ - lib/click_and_send/request/print_item.rb
105
+ - lib/click_and_send/version.rb
106
+ - lib/click_and_send/xml.rb
107
+ - spec/acceptance_spec.rb
108
+ - spec/click_and_send/request_spec.rb
109
+ - spec/click_and_send/xml_spec.rb
110
+ - spec/click_and_send_spec.rb
111
+ - spec/fixtures/config.example.yml
112
+ - spec/spec_helper.rb
113
+ - spec/support/factories.rb
114
+ - spec/support/helpers.rb
115
+ - spec/support/open_uri.rb
116
+ - spec/support/shared_examples.rb
117
+ homepage: https://github.com/zubin/click_and_send
118
+ licenses: []
119
+ post_install_message:
120
+ rdoc_options: []
121
+ require_paths:
122
+ - lib
123
+ required_ruby_version: !ruby/object:Gem::Requirement
124
+ none: false
125
+ requirements:
126
+ - - ! '>='
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ segments:
130
+ - 0
131
+ hash: 1242808500940722472
132
+ required_rubygems_version: !ruby/object:Gem::Requirement
133
+ none: false
134
+ requirements:
135
+ - - ! '>='
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
+ segments:
139
+ - 0
140
+ hash: 1242808500940722472
141
+ requirements: []
142
+ rubyforge_project:
143
+ rubygems_version: 1.8.6
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: Ruby adapter for Australia Post's ClickAndSend API
147
+ test_files:
148
+ - spec/acceptance_spec.rb
149
+ - spec/click_and_send/request_spec.rb
150
+ - spec/click_and_send/xml_spec.rb
151
+ - spec/click_and_send_spec.rb
152
+ - spec/fixtures/config.example.yml
153
+ - spec/spec_helper.rb
154
+ - spec/support/factories.rb
155
+ - spec/support/helpers.rb
156
+ - spec/support/open_uri.rb
157
+ - spec/support/shared_examples.rb