peddler 1.6.0 → 1.6.1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 6930c5791ac57b42110c8f753ec49b9e1916ebf1
4
- data.tar.gz: 79d8ed7d23625bc790f14f1df4f3cf02a5404ff9
3
+ metadata.gz: 4d83b099126be1823bbd9bd9c45ea1d7ec3455f9
4
+ data.tar.gz: 74b18adc957648244ac9b7bf04d60fe3006c4fc2
5
5
  SHA512:
6
- metadata.gz: ef37d5d4d265fe2e73d4ea6b077f59950f706d14e2fe20b0a7ebf8d363265681c9b0339449fe851484c332a3c614f86ddf8fbda833ecd7cc2b58f8b7d678ff1b
7
- data.tar.gz: 030eb49815c4c35e0825a08e1059469195313f2acbaa248bf82d2e3010efafcc31d8c8ddd8b3499a63dff07bc041203ade90795d02faedc5d2bc3fd25cbd368f
6
+ metadata.gz: 7f977ea894c96c6454bfd442bd8f5e866aaf56029c23649d7d133fbca85b4544a894d99e254847a77afaa36460dfb1caf39f0eefb272982739b0c375cf340ba0
7
+ data.tar.gz: 3b7f7bdf62a91254838b412ba634eca896f8a4cb14683ecb92b7e1621c6a7b72d92874e1a1f564abb0529e390ae85c7cf9cf9fb2d399d9ef695d0f888c6358ac
@@ -22,6 +22,7 @@ module MWS
22
22
  def get_eligible_shipping_services(shipment_request_details)
23
23
  operation('GetEligibleShippingServices')
24
24
  .add('ShipmentRequestDetails' => shipment_request_details)
25
+ .structure!('ItemList', 'Item')
25
26
 
26
27
  run
27
28
  end
@@ -42,6 +43,7 @@ module MWS
42
43
  'ShippingServiceId' => shipping_service_id
43
44
  )
44
45
  .add(opts)
46
+ .structure!('ItemList', 'Item')
45
47
 
46
48
  run
47
49
  end
@@ -129,13 +129,7 @@ module Peddler
129
129
 
130
130
  # @!parse attr_writer :body
131
131
  def body=(str)
132
- if str
133
- headers['Content-Type'] = content_type(str)
134
- else
135
- headers.delete('Content-Type')
136
- end
137
-
138
- @body = str
132
+ str ? add_content(str) : clear_content!
139
133
  end
140
134
 
141
135
  # @api private
@@ -169,6 +163,7 @@ module Peddler
169
163
  opts = build_options
170
164
  opts.store(:response_block, Proc.new) if block_given?
171
165
  res = post(opts)
166
+ self.body = nil if res.status == 200
172
167
 
173
168
  parser.new(res, encoding)
174
169
  rescue Excon::Error => e
@@ -181,11 +176,19 @@ module Peddler
181
176
  Marketplace.new(primary_marketplace_id)
182
177
  end
183
178
 
184
- def content_type(str)
185
- if str.start_with?('<?xml')
186
- 'text/xml'
179
+ def clear_content!
180
+ headers.delete('Content-Type')
181
+ @body = nil
182
+ end
183
+
184
+ def add_content(content)
185
+ if content.start_with?('<?xml')
186
+ headers['Content-Type'] = 'text/xml'
187
+ @body = content
187
188
  else
188
- "text/tab-separated-values; charset=#{encoding}"
189
+ headers['Content-Type'] =
190
+ "text/tab-separated-values; charset=#{encoding}"
191
+ @body = content.encode(encoding)
189
192
  end
190
193
  end
191
194
 
@@ -2,7 +2,7 @@ module Peddler
2
2
  # @api private
3
3
  module Errors
4
4
  # Known codes
5
- CODES = %w(
5
+ CODES = %w[
6
6
  AccessDenied
7
7
  InvalidMarketplace
8
8
  InvalidParameterValue
@@ -10,7 +10,7 @@ module Peddler
10
10
  MalformedInput
11
11
  QuotaExceeded
12
12
  RequestThrottled
13
- ).freeze
13
+ ].freeze
14
14
 
15
15
  # @api private
16
16
  class Error < StandardError
@@ -5,6 +5,9 @@ module Peddler
5
5
  module Errors
6
6
  # @api private
7
7
  class Handler
8
+ DIGIT_AS_FIRST_CHAR = /^\d/
9
+ private_constant :DIGIT_AS_FIRST_CHAR
10
+
8
11
  def self.call(exception)
9
12
  new(exception).handle
10
13
  end
@@ -20,26 +23,35 @@ module Peddler
20
23
  end
21
24
 
22
25
  def handle
23
- if http_status_error?
24
- raise error_class.new(exception.response.message, exception)
25
- else
26
- raise exception
27
- end
28
- rescue NameError
29
- raise exception
26
+ raise exception unless http_status_error?
27
+ raise exception if bad_name_for_error_class?
28
+
29
+ raise error_class.new(error_message, exception)
30
30
  end
31
31
 
32
32
  private
33
33
 
34
34
  def error_class
35
- Errors.const_get(exception.response.code)
35
+ Errors.const_get(error_name)
36
36
  rescue NameError
37
- Builder.build(exception.response.code)
37
+ Builder.build(error_name)
38
38
  end
39
39
 
40
40
  def http_status_error?
41
41
  exception.is_a?(::Excon::Error::HTTPStatus)
42
42
  end
43
+
44
+ def bad_name_for_error_class?
45
+ error_name =~ DIGIT_AS_FIRST_CHAR
46
+ end
47
+
48
+ def error_name
49
+ exception.response.code
50
+ end
51
+
52
+ def error_message
53
+ exception.response.message
54
+ end
43
55
  end
44
56
  end
45
57
  end
@@ -19,9 +19,7 @@ module Peddler
19
19
  private
20
20
 
21
21
  def find_data
22
- xml
23
- .fetch('ErrorResponse', {})
24
- .fetch('Error', {})
22
+ xml.dig('ErrorResponse', 'Error')
25
23
  end
26
24
  end
27
25
  end
@@ -11,20 +11,20 @@ module Peddler
11
11
  # http://stackoverflow.com/questions/8073920/importing-csv-quoting-error-is-driving-me-nuts
12
12
  OPTIONS = { col_sep: "\t", quote_char: "\x00", headers: true }.freeze
13
13
 
14
- attr_reader :content, :summary, :encoding
14
+ attr_reader :content, :summary
15
15
 
16
16
  def initialize(res, encoding)
17
17
  super(res)
18
- @encoding = encoding
19
- extract_content
18
+ scrub_body!(encoding)
19
+ extract_content_and_summary
20
20
  end
21
21
 
22
22
  def parse(&blk)
23
- CSV.parse(scrub_content, OPTIONS, &blk) if content
23
+ CSV.parse(content, OPTIONS, &blk) if content
24
24
  end
25
25
 
26
26
  def records_count
27
- summarize if summary?
27
+ summarize if summary
28
28
  end
29
29
 
30
30
  def valid?
@@ -33,22 +33,13 @@ module Peddler
33
33
 
34
34
  private
35
35
 
36
- def extract_content
37
- if summary?
38
- @summary, @content = body.split("\n\n")
39
- else
40
- @content = body.dup
41
- end
36
+ def scrub_body!(encoding)
37
+ body.force_encoding(encoding) unless body.encoding == 'UTF-8'
42
38
  end
43
39
 
44
- def scrub_content
45
- content
46
- .force_encoding(encoding)
47
- .encode('UTF-8', invalid: :replace, undef: :replace, replace: '?')
48
- end
49
-
50
- def summary?
51
- body.include?("\n\n")
40
+ def extract_content_and_summary
41
+ @content = body.encode('UTF-8', invalid: :replace, undef: :replace)
42
+ @summary, @content = @content.split("\n\n") if @content.include?("\n\n")
52
43
  end
53
44
 
54
45
  def summarize
@@ -4,6 +4,8 @@ module Peddler
4
4
  Quota = Struct.new(:max, :remaining, :resets_on)
5
5
 
6
6
  def quota
7
+ return if headers.keys.none? { |key| key.include?('quota') }
8
+
7
9
  Quota.new(
8
10
  headers['x-mws-quota-max'].to_i,
9
11
  headers['x-mws-quota-remaining'].to_i,
@@ -2,13 +2,13 @@ module Peddler
2
2
  # A custom matcher that can be used to record MWS interactions when
3
3
  # integration-testing
4
4
  class VCRMatcher
5
- TRANSIENT_PARAMS = %w(
5
+ TRANSIENT_PARAMS = %w[
6
6
  Signature Timestamp StartDate CreatedAfter QueryStartDateTime
7
- ).freeze
7
+ ].freeze
8
8
 
9
- SELLER_PARAMS = %w(
9
+ SELLER_PARAMS = %w[
10
10
  AWSAccessKeyId SellerId
11
- ).freeze
11
+ ].freeze
12
12
 
13
13
  class << self
14
14
  def call(*requests)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Peddler
3
- VERSION = '1.6.0'.freeze
4
+ VERSION = '1.6.1'.freeze
4
5
  end
data/test/helper.rb CHANGED
@@ -7,6 +7,7 @@ if RUBY_ENGINE == 'ruby'
7
7
  end
8
8
 
9
9
  require 'minitest/autorun'
10
+ require 'minitest/focus'
10
11
  begin
11
12
  require 'pry'
12
13
  rescue LoadError
@@ -17,19 +17,21 @@ class TestFeeds < IntegrationTest
17
17
  end
18
18
 
19
19
  def test_submits_feeds
20
- content = "sku\tprice\tquantity\nwidget\t\t0\n"
21
- type = '_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_'
20
+ feed_content = "sku\tprice\tquantity\nwidget\t\t0\n"
21
+ feed_type = '_POST_FLAT_FILE_PRICEANDQUANTITYONLY_UPDATE_DATA_'
22
22
 
23
23
  clients.each do |client|
24
- res = client.submit_feed(content, type)
25
- id = res.parse['FeedSubmissionInfo']['FeedSubmissionId']
26
- refute_nil id
24
+ res = client.submit_feed(feed_content, feed_type)
25
+ feed_submission_id = res.dig('FeedSubmissionInfo', 'FeedSubmissionId')
26
+ assert feed_submission_id
27
+
28
+ res = client.get_feed_submission_result(feed_submission_id)
29
+ assert res.records_count
27
30
 
28
31
  # Clean up
29
- client.body = nil
30
32
  client.cancel_feed_submissions(
31
- feed_submission_id: id,
32
- feed_type_list: type
33
+ feed_submission_id: feed_submission_id,
34
+ feed_type_list: feed_type
33
35
  )
34
36
  end
35
37
  end
@@ -37,18 +37,12 @@ class TestReports < IntegrationTest
37
37
  end
38
38
  end
39
39
 
40
- def test_gets_report_list
41
- clients.each do |client|
42
- res = client.get_report_list
43
- refute_empty res.parse
44
- end
45
- end
46
-
47
40
  def test_gets_report
48
41
  clients.each do |client|
49
42
  res = client.get_report_list(max_count: 1)
50
43
  id = res.parse['ReportInfo']['ReportId']
51
44
  res = client.get_report(id)
45
+ assert res.valid?
52
46
  refute_empty res.parse || res.records_count
53
47
  end
54
48
  end
@@ -1,7 +1,7 @@
1
1
  require 'helper'
2
2
  require 'recorder'
3
3
 
4
- %w(mws.yml mws.yml.example).each do |path|
4
+ %w[mws.yml mws.yml.example].each do |path|
5
5
  file = File.expand_path("../#{path}", __FILE__)
6
6
  if File.exist?(file)
7
7
  $mws = YAML.load_file(file)
@@ -16,9 +16,7 @@ class IntegrationTest < MiniTest::Test
16
16
 
17
17
  def clients
18
18
  api = @api || test_name
19
- $mws.map do |record|
20
- MWS.const_get("#{api}::Client").new(record)
21
- end
19
+ $mws.map { |record| MWS.const_get("#{api}::Client").new(record) }.shuffle
22
20
  end
23
21
  end
24
22
 
@@ -26,9 +24,9 @@ end
26
24
 
27
25
  VCR.configure do |c|
28
26
  c.before_record do |interaction|
29
- %w(
27
+ %w[
30
28
  BuyerName BuyerEmail Name AddressLine1 PostalCode Phone Amount
31
- ).each do |key|
29
+ ].each do |key|
32
30
  interaction.response.body.gsub!(/<#{key}>[^<]+</, "<#{key}>FILTERED<")
33
31
  end
34
32
  end
@@ -19,7 +19,6 @@ class TestMWSFeedsClient < MiniTest::Test
19
19
  end
20
20
 
21
21
  assert_equal operation, @client.operation
22
- assert_equal 'content', @client.body
23
22
  end
24
23
 
25
24
  def test_gets_feed_submission_list
@@ -267,7 +267,7 @@ class TestMWSFulfillmentInboundShipmentClient < MiniTest::Test
267
267
  @client.get_unique_package_labels(
268
268
  'FBAQFGQZ',
269
269
  'PackageLabel_Letter_6',
270
- %w(CartonA CartonB CartonC CartonD)
270
+ %w[CartonA CartonB CartonC CartonD]
271
271
  )
272
272
  end
273
273
 
@@ -9,14 +9,26 @@ class TestMWSMerchantFulfillmentClient < MiniTest::Test
9
9
  def test_gets_eligible_shipping_services
10
10
  operation = {
11
11
  'Action' => 'GetEligibleShippingServices',
12
- 'ShipmentRequestDetails.Id' => '123',
13
- 'ShipmentRequestDetails.Foo.Bar' => 'baz'
12
+ 'ShipmentRequestDetails.AmazonOrderId' => '123',
13
+ 'ShipmentRequestDetails.Weight.Value' => '10',
14
+ 'ShipmentRequestDetails.Weight.Unit' => 'ounces',
15
+ 'ShipmentRequestDetails.ItemList.Item.1.OrderItemId' => '123',
16
+ 'ShipmentRequestDetails.ItemList.Item.1.Quantity' => '1'
14
17
  }
15
18
 
16
19
  @client.stub(:run, nil) do
17
20
  shipment_request_details = {
18
- id: '123',
19
- foo: { bar: 'baz' }
21
+ amazon_order_id: '123',
22
+ weight: {
23
+ value: '10',
24
+ unit: 'ounces'
25
+ },
26
+ item_list: [
27
+ {
28
+ order_item_id: '123',
29
+ quantity: '1'
30
+ }
31
+ ]
20
32
  }
21
33
  @client.get_eligible_shipping_services(shipment_request_details)
22
34
  end
@@ -27,15 +39,27 @@ class TestMWSMerchantFulfillmentClient < MiniTest::Test
27
39
  def test_creates_shipment
28
40
  operation = {
29
41
  'Action' => 'CreateShipment',
30
- 'ShipmentRequestDetails.Id' => '123',
31
- 'ShipmentRequestDetails.Foo.Bar' => 'baz',
42
+ 'ShipmentRequestDetails.AmazonOrderId' => '123',
43
+ 'ShipmentRequestDetails.Weight.Value' => '10',
44
+ 'ShipmentRequestDetails.Weight.Unit' => 'ounces',
45
+ 'ShipmentRequestDetails.ItemList.Item.1.OrderItemId' => '123',
46
+ 'ShipmentRequestDetails.ItemList.Item.1.Quantity' => '1',
32
47
  'ShippingServiceId' => 'FOO'
33
48
  }
34
49
 
35
50
  @client.stub(:run, nil) do
36
51
  shipment_request_details = {
37
- id: '123',
38
- foo: { bar: 'baz' }
52
+ amazon_order_id: '123',
53
+ weight: {
54
+ value: '10',
55
+ unit: 'ounces'
56
+ },
57
+ item_list: [
58
+ {
59
+ order_item_id: '123',
60
+ quantity: '1'
61
+ }
62
+ ]
39
63
  }
40
64
  @client.create_shipment(shipment_request_details, 'FOO')
41
65
  end
@@ -3,9 +3,9 @@ require 'null_client'
3
3
 
4
4
  class TestPeddlerClient < MiniTest::Test
5
5
  def setup
6
- @body = 'foo'
6
+ @response_body = 'foo'
7
7
  Excon.defaults[:mock] = true
8
- Excon.stub({}, body: @body, status: 200)
8
+ Excon.stub({}, body: @response_body, status: 200)
9
9
 
10
10
  @klass = Class.new(Null::Client)
11
11
  @client = @klass.new
@@ -71,14 +71,14 @@ class TestPeddlerClient < MiniTest::Test
71
71
  assert_equal '123', client.aws_access_key_id
72
72
  end
73
73
 
74
- def test_sets_content_type_header_for_latin_flat_file_body
74
+ def test_sets_content_type_header_for_latin_flat_file
75
75
  @client.body = 'foo'
76
76
  content_type = @client.headers.fetch('Content-Type')
77
77
 
78
78
  assert_equal 'text/tab-separated-values; charset=CP1252', content_type
79
79
  end
80
80
 
81
- def test_sets_content_type_header_for_chinese_flat_file_body
81
+ def test_sets_content_type_header_for_chinese_flat_file
82
82
  @client.primary_marketplace_id = 'AAHKV2X7AFYLW'
83
83
  @client.body = 'foo'
84
84
  content_type = @client.headers.fetch('Content-Type')
@@ -86,7 +86,7 @@ class TestPeddlerClient < MiniTest::Test
86
86
  assert_equal 'text/tab-separated-values; charset=UTF-16', content_type
87
87
  end
88
88
 
89
- def test_sets_content_type_header_for_japanese_flat_file_body
89
+ def test_sets_content_type_header_for_japanese_flat_file
90
90
  @client.primary_marketplace_id = 'A1VC38T7YXB528'
91
91
  @client.body = 'foo'
92
92
  content_type = @client.headers.fetch('Content-Type')
@@ -94,16 +94,46 @@ class TestPeddlerClient < MiniTest::Test
94
94
  assert_equal 'text/tab-separated-values; charset=Windows-31J', content_type
95
95
  end
96
96
 
97
- def test_sets_content_type_header_for_xml_body
97
+ def test_sets_content_type_header_for_xml
98
98
  @client.body = '<?xml version="1.0"?><Foo></Foo>'
99
99
  content_type = @client.headers.fetch('Content-Type')
100
100
 
101
101
  assert_equal 'text/xml', content_type
102
102
  end
103
103
 
104
+ def test_encodes_body_for_latin_flat_file
105
+ @client.body = 'foo'
106
+ assert_equal 'Windows-1252', @client.body.encoding.to_s
107
+ end
108
+
109
+ def test_encodes_body_for_chinese_flat_file
110
+ @client.primary_marketplace_id = 'AAHKV2X7AFYLW'
111
+ @client.body = 'foo'
112
+ assert_equal 'UTF-16', @client.body.encoding.to_s
113
+ end
114
+
115
+ def test_encodes_body_for_japanese_flat_file
116
+ @client.primary_marketplace_id = 'A1VC38T7YXB528'
117
+ @client.body = 'foo'
118
+ assert_equal 'Windows-31J', @client.body.encoding.to_s
119
+ end
120
+
104
121
  def test_runs_a_request
105
122
  res = @client.run
106
- assert_equal @body, res.body
123
+ assert_equal @response_body, res.body
124
+ end
125
+
126
+ def test_clears_body_when_run_succeeds
127
+ @client.body = 'foo'
128
+ @client.run
129
+ assert_nil @client.body
130
+ end
131
+
132
+ def test_does_not_clear_body_when_run_fails
133
+ Excon.stub({}, status: 503)
134
+ @client.body = 'foo'
135
+ assert_raises { @client.run }
136
+ refute_nil @client.body
107
137
  end
108
138
 
109
139
  def test_streams_response
@@ -111,7 +141,7 @@ class TestPeddlerClient < MiniTest::Test
111
141
  streamer = ->(chunk, _, _) { chunks << chunk }
112
142
  @client.run(&streamer)
113
143
 
114
- assert_equal @body, chunks
144
+ assert_equal @response_body, chunks
115
145
  end
116
146
 
117
147
  class Instrumentor
@@ -70,7 +70,7 @@ class TestPeddlerFlatFileParser < MiniTest::Test
70
70
  body = "Foo\n\xFF\n"
71
71
  body.force_encoding('ASCII-8BIT')
72
72
  parser = Peddler::FlatFileParser.new(build_mock_response(body), 'ASCII-8BIT')
73
- assert_equal '?', parser.parse['Foo'][0]
73
+ assert_equal '', parser.parse['Foo'][0]
74
74
  end
75
75
 
76
76
  private
@@ -35,4 +35,9 @@ class TestPeddlerHeaders < MiniTest::Test
35
35
  def test_response_context
36
36
  assert response_context
37
37
  end
38
+
39
+ def test_handles_no_quota
40
+ @headers = {}
41
+ assert_nil quota
42
+ end
38
43
  end
@@ -15,10 +15,10 @@ class TestPeddlerOperation < MiniTest::Test
15
15
  end
16
16
 
17
17
  def test_converts_nested_key_to_structured_list
18
- @operation.store('Foo.Status', [1])
18
+ @operation.store('Foo.Status', [{ 'Baz' => 1 }])
19
19
  @operation.structure!('Status', 'Bar')
20
20
  refute @operation.key?('FooStatus')
21
- assert_equal 1, @operation['Foo.Status.Bar.1']
21
+ assert_equal 1, @operation['Foo.Status.Bar.1.Baz']
22
22
  end
23
23
 
24
24
  def test_store_camelizes_symbol_key
@@ -13,12 +13,12 @@ class TestPeddlerStructuredList < MiniTest::Test
13
13
 
14
14
  def test_builds_a_structured_list_for_an_array_of_values
15
15
  exp = { 'OrderStatus.Status.1' => 'foo', 'OrderStatus.Status.2' => 'bar' }
16
- assert_equal exp, @list.build(%w(foo bar))
16
+ assert_equal exp, @list.build(%w[foo bar])
17
17
  end
18
18
 
19
19
  def test_flattens_nested_arrays_of_values
20
20
  exp = { 'OrderStatus.Status.1' => 'foo', 'OrderStatus.Status.2' => 'bar' }
21
- assert_equal exp, @list.build([%w(foo bar)])
21
+ assert_equal exp, @list.build([%w[foo bar]])
22
22
  end
23
23
 
24
24
  def test_handles_single_key
@@ -31,7 +31,7 @@ class TestPeddlerXMLResponseParser < MiniTest::Test
31
31
  def response(body)
32
32
  OpenStruct.new(
33
33
  body: body,
34
- headers: { 'Content-Type' => 'text/xml', 'Content-Length' => '78' }
34
+ headers: { 'Content-Type' => 'text/xml', 'Content-Length' => body.size }
35
35
  )
36
36
  end
37
37
  end