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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 386841f534181fd55d441707f8390b584eec290b
4
- data.tar.gz: 6340fe9f3b19364404d631e2c2b5e0183ba393fe
3
+ metadata.gz: a735be75b2ea0f7c4762ee44ca3a8c2a2a2a4d22
4
+ data.tar.gz: 6a8927da2492a41f0c3454925cae1c4ab1d2caeb
5
5
  SHA512:
6
- metadata.gz: 9fc10a61d17cb844293e3ab24b96164c6ed8b2a6c07df16ca92360886564122aaeea75326076fdbf056444753749b850c99bd5bfca44079cd13228ab1a5f8e0b
7
- data.tar.gz: 1dba2ca970417275864f71ab97f136423801104c69f3e3c18a1643964564cf0520e1b35a41fdebf1cbbe8be8a3ea2d773f3215649ba780e99fe6616227d3833c
6
+ metadata.gz: 1d894337c5a7297f935866ad69749e118e68959402dfd8ec276241a2af31adab03c8937c074bb5b10360e3ddf05221cddb52d445989bcfab323667c5ab42c176
7
+ data.tar.gz: df8fec33376e1dc2b85660af9bd6d097fa251a15225011f0f840023ebc3be1125032dca99dc932250970a1f9754168e0b33ca3350e5c62f017b44d7a105b2b8a
data/CHANGELOG CHANGED
@@ -1,3 +1,9 @@
1
+ == Version 4.3.5
2
+
3
+ * Added support for online mode access tokens, token expiry, and associated_user information.
4
+ * Added `ShopifyAPI::DraftOrder`
5
+ * Added `ShopifyAPI::DraftOrderInvoice`
6
+
1
7
  == Version 4.3.4
2
8
 
3
9
  * Added `ShopifyAPI::ProductListing`
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
- ```ruby
71
- ShopifyAPI::Session.setup({:api_key => API_KEY, :secret => SHARED_SECRET})
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``– Required – The API key for your app
83
- * ``scope`` – Required – The list of required scopes (explained here: http://docs.shopify.com/api/tutorials/oauth)
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. For future sessions simply pass the token in when creating the session object:
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
@@ -0,0 +1,4 @@
1
+ module ShopifyAPI
2
+ class DraftOrderInvoice < Base
3
+ end
4
+ end
@@ -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
- code = params['code']
89
-
90
- response = access_token_request(code)
91
-
89
+ response = access_token_request(params['code'])
92
90
  if response.code == "200"
93
- token = JSON.parse(response.body)['access_token']
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('&'))
@@ -1,3 +1,3 @@
1
1
  module ShopifyAPI
2
- VERSION = "4.3.4"
2
+ VERSION = "4.3.5"
3
3
  end
@@ -32,4 +32,5 @@ Gem::Specification.new do |s|
32
32
  s.add_development_dependency("fakeweb")
33
33
  s.add_development_dependency("minitest", ">= 4.0")
34
34
  s.add_development_dependency("rake")
35
+ s.add_development_dependency("timecop")
35
36
  end
@@ -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,9 @@
1
+ {
2
+ "draft_order_invoice": {
3
+ "to": "paul.norman@example.com",
4
+ "from": "steve@apple.com",
5
+ "subject": "Here is your new order invoice!",
6
+ "custom_message": "This is a test custom message.",
7
+ "bcc": [ ]
8
+ }
9
+ }
@@ -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/695890229", method: :get, body: load_fixture('fulfillment_request')
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
- fake "orders/450789469/fulfillment_requests/695890229/mark_as_failed", method: :put, body: ActiveSupport::JSON.encode(cancelled)
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
@@ -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
- params = {:code => 'any-code', :timestamp => Time.now}
145
- sorted_params = make_sorted_params(params)
146
- signature = OpenSSL::HMAC.hexdigest(OpenSSL::Digest::SHA256.new(), ShopifyAPI::Session.secret, sorted_params)
147
- fake nil, :url => 'https://testshop.myshopify.com/admin/oauth/access_token',:method => :post, :body => '{"access_token" : "any-token"}'
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
- token = session.request_token(params.merge(:hmac => signature))
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
- sorted_params = make_sorted_params(params)
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
- sorted_params = make_sorted_params(params)
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
- now = Time.now
177
- params = {"code" => "any-code", "timestamp" => now}
178
- sorted_params = make_sorted_params(params)
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'] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ShopifyAPI::Session.secret, to_sign)
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'] = OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), ShopifyAPI::Session.secret, to_sign)
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
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-01-12 00:00:00.000000000 Z
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