active_fulfillment 2.1.6 → 2.1.7
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/CHANGELOG +5 -0
- data/lib/active_fulfillment/fulfillment/service.rb +7 -12
- data/lib/active_fulfillment/fulfillment/services.rb +1 -0
- data/lib/active_fulfillment/fulfillment/services/shopify_api.rb +123 -0
- data/lib/active_fulfillment/fulfillment/version.rb +1 -1
- data/test/test_helper.rb +8 -7
- data/test/unit/services/shopify_api_test.rb +134 -0
- metadata +33 -2
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 36a68a21c6b752b4ed78e4d11fb5b4da3e05080a
|
4
|
+
data.tar.gz: 755d255c35294881ac9c1a815836f0adce457876
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4023404d90d16da1415227009f16348767890d6ea2f8799738e29cc801731f3cee43c50fd6c29b743672f4b6c7a99473383cd6c022129e4bf1112bd4838ec2f7
|
7
|
+
data.tar.gz: c15a97a53539e0d597aaa08c1760bcee3fbe751920e9e53ee92b121c824e9bd895a8463059c0e1dabb602624e148d5bdcbaf0db46291a5658612ac8b8612fee2
|
checksums.yaml.gz.sig
CHANGED
Binary file
|
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
CHANGED
@@ -1,5 +1,10 @@
|
|
1
1
|
= ActiveFulfillment CHANGELOG
|
2
2
|
|
3
|
+
== Version 2.1.7
|
4
|
+
|
5
|
+
* Add shopify_api service
|
6
|
+
* Drop Ruby 1.9.3 support
|
7
|
+
|
3
8
|
== Version 2.1.0
|
4
9
|
|
5
10
|
* Added fetch_tracking_data methods which returns tracking_companies and tracking_urls if available in addition to tracking_numbers for each service
|
@@ -5,6 +5,8 @@ module ActiveMerchant
|
|
5
5
|
include RequiresParameters
|
6
6
|
include PostsData
|
7
7
|
|
8
|
+
attr_accessor :logger
|
9
|
+
|
8
10
|
def initialize(options = {})
|
9
11
|
check_test_mode(options)
|
10
12
|
|
@@ -20,12 +22,12 @@ module ActiveMerchant
|
|
20
22
|
@options[:test] || Base.mode == :test
|
21
23
|
end
|
22
24
|
|
23
|
-
|
24
|
-
|
25
|
-
raise NotImplementedError.new("Subclasses must implement")
|
25
|
+
def valid_credentials?
|
26
|
+
true
|
26
27
|
end
|
27
28
|
|
28
|
-
|
29
|
+
# API Requirements for Implementors
|
30
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
29
31
|
raise NotImplementedError.new("Subclasses must implement")
|
30
32
|
end
|
31
33
|
|
@@ -44,15 +46,8 @@ module ActiveMerchant
|
|
44
46
|
raise NotImplementedError.new("Subclasses must implement")
|
45
47
|
end
|
46
48
|
|
47
|
-
def valid_credentials?
|
48
|
-
raise NotImplementedError.new("Subclasses must implement")
|
49
|
-
end
|
50
|
-
|
51
|
-
def test_mode?
|
52
|
-
raise NotImplementedError.new("Subclasses must implement")
|
53
|
-
end
|
54
|
-
|
55
49
|
private
|
50
|
+
|
56
51
|
def check_test_mode(options)
|
57
52
|
if options[:test] and not test_mode?
|
58
53
|
raise ArgumentError, 'Test mode is not supported by this gateway'
|
@@ -0,0 +1,123 @@
|
|
1
|
+
module ActiveMerchant
|
2
|
+
module Fulfillment
|
3
|
+
class ShopifyAPIService < Service
|
4
|
+
|
5
|
+
RESCUABLE_CONNECTION_ERRORS = [
|
6
|
+
Net::ReadTimeout,
|
7
|
+
Net::OpenTimeout,
|
8
|
+
TimeoutError,
|
9
|
+
Errno::ETIMEDOUT,
|
10
|
+
Timeout::Error,
|
11
|
+
IOError,
|
12
|
+
EOFError,
|
13
|
+
SocketError,
|
14
|
+
Errno::ECONNRESET,
|
15
|
+
Errno::ECONNABORTED,
|
16
|
+
Errno::EPIPE,
|
17
|
+
Errno::ECONNREFUSED,
|
18
|
+
Errno::EAGAIN,
|
19
|
+
Errno::EHOSTUNREACH,
|
20
|
+
Errno::ENETUNREACH,
|
21
|
+
Resolv::ResolvError,
|
22
|
+
Net::HTTPBadResponse,
|
23
|
+
Net::HTTPHeaderSyntaxError,
|
24
|
+
Net::ProtocolError,
|
25
|
+
ActiveMerchant::ConnectionError,
|
26
|
+
ActiveMerchant::ResponseError,
|
27
|
+
ActiveMerchant::InvalidResponseError
|
28
|
+
]
|
29
|
+
|
30
|
+
def initialize(options = {})
|
31
|
+
@name = options[:name]
|
32
|
+
@callback_url = options[:callback_url]
|
33
|
+
@format = options[:format]
|
34
|
+
end
|
35
|
+
|
36
|
+
def fulfill(order_id, shipping_address, line_items, options = {})
|
37
|
+
raise NotImplementedError.new("Shopify API Service must listen to fulfillment/create Webhooks")
|
38
|
+
end
|
39
|
+
|
40
|
+
def fetch_stock_levels(options = {})
|
41
|
+
response = send_app_request('fetch_stock', options.delete(:headers), options)
|
42
|
+
if response
|
43
|
+
stock_levels = parse_response(response, 'StockLevels', 'Product', 'Sku', 'Quantity') { |p| p.to_i }
|
44
|
+
Response.new(true, "API stock levels", {:stock_levels => stock_levels})
|
45
|
+
else
|
46
|
+
Response.new(false, "Unable to fetch remote stock levels")
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def fetch_tracking_data(order_ids, options = {})
|
51
|
+
options.merge!({:order_ids => order_ids})
|
52
|
+
response = send_app_request('fetch_tracking_numbers', options.delete(:headers), options)
|
53
|
+
if response
|
54
|
+
tracking_numbers = parse_response(response, 'TrackingNumbers', 'Order', 'ID', 'Tracking') { |o| o }
|
55
|
+
Response.new(true, "API tracking_numbers", {:tracking_numbers => tracking_numbers,
|
56
|
+
:tracking_companies => {},
|
57
|
+
:tracking_urls => {}})
|
58
|
+
else
|
59
|
+
Response.new(false, "Unable to fetch remote tracking numbers #{order_ids.inspect}")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def request_uri(action, data)
|
66
|
+
URI.parse "#{@callback_url}/#{action}.#{@format}?#{data.to_param}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def send_app_request(action, headers, data)
|
70
|
+
uri = request_uri(action, data)
|
71
|
+
|
72
|
+
logger.info "[" + @name.upcase + " APP] Post #{uri}"
|
73
|
+
|
74
|
+
response = nil
|
75
|
+
realtime = Benchmark.realtime do
|
76
|
+
begin
|
77
|
+
Timeout.timeout(20.seconds) do
|
78
|
+
response = ssl_get(uri, headers)
|
79
|
+
end
|
80
|
+
rescue *(RESCUABLE_CONNECTION_ERRORS) => e
|
81
|
+
logger.warn "[#{self}] Error while contacting fulfillment service error =\"#{e.message}\""
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
line = "[" + @name.upcase + "APP] Response from #{uri} --> "
|
86
|
+
line << "#{response} #{"%.4fs" % realtime}"
|
87
|
+
logger.info line
|
88
|
+
|
89
|
+
response
|
90
|
+
end
|
91
|
+
|
92
|
+
def parse_response(response, root, type, key, value)
|
93
|
+
case @format
|
94
|
+
when 'json'
|
95
|
+
response_data = ActiveSupport::JSON.decode(response)
|
96
|
+
return {} unless response_data.is_a?(Hash)
|
97
|
+
response_data[root.underscore] || response_data
|
98
|
+
when 'xml'
|
99
|
+
response_data = {}
|
100
|
+
document = REXML::Document.new(response)
|
101
|
+
document.elements[root].each do |node|
|
102
|
+
if node.name == type
|
103
|
+
response_data[node.elements[key].text] = node.elements[value].text
|
104
|
+
end
|
105
|
+
end
|
106
|
+
response_data
|
107
|
+
end
|
108
|
+
|
109
|
+
rescue ActiveSupport::JSON.parse_error, REXML::ParseException
|
110
|
+
{}
|
111
|
+
end
|
112
|
+
|
113
|
+
def encode_payload(payload, root)
|
114
|
+
case @format
|
115
|
+
when 'json'
|
116
|
+
{root => payload}.to_json
|
117
|
+
when 'xml'
|
118
|
+
payload.to_xml(:root => root)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
data/test/test_helper.rb
CHANGED
@@ -9,6 +9,7 @@ require 'minitest/autorun'
|
|
9
9
|
require 'digest/md5'
|
10
10
|
require 'active_fulfillment'
|
11
11
|
require 'active_utils'
|
12
|
+
require 'timecop'
|
12
13
|
|
13
14
|
require 'mocha/setup'
|
14
15
|
|
@@ -16,35 +17,35 @@ module Test
|
|
16
17
|
module Unit
|
17
18
|
class TestCase < MiniTest::Unit::TestCase
|
18
19
|
include ActiveMerchant::Fulfillment
|
19
|
-
|
20
|
+
|
20
21
|
LOCAL_CREDENTIALS = ENV['HOME'] + '/.active_merchant/fixtures.yml' unless defined?(LOCAL_CREDENTIALS)
|
21
22
|
DEFAULT_CREDENTIALS = File.dirname(__FILE__) + '/fixtures.yml' unless defined?(DEFAULT_CREDENTIALS)
|
22
23
|
|
23
24
|
def all_fixtures
|
24
25
|
@@fixtures ||= load_fixtures
|
25
26
|
end
|
26
|
-
|
27
|
+
|
27
28
|
def fixtures(key)
|
28
29
|
data = all_fixtures[key] || raise(StandardError, "No fixture data was found for '#{key}'")
|
29
|
-
|
30
|
+
|
30
31
|
data.dup
|
31
32
|
end
|
32
|
-
|
33
|
+
|
33
34
|
def load_fixtures
|
34
35
|
file = File.exists?(LOCAL_CREDENTIALS) ? LOCAL_CREDENTIALS : DEFAULT_CREDENTIALS
|
35
36
|
yaml_data = YAML.load(File.read(file))
|
36
37
|
symbolize_keys(yaml_data)
|
37
|
-
|
38
|
+
|
38
39
|
yaml_data
|
39
40
|
end
|
40
41
|
|
41
42
|
def xml_fixture(path) # where path is like 'usps/beverly_hills_to_ottawa_response'
|
42
43
|
open(File.join(File.dirname(__FILE__),'fixtures','xml',"#{path}.xml")) {|f| f.read}
|
43
44
|
end
|
44
|
-
|
45
|
+
|
45
46
|
def symbolize_keys(hash)
|
46
47
|
return unless hash.is_a?(Hash)
|
47
|
-
|
48
|
+
|
48
49
|
hash.symbolize_keys!
|
49
50
|
hash.each{|k,v| symbolize_keys(v)}
|
50
51
|
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ShopifyAPITest < Test::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@service = build_service()
|
7
|
+
@service.logger = stub(:info => nil, :debug => nil, :warn => nil, :error => nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
def test_request_uri_is_correct_when_no_sku_passed
|
11
|
+
Timecop.freeze do
|
12
|
+
timestamp = Time.now.utc.to_i
|
13
|
+
uri = @service.send(:request_uri, 'fetch_stock', {timestamp: timestamp, shop: 'www.snowdevil.ca'})
|
14
|
+
assert_equal "http://supershopifyapptwin.com/fetch_stock.json?shop=www.snowdevil.ca×tamp=#{timestamp}", uri.to_s
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_request_uri_is_correct_when_sku_is_passed
|
19
|
+
Timecop.freeze do
|
20
|
+
timestamp = Time.now.utc.to_i
|
21
|
+
uri = @service.send(:request_uri, 'fetch_stock', {sku: '123', timestamp: timestamp, shop: 'www.snowdevil.ca'})
|
22
|
+
assert_equal "http://supershopifyapptwin.com/fetch_stock.json?shop=www.snowdevil.ca&sku=123×tamp=#{timestamp}", uri.to_s
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def test_response_from_failed_stock_request
|
27
|
+
mock_app_request('fetch_stock', anything, nil)
|
28
|
+
response = @service.fetch_stock_levels()
|
29
|
+
refute response.success?
|
30
|
+
assert_equal "Unable to fetch remote stock levels", response.message
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_response_from_failed_tracking_request
|
34
|
+
mock_app_request('fetch_tracking_numbers', anything, nil)
|
35
|
+
response = @service.fetch_tracking_numbers([1,2])
|
36
|
+
refute response.success?
|
37
|
+
assert_equal "Unable to fetch remote tracking numbers [1, 2]", response.message
|
38
|
+
end
|
39
|
+
|
40
|
+
def test_response_with_invalid_json_is_parsed_to_empty_hash
|
41
|
+
bad_json = '{a: 9, 0}'
|
42
|
+
mock_app_request('fetch_stock', anything, bad_json)
|
43
|
+
assert_equal({}, @service.fetch_stock_levels().stock_levels)
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_response_with_valid_but_incorrect_json_is_parsed_to_empty_hash
|
47
|
+
incorrect_json = '[]'
|
48
|
+
mock_app_request('fetch_stock', anything, incorrect_json)
|
49
|
+
assert_equal({}, @service.fetch_stock_levels().stock_levels)
|
50
|
+
end
|
51
|
+
|
52
|
+
def test_response_with_invalid_xml_is_parsed_to_empty_hash
|
53
|
+
service = build_service(format: 'xml')
|
54
|
+
bad_xml = '<A><B></C></A>'
|
55
|
+
mock_app_request('fetch_stock', anything, bad_xml)
|
56
|
+
assert_equal({}, service.fetch_stock_levels().stock_levels)
|
57
|
+
end
|
58
|
+
|
59
|
+
def test_parse_stock_level_response_parses_xml_correctly
|
60
|
+
service = build_service(format: 'xml')
|
61
|
+
xml = '<StockLevels><Product><Sku>sku1</Sku><Quantity>1</Quantity></Product><Product><Sku>sku2</Sku><Quantity>2</Quantity></Product></StockLevels>'
|
62
|
+
expected = {'sku1' => '1', 'sku2' => '2'}
|
63
|
+
|
64
|
+
mock_app_request('fetch_stock', anything, xml)
|
65
|
+
assert_equal expected, service.fetch_stock_levels().stock_levels
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_parse_tracking_data_response_parses_xml_correctly
|
69
|
+
service = build_service(format: 'xml')
|
70
|
+
xml = '<TrackingNumbers><Order><ID>123</ID><Tracking>abc</Tracking></Order><Order><ID>456</ID><Tracking>def</Tracking></Order></TrackingNumbers>'
|
71
|
+
expected = {'123' => 'abc', '456' => 'def'}
|
72
|
+
|
73
|
+
mock_app_request('fetch_tracking_numbers', anything, xml)
|
74
|
+
assert_equal expected, service.fetch_tracking_data([1,2,4]).tracking_numbers
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_parse_stock_level_response_parses_json_with_root_correctly
|
78
|
+
json = '{"stock_levels": {"998KIB":"10"}}'
|
79
|
+
expected = {'998KIB' => "10"}
|
80
|
+
|
81
|
+
mock_app_request('fetch_stock', anything, json)
|
82
|
+
assert_equal expected, @service.fetch_stock_levels().stock_levels
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_parse_tracking_data_response_parses_json_with_root_correctly
|
86
|
+
json = '{"tracking_numbers": {"order1":"a","order2":"b"}}'
|
87
|
+
expected = {'order1' => 'a', 'order2' => 'b'}
|
88
|
+
|
89
|
+
mock_app_request('fetch_tracking_numbers', anything, json)
|
90
|
+
assert_equal expected, @service.fetch_tracking_data([1,2]).tracking_numbers
|
91
|
+
end
|
92
|
+
|
93
|
+
def test_parse_stock_level_response_parses_json_without_root_correctly
|
94
|
+
json = '{"998KIB":"10"}'
|
95
|
+
expected = {'998KIB' => "10"}
|
96
|
+
|
97
|
+
mock_app_request('fetch_stock', anything, json)
|
98
|
+
assert_equal expected, @service.fetch_stock_levels().stock_levels
|
99
|
+
end
|
100
|
+
|
101
|
+
def test_parse_tracking_data_response_parses_json_without_root_correctly
|
102
|
+
json = '{"order1":"a","order2":"b"}'
|
103
|
+
expected = {'order1' => 'a', 'order2' => 'b'}
|
104
|
+
|
105
|
+
mock_app_request('fetch_tracking_numbers', anything, json)
|
106
|
+
assert_equal expected, @service.fetch_tracking_data([1,2]).tracking_numbers
|
107
|
+
end
|
108
|
+
|
109
|
+
def test_send_app_request_rescues_response_errors
|
110
|
+
response = stub(code: "404", message: "Not Found")
|
111
|
+
@service.expects(:ssl_get).raises(ActiveMerchant::ResponseError, response)
|
112
|
+
refute @service.fetch_stock_levels().success?
|
113
|
+
end
|
114
|
+
|
115
|
+
def test_send_app_request_rescues_invalid_response_errors
|
116
|
+
@service.expects(:ssl_get).raises(ActiveMerchant::InvalidResponseError.new("error html"))
|
117
|
+
refute @service.fetch_stock_levels().success?
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def mock_app_request(action, input, output)
|
123
|
+
ShopifyAPIService.any_instance.expects(:send_app_request).with(action, nil, input).returns(output)
|
124
|
+
end
|
125
|
+
|
126
|
+
def build_service(options = {})
|
127
|
+
options.reverse_merge!({
|
128
|
+
name: "fulfillment_app",
|
129
|
+
callback_url: 'http://supershopifyapptwin.com',
|
130
|
+
format: 'json'
|
131
|
+
})
|
132
|
+
ShopifyAPIService.new(options)
|
133
|
+
end
|
134
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_fulfillment
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.1.
|
4
|
+
version: 2.1.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Cody Fauser
|
@@ -31,7 +31,7 @@ cert_chain:
|
|
31
31
|
fl3hbtVFTqbOlwL9vy1fudXcolIE/ZTcxQ+er07ZFZdKCXayR9PPs64heamfn0fp
|
32
32
|
TConQSX2BnZdhIEYW+cKzEC/bLc=
|
33
33
|
-----END CERTIFICATE-----
|
34
|
-
date: 2014-
|
34
|
+
date: 2014-09-03 00:00:00.000000000 Z
|
35
35
|
dependencies:
|
36
36
|
- !ruby/object:Gem::Dependency
|
37
37
|
name: activesupport
|
@@ -89,6 +89,20 @@ dependencies:
|
|
89
89
|
- - ">="
|
90
90
|
- !ruby/object:Gem::Version
|
91
91
|
version: '0'
|
92
|
+
- !ruby/object:Gem::Dependency
|
93
|
+
name: byebug
|
94
|
+
requirement: !ruby/object:Gem::Requirement
|
95
|
+
requirements:
|
96
|
+
- - ">="
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
type: :development
|
100
|
+
prerelease: false
|
101
|
+
version_requirements: !ruby/object:Gem::Requirement
|
102
|
+
requirements:
|
103
|
+
- - ">="
|
104
|
+
- !ruby/object:Gem::Version
|
105
|
+
version: '0'
|
92
106
|
- !ruby/object:Gem::Dependency
|
93
107
|
name: mocha
|
94
108
|
requirement: !ruby/object:Gem::Requirement
|
@@ -117,6 +131,20 @@ dependencies:
|
|
117
131
|
- - ">="
|
118
132
|
- !ruby/object:Gem::Version
|
119
133
|
version: '0'
|
134
|
+
- !ruby/object:Gem::Dependency
|
135
|
+
name: timecop
|
136
|
+
requirement: !ruby/object:Gem::Requirement
|
137
|
+
requirements:
|
138
|
+
- - ">="
|
139
|
+
- !ruby/object:Gem::Version
|
140
|
+
version: '0'
|
141
|
+
type: :development
|
142
|
+
prerelease: false
|
143
|
+
version_requirements: !ruby/object:Gem::Requirement
|
144
|
+
requirements:
|
145
|
+
- - ">="
|
146
|
+
- !ruby/object:Gem::Version
|
147
|
+
version: '0'
|
120
148
|
- !ruby/object:Gem::Dependency
|
121
149
|
name: rdoc
|
122
150
|
requirement: !ruby/object:Gem::Requirement
|
@@ -146,6 +174,7 @@ files:
|
|
146
174
|
- lib/active_fulfillment/fulfillment/services/amazon.rb
|
147
175
|
- lib/active_fulfillment/fulfillment/services/amazon_mws.rb
|
148
176
|
- lib/active_fulfillment/fulfillment/services/shipwire.rb
|
177
|
+
- lib/active_fulfillment/fulfillment/services/shopify_api.rb
|
149
178
|
- lib/active_fulfillment/fulfillment/services/webgistix.rb
|
150
179
|
- lib/active_fulfillment/fulfillment/version.rb
|
151
180
|
- test/fixtures.yml
|
@@ -184,6 +213,7 @@ files:
|
|
184
213
|
- test/unit/services/amazon_mws_test.rb
|
185
214
|
- test/unit/services/amazon_test.rb
|
186
215
|
- test/unit/services/shipwire_test.rb
|
216
|
+
- test/unit/services/shopify_api_test.rb
|
187
217
|
- test/unit/services/webgistix_test.rb
|
188
218
|
homepage: http://github.com/shopify/active_fulfillment
|
189
219
|
licenses: []
|
@@ -215,6 +245,7 @@ test_files:
|
|
215
245
|
- test/remote/webgistix_test.rb
|
216
246
|
- test/remote/shipwire_test.rb
|
217
247
|
- test/remote/amazon_mws_test.rb
|
248
|
+
- test/unit/services/shopify_api_test.rb
|
218
249
|
- test/unit/services/amazon_test.rb
|
219
250
|
- test/unit/services/webgistix_test.rb
|
220
251
|
- test/unit/services/shipwire_test.rb
|
metadata.gz.sig
CHANGED
Binary file
|