diff --git a/.gitignore b/.gitignore index eb8e985..bb4cb16 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,9 @@ doc # jeweler generated pkg +# rvm +.rvmrc + # Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore: # # * Create a file at ~/.gitignore @@ -46,3 +49,9 @@ pkg # For rubinius: #*.rbc + +# Sample labels created by integration tests +test/images/*.gif +test/images/*.jpg +test/images/*.png +test/images/*.pdf diff --git a/Gemfile b/Gemfile index 0902836..f19c1f2 100644 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,18 @@ source "http://rubygems.org" # Add dependencies required to use your gem here. # Example: -# gem "activesupport", ">= 2.3.5" +gem "activesupport", ">= 2.3.5" +gem "i18n", ">= 0.6.0" gem "httparty", ">= 0.4.4" +gem "builder", ">= 3.0.0" # Add dependencies to develop your gem here. # Include everything needed to run rake, tests, features, etc. group :development do - # gem "shoulda", ">= 0" + gem "shoulda", ">= 0" + gem "mocha", ">= 0" gem "bundler", "~> 1.0.0" gem "jeweler", "~> 1.6.0" gem "rcov", ">= 0" + gem "nokogiri", ">= 0" end diff --git a/Gemfile.lock b/Gemfile.lock index 5a8e39a..07bb978 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,22 +1,34 @@ GEM remote: http://rubygems.org/ specs: + activesupport (3.0.9) + builder (3.0.0) crack (0.1.6) git (1.2.5) httparty (0.5.2) crack (= 0.1.6) + i18n (0.6.0) jeweler (1.6.0) bundler (~> 1.0.0) git (>= 1.2.5) rake + mocha (0.9.12) + nokogiri (1.5.0) rake (0.8.7) rcov (0.9.9) + shoulda (2.11.3) PLATFORMS ruby DEPENDENCIES + activesupport (>= 2.3.5) + builder (>= 3.0.0) bundler (~> 1.0.0) httparty (>= 0.4.4) + i18n (>= 0.6.0) jeweler (~> 1.6.0) + mocha + nokogiri rcov + shoulda diff --git a/README.rdoc b/README.rdoc index d3871df..1a4840f 100644 --- a/README.rdoc +++ b/README.rdoc @@ -1,36 +1,63 @@ = endicia -This gem allows you to connect to your Endicia API to print USPS labels. It is in no way meant to be exhaustive--fork away! - +This gem allows you to connect to your Endicia API to print USPS labels. It is +in no way meant to be exhaustive--fork away! + == Configuration -You'll need an endicia.yml file in config, with: - AccountID (6 digits) - RequesterID (string--you have to request this from Endicia) - PassPhrase (string) -and optionally - Test (set this to true while in testing and/or in development env) - + +If you're using Rails, you'll want an endicia.yml file in config, with: + + development: &development + AccountID: (any 6 digit number for dev) + RequesterID: (any string for dev) + PassPhrase: (any string for dev) + Test: YES + + test: + <<: *development + + production: + AccountID: (6 digits) + RequesterID (string--you have to request this from Endicia) + PassPhrase (string) + Test: NO + Additionally, you may want to set a few defaults like your address: - FromCompany - FromCity - FromState - FromPostalCode - ReturnCompany - ReturnCity - ReturnState - ReturnCode -You can also specify any of the options that Endicia allows for 'GetPostageLabel' as defaults. Most of them you'll want to pass in, but you might find it convenient to switch the default image format globally, for instance. Any options you pass in directly will override that defaults in the config file. + FromCompany: + FromCity: + FromState: + FromPostalCode: + ReturnCompany: + ReturnCity: + ReturnState: + ReturnCode: + +You can also specify any of the options that Endicia allows for +'GetPostageLabel' as defaults. Most of them you'll want to pass in, but you +might find it convenient to switch the default image format globally, for +instance. Any options you pass in directly will override that defaults in the +config file. + +Outside of Rails, you will need to pass these options to Endicia.get_label. Cheers! == Usage -There's only one method--_get_label_. -Ex. Endicia.get_label({:ToPostalCode => RECIPIENT ZIP, :ToAddress1 => RECIPIENT ADDRESS, :ToCity => RECIPIENT CITY, :ToState => RECIPIENT STATE, :PartnerTransactionID => GENERALLY THE ORDER (OR SOME MODEL) ID, :PartnerCustomerID => GENERALLY CUSTOMER/USER ID, :MailClass => SHIPPING METHOD, :WeightOz => INTEGER WEIGHT}) + label = Endicia.get_label({ + :ToPostalCode => RECIPIENT ZIP, + :ToAddress1 => RECIPIENT ADDRESS, + :ToCity => RECIPIENT CITY, + :ToState => RECIPIENT STATE, + :PartnerTransactionID => GENERALLY THE ORDER (OR SOME MODEL) ID, + :PartnerCustomerID => GENERALLY CUSTOMER/USER ID, + :MailClass => SHIPPING METHOD, + :WeightOz => INTEGER WEIGHT + }) == Contributing to endicia - + * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it * Fork the project @@ -43,4 +70,3 @@ Ex. Endicia.get_label({:ToPostalCode => RECIPIENT ZIP, :ToAddress1 => RECIPIENT Copyright (c) 2011 Mark Dickson. See LICENSE.txt for further details. - diff --git a/Rakefile b/Rakefile index 6a9cc33..f67d721 100644 --- a/Rakefile +++ b/Rakefile @@ -32,6 +32,17 @@ Rake::TestTask.new(:test) do |test| test.verbose = true end +namespace(:test) do + Rake::TestTask.new(:integration) do |test| + test.libs << 'lib' << 'test' + test.pattern = 'test/integration.rb' + test.verbose = true + end + + desc "Run unit and integration tests" + task(:all => ['test', 'test:integration']) +end + require 'rcov/rcovtask' Rcov::RcovTask.new do |test| test.libs << 'test' diff --git a/VERSION b/VERSION index 341cf11..0c62199 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.0 \ No newline at end of file +0.2.1 diff --git a/lib/endicia.rb b/lib/endicia.rb index 1c49e2d..8f36879 100644 --- a/lib/endicia.rb +++ b/lib/endicia.rb @@ -1,52 +1,464 @@ require 'rubygems' require 'httparty' +require 'active_support/core_ext' +require 'builder' +require 'uri' + +require 'endicia/label' +require 'endicia/rails_helper' + +# Hack fix because Endicia sends response back without protocol in xmlns uri +module HTTParty + class Request + alias_method :parse_response_without_hack, :parse_response + def parse_response(body) + Rails.logger.info("RESPONSE>") + Rails.logger.info(body) + Rails.logger.info("884 Railroad Street, Suite CYpsilantiMI48197YpsilantiMIVGKids481971237 Elbridge StYpsilantiMI12371212MediaMailYESpoopants792190whiplash110 + # 884 Railroad Street, Suite CYpsilantiMI48197YpsilantiMIVGKids481971237 Elbridge StYpsilantiMI12371212MediaMailYESpoopants792190whiplash110 + # Request a shipping label. + # + # Accepts a hash of options in the form: + # { :NodeOrAttributeName => "value", ... } + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 3-1 + # for available options. + # + # Note: options should be specified in a "flat" hash, they should not be + # formated to fit the nesting of the XML. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a Endicia::Label object. def self.get_label(opts={}) - body = "labelRequestXML=" + @defaults.merge(opts).to_xml(:skip_instruct => true, :skip_types => true, :root => 'LabelRequest', :indent => 0) - result = self.post("https://www.envmgr.com/LabelService/EwsLabelService.asmx/GetPostageLabelXML", :body => body) - return Endicia::Label.new(result["LabelRequestResponse"]) - end - - class Label - attr_accessor :image, - :status, - :tracking_number, - :final_postage, - :transaction_date_time, - :transaction_id, - :postmark_date, - :postage_balance, - :pic, - :error_message - def initialize(data) - data.each do |k, v| - k = "image" if k == 'Base64LabelImage' - send(:"#{k.tableize.singularize}=", v) if !k['xmlns'] + opts = defaults.merge(opts) + opts[:Test] ||= "NO" + url = "#{label_service_url(opts)}/GetPostageLabelXML" + insurance = extract_insurance(opts) + handle_extended_zip_code(opts) + + root_keys = :LabelType, :Test, :LabelSize, :ImageFormat, :ImageResolution + root_attributes = extract(opts, root_keys) + root_attributes[:LabelType] ||= "Default" + + dimension_keys = :Length, :Width, :Height + mailpiece_dimenions = extract(opts, dimension_keys) + + xml = Builder::XmlMarkup.new + body = "labelRequestXML=" + xml.LabelRequest(root_attributes) do |xm| + opts.each { |key, value| xm.tag!(key, value) } + xm.Services({ :InsuredMail => insurance }) if insurance + unless mailpiece_dimenions.empty? + xm.MailpieceDimensions do |md| + mailpiece_dimenions.each { |key, value| md.tag!(key, value) } + end + end + end + + result = self.post(url, :body => body) + Endicia::Label.new(result).tap do |the_label| + the_label.request_body = body.to_s + the_label.request_url = url + end + end + + # Change your account pass phrase. This is a required step to move to + # production use after requesting an account. + # + # Accepts the new phrase and a hash of options in the form: + # + # { :Name => "value", ... } + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 5-1 + # for available/required options. + # + # Note: options should be specified in a "flat" hash, they should not be + # formated to fit the nesting of the XML. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a hash in the form: + # + # { + # :success => true, # or false + # :error_message => "the message", # or nil + # :response_body => "the response body" + # } + def self.change_pass_phrase(new_phrase, options = {}) + xml = Builder::XmlMarkup.new + body = "changePassPhraseRequestXML=" + xml.ChangePassPhraseRequest do |xml| + authorize_request(xml, options) + xml.NewPassPhrase new_phrase + xml.RequestID "CPP#{Time.now.to_f}" + end + + url = "#{label_service_url(options)}/ChangePassPhraseXML" + result = self.post(url, { :body => body }) + parse_result(result, "ChangePassPhraseRequestResponse") + end + + # Add postage to your account (submit a RecreditRequest). This is a required + # step to move to production use after requesting an account and changing + # your pass phrase. + # + # Accepts the amount (in dollars) and a hash of options in the form: + # + # { :Name => "value", ... } + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 5-1 + # for available/required options. + # + # Note: options should be specified in a "flat" hash, they should not be + # formated to fit the nesting of the XML. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a hash in the form: + # + # { + # :success => true, # or false + # :error_message => "the message", # or nil if no error + # :response_body => "the response body" + # } + def self.buy_postage(amount, options = {}) + xml = Builder::XmlMarkup.new + body = "recreditRequestXML=" + xml.RecreditRequest do |xml| + authorize_request(xml, options) + xml.RecreditAmount amount + xml.RequestID "BP#{Time.now.to_f}" + end + + url = "#{label_service_url(options)}/BuyPostageXML" + result = self.post(url, { :body => body }) + parse_result(result, "RecreditRequestResponse") + end + + # Given a tracking number, return a status message for the shipment. + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 12-1 + # for available/required options. + # + # Note: options should be specified in a "flat" hash, they should not be + # formated to fit the nesting of the XML. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a hash in the form: + # + # { + # :success => true, # or false + # :error_message => "the message", # or nil if no error + # :status => "the package status", # or nil if error + # :response_body => "the response body" + # } + def self.status_request(tracking_number, options = {}) + xml = Builder::XmlMarkup.new.StatusRequest do |xml| + xml.AccountID(options[:AccountID] || defaults[:AccountID]) + xml.PassPhrase(options[:PassPhrase] || defaults[:PassPhrase]) + xml.Test(options[:Test] || defaults[:Test] || "NO") + xml.FullStatus(options[:FullStatus] || defaults[:FullStatus] || '') + xml.StatusList { |xml| xml.PICNumber(tracking_number) } + end + + if options[:logger] + options[:logger].info("ENDICIA REQUEST: #{tracking_number}") + options[:logger].info("\n[REQUEST]") + options[:logger].info(xml) + options[:logger].info("[ENDREQUEST]") + end + + params = { :method => 'StatusRequest', :XMLInput => URI.encode(xml) } + result = self.get(els_service_url(params)) + response_body = result.body + response_body.gsub!(/[^<]*/, "") + response = { + :success => false, + :error_message => nil, + :status => nil, + :response_body => response_body + } + + if options[:logger] + options[:logger].info("\n[RESPONSE]") + options[:logger].info(xml) + options[:logger].info("[ENDRESPONSE]") + end + + # TODO: It is possible to make a batch status request, currently this only + # supports one at a time. The response that comes back is not parsed + # well by HTTParty. So we have to assume there is only one tracking + # number in order to parse it with a regex. + + if result && result = result['StatusResponse'] + unless response[:error_message] = result['ErrorMsg'] + response[:status] = response_body.match(/(.+)<\/Status>/)[1] + status_code = response_body.match(/(.+)<\/StatusCode>/)[1] + response[:success] = (status_code.to_s != '-1') + end + end + + response + end + + # Given a tracking number, try and void the label generated in a previous call + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 11-1 + # for available/required options. + # + # Note: options should be specified in a "flat" hash, they should not be + # formated to fit the nesting of the XML. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a hash in the form: + # + # { + # :success => true, # or false + # :error_message => "the message", # message describing success + # # or failure + # :form_number => 12345, # Form Number for refunded label + # :response_body => "the response body" + # } + def self.refund_request(tracking_number, options = {}) + xml = Builder::XmlMarkup.new.RefundRequest do |xml| + xml.AccountID(options[:AccountID] || defaults[:AccountID]) + xml.PassPhrase(options[:PassPhrase] || defaults[:PassPhrase]) + xml.Test(options[:Test] || defaults[:Test] || "NO") + xml.RefundList { |xml| xml.PICNumber(tracking_number) } + end + + params = { :method => 'RefundRequest', :XMLInput => URI.encode(xml) } + result = self.get(els_service_url(params)) + + response = { + :success => false, + :error_message => nil, + :response_body => result.body + } + + # TODO: It is possible to make a batch refund request, currently this only + # supports one at a time. The response that comes back is not parsed + # well by HTTParty. So we have to assume there is only one IsApproved + # and ErrorMsg in order to return them + if result && result = result['RefundResponse'] + unless response[:error_message] = result['ErrorMsg'] + response[:form_number] = result['FormNumber'] + + result = result['RefundList']['PICNumber'] + response[:success] = (result.match(/YES<\/IsApproved>/) ? true : false) + response[:error_message] = result.match(/(.+)<\/ErrorMsg>/)[1] + end + end + + response + end + + + # Given a tracking number and package location code, + # return a carrier pickup confirmation. + # + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf Table 15-1 + # for available/required options, and package location codes. + # + # If you are using rails, any applicable options specified in + # config/endicia.yml will be used as defaults. For example: + # + # development: + # Test: YES + # AccountID: 123 + # ... + # + # Returns a hash in the form: + # + # { + # :success => true, # or false + # :error_message => "the message", # or nil if no error message + # :error_code => "usps error code", # or nil if no error + # :error_description => "usps error description", # or nil if no error + # :day_of_week => "pickup day of week (ex: Monday)", + # :date => "xx/xx/xxxx", # date of pickup, + # :confirmation_number => "confirmation number of the pickup", # save this! + # :response_body => "the response body" + # } + def self.carrier_pickup_request(tracking_number, package_location, options = {}) + xml = Builder::XmlMarkup.new.CarrierPickupRequest do |xml| + xml.AccountID(options.delete(:AccountID) || defaults[:AccountID]) + xml.PassPhrase(options.delete(:PassPhrase) || defaults[:PassPhrase]) + xml.Test(options.delete(:Test) || defaults[:Test] || "NO") + xml.PackageLocation(package_location) + xml.PickupList { |xml| xml.PICNumber(tracking_number) } + options.each { |key, value| xml.tag!(key, value) } + end + + params = { :method => 'CarrierPickupRequest', :XMLInput => URI.encode(xml) } + result = self.get(els_service_url(params)) + + response = { + :success => false, + :response_body => result.body + } + + # TODO: this is some nasty logic... + if result && result = result["CarrierPickupRequestResponse"] + unless response[:error_message] = result['ErrorMsg'] + if result = result["Response"] + if error = result.delete("Error") + response[:error_code] = error["Number"] + response[:error_description] = error["Description"] + else + response[:success] = true + end + result.each { |key, value| response[key.underscore.to_sym] = value } + end + end + end + + response + end + + private + + def self.extract(hash, keys) + {}.tap do |return_hash| + keys.each do |key| + value = return_hash[key] = hash.delete(key) + return_hash.delete(key) if value.nil? || value.empty? + end + end + end + + # Given a builder object, add the auth nodes required for many api calls. + # Will pull values from options hash or defaults if not found. + def self.authorize_request(xml_builder, options = {}) + requester_id = options[:RequesterID] || defaults[:RequesterID] + account_id = options[:AccountID] || defaults[:AccountID] + pass_phrase = options[:PassPhrase] || defaults[:PassPhrase] + + xml_builder.RequesterID requester_id + xml_builder.CertifiedIntermediary do |xml_builder| + xml_builder.AccountID account_id + xml_builder.PassPhrase pass_phrase + end + end + + # Return the url for making requests. + # Pass options hash with :Test => "YES" to return the url of the test server + # (this matches the Test attribute/node value for most API calls). + def self.label_service_url(options = {}) + test = (options[:Test] || defaults[:Test] || "NO").upcase == "YES" + url = test ? "https://www.envmgr.com" : "https://LabelServer.Endicia.com" + "#{url}/LabelService/EwsLabelService.asmx" + end + + # Some requests use the ELS service url. This URL is used for requests that + # can accept GET, and have params passed via URL instead of a POST body. + # Pass a hash of params to have them converted to a &key=value string and + # appended to the URL. + def self.els_service_url(params = {}) + params = params.to_a.map { |i| "#{i[0]}=#{i[1]}"}.join('&') + "http://www.endicia.com/ELS/ELSServices.cfc?wsdl&#{params}" + end + + def self.defaults + if rails? && @defaults.nil? + config_file = File.join(rails_root, 'config', 'endicia.yml') + if File.exist?(config_file) + @defaults = YAML.load_file(config_file)[rails_env].symbolize_keys + end + end + + @defaults || {} + end + + def self.parse_result(result, root) + parsed_result = { + :success => false, + :error_message => nil, + :response_body => result.body + } + + if result && result[root] + root = result[root] + parsed_result[:error_message] = root["ErrorMessage"] + parsed_result[:success] = root["Status"] && root["Status"].to_s == "0" + end + + parsed_result + end + + # Handle special case where jewelry can't have insurance if sent to certain zips + def self.extract_insurance(opts) + jewelry = opts.delete(:Jewelry) + opts.delete(:InsuredMail).tap do |insurance| + if insurance && insurance == "Endicia" && jewelry + if JEWELRY_INSURANCE_EXCLUDED_ZIPS.include? opts[:ToPostalCode] + raise InsuranceError, "Can't ship jewelry with insurance to #{opts[:ToPostalCode]}" + end end end end + + def self.handle_extended_zip_code(opts) + if m = /([0-9]{5})-([0-9]{4})/.match(opts[:ToPostalCode]) + opts[:ToPostalCode] = m[1] + opts[:ToZIP4] = m[2] + end + end end diff --git a/lib/endicia/label.rb b/lib/endicia/label.rb new file mode 100644 index 0000000..fd62c36 --- /dev/null +++ b/lib/endicia/label.rb @@ -0,0 +1,34 @@ +module Endicia + class Label + attr_accessor :image, + :status, + :tracking_number, + :final_postage, + :transaction_date_time, + :transaction_id, + :postmark_date, + :postage_balance, + :pic, + :error_message, + :reference_id, + :cost_center, + :request_body, + :request_url, + :response_body + def initialize(result) + self.response_body = filter_response_body(result.body.dup) + data = result["LabelRequestResponse"] || {} + data.each do |k, v| + k = "image" if k == 'Base64LabelImage' + send(:"#{k.tableize.singularize}=", v) if !k['xmlns'] + end + end + + private + def filter_response_body(string) + # Strip image data for readability: + string.sub(/.+<\/Base64LabelImage>/, + "[data]") + end + end +end diff --git a/lib/endicia/rails_helper.rb b/lib/endicia/rails_helper.rb new file mode 100644 index 0000000..decae99 --- /dev/null +++ b/lib/endicia/rails_helper.rb @@ -0,0 +1,21 @@ +module Endicia + module RailsHelper + private + + def rails? + defined?(Rails) || defined?(RAILS_ROOT) + end + + def rails_root + if rails? + defined?(Rails.root) ? Rails.root : RAILS_ROOT + end + end + + def rails_env + if rails? + defined?(Rails.env) ? Rails.env : ENV['RAILS_ENV'] + end + end + end +end diff --git a/test/helper.rb b/test/helper.rb index dd8a565..866d79c 100644 --- a/test/helper.rb +++ b/test/helper.rb @@ -9,6 +9,7 @@ end require 'test/unit' require 'shoulda' +require 'mocha' $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) diff --git a/test/images/README b/test/images/README new file mode 100644 index 0000000..15d01d0 --- /dev/null +++ b/test/images/README @@ -0,0 +1 @@ +Integration tests will generate sample labels and save them here. \ No newline at end of file diff --git a/test/integration.rb b/test/integration.rb new file mode 100644 index 0000000..bf9829e --- /dev/null +++ b/test/integration.rb @@ -0,0 +1,101 @@ +require 'helper' +require 'base64' + +class IntegrationTest < Test::Unit::TestCase + def self.label_request_options + { + :Test => "YES", + :AccountID => "123456", + :RequesterID => "abc123", + :PassPhrase => "abc123", + :PartnerCustomerID => "abc123", + :PartnerTransactionID => "abc123", + :MailClass => "First", + :WeightOz => 1, + :LabelSize => "4x6", + :ImageFormat => "PNG", + :FromCompany => "Acquisitions, Inc.", + :ReturnAddress1 => "123 Fake Street", + :FromCity => "Orlando", + :FromState => "FL", + :FromPostalCode => "32862", + :ToAddress1 => "123 Fake Street", + :ToCity => "Austin", + :ToState => "TX", + :ToPostalCode => "78723" + } + end + + def self.should_generate_label_from(description, options) + should "return an Endicia::Label object given #{description}" do + result = Endicia.get_label(options) + assert result.is_a?(Endicia::Label) + end + + should "not result in an error message given #{description}" do + result = Endicia.get_label(options) + assert_nil result.error_message + end + + should "result in a status of 0 (success) given #{description}" do + result = Endicia.get_label(options) + assert_equal '0', result.status + end + + should "get back a reasonable amount of image data given #{description}" do + result = Endicia.get_label(options) + assert result.image.length > 1024 + end + end + + def self.save_sample_label_to(filename, options) + result = Endicia.get_label(options) + sample = File.expand_path("images/#{filename}", File.dirname(__FILE__)) + File.open(sample, 'w') { |f| f.write(Base64.decode64(result.image)) } + if ENV['SHOW_SAMPLE_PATHS'] + puts "\n=============================================================" + puts " Saved example shipping label at:\n #{sample} " + puts "=============================================================" + end + end + + context 'calling .get_label' do + should_generate_label_from("required options", label_request_options) + save_sample_label_to("label.png", label_request_options) + + %w(OFF ON UspsOnline Endicia).each do |value| + options = label_request_options.merge({ + :InsuredMail => value, + :InsuredValue => "1.00" + }) + + should_generate_label_from("options with insurance value of #{value}", options) + save_sample_label_to("sample-insurance-#{value}.png", options) + end + + should_generate_label_from("a 9-digit zip code", + label_request_options.merge(:ToPostalCode => "78723-5374")) + end + + context 'calling .carrier_pickup_request with valid options' do + should "be successful" do + result = Endicia.carrier_pickup_request("abc123", "sd", { + :AccountID => "123456", + :PassPhrase => "abc123", + :Test => "YES" + }) + + assert result + assert result[:success] + + assert_not_nil result[:day_of_week] + assert_not_nil result[:date] + assert_not_nil result[:confirmation_number] + assert_not_nil result[:response_body] + + assert_nil result[:error_message] + assert_nil result[:error_code] + assert_nil result[:error_description] + end + end +end diff --git a/test/test_endicia.rb b/test/test_endicia.rb index 40ea37c..18006c0 100644 --- a/test/test_endicia.rb +++ b/test/test_endicia.rb @@ -1,7 +1,899 @@ require 'helper' +require 'base64' +require 'ostruct' +require 'nokogiri' + +module TestEndiciaHelper + def fake_response(hash = {}, body = "") + hash.stubs(:body).returns(body) + hash + end + + def expect_label_request_attribute(key, value, returns = fake_response) + Endicia.expects(:post).with do |request_url, options| + doc = Nokogiri::XML(options[:body].sub("labelRequestXML=", "")) + !doc.css("LabelRequest[#{key}='#{value}']").empty? + end.returns(returns) + end + + def expect_label_request_body_with(&block) + Endicia.expects(:post).with do |url, options| + doc = Nokogiri::XML(options[:body].sub("labelRequestXML=", "")) + block.call(doc) + end.returns(fake_response) + end + + def expect_request_url(url) + Endicia.expects(:post).with do |request_url, options| + request_url == url + end.returns(fake_response) + end + + def assert_label_request_attributes(key, values) + values.each do |value| + expect_label_request_attribute(key, value) + Endicia.get_label(key.to_sym => value) + end + end + + def with_rails_endicia_config(attrs) + Endicia.instance_variable_set(:@defaults, nil) + + Endicia.stubs(:rails?).returns(true) + Endicia.stubs(:rails_root).returns("/project/root") + Endicia.stubs(:rails_env).returns("development") + + config = { "development" => attrs } + config_path = "/project/root/config/endicia.yml" + + File.stubs(:exist?).with(config_path).returns(true) + YAML.stubs(:load_file).with(config_path).returns(config) + + yield + + Endicia.instance_variable_set(:@defaults, nil) + end + + def the_production_server_url(req_path) + "https://LabelServer.Endicia.com/LabelService/EwsLabelService.asmx/#{req_path}" + end + + # Don't call this "test_server_url" or ruby will try to run it as a test. + def the_test_server_url(req_path) + "https://www.envmgr.com/LabelService/EwsLabelService.asmx/#{req_path}" + end +end class TestEndicia < Test::Unit::TestCase - should "probably rename this file and start testing for real" do - flunk "hey buddy, you should probably rename this file and start testing for real" + include TestEndiciaHelper + + context '.get_label' do + should "use test server url if :Test option is YES" do + expect_request_url(the_test_server_url("GetPostageLabelXML")) + Endicia.get_label(:Test => "YES") + end + + should "use production server url if :Test option is NO" do + expect_request_url(the_production_server_url("GetPostageLabelXML")) + Endicia.get_label(:Test => "NO") + end + + should "use production server url if passed no :Test option" do + expect_request_url(the_production_server_url("GetPostageLabelXML")) + Endicia.get_label + end + + should "break 9-digit zip codes into two fields" do + expect_label_request_body_with do |doc| + doc.at_css("LabelRequest > ToPostalCode").content == "12345" + doc.at_css("LabelRequest > ToZIP4").content == "6789" + end + + Endicia.get_label(:ToPostalCode => "12345-6789") + end + + should "send insurance option to endicia" do + %w(OFF ON UspsOnline Endicia).each do |value| + expect_label_request_body_with do |doc| + !doc.css("LabelRequest > Services[InsuredMail=#{value}]").empty? + end + Endicia.get_label({ :InsuredMail => value }) + end + end + + should "raise if requesting insurance for jewelry to an excluded zip" do + %w(10036 10017 94102 94108).each do |zip| + options = { :InsuredMail => "Endicia", :ToPostalCode => zip, :Jewelry => true } + assert_raise(Endicia::InsuranceError, /#{zip}/) { Endicia.get_label(options) } + end + end + + should "not include Jewelry in request XML" do + options = { :InsuredMail => nil, :Jewelry => true } + expect_label_request_body_with { |doc| doc.xpath("//Jewelry").empty? } + Endicia.get_label(options) + end + + should "return an Endicia::Label object" do + assert_kind_of Endicia::Label, Endicia.get_label + end + + should "save the request body on the returned label object" do + request_body = nil + Endicia.expects(:post).with do |url, params| + request_body = params[:body] + end.returns(fake_response) + + the_label = Endicia.get_label + assert_equal request_body, the_label.request_body + end + + should "save the request url to the returned label object" do + request_url = nil + Endicia.expects(:post).with do |url, params| + request_url = url + end.returns(fake_response) + + the_label = Endicia.get_label + assert_equal request_url, the_label.request_url + end + end + + context 'root node attributes on .get_label request' do + setup do + @request_url = "http://test.com" + Endicia.stubs(:label_service_url).returns(@request_url) + end + + should "pass LabelType option" do + assert_label_request_attributes("LabelType", %w(Express CertifiedMail Priority)) + end + + should "set LabelType attribute to Default by default" do + expect_label_request_attribute("LabelType", "Default") + Endicia.get_label + end + + should "pass Test option" do + assert_label_request_attributes("Test", %w(YES NO)) + end + + should "pass LabelSize option" do + assert_label_request_attributes("LabelSize", %w(4x6 6x4 7x3)) + end + + should "pass ImageFormat option" do + assert_label_request_attributes("ImageFormat", %w(PNG GIFT PDF)) + end + end + + context 'Label' do + setup do + # See https://app.sgizmo.com/users/4508/Endicia_Label_Server.pdf + # Table 3-2: LabelRequestResponse XML Elements + @response = fake_response({ + "LabelRequestResponse" => { + "Status" => 123, + "ErrorMessage" => "If there's an error it would be here", + "Base64LabelImage" => Base64.encode64("the label image"), + "TrackingNumber" => "abc123", + "PIC" => "abcd1234", + "FinalPostage" => 1.2, + "TransactionID" => 1234, + "TransactionDateTime" => "20110102030405", + "CostCenter" => 12345, + "ReferenceID" => "abcde12345", + "PostmarkDate" => "20110102", + "PostageBalance" => 3.4 + } + }) + end + + should "initialize with relevant data from an endicia api response without error" do + assert_nothing_raised { Endicia::Label.new(@response) } + end + + should "include response body" do + @response.stubs(:body).returns("the response body") + the_label = Endicia::Label.new(@response) + assert_equal "the response body", the_label.response_body + end + + should "strip image data from #response_body" do + @response.stubs(:body).returns("twobinary data") + the_label = Endicia::Label.new(@response) + assert_equal "two[data]", the_label.response_body + end + end + + context 'defaults in rails' do + should "load from config/endicia.yml" do + attrs = { + :AccountID => 123, + :RequesterID => "abc", + :PassPhrase => "123", + } + + with_rails_endicia_config(attrs) do + assert_equal attrs, Endicia.defaults + end + end + + should "support root node request attributes" do + attrs = { + :Test => "YES", + :LabelType => "Priority", + :LabelSize => "6x4", + :ImageFormat => "PNG" + } + + with_rails_endicia_config(attrs) do + attrs.each do |key, value| + expect_label_request_attribute(key, value) + Endicia.get_label + end + end + end + end + + context '.change_pass_phrase(new, options)' do + should 'make a ChangePassPhraseRequest call to the Endicia API' do + Endicia.stubs(:label_service_url).returns("http://example.com/api") + Time.any_instance.stubs(:to_f).returns("timestamp") + + Endicia.expects(:post).with do |request_url, options| + request_url == "http://example.com/api/ChangePassPhraseXML" && + options[:body] && + options[:body].match(/changePassPhraseRequestXML=(.+)/) do |match| + doc = Nokogiri::Slop(match[1]) + doc.ChangePassPhraseRequest && + doc.ChangePassPhraseRequest.RequesterID.content == "abcd" && + doc.ChangePassPhraseRequest.RequestID.content == "CPPtimestamp" && + doc.ChangePassPhraseRequest.CertifiedIntermediary.AccountID.content == "123456" && + doc.ChangePassPhraseRequest.CertifiedIntermediary.PassPhrase.content == "oldPassPhrase" && + doc.ChangePassPhraseRequest.NewPassPhrase.content == "newPassPhrase" + end + end.returns(fake_response) + + Endicia.change_pass_phrase("newPassPhrase", { + :PassPhrase => "oldPassPhrase", + :RequesterID => "abcd", + :AccountID => "123456" + }) + end + + should 'use credentials from rails endicia config if present' do + attrs = { + :PassPhrase => "old_phrase", + :RequesterID => "efgh", + :AccountID => "456789" + } + with_rails_endicia_config(attrs) do + Endicia.expects(:post).with do |request_url, options| + options[:body] && + options[:body].match(/changePassPhraseRequestXML=(.+)/) do |match| + doc = Nokogiri::Slop(match[1]) + doc.ChangePassPhraseRequest && + doc.ChangePassPhraseRequest.RequesterID.content == "efgh" && + doc.ChangePassPhraseRequest.CertifiedIntermediary.AccountID.content == "456789" && + doc.ChangePassPhraseRequest.CertifiedIntermediary.PassPhrase.content == "old_phrase" + end + end.returns(fake_response) + + Endicia.change_pass_phrase("new") + end + end + + should 'use test url if passed :Test => YES option' do + expect_request_url(the_test_server_url("ChangePassPhraseXML")) + Endicia.change_pass_phrase("new", { :Test => "YES" }) + end + + should 'use production url if not passed :Test => YES option' do + expect_request_url(the_production_server_url("ChangePassPhraseXML")) + Endicia.change_pass_phrase("new") + end + + should 'use test option from rails endicia config if present' do + attrs = { :Test => "YES" } + with_rails_endicia_config(attrs) do + expect_request_url(the_test_server_url("ChangePassPhraseXML")) + Endicia.change_pass_phrase("new") + end + end + + should "include response body in return hash" do + response = stub_everything("response", :body => "the response body") + Endicia.stubs(:post).returns(response) + result = Endicia.change_pass_phrase("new") + assert_equal "the response body", result[:response_body] + end + + context 'when successful' do + setup do + Endicia.stubs(:post).returns(fake_response({ + "ChangePassPhraseRequestResponse" => { "Status" => "0" } + })) + end + + should 'return hash with :success => true' do + result = Endicia.change_pass_phrase("new_phrase") + assert result[:success], "result[:success] should be true but it's #{result[:success].inspect}" + end + end + + context 'when not successful' do + setup do + Endicia.stubs(:post).returns(fake_response({ + "ChangePassPhraseRequestResponse" => { + "Status" => "1", "ErrorMessage" => "the error message" } + })) + end + + should 'return hash with :success => false' do + result = Endicia.change_pass_phrase("new_phrase") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'return hash with an :error_message' do + result = Endicia.change_pass_phrase("new_phrase") + assert_equal "the error message", result[:error_message] + end + end + end + + context '.buy_postage(amount)' do + should 'make a BuyPostage call to the Endicia API' do + Endicia.stubs(:label_service_url).returns("http://example.com/api") + Time.any_instance.stubs(:to_f).returns("timestamp") + + Endicia.expects(:post).with do |request_url, options| + request_url == "http://example.com/api/BuyPostageXML" && + options[:body] && + options[:body].match(/recreditRequestXML=(.+)/) do |match| + doc = Nokogiri::Slop(match[1]) + doc.RecreditRequest && + doc.RecreditRequest.RequesterID.content == "abcd" && + doc.RecreditRequest.RequestID.content == "BPtimestamp" && + doc.RecreditRequest.CertifiedIntermediary.AccountID.content == "123456" && + doc.RecreditRequest.CertifiedIntermediary.PassPhrase.content == "PassPhrase" && + doc.RecreditRequest.RecreditAmount.content == "125.99" + end + end.returns(fake_response) + + Endicia.buy_postage("125.99", { + :PassPhrase => "PassPhrase", + :RequesterID => "abcd", + :AccountID => "123456" + }) + end + + should 'use credentials from rails endicia config if present' do + attrs = { + :PassPhrase => "my_phrase", + :RequesterID => "efgh", + :AccountID => "456789" + } + with_rails_endicia_config(attrs) do + Endicia.expects(:post).with do |request_url, options| + options[:body] && + options[:body].match(/recreditRequestXML=(.+)/) do |match| + doc = Nokogiri::Slop(match[1]) + doc.RecreditRequest && + doc.RecreditRequest.RequesterID.content == "efgh" && + doc.RecreditRequest.CertifiedIntermediary.AccountID.content == "456789" && + doc.RecreditRequest.CertifiedIntermediary.PassPhrase.content == "my_phrase" + end + end.returns(fake_response) + + Endicia.buy_postage("100") + end + end + + should 'use test url if passed :Test => YES option' do + expect_request_url(the_test_server_url("BuyPostageXML")) + Endicia.buy_postage("100", { :Test => "YES" }) + end + + should 'use production url if not passed :Test => YES option' do + expect_request_url(the_production_server_url("BuyPostageXML")) + Endicia.buy_postage("100") + end + + should 'use test option from rails endicia config if present' do + attrs = { :Test => "YES" } + with_rails_endicia_config(attrs) do + expect_request_url(the_test_server_url("BuyPostageXML")) + Endicia.buy_postage("100") + end + end + + should "include response body in return hash" do + response = stub_everything("response", :body => "the response body") + Endicia.stubs(:post).returns(response) + result = Endicia.buy_postage("100") + assert_equal "the response body", result[:response_body] + end + + context 'when successful' do + setup do + Endicia.stubs(:post).returns(fake_response({ + "RecreditRequestResponse" => { "Status" => "0" } + })) + end + + should 'return hash with :success => true' do + result = Endicia.buy_postage("100") + assert result[:success], "result[:success] should be true but it's #{result[:success].inspect}" + end + end + + context 'when not successful' do + setup do + Endicia.stubs(:post).returns(fake_response({ + "RecreditRequestResponse" => { + "Status" => "1", "ErrorMessage" => "the error message" } + })) + end + + should 'return hash with :success => false' do + result = Endicia.buy_postage("100") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'return hash with an :error_message' do + result = Endicia.buy_postage("100") + assert_equal "the error message", result[:error_message] + end + end + end + + context '.status_request(tracking_number, options)' do + should 'make a StatusRequest call to the Endicia API' do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=StatusRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.StatusRequest && + doc.StatusRequest.AccountID.content == "123456" && + doc.StatusRequest.PassPhrase.content == "PassPhrase" && + doc.StatusRequest.Test.content == "YES" && + doc.StatusRequest.StatusList.PICNumber.content == "the tracking number" + end + end.returns(fake_response) + + Endicia.status_request("the tracking number", { + :AccountID => "123456", + :PassPhrase => "PassPhrase", + :Test => "YES" + }) + end + + should 'use options from rails endicia config if present' do + attrs = { + :PassPhrase => "my_phrase", + :AccountID => "456789", + :Test => "YES" + } + + with_rails_endicia_config(attrs) do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=StatusRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.StatusRequest.Test.content == "YES" && + doc.StatusRequest.AccountID.content == "456789" && + doc.StatusRequest.PassPhrase.content == "my_phrase" + end + end.returns(fake_response) + Endicia.status_request("the tracking number") + end + end + + should "include response body in return hash" do + response = stub_everything("response", :body => "the response body") + Endicia.stubs(:get).returns(response) + result = Endicia.status_request("the tracking number") + assert_equal "the response body", result[:response_body] + end + + should "allow you to request full status" do + response_body = "" + <<-EOXML + + 987654 + + + 1234567890987654321234 + Your item was delivered at 11:06 AM on 01/14/2012 in WANTHAM MA 02492. + + Out for Delivery January 14 2012 9:07 am WANTHAM MA 02492 + Sorting Complete January 14 2012 8:57 am WANTHAM MA 02492 + Arrival at Post Office January 14 2012 8:27 am WANTHAM MA 02492 + Processed at USPS Origin Sort Facility January 13 2012 12:19 am NASHUA NH 03063 + Dispatched to Sort Facility January 12 2012 5:02 pm KILLINGTON VT 05751 + Acceptance January 12 2012 3:58 pm KILLINGTON VT 05751 + Electronic Shipping Info Received January 10 2012 + + D + + + + EOXML + response = stub_everything("response", :body => HTTParty.parse_response(response_body)) + Endicia.stubs(:get).returns(response) + result = Endicia.status_request("1234567890987654321234") + result[:success].should == true + result[:status].should == "Your item was delivered at 11:06 AM on 01/14/2012 in WANTHAM MA 02492." + result[:status_code].should == "D" + end + + context 'when successful' do + setup do + Endicia.stubs(:get).returns(fake_response( + { "StatusResponse" => {} }, + "the status message + A")) + end + + should 'include :success => true in returned hash' do + result = Endicia.status_request("the tracking number") + assert result[:success], "result[:success] should be true but it's #{result[:success].inspect}" + end + + should 'include status message in returned hash' do + result = Endicia.status_request("the tracking number") + assert_equal "the status message", result[:status] + end + end + + context 'when not successful' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "StatusResponse" => { + "ErrorMsg" => "I played your man and he died." + } + })) + end + + should 'include :success => false in the returned hash' do + result = Endicia.status_request("the tracking number") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include error message in the returned hash' do + result = Endicia.status_request("the tracking number") + assert_equal "I played your man and he died.", result[:error_message] + end + end + + context 'when tracking code is not found' do + setup do + Endicia.stubs(:get).returns(fake_response( + { "StatusResponse" => {} }, + "not found + -1")) + end + + should 'include :success => false in the returned hash' do + result = Endicia.status_request("the tracking number") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include status message in returned hash' do + result = Endicia.status_request("the tracking number") + assert_equal "not found", result[:status] + end + end + end + + context '.refund_request(tracking_number, options)' do + should 'make a RefundRequest call to the Endicia API' do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=RefundRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.RefundRequest && + doc.RefundRequest.AccountID.content == "123456" && + doc.RefundRequest.PassPhrase.content == "PassPhrase" && + doc.RefundRequest.Test.content == "YES" && + doc.RefundRequest.RefundList.PICNumber.content == "the tracking number" + end + end.returns(fake_response) + + Endicia.refund_request("the tracking number", { + :AccountID => "123456", + :PassPhrase => "PassPhrase", + :Test => "YES" + }) + end + + should 'use options from rails endicia config if present' do + attrs = { + :PassPhrase => "my_phrase", + :AccountID => "456789", + :Test => "YES" + } + + with_rails_endicia_config(attrs) do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=RefundRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.RefundRequest.Test.content == "YES" && + doc.RefundRequest.AccountID.content == "456789" && + doc.RefundRequest.PassPhrase.content == "my_phrase" + end + end.returns(fake_response) + Endicia.refund_request("the tracking number") + end + end + + should "include response body in return hash" do + response = stub_everything("response", :body => "the response body") + Endicia.stubs(:get).returns(response) + result = Endicia.refund_request("the tracking number") + assert_equal "the response body", result[:response_body] + end + + context 'when successful' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "RefundResponse" => { + "ErrorMsg" => nil, + "FormNumber" => 567890, + "RefundList" => { + "PICNumber" => %Q{ + the tracking number + YES + Approved - Less than 10 days. + } + } + } + })) + end + + should 'include :success => true in returned hash' do + result = Endicia.refund_request("the tracking number") + assert result[:success], "result[:success] should be true but it's #{result[:success].inspect}" + end + + should 'include error_message in response' do + result = Endicia.refund_request("the tracking number") + assert result[:error_message] == 'Approved - Less than 10 days.' + end + + should 'include form_number in response' do + result = Endicia.refund_request("the tracking number") + assert result[:form_number] == 567890 + end + end + + context 'when login not successful' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "RefundResponse" => { + "ErrorMsg" => "I played your man and he died." + } + })) + end + + should 'include :success => false in the returned hash' do + result = Endicia.refund_request("the tracking number") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include error message in the returned hash' do + result = Endicia.refund_request("the tracking number") + assert_equal "I played your man and he died.", result[:error_message] + end + end + + context 'when refund not successful' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "RefundResponse" => { + "ErrorMsg" => nil, + "FormNumber" => nil, + "RefundList" => { + "PICNumber" => %Q{ + the tracking number + NO + Denied - Must be within 10 days. + } + } + } + })) + end + + should 'include :success => false in the returned hash' do + result = Endicia.refund_request("the tracking number") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include error message in the returned hash' do + result = Endicia.refund_request("the tracking number") + assert_equal "Denied - Must be within 10 days.", result[:error_message] + end + end + end + + context '.carrier_pickup_request(tacking_number, package_location, options)' do + should 'make a CarrierPickupRequest call to the Endicia API' do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=CarrierPickupRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.CarrierPickupRequest && + doc.CarrierPickupRequest.AccountID.content == "123456" && + doc.CarrierPickupRequest.PassPhrase.content == "PassPhrase" && + doc.CarrierPickupRequest.Test.content == "YES" && + doc.CarrierPickupRequest.PackageLocation.content == "sd" && + doc.CarrierPickupRequest.PickupList.PICNumber.content == "the tracking number" + end + end.returns(fake_response) + + Endicia.carrier_pickup_request("the tracking number", "sd", { + :AccountID => "123456", + :PassPhrase => "PassPhrase", + :Test => "YES" + }) + end + + should 'accept custom pickup address' do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=CarrierPickupRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.CarrierPickupRequest.UseAddressOnFile.content == "N" && + doc.CarrierPickupRequest.FirstName.content == "Slick" && + doc.CarrierPickupRequest.LastName.content == "Nick" && + doc.CarrierPickupRequest.CompanyName.content == "Hair Product, Inc." && + doc.CarrierPickupRequest.SuiteOrApt.content == "Apt. 123" && + doc.CarrierPickupRequest.Address.content == "123 Fake Street" && + doc.CarrierPickupRequest.City.content == "Orlando" && + doc.CarrierPickupRequest.State.content == "FL" && + doc.CarrierPickupRequest.ZIP5.content == "12345" && + doc.CarrierPickupRequest.ZIP4.content == "1234" && + doc.CarrierPickupRequest.Phone.content == "1234567890" && + doc.CarrierPickupRequest.Extension.content == "12345" + end + end.returns(fake_response) + + Endicia.carrier_pickup_request("the tracking number", "sd", { + :UseAddressOnFile => "N", + :FirstName => "Slick", + :LastName => "Nick", + :CompanyName => "Hair Product, Inc.", + :SuiteOrApt => "Apt. 123", + :Address => "123 Fake Street", + :City => "Orlando", + :State => "FL", + :ZIP5 => "12345", + :ZIP4 => "1234", + :Phone => "1234567890", + :Extension => "12345" + }) + end + + should 'accept custom pickup location' do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=CarrierPickupRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.CarrierPickupRequest.PackageLocation.content == "ot" && + doc.CarrierPickupRequest.SpecialInstructions.content == "the special instructions" + end + end.returns(fake_response) + + Endicia.carrier_pickup_request("the tracking number", "ot", { + :SpecialInstructions => "the special instructions" + }) + end + + should 'use options from rails endicia config if present' do + attrs = { + :PassPhrase => "my_phrase", + :AccountID => "456789", + :Test => "YES" + } + + with_rails_endicia_config(attrs) do + Endicia.expects(:get).with do |els_service_url| + regex = /http.+&method=CarrierPickupRequest&XMLInput=(.+)/ + els_service_url.match(regex) do |match| + doc = Nokogiri::Slop(URI.decode(match[1])) + doc.CarrierPickupRequest.Test.content == "YES" && + doc.CarrierPickupRequest.AccountID.content == "456789" && + doc.CarrierPickupRequest.PassPhrase.content == "my_phrase" + end + end.returns(fake_response) + Endicia.carrier_pickup_request("the tracking number", "sd") + end + end + + should "include response body in return hash" do + response = stub_everything("response", :body => "the response body") + Endicia.stubs(:get).returns(response) + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert_equal "the response body", result[:response_body] + end + + context 'when successful' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "CarrierPickupRequestResponse" => { + "Response" => { + "DayOfWeek" => "Monday", + "Date" => "11/11/2011", + "CarrierRoute" => "C", + "ConfirmationNumber" => "abc123" + } + } + })) + end + + should 'include :success => true in returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert result[:success], "result[:success] should be true but it's #{result[:success].inspect}" + end + + should 'include pickup information in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert_equal "Monday", result[:day_of_week] + assert_equal "11/11/2011", result[:date] + assert_equal "C", result[:carrier_route] + assert_equal "abc123", result[:confirmation_number] + end + end + + context 'when there is an error message' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "CarrierPickupRequestResponse" => { + "ErrorMsg" => "your ego is out of control" + } + })) + end + + should 'include :success => false in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include error message in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert_equal "your ego is out of control", result[:error_message] + end + end + + context 'when there is an error code' do + setup do + Endicia.stubs(:get).returns(fake_response({ + "CarrierPickupRequestResponse" => { + "Response" => { + "Error" => { + "Number" => "123", + "Description" => "OverThere is an invalid package location" + } + } + } + })) + end + + should 'include :success => false in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert !result[:success], "result[:success] should be false but it's #{result[:success].inspect}" + end + + should 'include error code in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert_equal "123", result[:error_code] + end + + should 'include error message in the returned hash' do + result = Endicia.carrier_pickup_request("the tracking number", "sd") + assert_equal "OverThere is an invalid package location", result[:error_description] + end + end end end