click_and_send 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/.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