peddler 1.6.0 → 1.6.1

Sign up to get free protection for your applications and to get access to all the features.
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