shopify_api 4.3.4 → 4.3.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +6 -0
- data/README.md +28 -8
- data/lib/shopify_api/resources/draft_order.rb +10 -0
- data/lib/shopify_api/resources/draft_order_invoice.rb +4 -0
- data/lib/shopify_api/session.rb +26 -7
- data/lib/shopify_api/version.rb +1 -1
- data/shopify_api.gemspec +1 -0
- data/test/draft_order_test.rb +114 -0
- data/test/fixtures/draft_order.json +159 -0
- data/test/fixtures/draft_order_invoice.json +9 -0
- data/test/fixtures/draft_orders.json +161 -0
- data/test/fulfillment_request_test.rb +6 -2
- data/test/session_test.rb +59 -16
- metadata +22 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a735be75b2ea0f7c4762ee44ca3a8c2a2a2a4d22
|
4
|
+
data.tar.gz: 6a8927da2492a41f0c3454925cae1c4ab1d2caeb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1d894337c5a7297f935866ad69749e118e68959402dfd8ec276241a2af31adab03c8937c074bb5b10360e3ddf05221cddb52d445989bcfab323667c5ab42c176
|
7
|
+
data.tar.gz: df8fec33376e1dc2b85660af9bd6d097fa251a15225011f0f840023ebc3be1125032dca99dc932250970a1f9754168e0b33ca3350e5c62f017b44d7a105b2b8a
|
data/CHANGELOG
CHANGED
data/README.md
CHANGED
@@ -67,9 +67,11 @@ ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveR
|
|
67
67
|
|
68
68
|
For a partner app you will need to supply two parameters to the Session class before you instantiate it:
|
69
69
|
|
70
|
-
|
71
|
-
|
72
|
-
|
70
|
+
```ruby
|
71
|
+
ShopifyAPI::Session.setup(api_key: API_KEY, secret: SHARED_SECRET)
|
72
|
+
```
|
73
|
+
|
74
|
+
Shopify maintains [`omniauth-shopify-oauth2`](https://github.com/Shopify/omniauth-shopify-oauth2) which securely wraps the OAuth flow and interactions with Shopify (steps 3 and 4 above). Using this gem is the recommended way to use OAuth authentication in your application.
|
73
75
|
|
74
76
|
3. In order to access a shop's data, apps need an access token from that specific shop. This is a two-stage process. Before interacting with a shop for the first time an app should redirect the user to the following URL:
|
75
77
|
|
@@ -79,10 +81,11 @@ ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveR
|
|
79
81
|
|
80
82
|
with the following parameters:
|
81
83
|
|
82
|
-
* ``client_id
|
83
|
-
* ``scope`` – Required – The list of required scopes (explained here:
|
84
|
+
* ``client_id`` – Required – The API key for your app
|
85
|
+
* ``scope`` – Required – The list of required scopes (explained here: https://help.shopify.com/api/guides/authentication/oauth#scopes)
|
84
86
|
* ``redirect_uri`` – Required – The URL where you want to redirect the users after they authorize the client. The complete URL specified here must be identical to one of the Application Redirect URLs set in the App's section of the Partners dashboard. Note: in older applications, this parameter was optional, and redirected to the Application Callback URL when no other value was specified.
|
85
87
|
* ``state`` – Optional – A randomly selected value provided by your application, which is unique for each authorization request. During the OAuth callback phase, your application must check that this value matches the one you provided during authorization. [This mechanism is important for the security of your application](https://tools.ietf.org/html/rfc6819#section-3.6).
|
88
|
+
* ``grant_options[]`` - Optional - Set this parameter to `per-user` to receive an access token that respects the user's permission level when making API requests (called online access). This is strongly recommended for embedded apps.
|
86
89
|
|
87
90
|
We've added the create_permission_url method to make this easier, first instantiate your session object:
|
88
91
|
|
@@ -133,10 +136,28 @@ ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveR
|
|
133
136
|
token = session.request_token(params)
|
134
137
|
```
|
135
138
|
|
136
|
-
This method will save the token to the session object and return it.
|
139
|
+
This method will save the token to the session object and return it. All fields returned by Shopify, other than the access token itself, are stored in the session's `extra` attribute. For a list of all fields returned by Shopify, read [our OAuth documentation](https://help.shopify.com/api/guides/authentication/oauth#confirming-installation). If you requested an access token that is associated with a specific user, you can retreive information about this user from the `extra` hash:
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
# a list of all granted scopes
|
143
|
+
granted_scopes = session.extra['scope']
|
144
|
+
# a hash containing the user information
|
145
|
+
user = session.extra['associated_user']
|
146
|
+
# the access scopes available to this user, which may be a subset of the access scopes granted to this app.
|
147
|
+
active_scopes = session.extra['associated_user_scope']
|
148
|
+
# the time at which this token expires; this is automatically converted from 'expires_in' returned by Shopify
|
149
|
+
expires_at = session.extra['expires_at']
|
150
|
+
```
|
151
|
+
|
152
|
+
For the security of your application, after retrieving an access token you must validate the following:
|
153
|
+
1) The list of scopes in `session.extra['scope']` is the same as you requested.
|
154
|
+
2) If you requested an online-mode access token, `session.extra['associated_user']` must be present.
|
155
|
+
Failing either of these tests means the end-user may have tampered with the url parameters during the OAuth authentication phase. You should avoid using this access token and revoke it immediately. If you use the [`omniauth-shopify-oauth2`](https://github.com/Shopify/omniauth-shopify-oauth2) gem these checks are done automatically for you.
|
156
|
+
|
157
|
+
For future sessions simply pass in the `token` and `extra` hash (optional) when creating the session object:
|
137
158
|
|
138
159
|
```ruby
|
139
|
-
session = ShopifyAPI::Session.new("SHOP_NAME.myshopify.com", token)
|
160
|
+
session = ShopifyAPI::Session.new("SHOP_NAME.myshopify.com", token, extra)
|
140
161
|
```
|
141
162
|
|
142
163
|
5. The session must be activated before use:
|
@@ -177,7 +198,6 @@ ShopifyAPI uses ActiveResource to communicate with the REST web service. ActiveR
|
|
177
198
|
ShopifyAPI::Base.clear_session
|
178
199
|
```
|
179
200
|
|
180
|
-
|
181
201
|
### Console
|
182
202
|
|
183
203
|
This package also supports the ``shopify-cli`` executable to make it easy to open up an interactive console to use the API with a shop.
|
@@ -0,0 +1,10 @@
|
|
1
|
+
module ShopifyAPI
|
2
|
+
class DraftOrder < Base
|
3
|
+
include Metafields
|
4
|
+
|
5
|
+
def send_invoice(draft_order_invoice = ShopifyAPI::DraftOrderInvoice.new)
|
6
|
+
resource = post(:send_invoice, {}, draft_order_invoice.encode)
|
7
|
+
ShopifyAPI::DraftOrderInvoice.new(ShopifyAPI::DraftOrder.format.decode(resource.body))
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/lib/shopify_api/session.rb
CHANGED
@@ -11,7 +11,7 @@ module ShopifyAPI
|
|
11
11
|
self.protocol = 'https'
|
12
12
|
self.myshopify_domain = 'myshopify.com'
|
13
13
|
|
14
|
-
attr_accessor :url, :token, :name
|
14
|
+
attr_accessor :url, :token, :name, :extra
|
15
15
|
|
16
16
|
class << self
|
17
17
|
|
@@ -67,9 +67,10 @@ module ShopifyAPI
|
|
67
67
|
end
|
68
68
|
end
|
69
69
|
|
70
|
-
def initialize(url, token = nil)
|
70
|
+
def initialize(url, token = nil, extra = {})
|
71
71
|
self.url = self.class.prepare_url(url)
|
72
72
|
self.token = token
|
73
|
+
self.extra = extra
|
73
74
|
end
|
74
75
|
|
75
76
|
def create_permission_url(scope, redirect_uri = nil)
|
@@ -85,12 +86,15 @@ module ShopifyAPI
|
|
85
86
|
raise ShopifyAPI::ValidationException, "Invalid Signature: Possible malicious login"
|
86
87
|
end
|
87
88
|
|
88
|
-
|
89
|
-
|
90
|
-
response = access_token_request(code)
|
91
|
-
|
89
|
+
response = access_token_request(params['code'])
|
92
90
|
if response.code == "200"
|
93
|
-
|
91
|
+
self.extra = JSON.parse(response.body)
|
92
|
+
self.token = extra.delete('access_token')
|
93
|
+
|
94
|
+
if expires_in = extra.delete('expires_in')
|
95
|
+
extra['expires_at'] = Time.now.utc.to_i + expires_in
|
96
|
+
end
|
97
|
+
token
|
94
98
|
else
|
95
99
|
raise RuntimeError, response.msg
|
96
100
|
end
|
@@ -108,6 +112,21 @@ module ShopifyAPI
|
|
108
112
|
url.present? && token.present?
|
109
113
|
end
|
110
114
|
|
115
|
+
def expires_in
|
116
|
+
return unless expires_at.present?
|
117
|
+
[0, expires_at.to_i - Time.now.utc.to_i].max
|
118
|
+
end
|
119
|
+
|
120
|
+
def expires_at
|
121
|
+
return unless extra.present?
|
122
|
+
@expires_at ||= Time.at(extra['expires_at']).utc
|
123
|
+
end
|
124
|
+
|
125
|
+
def expired?
|
126
|
+
return false if expires_in.nil?
|
127
|
+
expires_in <= 0
|
128
|
+
end
|
129
|
+
|
111
130
|
private
|
112
131
|
def parameterize(params)
|
113
132
|
URI.escape(params.collect{|k,v| "#{k}=#{v}"}.join('&'))
|
data/lib/shopify_api/version.rb
CHANGED
data/shopify_api.gemspec
CHANGED
@@ -0,0 +1,114 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class DraftOrderTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
super
|
6
|
+
|
7
|
+
fake 'draft_orders/517119332', body: load_fixture('draft_order')
|
8
|
+
|
9
|
+
@draft_order = ShopifyAPI::DraftOrder.find(517119332)
|
10
|
+
end
|
11
|
+
|
12
|
+
def test_get_draft_order
|
13
|
+
fake 'draft_orders/517119332', method: :get, status: 200, body: load_fixture('draft_order')
|
14
|
+
|
15
|
+
draft_order = ShopifyAPI::DraftOrder.find(517119332)
|
16
|
+
|
17
|
+
assert_equal 517119332, draft_order.id
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_get_all_draft_orders
|
21
|
+
fake 'draft_orders', method: :get, status: 200, body: load_fixture('draft_orders')
|
22
|
+
|
23
|
+
draft_orders = ShopifyAPI::DraftOrder.all
|
24
|
+
|
25
|
+
assert_equal 1, draft_orders.length
|
26
|
+
assert_equal 517119332, draft_orders.first.id
|
27
|
+
end
|
28
|
+
|
29
|
+
def test_get_count_draft_orders
|
30
|
+
fake 'draft_orders/count', method: :get, status: 200, body: '{"count": 16}'
|
31
|
+
|
32
|
+
draft_orders_count = ShopifyAPI::DraftOrder.count
|
33
|
+
|
34
|
+
assert_equal 16, draft_orders_count
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_create_draft_order
|
38
|
+
fake 'draft_orders', method: :post, status: 201, body: load_fixture('draft_order')
|
39
|
+
|
40
|
+
draft_order = ShopifyAPI::DraftOrder.create(line_items: [{ quantity: 1, variant_id: 39072856 }])
|
41
|
+
|
42
|
+
assert_equal '{"draft_order":{"line_items":[{"quantity":1,"variant_id":39072856}]}}', FakeWeb.last_request.body
|
43
|
+
assert_equal 39072856, draft_order.line_items.first.variant_id
|
44
|
+
end
|
45
|
+
|
46
|
+
def test_create_draft_order_202
|
47
|
+
fake 'draft_orders', method: :post, status: 202, body: load_fixture('draft_order')
|
48
|
+
|
49
|
+
draft_order = ShopifyAPI::DraftOrder.create(line_items: [{ quantity: 1, variant_id: 39072856 }])
|
50
|
+
|
51
|
+
assert_equal 39072856, draft_order.line_items.first.variant_id
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_update_draft_order
|
55
|
+
draft_order_response = ActiveSupport::JSON.decode(load_fixture('draft_order'))
|
56
|
+
draft_order_response['draft_order']['note'] = 'Test new note'
|
57
|
+
@draft_order.note = 'Test new note'
|
58
|
+
fake 'draft_orders/517119332', method: :put, status: 200, body: ActiveSupport::JSON.encode(draft_order_response)
|
59
|
+
|
60
|
+
@draft_order.save
|
61
|
+
|
62
|
+
assert_equal draft_order_response['draft_order']['note'], @draft_order.note
|
63
|
+
end
|
64
|
+
|
65
|
+
def test_send_invoice_with_no_params
|
66
|
+
draft_order_invoice_fixture = load_fixture('draft_order_invoice')
|
67
|
+
draft_order_invoice = ActiveSupport::JSON.decode(draft_order_invoice_fixture)
|
68
|
+
fake 'draft_orders/517119332/send_invoice', method: :post, body: draft_order_invoice_fixture
|
69
|
+
|
70
|
+
draft_order_invoice_response = @draft_order.send_invoice
|
71
|
+
|
72
|
+
assert_equal '{"draft_order_invoice":{}}', FakeWeb.last_request.body
|
73
|
+
assert_kind_of ShopifyAPI::DraftOrderInvoice, draft_order_invoice_response
|
74
|
+
assert_equal draft_order_invoice['draft_order_invoice']['to'], draft_order_invoice_response.to
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_send_invoice_with_params
|
78
|
+
draft_order_invoice_fixture = load_fixture('draft_order_invoice')
|
79
|
+
draft_order_invoice = ActiveSupport::JSON.decode(draft_order_invoice_fixture)
|
80
|
+
fake 'draft_orders/517119332/send_invoice', method: :post, body: draft_order_invoice_fixture
|
81
|
+
|
82
|
+
draft_order_invoice_response = @draft_order.send_invoice(ShopifyAPI::DraftOrderInvoice.new(draft_order_invoice['draft_order_invoice']))
|
83
|
+
|
84
|
+
assert_equal draft_order_invoice, ActiveSupport::JSON.decode(FakeWeb.last_request.body)
|
85
|
+
assert_kind_of ShopifyAPI::DraftOrderInvoice, draft_order_invoice_response
|
86
|
+
assert_equal draft_order_invoice['draft_order_invoice']['to'], draft_order_invoice_response.to
|
87
|
+
end
|
88
|
+
|
89
|
+
def test_delete_draft_order
|
90
|
+
fake 'draft_orders/517119332', method: :delete, body: 'destroyed'
|
91
|
+
assert @draft_order.destroy
|
92
|
+
end
|
93
|
+
|
94
|
+
def test_add_metafields_to_draft_order
|
95
|
+
fake 'draft_orders/517119332/metafields', method: :post, status: 201, body: load_fixture('metafield')
|
96
|
+
|
97
|
+
field = @draft_order.add_metafield(ShopifyAPI::Metafield.new(namespace: 'contact', key: 'email', value: '123@example.com', value_type: 'string'))
|
98
|
+
|
99
|
+
assert_equal ActiveSupport::JSON.decode('{"metafield":{"namespace":"contact","key":"email","value":"123@example.com","value_type":"string"}}'), ActiveSupport::JSON.decode(FakeWeb.last_request.body)
|
100
|
+
assert !field.new_record?
|
101
|
+
assert_equal 'contact', field.namespace
|
102
|
+
assert_equal 'email', field.key
|
103
|
+
assert_equal '123@example.com', field.value
|
104
|
+
end
|
105
|
+
|
106
|
+
def test_get_metafields_for_draft_order
|
107
|
+
fake 'draft_orders/517119332/metafields', body: load_fixture('metafields')
|
108
|
+
|
109
|
+
metafields = @draft_order.metafields
|
110
|
+
|
111
|
+
assert_equal 2, metafields.length
|
112
|
+
assert metafields.all? { |m| m.is_a?(ShopifyAPI::Metafield) }
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
{
|
2
|
+
"draft_order": {
|
3
|
+
"id": 517119332,
|
4
|
+
"note": "This is a note",
|
5
|
+
"email": "montana_hilpert@example.com",
|
6
|
+
"taxes_included": false,
|
7
|
+
"currency": "CAD",
|
8
|
+
"subtotal_price": "1007.41",
|
9
|
+
"total_tax": "0.00",
|
10
|
+
"total_price": "1027.41",
|
11
|
+
"invoice_sent_at": null,
|
12
|
+
"created_at": "2017-02-02T13:14:38-05:00",
|
13
|
+
"updated_at": "2017-02-02T13:14:38-05:00",
|
14
|
+
"tax_exempt": false,
|
15
|
+
"completed_at": null,
|
16
|
+
"name": "#D1",
|
17
|
+
"status": "open",
|
18
|
+
"line_items": [
|
19
|
+
{
|
20
|
+
"variant_id": 39072856,
|
21
|
+
"product_id": 632910392,
|
22
|
+
"title": "IPod Nano - 8gb",
|
23
|
+
"variant_title": "green",
|
24
|
+
"sku": "IPOD2008GREEN",
|
25
|
+
"vendor": null,
|
26
|
+
"price": "199.00",
|
27
|
+
"grams": 200,
|
28
|
+
"quantity": 1,
|
29
|
+
"requires_shipping": true,
|
30
|
+
"taxable": true,
|
31
|
+
"gift_card": false,
|
32
|
+
"fulfillment_service": "manual",
|
33
|
+
"tax_lines": [],
|
34
|
+
"applied_discount": null,
|
35
|
+
"name": "IPod Nano - 8gb - green",
|
36
|
+
"properties": [
|
37
|
+
{
|
38
|
+
"name": "Custom Engraving",
|
39
|
+
"value": "Happy Birthday"
|
40
|
+
}
|
41
|
+
],
|
42
|
+
"custom": false
|
43
|
+
},
|
44
|
+
{
|
45
|
+
"variant_id": null,
|
46
|
+
"product_id": null,
|
47
|
+
"title": "Custom Item",
|
48
|
+
"variant_title": null,
|
49
|
+
"sku": null,
|
50
|
+
"vendor": null,
|
51
|
+
"price": "494.14",
|
52
|
+
"grams": 0,
|
53
|
+
"quantity": 2,
|
54
|
+
"requires_shipping": false,
|
55
|
+
"taxable": false,
|
56
|
+
"gift_card": false,
|
57
|
+
"fulfillment_service": "manual",
|
58
|
+
"tax_lines": [],
|
59
|
+
"applied_discount": {
|
60
|
+
"description": "A percentage discount for a custom line item",
|
61
|
+
"value": "3.58",
|
62
|
+
"title": "Custom",
|
63
|
+
"amount": "35.38",
|
64
|
+
"value_type": "percentage"
|
65
|
+
},
|
66
|
+
"name": "Custom item",
|
67
|
+
"properties": [],
|
68
|
+
"custom": true
|
69
|
+
}
|
70
|
+
],
|
71
|
+
"shipping_address": {
|
72
|
+
"first_name": "Jan",
|
73
|
+
"address1": "512 Ernestina Forks",
|
74
|
+
"phone": "(639) 372 1289",
|
75
|
+
"city": "Lakefurt",
|
76
|
+
"zip": "24093",
|
77
|
+
"province": "Virginia",
|
78
|
+
"country": "United States",
|
79
|
+
"last_name": "Fisher",
|
80
|
+
"address2": "Apt. 702",
|
81
|
+
"company": "Steuber and Sons",
|
82
|
+
"latitude": 45.416311,
|
83
|
+
"longitude": -75.68683,
|
84
|
+
"name": "Jan Fisher",
|
85
|
+
"country_code": "US",
|
86
|
+
"province_code": "VA"
|
87
|
+
},
|
88
|
+
"billing_address": {
|
89
|
+
"first_name": "Jan",
|
90
|
+
"address1": "512 Ernestina Forks",
|
91
|
+
"phone": "(639) 372 1289",
|
92
|
+
"city": "Lakefurt",
|
93
|
+
"zip": "24093",
|
94
|
+
"province": "Virginia",
|
95
|
+
"country": "United States",
|
96
|
+
"last_name": "Fisher",
|
97
|
+
"address2": "Apt. 702",
|
98
|
+
"company": "Steuber and Sons",
|
99
|
+
"latitude": 45.416311,
|
100
|
+
"longitude": -75.68683,
|
101
|
+
"name": "Jan Fisher",
|
102
|
+
"country_code": "US",
|
103
|
+
"province_code": "VA"
|
104
|
+
},
|
105
|
+
"invoice_url": "https://checkout.myshopify.io/1/invoices/8e72bdccd0ac51067b947ac68c6f3804",
|
106
|
+
"applied_discount": {
|
107
|
+
"description": "A discount on the entire order",
|
108
|
+
"value": "1.48",
|
109
|
+
"title": "Custom",
|
110
|
+
"amount": "1.48",
|
111
|
+
"value_type": "fixed_amount"
|
112
|
+
},
|
113
|
+
"order_id": null,
|
114
|
+
"shipping_line": {
|
115
|
+
"title": "Custom shipping",
|
116
|
+
"price": "20.00",
|
117
|
+
"custom": true,
|
118
|
+
"handle": null
|
119
|
+
},
|
120
|
+
"tax_lines": [],
|
121
|
+
"tags": "",
|
122
|
+
"customer": {
|
123
|
+
"accepts_marketing": false,
|
124
|
+
"created_at": "2014-03-07T16:14:08-05:00",
|
125
|
+
"email": "bob.norman@hostmail.com",
|
126
|
+
"first_name": "Bob",
|
127
|
+
"id": 207119551,
|
128
|
+
"last_name": "Norman",
|
129
|
+
"last_order_id": null,
|
130
|
+
"multipass_identifier": null,
|
131
|
+
"note": null,
|
132
|
+
"orders_count": 0,
|
133
|
+
"state": "disabled",
|
134
|
+
"total_spent": "0.00",
|
135
|
+
"updated_at": "2014-03-07T16:14:08-05:00",
|
136
|
+
"verified_email": true,
|
137
|
+
"tags": "",
|
138
|
+
"last_order_name": null,
|
139
|
+
"default_address": {
|
140
|
+
"address1": "Chestnut Street 92",
|
141
|
+
"address2": "",
|
142
|
+
"city": "Louisville",
|
143
|
+
"company": null,
|
144
|
+
"country": "United States",
|
145
|
+
"first_name": null,
|
146
|
+
"id": 207119551,
|
147
|
+
"last_name": null,
|
148
|
+
"phone": "555-625-1199",
|
149
|
+
"province": "Kentucky",
|
150
|
+
"zip": "40202",
|
151
|
+
"name": null,
|
152
|
+
"province_code": "KY",
|
153
|
+
"country_code": "US",
|
154
|
+
"country_name": "United States",
|
155
|
+
"default": true
|
156
|
+
}
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
@@ -0,0 +1,161 @@
|
|
1
|
+
{
|
2
|
+
"draft_orders": [
|
3
|
+
{
|
4
|
+
"id": 517119332,
|
5
|
+
"note": "This is a note",
|
6
|
+
"email": "montana_hilpert@example.com",
|
7
|
+
"taxes_included": false,
|
8
|
+
"currency": "CAD",
|
9
|
+
"subtotal_price": "1007.41",
|
10
|
+
"total_tax": "0.00",
|
11
|
+
"total_price": "1027.41",
|
12
|
+
"invoice_sent_at": null,
|
13
|
+
"created_at": "2017-02-02T13:14:38-05:00",
|
14
|
+
"updated_at": "2017-02-02T13:14:38-05:00",
|
15
|
+
"tax_exempt": false,
|
16
|
+
"completed_at": null,
|
17
|
+
"name": "#D1",
|
18
|
+
"status": "open",
|
19
|
+
"line_items": [
|
20
|
+
{
|
21
|
+
"variant_id": 39072856,
|
22
|
+
"product_id": 632910392,
|
23
|
+
"title": "IPod Nano - 8gb",
|
24
|
+
"variant_title": "green",
|
25
|
+
"sku": "IPOD2008GREEN",
|
26
|
+
"vendor": null,
|
27
|
+
"price": "199.00",
|
28
|
+
"grams": 200,
|
29
|
+
"quantity": 1,
|
30
|
+
"requires_shipping": true,
|
31
|
+
"taxable": true,
|
32
|
+
"gift_card": false,
|
33
|
+
"fulfillment_service": "manual",
|
34
|
+
"tax_lines": [],
|
35
|
+
"applied_discount": null,
|
36
|
+
"name": "IPod Nano - 8gb - green",
|
37
|
+
"properties": [
|
38
|
+
{
|
39
|
+
"name": "Custom Engraving",
|
40
|
+
"value": "Happy Birthday"
|
41
|
+
}
|
42
|
+
],
|
43
|
+
"custom": false
|
44
|
+
},
|
45
|
+
{
|
46
|
+
"variant_id": null,
|
47
|
+
"product_id": null,
|
48
|
+
"title": "Custom Item",
|
49
|
+
"variant_title": null,
|
50
|
+
"sku": null,
|
51
|
+
"vendor": null,
|
52
|
+
"price": "494.14",
|
53
|
+
"grams": 0,
|
54
|
+
"quantity": 2,
|
55
|
+
"requires_shipping": false,
|
56
|
+
"taxable": false,
|
57
|
+
"gift_card": false,
|
58
|
+
"fulfillment_service": "manual",
|
59
|
+
"tax_lines": [],
|
60
|
+
"applied_discount": {
|
61
|
+
"description": "A percentage discount for a custom line item",
|
62
|
+
"value": "3.58",
|
63
|
+
"title": "Custom",
|
64
|
+
"amount": "35.38",
|
65
|
+
"value_type": "percentage"
|
66
|
+
},
|
67
|
+
"name": "Custom item",
|
68
|
+
"properties": [],
|
69
|
+
"custom": true
|
70
|
+
}
|
71
|
+
],
|
72
|
+
"shipping_address": {
|
73
|
+
"first_name": "Jan",
|
74
|
+
"address1": "512 Ernestina Forks",
|
75
|
+
"phone": "(639) 372 1289",
|
76
|
+
"city": "Lakefurt",
|
77
|
+
"zip": "24093",
|
78
|
+
"province": "Virginia",
|
79
|
+
"country": "United States",
|
80
|
+
"last_name": "Fisher",
|
81
|
+
"address2": "Apt. 702",
|
82
|
+
"company": "Steuber and Sons",
|
83
|
+
"latitude": 45.416311,
|
84
|
+
"longitude": -75.68683,
|
85
|
+
"name": "Jan Fisher",
|
86
|
+
"country_code": "US",
|
87
|
+
"province_code": "VA"
|
88
|
+
},
|
89
|
+
"billing_address": {
|
90
|
+
"first_name": "Jan",
|
91
|
+
"address1": "512 Ernestina Forks",
|
92
|
+
"phone": "(639) 372 1289",
|
93
|
+
"city": "Lakefurt",
|
94
|
+
"zip": "24093",
|
95
|
+
"province": "Virginia",
|
96
|
+
"country": "United States",
|
97
|
+
"last_name": "Fisher",
|
98
|
+
"address2": "Apt. 702",
|
99
|
+
"company": "Steuber and Sons",
|
100
|
+
"latitude": 45.416311,
|
101
|
+
"longitude": -75.68683,
|
102
|
+
"name": "Jan Fisher",
|
103
|
+
"country_code": "US",
|
104
|
+
"province_code": "VA"
|
105
|
+
},
|
106
|
+
"invoice_url": "https://checkout.myshopify.io/1/invoices/8e72bdccd0ac51067b947ac68c6f3804",
|
107
|
+
"applied_discount": {
|
108
|
+
"description": "A discount on the entire order",
|
109
|
+
"value": "1.48",
|
110
|
+
"title": "Custom",
|
111
|
+
"amount": "1.48",
|
112
|
+
"value_type": "fixed_amount"
|
113
|
+
},
|
114
|
+
"order_id": null,
|
115
|
+
"shipping_line": {
|
116
|
+
"title": "Custom shipping",
|
117
|
+
"price": "20.00",
|
118
|
+
"custom": true,
|
119
|
+
"handle": null
|
120
|
+
},
|
121
|
+
"tax_lines": [],
|
122
|
+
"tags": "",
|
123
|
+
"customer": {
|
124
|
+
"accepts_marketing": false,
|
125
|
+
"created_at": "2014-03-07T16:14:08-05:00",
|
126
|
+
"email": "bob.norman@hostmail.com",
|
127
|
+
"first_name": "Bob",
|
128
|
+
"id": 207119551,
|
129
|
+
"last_name": "Norman",
|
130
|
+
"last_order_id": null,
|
131
|
+
"multipass_identifier": null,
|
132
|
+
"note": null,
|
133
|
+
"orders_count": 0,
|
134
|
+
"state": "disabled",
|
135
|
+
"total_spent": "0.00",
|
136
|
+
"updated_at": "2014-03-07T16:14:08-05:00",
|
137
|
+
"verified_email": true,
|
138
|
+
"tags": "",
|
139
|
+
"last_order_name": null,
|
140
|
+
"default_address": {
|
141
|
+
"address1": "Chestnut Street 92",
|
142
|
+
"address2": "",
|
143
|
+
"city": "Louisville",
|
144
|
+
"company": null,
|
145
|
+
"country": "United States",
|
146
|
+
"first_name": null,
|
147
|
+
"id": 207119551,
|
148
|
+
"last_name": null,
|
149
|
+
"phone": "555-625-1199",
|
150
|
+
"province": "Kentucky",
|
151
|
+
"zip": "40202",
|
152
|
+
"name": null,
|
153
|
+
"province_code": "KY",
|
154
|
+
"country_code": "US",
|
155
|
+
"country_name": "United States",
|
156
|
+
"default": true
|
157
|
+
}
|
158
|
+
}
|
159
|
+
}
|
160
|
+
]
|
161
|
+
}
|
@@ -2,7 +2,7 @@ require 'test_helper'
|
|
2
2
|
|
3
3
|
class FulFillmentRequestTest < Test::Unit::TestCase
|
4
4
|
def setup
|
5
|
-
fake "orders/450789469/fulfillment_requests/
|
5
|
+
fake "orders/450789469/fulfillment_requests/255858046", method: :get, body: load_fixture('fulfillment_request')
|
6
6
|
end
|
7
7
|
|
8
8
|
context "#mark_as_failed" do
|
@@ -11,7 +11,11 @@ class FulFillmentRequestTest < Test::Unit::TestCase
|
|
11
11
|
|
12
12
|
cancelled = ActiveSupport::JSON.decode(load_fixture('fulfillment_request'))
|
13
13
|
cancelled['failure_message'] = 'failure reason'
|
14
|
-
|
14
|
+
cancelled['message'] = nil
|
15
|
+
fake "orders/450789469/fulfillment_requests/695890229/mark_as_failed.json?message=",
|
16
|
+
method: :put,
|
17
|
+
body: ActiveSupport::JSON.encode(cancelled),
|
18
|
+
extension: false
|
15
19
|
|
16
20
|
assert fulfillment_request.failure_message.blank?
|
17
21
|
assert fulfillment_request.mark_as_failed
|
data/test/session_test.rb
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'test_helper'
|
2
|
+
require 'timecop'
|
2
3
|
|
3
4
|
class SessionTest < Test::Unit::TestCase
|
4
5
|
|
@@ -141,19 +142,59 @@ class SessionTest < Test::Unit::TestCase
|
|
141
142
|
end
|
142
143
|
|
143
144
|
test "return_token_if_signature_is_valid" do
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
145
|
+
fake nil,
|
146
|
+
url: 'https://testshop.myshopify.com/admin/oauth/access_token',
|
147
|
+
method: :post,
|
148
|
+
body: '{"access_token":"any-token"}'
|
148
149
|
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
149
|
-
|
150
|
+
|
151
|
+
params = { code: 'any-code', timestamp: Time.now }
|
152
|
+
token = session.request_token(params.merge(hmac: generate_signature(params)))
|
153
|
+
|
150
154
|
assert_equal "any-token", token
|
155
|
+
assert_equal "any-token", session.token
|
156
|
+
end
|
157
|
+
|
158
|
+
test "extra parameters are stored in session" do
|
159
|
+
fake nil,
|
160
|
+
url: 'https://testshop.myshopify.com/admin/oauth/access_token',
|
161
|
+
method: :post,
|
162
|
+
body: '{"access_token":"any-token","foo":"example"}'
|
163
|
+
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
164
|
+
|
165
|
+
params = { code: 'any-code', timestamp: Time.now }
|
166
|
+
assert session.request_token(params.merge(hmac: generate_signature(params)))
|
167
|
+
|
168
|
+
assert_equal ({ "foo" => "example" }), session.extra
|
169
|
+
end
|
170
|
+
|
171
|
+
test "expires_in is automatically converted in expires_at" do
|
172
|
+
fake nil,
|
173
|
+
url: 'https://testshop.myshopify.com/admin/oauth/access_token',
|
174
|
+
method: :post,
|
175
|
+
body: '{"access_token":"any-token","expires_in":86393}'
|
176
|
+
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
177
|
+
|
178
|
+
Timecop.freeze do
|
179
|
+
params = { code: 'any-code', timestamp: Time.now }
|
180
|
+
assert session.request_token(params.merge(hmac: generate_signature(params)))
|
181
|
+
|
182
|
+
expires_at = Time.now.utc + 86393
|
183
|
+
assert_equal ({ "expires_at" => expires_at.to_i }), session.extra
|
184
|
+
assert session.expires_at.is_a?(Time)
|
185
|
+
assert_equal expires_at.to_i, session.expires_at.to_i
|
186
|
+
assert_equal 86393, session.expires_in
|
187
|
+
assert_equal false, session.expired?
|
188
|
+
|
189
|
+
Timecop.travel(session.expires_at) do
|
190
|
+
assert_equal true, session.expired?
|
191
|
+
end
|
192
|
+
end
|
151
193
|
end
|
152
194
|
|
153
195
|
test "raise error if signature does not match expected" do
|
154
196
|
params = {:code => "any-code", :timestamp => Time.now}
|
155
|
-
|
156
|
-
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new(), ShopifyAPI::Session.secret, sorted_params)
|
197
|
+
signature = generate_signature(params)
|
157
198
|
params[:foo] = 'world'
|
158
199
|
assert_raises(ShopifyAPI::ValidationException) do
|
159
200
|
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
@@ -163,8 +204,7 @@ class SessionTest < Test::Unit::TestCase
|
|
163
204
|
|
164
205
|
test "raise error if timestamp is too old" do
|
165
206
|
params = {:code => "any-code", :timestamp => Time.now - 2.days}
|
166
|
-
|
167
|
-
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new(), ShopifyAPI::Session.secret, sorted_params)
|
207
|
+
signature = generate_signature(params)
|
168
208
|
params[:foo] = 'world'
|
169
209
|
assert_raises(ShopifyAPI::ValidationException) do
|
170
210
|
session = ShopifyAPI::Session.new("testshop.myshopify.com")
|
@@ -173,18 +213,16 @@ class SessionTest < Test::Unit::TestCase
|
|
173
213
|
end
|
174
214
|
|
175
215
|
test "return true when the signature is valid and the keys of params are strings" do
|
176
|
-
|
177
|
-
params
|
178
|
-
|
179
|
-
signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new(), ShopifyAPI::Session.secret, sorted_params)
|
180
|
-
params = {"code" => "any-code", "timestamp" => now, "hmac" => signature}
|
216
|
+
params = {"code" => "any-code", "timestamp" => Time.now}
|
217
|
+
params["hmac"] = generate_signature(params)
|
218
|
+
assert_equal true, ShopifyAPI::Session.validate_signature(params)
|
181
219
|
end
|
182
220
|
|
183
221
|
test "return true when validating signature of params with ampersand and equal sign characters" do
|
184
222
|
ShopifyAPI::Session.secret = 'secret'
|
185
223
|
params = {'a' => '1&b=2', 'c=3&d' => '4'}
|
186
224
|
to_sign = "a=1%26b=2&c%3D3%26d=4"
|
187
|
-
params['hmac'] =
|
225
|
+
params['hmac'] = generate_signature(to_sign)
|
188
226
|
|
189
227
|
assert_equal true, ShopifyAPI::Session.validate_signature(params)
|
190
228
|
end
|
@@ -193,7 +231,7 @@ class SessionTest < Test::Unit::TestCase
|
|
193
231
|
ShopifyAPI::Session.secret = 'secret'
|
194
232
|
params = {'a%3D1%26b' => '2%26c%3D3'}
|
195
233
|
to_sign = "a%253D1%2526b=2%2526c%253D3"
|
196
|
-
params['hmac'] =
|
234
|
+
params['hmac'] = generate_signature(to_sign)
|
197
235
|
|
198
236
|
assert_equal true, ShopifyAPI::Session.validate_signature(params)
|
199
237
|
end
|
@@ -203,4 +241,9 @@ class SessionTest < Test::Unit::TestCase
|
|
203
241
|
def make_sorted_params(params)
|
204
242
|
sorted_params = params.with_indifferent_access.except(:signature, :hmac, :action, :controller).collect{|k,v|"#{k}=#{v}"}.sort.join('&')
|
205
243
|
end
|
244
|
+
|
245
|
+
def generate_signature(params)
|
246
|
+
params = make_sorted_params(params) if params.is_a?(Hash)
|
247
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new, ShopifyAPI::Session.secret, params)
|
248
|
+
end
|
206
249
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify_api
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 4.3.
|
4
|
+
version: 4.3.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-02-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activeresource
|
@@ -94,6 +94,20 @@ dependencies:
|
|
94
94
|
- - ">="
|
95
95
|
- !ruby/object:Gem::Version
|
96
96
|
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: timecop
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
97
111
|
description: The Shopify API gem allows Ruby developers to programmatically access
|
98
112
|
the admin section of Shopify stores. The API is implemented as JSON or XML over
|
99
113
|
HTTP using all four verbs (GET/POST/PUT/DELETE). Each resource, like Order, Product,
|
@@ -159,6 +173,8 @@ files:
|
|
159
173
|
- lib/shopify_api/resources/customer_group.rb
|
160
174
|
- lib/shopify_api/resources/customer_saved_search.rb
|
161
175
|
- lib/shopify_api/resources/discount.rb
|
176
|
+
- lib/shopify_api/resources/draft_order.rb
|
177
|
+
- lib/shopify_api/resources/draft_order_invoice.rb
|
162
178
|
- lib/shopify_api/resources/event.rb
|
163
179
|
- lib/shopify_api/resources/fulfillment.rb
|
164
180
|
- lib/shopify_api/resources/fulfillment_event.rb
|
@@ -223,6 +239,7 @@ files:
|
|
223
239
|
- test/customer_test.rb
|
224
240
|
- test/detailed_log_subscriber_test.rb
|
225
241
|
- test/discount_test.rb
|
242
|
+
- test/draft_order_test.rb
|
226
243
|
- test/fixtures/access_token_delegate.json
|
227
244
|
- test/fixtures/application_charge.json
|
228
245
|
- test/fixtures/application_charges.json
|
@@ -251,6 +268,9 @@ files:
|
|
251
268
|
- test/fixtures/discount.json
|
252
269
|
- test/fixtures/discount_disabled.json
|
253
270
|
- test/fixtures/discounts.json
|
271
|
+
- test/fixtures/draft_order.json
|
272
|
+
- test/fixtures/draft_order_invoice.json
|
273
|
+
- test/fixtures/draft_orders.json
|
254
274
|
- test/fixtures/events.json
|
255
275
|
- test/fixtures/fulfillment.json
|
256
276
|
- test/fixtures/fulfillment_event.json
|