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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +12 -0
  3. data/CONTRIBUTING.md +14 -0
  4. data/README.md +38 -32
  5. data/RELEASING +1 -4
  6. data/lib/generators/shopify_app/install/install_generator.rb +1 -0
  7. data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +13 -0
  8. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -0
  9. data/lib/generators/shopify_app/install/templates/shopify_app_ready_script.html +1 -1
  10. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +4 -4
  11. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  12. data/lib/generators/shopify_app/shop_model/templates/shopify_session_repository.rb +2 -2
  13. data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
  14. data/lib/shopify_app.rb +6 -0
  15. data/lib/shopify_app/configuration.rb +5 -0
  16. data/lib/shopify_app/session_storage.rb +23 -0
  17. data/lib/shopify_app/sessions_controller.rb +6 -1
  18. data/lib/shopify_app/version.rb +1 -1
  19. data/lib/shopify_app/webhooks_controller.rb +35 -0
  20. data/lib/shopify_app/webhooks_manager.rb +68 -0
  21. data/lib/shopify_app/webhooks_manager_job.rb +11 -0
  22. data/test/controllers/sessions_controller_test.rb +22 -0
  23. data/test/generators/install_generator_test.rb +1 -0
  24. data/test/generators/shop_model_generator_test.rb +7 -8
  25. data/test/shopify_app/configuration_test.rb +20 -0
  26. data/test/shopify_app/webhooks_controller_test.rb +50 -0
  27. data/test/shopify_app/webhooks_manager_test.rb +86 -0
  28. metadata +11 -3
  29. 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: 24a872186951e1f4c991d937f7f2a30e798f357e
4
- data.tar.gz: 38c56b9a5e8255b6b59a425819b3390d5962ad7b
3
+ metadata.gz: 0b424982cf65b0c9b98ba51dd08c9c6a4ce992c3
4
+ data.tar.gz: 10e9c18f779eec012ab057afd1c5e2d220900aa3
5
5
  SHA512:
6
- metadata.gz: a2ccfb1a9ef061db062030078ee3015d9e832abedf8ba01a65eb826dc3a51c7f2f47787b3cf822c2bb4ac31b08b4c6dbc2ee2615981631d30459a7aff9cbe615
7
- data.tar.gz: 1b7dee2021821fe6182b747b6392ae9887d746f33c86c8b41b292d3e4b041220c225d86c7df54c4ec48194e139c998d5f93548bbac26304f118b84ac8fbda889
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
- http://localhost:3000/login
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
- `ShopifyApp::SessionRepository` allows you as a developer to define how your sessions are retrieved and
115
- stored for a shop. This can simply be your `Shop` model that stores the API Token and shop name. If
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
- Your ActiveRecord model would look something like this:
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
- class Shop < ActiveRecord::Base
125
- def self.store(session)
126
- shop = self.new(domain: session.url, token: session.token)
127
- shop.save!
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
- By default you will have an in memory store but it **won't work** on multi-server environments since
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
- Changing the `ShopifyApp::SessionRepository.storage` can simply be done by editing
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
- ShopifyApp::SessionRepository.storage = 'Shop'
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
- If you run the `shop_model` generator it will create the required code to use the generated Shop model as the SessionRepository and update the initializer.
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. Build the new .gem from the updated .gemspec
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
@@ -63,6 +63,7 @@ module ShopifyApp
63
63
  def create_embedded_app_layout
64
64
  if embedded_app?
65
65
  copy_file 'embedded_app.html.erb', 'app/views/layouts/embedded_app.html.erb'
66
+ copy_file '_flash_messages.html.erb', 'app/views/layouts/_flash_messages.html.erb'
66
67
  end
67
68
  end
68
69
 
@@ -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>
@@ -19,5 +19,6 @@
19
19
 
20
20
  <body>
21
21
  <%= yield %>
22
+ <%= render 'layouts/flash_messages' %>
22
23
  </body>
23
24
  </html>
@@ -2,7 +2,7 @@
2
2
  ShopifyApp.ready(function(){
3
3
  ShopifyApp.Bar.initialize({
4
4
  title: "Home",
5
- icon: "<%= asset_path('faveicon.png') %>"
5
+ icon: "<%= asset_path('favicon.ico') %>"
6
6
  });
7
7
  });
8
8
  </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,3 +1,4 @@
1
1
  class Shop < ActiveRecord::Base
2
2
  include ShopifyApp::Shop
3
+ include ShopifyApp::SessionStorage
3
4
  end
@@ -1,7 +1,7 @@
1
1
  if Rails.configuration.cache_classes
2
- ShopifyApp::SessionRepository.storage = SessionStorage
2
+ ShopifyApp::SessionRepository.storage = Shop
3
3
  else
4
4
  ActionDispatch::Reloader.to_prepare do
5
- ShopifyApp::SessionRepository.storage = SessionStorage
5
+ ShopifyApp::SessionRepository.storage = Shop
6
6
  end
7
7
  end
@@ -0,0 +1,3 @@
1
+ regular_shop:
2
+ shopify_domain: 'regular-shop.myshopify.com'
3
+ shopify_token: 'token'
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
- sess = ShopifyAPI::Session.new(shop_name, response['credentials']['token'])
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
@@ -1,3 +1,3 @@
1
1
  module ShopifyApp
2
- VERSION = '6.2.1'
2
+ VERSION = '6.3.0'
3
3
  end
@@ -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
@@ -0,0 +1,11 @@
1
+ module ShopifyApp
2
+ class WebhooksManagerJob < ActiveJob::Base
3
+ def perform(params = {})
4
+ shop_name = params.fetch(:shop_name)
5
+ token = params.fetch(:token)
6
+
7
+ manager = WebhooksManager.new(shop_name, token)
8
+ manager.create_webhooks
9
+ end
10
+ end
11
+ 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 session_storage model" do
25
+ test "adds the shopify_session_repository initializer" do
25
26
  run_generator
26
- assert_file "app/models/session_storage.rb" do |session_storage|
27
- assert_match "class SessionStorage", session_storage
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 "adds the shopify_session_repository initializer" do
32
+ test "creates default shop fixtures" do
34
33
  run_generator
35
- assert_file "config/initializers/shopify_session_repository.rb" do |file|
36
- assert_match "ShopifyApp::SessionRepository.storage = SessionStorage", file
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.2.1
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-10-29 00:00:00.000000000 Z
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