shopify_app 6.2.1 → 6.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG +12 -0
- data/CONTRIBUTING.md +14 -0
- data/README.md +38 -32
- data/RELEASING +1 -4
- data/lib/generators/shopify_app/install/install_generator.rb +1 -0
- data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +13 -0
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_ready_script.html +1 -1
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +4 -4
- data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
- data/lib/generators/shopify_app/shop_model/templates/shopify_session_repository.rb +2 -2
- data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
- data/lib/shopify_app.rb +6 -0
- data/lib/shopify_app/configuration.rb +5 -0
- data/lib/shopify_app/session_storage.rb +23 -0
- data/lib/shopify_app/sessions_controller.rb +6 -1
- data/lib/shopify_app/version.rb +1 -1
- data/lib/shopify_app/webhooks_controller.rb +35 -0
- data/lib/shopify_app/webhooks_manager.rb +68 -0
- data/lib/shopify_app/webhooks_manager_job.rb +11 -0
- data/test/controllers/sessions_controller_test.rb +22 -0
- data/test/generators/install_generator_test.rb +1 -0
- data/test/generators/shop_model_generator_test.rb +7 -8
- data/test/shopify_app/configuration_test.rb +20 -0
- data/test/shopify_app/webhooks_controller_test.rb +50 -0
- data/test/shopify_app/webhooks_manager_test.rb +86 -0
- metadata +11 -3
- data/lib/generators/shopify_app/shop_model/templates/session_storage.rb +0 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0b424982cf65b0c9b98ba51dd08c9c6a4ce992c3
|
4
|
+
data.tar.gz: 10e9c18f779eec012ab057afd1c5e2d220900aa3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 439c8119016ab19b45dee3dcab077626438cea6d7c16c151e5c06cf441a7d3131e8660c29939f4e303a164412b7dbe048e8ba8bf43e77e6e851ebc0f0ad186ea
|
7
|
+
data.tar.gz: cbfdb0d91cdd3af92b08bff988f1389f85ac348e923a63c8d43d7e98007835593b432d6240dcf4248de6ce9d05fa65e42623ca9b9b3559ca1560f58903b0bbe7
|
data/CHANGELOG
CHANGED
@@ -1,3 +1,15 @@
|
|
1
|
+
6.3.0
|
2
|
+
-----
|
3
|
+
|
4
|
+
* Move SessionStorage from a generated class to a re-usable module. To
|
5
|
+
migrate to the new module, delete the old generated SessionStorage class
|
6
|
+
in the models directory and include the SessionStorage module in your Shop model.
|
7
|
+
* Adds a WebhooksManager class and allows you to configure what webhooks your app
|
8
|
+
needs. The rest is taken care of by ShopifyApp provided you set up a backgroud queue
|
9
|
+
with ActiveJob
|
10
|
+
* Adds a WebhooksController module which can be included to handle the boiler plate code
|
11
|
+
for a controller receiving webhooks from Shopify
|
12
|
+
|
1
13
|
6.2.1
|
2
14
|
-----
|
3
15
|
|
data/CONTRIBUTING.md
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Submitting Issues
|
2
|
+
-----------------
|
3
|
+
|
4
|
+
Please open an issue here if you encounter a specific bug with this gem or the generators
|
5
|
+
|
6
|
+
General questions about the Shopify API should be posted on the [Shopify forums](https://ecommerce.shopify.com/c/shopify-apis-and-technology).
|
7
|
+
|
8
|
+
|
9
|
+
Authentication Issues
|
10
|
+
---------------------
|
11
|
+
|
12
|
+
A great deal of the issues surrounding this repo are around authenticating the generated app with Shopify.
|
13
|
+
|
14
|
+
If you are experiencing issues with your app authenticating the best way to get help fast is to create a repo with the minimal amount of code to demonstrate the issue and a clearly documented set of steps you took to arrive there. This will help us solve your problem quicker since we won't need to spend any time figuring out how to reproduce the bug.
|
data/README.md
CHANGED
@@ -25,10 +25,20 @@ If you don't have a Shopify Partner account yet head over to http://shopify.com/
|
|
25
25
|
|
26
26
|
Once you have a Partner account create a new application to get an Api key and other Api credentials. To create a development application set the Application Callback URL to
|
27
27
|
|
28
|
-
|
28
|
+
```
|
29
|
+
http://localhost:3000/login
|
30
|
+
```
|
31
|
+
|
32
|
+
and the `redirect_uri` to
|
33
|
+
|
34
|
+
```
|
35
|
+
http://localhost:3000/auth/shopify/callback
|
36
|
+
```
|
29
37
|
|
30
38
|
This way you'll be able to run the app on your local machine.
|
31
39
|
|
40
|
+
Also note, ShopifyApp creates embedded apps by default, so remember to check `enabled` for the embedded settings.
|
41
|
+
|
32
42
|
|
33
43
|
Installation
|
34
44
|
------------
|
@@ -108,50 +118,44 @@ ShopifyApp.configure do |config|
|
|
108
118
|
end
|
109
119
|
```
|
110
120
|
|
111
|
-
ShopifyApp::SessionRepository
|
112
|
-
-----------------------------
|
113
121
|
|
114
|
-
|
115
|
-
|
116
|
-
you are using ActiveRecord, then all you need to implement is `self.store(shopify_session)` and
|
117
|
-
`self.retrieve(id)` in order to store the record on disk or retrieve it for use at a later point.
|
118
|
-
It is imperative that your store method returns the identifier for the session. Typically this is
|
119
|
-
just the record ID.
|
122
|
+
WebhooksManager
|
123
|
+
---------------
|
120
124
|
|
121
|
-
|
125
|
+
ShopifyApp can manage your app's webhooks for you (requires ActiveJob). Set which webhooks you require in the initializer:
|
122
126
|
|
123
127
|
```ruby
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
shop.id
|
129
|
-
end
|
130
|
-
|
131
|
-
def self.retrieve(id)
|
132
|
-
if shop = self.where(id: id).first
|
133
|
-
ShopifyAPI::Session.new(shop.domain, shop.token)
|
134
|
-
end
|
135
|
-
end
|
128
|
+
ShopifyApp.configure do |config|
|
129
|
+
config.webhooks = [
|
130
|
+
{topic: 'carts/update', address: 'example-app.com/webhooks'}
|
131
|
+
]
|
136
132
|
end
|
137
133
|
```
|
138
134
|
|
139
|
-
|
140
|
-
they won't be sharing the static data that would be required in case your user gets directed to a
|
141
|
-
different server by your load balancer.
|
142
|
-
|
143
|
-
The in memory store also does not behave well on Heroku because the session data would be destroyed
|
144
|
-
when a dyno is killed due to inactivity.
|
135
|
+
When the oauth callback is completed successfully ShopifyApp will queue a background job which will ensure all the specified webhooks exist for that shop. Because this runs on every oauth callback it means your app will always have the webhooks it needs even if the user uninstalls and re-installs the app.
|
145
136
|
|
146
|
-
|
147
|
-
`config/initializers/shopify_session_repository.rb` to use the correct model.
|
137
|
+
There is also a WebhooksController module that you can include in a controller that receives Shopify webhooks. For example:
|
148
138
|
|
149
139
|
```ruby
|
150
|
-
|
140
|
+
class WebhooksController < ApplicationController
|
141
|
+
include ShopifyApp::WebhooksController
|
142
|
+
|
143
|
+
def carts_update
|
144
|
+
SomeJob.perform_later(shopify_domain: shop_domain)
|
145
|
+
head :ok
|
146
|
+
end
|
147
|
+
end
|
151
148
|
```
|
152
149
|
|
153
|
-
|
150
|
+
The module skips the `verify_authenticity_token` before_action and adds an action to verify that the webhook came from Shopify.
|
151
|
+
|
154
152
|
|
153
|
+
ShopifyApp::SessionRepository
|
154
|
+
-----------------------------
|
155
|
+
|
156
|
+
`ShopifyApp::SessionRepository` allows you as a developer to define how your sessions are retrieved and stored for a shop. The `SessionRepository` is configured using the `config/initializers/shopify_session_repository.rb` file and can be set to any object the implements `self.store(shopify_session)` which stores the session and returns a unique identifier and `self.retrieve(id)` which returns a `ShopifyAPI::Session` for the passed id. See either the `InMemorySessionStore` or the `SessionStorage` module for examples.
|
157
|
+
|
158
|
+
If you only run the install generator then by default you will have an in memory store but it **won't work** on multi-server environments including Heroku. If you ran all the generators including the shop_model generator then the Shop model itself will be the `SessionRepository`. If you look at the implementation of the generated shop model you'll see that this gem provides an activerecord mixin for the `SessionRepository`. You can use this mixin on any model that responds to `shopify_domain` and `shopify_token`.
|
155
159
|
|
156
160
|
AuthenticatedController
|
157
161
|
-----------------------
|
@@ -164,9 +168,11 @@ Troubleshooting
|
|
164
168
|
### Generator shopify_app:install hangs
|
165
169
|
|
166
170
|
Rails uses spring by default to speed up development. To run the generator, spring has to be stopped:
|
171
|
+
|
167
172
|
```sh
|
168
173
|
$ bundle exec spring stop
|
169
174
|
```
|
175
|
+
|
170
176
|
Run shopify_app generator again.
|
171
177
|
|
172
178
|
|
data/RELEASING
CHANGED
@@ -10,7 +10,4 @@ Releasing ShopifyApp
|
|
10
10
|
$ git push
|
11
11
|
7. Push out the tags
|
12
12
|
$ git push --tags
|
13
|
-
8.
|
14
|
-
$ gem build shopify_app.gemspec
|
15
|
-
9. Publish the Gem to gemcutter
|
16
|
-
$ gem push shopify_app-X.Y.Z.gem
|
13
|
+
8. Use Shipit to build and push the gem
|
@@ -0,0 +1,13 @@
|
|
1
|
+
<script type="text/javascript">
|
2
|
+
var eventName = typeof(Turbolinks) !== 'undefined' ? 'page:change' : 'DOMContentLoaded';
|
3
|
+
|
4
|
+
document.addEventListener(eventName, function() {
|
5
|
+
<% if flash[:notice] %>
|
6
|
+
ShopifyApp.flashNotice("<%= j flash[:notice].html_safe %>");
|
7
|
+
<% end %>
|
8
|
+
|
9
|
+
<% if flash[:error] %>
|
10
|
+
ShopifyApp.flashError("<%= j flash[:error].html_safe %>");
|
11
|
+
<% end %>
|
12
|
+
});
|
13
|
+
</script>
|
@@ -15,14 +15,14 @@ module ShopifyApp
|
|
15
15
|
copy_migration 'create_shops.rb'
|
16
16
|
end
|
17
17
|
|
18
|
-
def create_session_storage
|
19
|
-
copy_file 'session_storage.rb', 'app/models/session_storage.rb'
|
20
|
-
end
|
21
|
-
|
22
18
|
def create_session_storage_initializer
|
23
19
|
copy_file 'shopify_session_repository.rb', 'config/initializers/shopify_session_repository.rb', force: true
|
24
20
|
end
|
25
21
|
|
22
|
+
def create_shop_fixtures
|
23
|
+
copy_file 'shops.yml', 'test/fixtures/shops.yml'
|
24
|
+
end
|
25
|
+
|
26
26
|
private
|
27
27
|
|
28
28
|
def copy_migration(migration_name, config = {})
|
@@ -1,7 +1,7 @@
|
|
1
1
|
if Rails.configuration.cache_classes
|
2
|
-
ShopifyApp::SessionRepository.storage =
|
2
|
+
ShopifyApp::SessionRepository.storage = Shop
|
3
3
|
else
|
4
4
|
ActionDispatch::Reloader.to_prepare do
|
5
|
-
ShopifyApp::SessionRepository.storage =
|
5
|
+
ShopifyApp::SessionRepository.storage = Shop
|
6
6
|
end
|
7
7
|
end
|
data/lib/shopify_app.rb
CHANGED
@@ -10,11 +10,17 @@ require 'shopify_app/configuration'
|
|
10
10
|
# engine
|
11
11
|
require 'shopify_app/engine'
|
12
12
|
|
13
|
+
# jobs
|
14
|
+
require 'shopify_app/webhooks_manager_job'
|
15
|
+
|
13
16
|
# helpers and concerns
|
14
17
|
require 'shopify_app/shop'
|
18
|
+
require 'shopify_app/session_storage'
|
15
19
|
require 'shopify_app/controller'
|
16
20
|
require 'shopify_app/sessions_controller'
|
17
21
|
require 'shopify_app/login_protection'
|
22
|
+
require 'shopify_app/webhooks_manager'
|
23
|
+
require 'shopify_app/webhooks_controller'
|
18
24
|
require 'shopify_app/utils'
|
19
25
|
|
20
26
|
# session repository
|
@@ -10,6 +10,7 @@ module ShopifyApp
|
|
10
10
|
attr_accessor :scope
|
11
11
|
attr_accessor :embedded_app
|
12
12
|
alias_method :embedded_app?, :embedded_app
|
13
|
+
attr_accessor :webhooks
|
13
14
|
|
14
15
|
# configure myshopify domain for local shopify development
|
15
16
|
attr_accessor :myshopify_domain
|
@@ -17,6 +18,10 @@ module ShopifyApp
|
|
17
18
|
def initialize
|
18
19
|
@myshopify_domain = 'myshopify.com'
|
19
20
|
end
|
21
|
+
|
22
|
+
def has_webhooks?
|
23
|
+
webhooks.present?
|
24
|
+
end
|
20
25
|
end
|
21
26
|
|
22
27
|
def self.configuration
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module ShopifyApp
|
2
|
+
module SessionStorage
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
class_methods do
|
6
|
+
def store(session)
|
7
|
+
shop = self.find_or_initialize_by(shopify_domain: session.url)
|
8
|
+
shop.shopify_token = session.token
|
9
|
+
shop.save!
|
10
|
+
shop.id
|
11
|
+
end
|
12
|
+
|
13
|
+
def retrieve(id)
|
14
|
+
return unless id
|
15
|
+
|
16
|
+
if shop = self.find_by(id: id)
|
17
|
+
ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
@@ -13,9 +13,14 @@ module ShopifyApp
|
|
13
13
|
def callback
|
14
14
|
if response = request.env['omniauth.auth']
|
15
15
|
shop_name = response.uid
|
16
|
-
|
16
|
+
token = response['credentials']['token']
|
17
|
+
|
18
|
+
sess = ShopifyAPI::Session.new(shop_name, token)
|
17
19
|
session[:shopify] = ShopifyApp::SessionRepository.store(sess)
|
18
20
|
session[:shopify_domain] = shop_name
|
21
|
+
|
22
|
+
WebhooksManager.queue(shop_name, token) if ShopifyApp.configuration.has_webhooks?
|
23
|
+
|
19
24
|
flash[:notice] = "Logged in"
|
20
25
|
redirect_to return_address
|
21
26
|
else
|
data/lib/shopify_app/version.rb
CHANGED
@@ -0,0 +1,35 @@
|
|
1
|
+
module ShopifyApp
|
2
|
+
module WebhooksController
|
3
|
+
extend ActiveSupport::Concern
|
4
|
+
|
5
|
+
included do
|
6
|
+
skip_before_action :verify_authenticity_token
|
7
|
+
before_action :verify_request
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
def verify_request
|
13
|
+
request.body.rewind
|
14
|
+
data = request.body.read
|
15
|
+
|
16
|
+
unless validate_hmac(ShopifyApp.configuration.secret, data)
|
17
|
+
head :unauthorized
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def validate_hmac(secret, data)
|
22
|
+
digest = OpenSSL::Digest.new('sha256')
|
23
|
+
shopify_hmac == Base64.encode64(OpenSSL::HMAC.digest(digest, secret, data)).strip
|
24
|
+
end
|
25
|
+
|
26
|
+
def shop_domain
|
27
|
+
request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
|
28
|
+
end
|
29
|
+
|
30
|
+
def shopify_hmac
|
31
|
+
request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
module ShopifyApp
|
2
|
+
class WebhooksManager
|
3
|
+
class CreationFailed < StandardError; end
|
4
|
+
|
5
|
+
def self.queue(shop_name, token)
|
6
|
+
ShopifyApp::WebhooksManagerJob.perform_later(shop_name: shop_name, token: token)
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(shop_name, token)
|
10
|
+
@shop_name, @token = shop_name, token
|
11
|
+
end
|
12
|
+
|
13
|
+
def recreate_webhooks!
|
14
|
+
destroy_webhooks
|
15
|
+
create_webhooks
|
16
|
+
end
|
17
|
+
|
18
|
+
def create_webhooks
|
19
|
+
return unless required_webhooks.present?
|
20
|
+
|
21
|
+
with_shopify_session do
|
22
|
+
required_webhooks.each do |webhook|
|
23
|
+
create_webhook(webhook) unless webhook_exists?(webhook[:topic])
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def destroy_webhooks
|
29
|
+
with_shopify_session do
|
30
|
+
ShopifyAPI::Webhook.all.each do |webhook|
|
31
|
+
ShopifyAPI::Webhook.delete(webhook.id) if is_required_webhook?(webhook)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
@current_webhooks = nil
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def required_webhooks
|
40
|
+
ShopifyApp.configuration.webhooks
|
41
|
+
end
|
42
|
+
|
43
|
+
def is_required_webhook?(webhook)
|
44
|
+
required_webhooks.map{ |w| w[:address] }.include? webhook.address
|
45
|
+
end
|
46
|
+
|
47
|
+
def with_shopify_session
|
48
|
+
ShopifyAPI::Session.temp(@shop_name, @token) do
|
49
|
+
yield
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def create_webhook(attributes)
|
54
|
+
attributes.reverse_merge!(format: 'json')
|
55
|
+
webhook = ShopifyAPI::Webhook.create(attributes)
|
56
|
+
raise CreationFailed unless webhook.persisted?
|
57
|
+
webhook
|
58
|
+
end
|
59
|
+
|
60
|
+
def webhook_exists?(topic)
|
61
|
+
current_webhooks[topic]
|
62
|
+
end
|
63
|
+
|
64
|
+
def current_webhooks
|
65
|
+
@current_webhooks ||= ShopifyAPI::Webhook.all.index_by(&:topic)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
@@ -76,6 +76,28 @@ class SessionsControllerTest < ActionController::TestCase
|
|
76
76
|
assert_equal 'shop.myshopify.com', session[:shopify_domain]
|
77
77
|
end
|
78
78
|
|
79
|
+
test "#callback should start the WebhooksManager if webhooks are configured" do
|
80
|
+
ShopifyApp.configure do |config|
|
81
|
+
config.webhooks = [{topic: 'carts/update', address: 'example-app.com/webhooks'}]
|
82
|
+
end
|
83
|
+
|
84
|
+
ShopifyApp::WebhooksManager.expects(:queue)
|
85
|
+
|
86
|
+
mock_shopify_omniauth
|
87
|
+
get :callback, shop: 'shop'
|
88
|
+
end
|
89
|
+
|
90
|
+
test "#callback doesn't run the WebhooksManager if no webhooks are configured" do
|
91
|
+
ShopifyApp.configure do |config|
|
92
|
+
config.webhooks = []
|
93
|
+
end
|
94
|
+
|
95
|
+
ShopifyApp::WebhooksManager.expects(:queue).never
|
96
|
+
|
97
|
+
mock_shopify_omniauth
|
98
|
+
get :callback, shop: 'shop'
|
99
|
+
end
|
100
|
+
|
79
101
|
test "#destroy should clear shopify from session and redirect to login with notice" do
|
80
102
|
shop_id = 1
|
81
103
|
session[:shopify] = shop_id
|
@@ -73,6 +73,7 @@ class InstallGeneratorTest < Rails::Generators::TestCase
|
|
73
73
|
test "creates the embedded_app layout" do
|
74
74
|
run_generator
|
75
75
|
assert_file "app/views/layouts/embedded_app.html.erb"
|
76
|
+
assert_file "app/views/layouts/_flash_messages.html.erb"
|
76
77
|
end
|
77
78
|
|
78
79
|
test "creates the home controller" do
|
@@ -11,6 +11,7 @@ class ShopModelGeneratorTest < Rails::Generators::TestCase
|
|
11
11
|
assert_file "app/models/shop.rb" do |shop|
|
12
12
|
assert_match "class Shop < ActiveRecord::Base", shop
|
13
13
|
assert_match "include ShopifyApp::Shop", shop
|
14
|
+
assert_match "include ShopifyApp::SessionStorage", shop
|
14
15
|
end
|
15
16
|
end
|
16
17
|
|
@@ -21,19 +22,17 @@ class ShopModelGeneratorTest < Rails::Generators::TestCase
|
|
21
22
|
end
|
22
23
|
end
|
23
24
|
|
24
|
-
test "adds the
|
25
|
+
test "adds the shopify_session_repository initializer" do
|
25
26
|
run_generator
|
26
|
-
assert_file "
|
27
|
-
assert_match "
|
28
|
-
assert_match "def self.store(session)", session_storage
|
29
|
-
assert_match " def self.retrieve(id)", session_storage
|
27
|
+
assert_file "config/initializers/shopify_session_repository.rb" do |file|
|
28
|
+
assert_match "ShopifyApp::SessionRepository.storage = Shop", file
|
30
29
|
end
|
31
30
|
end
|
32
31
|
|
33
|
-
test "
|
32
|
+
test "creates default shop fixtures" do
|
34
33
|
run_generator
|
35
|
-
assert_file "
|
36
|
-
assert_match "
|
34
|
+
assert_file "test/fixtures/shops.yml" do |file|
|
35
|
+
assert_match "regular_shop:", file
|
37
36
|
end
|
38
37
|
end
|
39
38
|
|
@@ -26,4 +26,24 @@ class ConfigurationTest < ActiveSupport::TestCase
|
|
26
26
|
assert_equal "myshopify.io", ShopifyApp.configuration.myshopify_domain
|
27
27
|
end
|
28
28
|
|
29
|
+
test "can configure webhooks for creation" do
|
30
|
+
webhook = {topic: 'carts/update', address: 'example-app.com/webhooks', format: 'json'}
|
31
|
+
|
32
|
+
ShopifyApp.configure do |config|
|
33
|
+
config.webhooks = [webhook]
|
34
|
+
end
|
35
|
+
|
36
|
+
assert_equal webhook, ShopifyApp.configuration.webhooks.first
|
37
|
+
end
|
38
|
+
|
39
|
+
test "has_webhooks? is true if webhooks have been configured" do
|
40
|
+
refute ShopifyApp.configuration.has_webhooks?
|
41
|
+
|
42
|
+
ShopifyApp.configure do |config|
|
43
|
+
config.webhooks = [{topic: 'carts/update', address: 'example-app.com/webhooks', format: 'json'}]
|
44
|
+
end
|
45
|
+
|
46
|
+
assert ShopifyApp.configuration.has_webhooks?
|
47
|
+
end
|
48
|
+
|
29
49
|
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
require 'action_controller'
|
3
|
+
require 'action_controller/base'
|
4
|
+
|
5
|
+
class WebhooksController < ActionController::Base
|
6
|
+
include ShopifyApp::WebhooksController
|
7
|
+
def carts_update
|
8
|
+
head :ok
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
class WebhooksControllerTest < ActionController::TestCase
|
13
|
+
tests WebhooksController
|
14
|
+
|
15
|
+
setup do
|
16
|
+
ShopifyApp::SessionRepository.storage = InMemorySessionStore
|
17
|
+
ShopifyApp.configure do |config|
|
18
|
+
config.secret = 'secret'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
test "#carts_update should verify request" do
|
23
|
+
with_application_test_routes do
|
24
|
+
data = {foo: :bar}.to_json
|
25
|
+
@controller.expects(:validate_hmac).with('secret', data.to_s).returns(true)
|
26
|
+
post :carts_update, data
|
27
|
+
assert_response :ok
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
test "un-verified request returns unauthorized" do
|
32
|
+
with_application_test_routes do
|
33
|
+
data = {foo: :bar}.to_json
|
34
|
+
@controller.expects(:validate_hmac).with('secret', data.to_s).returns(false)
|
35
|
+
post :carts_update, data
|
36
|
+
assert_response :unauthorized
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def with_application_test_routes
|
43
|
+
with_routing do |set|
|
44
|
+
set.draw do
|
45
|
+
get '/carts_update' => 'webhooks#carts_update'
|
46
|
+
end
|
47
|
+
yield
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ShopifyApp::WebhooksManagerTest < ActiveSupport::TestCase
|
4
|
+
|
5
|
+
setup do
|
6
|
+
ShopifyApp.configure do |config|
|
7
|
+
config.webhooks = [
|
8
|
+
{topic: 'app/uninstalled', address: "https://example-app.com/webhooks/app_uninstalled"},
|
9
|
+
{topic: 'orders/create', address: "https://example-app.com/webhooks/order_create"},
|
10
|
+
{topic: 'orders/updated', address: "https://example-app.com/webhooks/order_updated"},
|
11
|
+
]
|
12
|
+
end
|
13
|
+
|
14
|
+
@manager = ShopifyApp::WebhooksManager.new("regular-shop.myshopify.com", "token")
|
15
|
+
end
|
16
|
+
|
17
|
+
test "#create_webhooks makes calls to create webhooks" do
|
18
|
+
ShopifyAPI::Webhook.stubs(all: [])
|
19
|
+
|
20
|
+
expect_webhook_creation('app/uninstalled', "https://example-app.com/webhooks/app_uninstalled")
|
21
|
+
expect_webhook_creation('orders/create', "https://example-app.com/webhooks/order_create")
|
22
|
+
expect_webhook_creation('orders/updated', "https://example-app.com/webhooks/order_updated")
|
23
|
+
|
24
|
+
@manager.create_webhooks
|
25
|
+
end
|
26
|
+
|
27
|
+
test "#create_webhooks when creating a webhook fails, raises an error" do
|
28
|
+
ShopifyAPI::Webhook.stubs(all: [])
|
29
|
+
webhook = stub(persisted?: false)
|
30
|
+
ShopifyAPI::Webhook.stubs(create: webhook)
|
31
|
+
|
32
|
+
assert_raise ShopifyApp::WebhooksManager::CreationFailed do
|
33
|
+
@manager.create_webhooks
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
test "#create_webhooks when creating a webhook fails and the webhook exists, do not raise an error" do
|
38
|
+
webhook = stub(persisted?: false)
|
39
|
+
webhooks = all_webhook_topics.map{|t| stub(topic: t)}
|
40
|
+
ShopifyAPI::Webhook.stubs(create: webhook, all: webhooks)
|
41
|
+
|
42
|
+
assert_nothing_raised ShopifyApp::WebhooksManager::CreationFailed do
|
43
|
+
@manager.create_webhooks
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
test "#recreate_webhooks! destroys all webhooks and recreates" do
|
48
|
+
@manager.expects(:destroy_webhooks)
|
49
|
+
@manager.expects(:create_webhooks)
|
50
|
+
|
51
|
+
@manager.recreate_webhooks!
|
52
|
+
end
|
53
|
+
|
54
|
+
test "#destroy_webhooks makes calls to destroy webhooks" do
|
55
|
+
ShopifyAPI::Webhook.stubs(:all).returns(Array.wrap(all_mock_webhooks.first))
|
56
|
+
ShopifyAPI::Webhook.expects(:delete).with(all_mock_webhooks.first.id)
|
57
|
+
|
58
|
+
@manager.destroy_webhooks
|
59
|
+
end
|
60
|
+
|
61
|
+
test "#destroy_webhooks does not destroy webhooks that do not have a matching address" do
|
62
|
+
ShopifyAPI::Webhook.stubs(:all).returns([stub(address: 'http://something-or-the-other.com/webhooks/product_update', id: 7214109)])
|
63
|
+
ShopifyAPI::Webhook.expects(:delete).never
|
64
|
+
|
65
|
+
@manager.destroy_webhooks
|
66
|
+
end
|
67
|
+
|
68
|
+
private
|
69
|
+
|
70
|
+
def expect_webhook_creation(topic, address)
|
71
|
+
stub_webhook = stub(persisted?: true)
|
72
|
+
ShopifyAPI::Webhook.expects(:create).with(topic: topic, address: address, format: 'json').returns(stub_webhook)
|
73
|
+
end
|
74
|
+
|
75
|
+
def all_webhook_topics
|
76
|
+
@webhooks ||= ['app/uninstalled', 'orders/create', 'orders/updated']
|
77
|
+
end
|
78
|
+
|
79
|
+
def all_mock_webhooks
|
80
|
+
[
|
81
|
+
stub(id: 1, address: "https://example-app.com/webhooks/app_uninstalled", topic: 'app/uninstalled'),
|
82
|
+
stub(id: 2, address: "https://example-app.com/webhooks/order_create", topic: 'orders/create'),
|
83
|
+
stub(id: 3, address: "https://example-app.com/webhooks/order_updated", topic: 'orders/updated'),
|
84
|
+
]
|
85
|
+
end
|
86
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: shopify_app
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 6.
|
4
|
+
version: 6.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Shopify
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-
|
11
|
+
date: 2015-11-06 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -137,6 +137,7 @@ files:
|
|
137
137
|
- ".gitignore"
|
138
138
|
- ".travis.yml"
|
139
139
|
- CHANGELOG
|
140
|
+
- CONTRIBUTING.md
|
140
141
|
- Gemfile
|
141
142
|
- LICENSE
|
142
143
|
- README.md
|
@@ -148,6 +149,7 @@ files:
|
|
148
149
|
- config/routes.rb
|
149
150
|
- lib/generators/shopify_app/controllers/controllers_generator.rb
|
150
151
|
- lib/generators/shopify_app/install/install_generator.rb
|
152
|
+
- lib/generators/shopify_app/install/templates/_flash_messages.html.erb
|
151
153
|
- lib/generators/shopify_app/install/templates/embedded_app.html.erb
|
152
154
|
- lib/generators/shopify_app/install/templates/home_controller.rb
|
153
155
|
- lib/generators/shopify_app/install/templates/index.html.erb
|
@@ -160,9 +162,9 @@ files:
|
|
160
162
|
- lib/generators/shopify_app/routes/templates/routes.rb
|
161
163
|
- lib/generators/shopify_app/shop_model/shop_model_generator.rb
|
162
164
|
- lib/generators/shopify_app/shop_model/templates/db/migrate/create_shops.rb
|
163
|
-
- lib/generators/shopify_app/shop_model/templates/session_storage.rb
|
164
165
|
- lib/generators/shopify_app/shop_model/templates/shop.rb
|
165
166
|
- lib/generators/shopify_app/shop_model/templates/shopify_session_repository.rb
|
167
|
+
- lib/generators/shopify_app/shop_model/templates/shops.yml
|
166
168
|
- lib/generators/shopify_app/shopify_app_generator.rb
|
167
169
|
- lib/generators/shopify_app/views/views_generator.rb
|
168
170
|
- lib/shopify_app.rb
|
@@ -171,11 +173,15 @@ files:
|
|
171
173
|
- lib/shopify_app/engine.rb
|
172
174
|
- lib/shopify_app/in_memory_session_store.rb
|
173
175
|
- lib/shopify_app/login_protection.rb
|
176
|
+
- lib/shopify_app/session_storage.rb
|
174
177
|
- lib/shopify_app/sessions_controller.rb
|
175
178
|
- lib/shopify_app/shop.rb
|
176
179
|
- lib/shopify_app/shopify_session_repository.rb
|
177
180
|
- lib/shopify_app/utils.rb
|
178
181
|
- lib/shopify_app/version.rb
|
182
|
+
- lib/shopify_app/webhooks_controller.rb
|
183
|
+
- lib/shopify_app/webhooks_manager.rb
|
184
|
+
- lib/shopify_app/webhooks_manager_job.rb
|
179
185
|
- shipit.rubygems.yml
|
180
186
|
- shopify_app.gemspec
|
181
187
|
- test/app_templates/app/controllers/application_controller.rb
|
@@ -207,6 +213,8 @@ files:
|
|
207
213
|
- test/shopify_app/login_protection_test.rb
|
208
214
|
- test/shopify_app/shopify_session_repository_test.rb
|
209
215
|
- test/shopify_app/utils_test.rb
|
216
|
+
- test/shopify_app/webhooks_controller_test.rb
|
217
|
+
- test/shopify_app/webhooks_manager_test.rb
|
210
218
|
- test/support/generator_test_helpers.rb
|
211
219
|
- test/test_helper.rb
|
212
220
|
homepage:
|
@@ -1,16 +0,0 @@
|
|
1
|
-
class SessionStorage
|
2
|
-
def self.store(session)
|
3
|
-
shop = Shop.find_or_initialize_by(shopify_domain: session.url)
|
4
|
-
shop.shopify_token = session.token
|
5
|
-
shop.save!
|
6
|
-
shop.id
|
7
|
-
end
|
8
|
-
|
9
|
-
def self.retrieve(id)
|
10
|
-
return unless id
|
11
|
-
shop = Shop.find(id)
|
12
|
-
ShopifyAPI::Session.new(shop.shopify_domain, shop.shopify_token)
|
13
|
-
rescue ActiveRecord::RecordNotFound
|
14
|
-
nil
|
15
|
-
end
|
16
|
-
end
|