diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 212bea42e..d28c43323 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Extract tag name id: tag - run: echo "::set-output name=value::${GITHUB_REF##*/}" + run: echo "value=${GITHUB_REF##*/}" >> "$GITHUB_OUTPUT" - uses: actions/checkout@v3 - name: Create Release diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f459af38..a9a4b93e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ Unreleased ---------- * Make type param for webhooks route optional. This will fix a bug with CLI initiated webhooks. -21.9.0 (January 16, 2023) +21.10.0 (January 24, 2024) +---------- +* Fix session deletion for users with customized session storage[#1773](https://github.com/Shopify/shopify_app/pull/1773) +* Add configuration flag `check_session_expiry_date` to trigger a re-auth when the (user) session is expired. The session expiry date must be stored and retrieved for this flag to be effective. When the `UserSessionStorageWithScopes` concern is used, a DB migration can be generated with `rails generate shopify_app:user_model --skip` and should be applied before enabling that flag[#1757](https://github.com/Shopify/shopify_app/pull/1757) + +21.9.0 (January 16, 2024) ---------- * Fix `add_webhook` generator to create the webhook jobs under the correct directory[#1748](https://github.com/Shopify/shopify_app/pull/1748) * Add support for metafield_namespaces in webhook registration [#1745](https://github.com/Shopify/shopify_app/pull/1745) diff --git a/Gemfile.lock b/Gemfile.lock index 1182a2da7..adc09cce4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ PATH remote: . specs: - shopify_app (21.9.0) + shopify_app (21.10.0) activeresource addressable (~> 2.7) browser_sniffer (~> 2.0) diff --git a/docs/shopify_app/generators.md b/docs/shopify_app/generators.md index e5400014a..2a03cfabc 100644 --- a/docs/shopify_app/generators.md +++ b/docs/shopify_app/generators.md @@ -68,13 +68,13 @@ Specify whether the app is an embedded app. Apps are embedded by default. #### `$ rails generate shopify_app:shop_model` -This generator creates a `Shop` model and a migration to store shop installation records. See [*Shop-based token strategy*](/docs/shopify_app/session-repository.md#shop-based-token-storage) to learn more. +This generator creates a `Shop` model and a migration to store shop installation records. See [*Shop-based token strategy*](/docs/shopify_app/sessions.md#shop-offline-token-storage) to learn more. --- #### `$ rails generate shopify_app:user_model` -This generator creates a `User` model and a migration to store user records. See [*User-based token strategy*](/docs/shopify_app/session-repository.md#user-based-token-storage) to learn more. +This generator creates a `User` model and a migration to store user records. See [*User-based token strategy*](/docs/shopify_app/sessions.md#user-online-token-storage) to learn more. --- diff --git a/docs/shopify_app/sessions.md b/docs/shopify_app/sessions.md index b5af10e16..8d4f156fd 100644 --- a/docs/shopify_app/sessions.md +++ b/docs/shopify_app/sessions.md @@ -4,23 +4,27 @@ Sessions are used to make contextual API calls for either a shop (offline sessio #### Table of contents -- [Sessions](#sessions-1) - - [Types of session tokens](#types-of-session-tokens) - Shop (offline) v.s. User (online) - - [Session token storage](#session-token-storage) - - [Shop (offline) token storage](#shop-offline-token-storage) - - [User (online) token storage](#user-online-token-storage) - - [In-Memory Session Storage for Testing](#in-memory-session-storage-for-testing) - - [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository) - - [Loading Sessions](#loading-sessions) +- [Sessions](#sessions) + - [Table of contents](#table-of-contents) + - [Sessions](#sessions-1) + - [Types of session tokens](#types-of-session-tokens) + - [Session token storage](#session-token-storage) + - [Shop (offline) token storage](#shop-offline-token-storage) + - [User (online) token storage](#user-online-token-storage) + - [In-memory Session Storage for testing](#in-memory-session-storage-for-testing) + - [Customizing Session Storage with `ShopifyApp::SessionRepository`](#customizing-session-storage-with-shopifyappsessionrepository) + - [⚠️ Custom Session Storage Requirements](#️--custom-session-storage-requirements) + - [Available `ActiveSupport::Concerns` that contains implementation of the above methods](#available-activesupportconcerns-that-contains-implementation-of-the-above-methods) + - [Loading Sessions](#loading-sessions) - [Getting Sessions with Controller Concerns](#getting-sessions-with-controller-concerns) - - [Shop session - "EnsureInstalled" ](#shop-sessions---ensureinstalled) - - [User session - "EnsureHasSession" ](#user-sessions---ensurehassession) - - [Getting Sessions from a Shop or User model record - "with_shopify_session"](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session) -- [Access scopes](#access-scopes) - - [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes) - - [``ShopifyApp::UserSessionStorageWithScopes``](#shopifyappusersessionstoragewithscopes) -- [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy) -- [Migrating from ShopifyApi::Auth::SessionStorage to ShopifyApp::SessionStorage](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage) + - [**Shop Sessions - `EnsureInstalled`**](#shop-sessions---ensureinstalled) + - [User Sessions - `EnsureHasSession`](#user-sessions---ensurehassession) + - [Getting sessions from a Shop or User model record - 'with\_shopify\_session'](#getting-sessions-from-a-shop-or-user-model-record---with_shopify_session) + - [Access scopes](#access-scopes) + - [`ShopifyApp::ShopSessionStorageWithScopes`](#shopifyappshopsessionstoragewithscopes) + - [`ShopifyApp::UserSessionStorageWithScopes`](#shopifyappusersessionstoragewithscopes) + - [Migrating from shop-based to user-based token strategy](#migrating-from-shop-based-to-user-based-token-strategy) + - [Migrating from `ShopifyApi::Auth::SessionStorage` to `ShopifyApp::SessionStorage`](#migrating-from-shopifyapiauthsessionstorage-to-shopifyappsessionstorage) ## Sessions #### Types of session tokens @@ -103,6 +107,7 @@ The custom **Shop** repository must implement the following methods: | `self.store(auth_session)` | `auth_session` (ShopifyAPI::Auth::Session) | - | | `self.retrieve(id)` | `id` (String) | ShopifyAPI::Auth::Session | | `self.retrieve_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | ShopifyAPI::Auth::Session | +| `self.destroy_by_shopify_domain(shopify_domain)` | `shopify_domain` (String) | - | The custom **User** repository must implement the following methods: | Method | Parameters | Return Type | @@ -110,6 +115,7 @@ The custom **User** repository must implement the following methods: | `self.store(auth_session, user)` |
  • `auth_session` (ShopifyAPI::Auth::Session)
  • `user` (ShopifyAPI::Auth::AssociatedUser) | - | | `self.retrieve(id)` | `id` (String) | `ShopifyAPI::Auth::Session` | | `self.retrieve_by_shopify_user_id(user_id)` | `user_id` (String) | `ShopifyAPI::Auth::Session` | +| `self.destroy_by_shopify_user_id(user_id)` | `user_id` (String) | - | These methods are already implemented as a part of the `User` and `Shop` models generated from this gem's generator. @@ -153,7 +159,7 @@ end ``` ##### User Sessions - `EnsureHasSession` -- [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application. If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application. +- [EnsureHasSession](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/ensure_has_session.rb) controller concern will load a user session via `current_shopify_session`. As part of loading this session, this concern will also ensure that the user session has the appropriate scopes needed for the application and that it is not expired (when `check_session_expiry_date` is enabled). If the user isn't found or has fewer permitted scopes than are required, they will be prompted to authorize the application. - This controller concern should be used if you don't need your app to make calls on behalf of a user. With that in mind, there are a few other embedded concerns that are mixed in to ensure that embedding, CSRF, localization, and billing allow the action for the user. - Example ```ruby @@ -228,6 +234,9 @@ class User < ActiveRecord::Base end ``` +## Expiry date +When the configuration flag `check_session_expiry_date` is set to true, the user session expiry date will be checked to trigger a re-auth and get a fresh user token when it is expired. This requires the `ShopifyAPI::Auth::Session` `expires` attribute to be stored. When the `User` model includes the `UserSessionStorageWithScopes` concern, a DB migration can be generated with `rails generate shopify_app:user_model --skip` to add the `expires_at` attribute to the model. + ## Migrating from shop-based to user-based token strategy 1. Run the `user_model` generator as [mentioned above](#user-online-token-storage). diff --git a/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb b/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb new file mode 100644 index 000000000..920d9e314 --- /dev/null +++ b/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_expires_at_column.erb @@ -0,0 +1,5 @@ +class AddUserExpiresAtColumn < ActiveRecord::Migration[<%= rails_migration_version %>] + def change + add_column :users, :expires_at, :datetime + end +end diff --git a/lib/generators/shopify_app/user_model/user_model_generator.rb b/lib/generators/shopify_app/user_model/user_model_generator.rb index 9b6ab5c43..4afc0cfe1 100644 --- a/lib/generators/shopify_app/user_model/user_model_generator.rb +++ b/lib/generators/shopify_app/user_model/user_model_generator.rb @@ -40,6 +40,26 @@ def create_scopes_storage_in_user_model end end + def create_expires_at_storage_in_user_model + expires_at_column_prompt = <<~PROMPT + It is highly recommended that apps record the User session expiry date. \ + This will allow to check if the session has expired and re-authenticate \ + without a first call to Shopify. + + After running the migration, the `check_session_expiry_date` configuration can be enabled. + + The following migration will add an `expires_at` column to the User model. \ + Do you want to include this migration? [y/n] + PROMPT + + if new_shopify_cli_app? || Rails.env.test? || yes?(expires_at_column_prompt) + migration_template( + "db/migrate/add_user_expires_at_column.erb", + "db/migrate/add_user_expires_at_column.rb", + ) + end + end + def update_shopify_app_initializer gsub_file("config/initializers/shopify_app.rb", "ShopifyApp::InMemoryUserSessionStore", "User") end diff --git a/lib/shopify_app/configuration.rb b/lib/shopify_app/configuration.rb index 63e9c8a2d..27f6316a5 100644 --- a/lib/shopify_app/configuration.rb +++ b/lib/shopify_app/configuration.rb @@ -20,6 +20,7 @@ class Configuration attr_accessor :api_version attr_accessor :reauth_on_access_scope_changes + attr_accessor :check_session_expiry_date attr_accessor :log_level # customise urls @@ -44,6 +45,9 @@ class Configuration # takes a ShopifyApp::BillingConfiguration object attr_accessor :billing + # Work in Progress: enables token exchange authentication flow + attr_accessor :wip_new_embedded_auth_strategy + def initialize @root_url = "/" @myshopify_domain = "myshopify.com" @@ -118,6 +122,10 @@ def shop_access_scopes def user_access_scopes @user_access_scopes || scope end + + def use_new_embedded_auth_strategy? + wip_new_embedded_auth_strategy && embedded_app? + end end class BillingConfiguration diff --git a/lib/shopify_app/controller_concerns/login_protection.rb b/lib/shopify_app/controller_concerns/login_protection.rb index 61eba2145..ac73a44de 100644 --- a/lib/shopify_app/controller_concerns/login_protection.rb +++ b/lib/shopify_app/controller_concerns/login_protection.rb @@ -30,6 +30,12 @@ def activate_shopify_session return redirect_to_login end + if ShopifyApp.configuration.check_session_expiry_date && current_shopify_session.expired? + ShopifyApp::Logger.debug("Session expired, redirecting to login") + clear_shopify_session + return redirect_to_login + end + if ShopifyApp.configuration.reauth_on_access_scope_changes && !ShopifyApp.configuration.user_access_scopes_strategy.covers_scopes?(current_shopify_session) clear_shopify_session diff --git a/lib/shopify_app/session/session_repository.rb b/lib/shopify_app/session/session_repository.rb index e02ff4771..d1652e7b1 100644 --- a/lib/shopify_app/session/session_repository.rb +++ b/lib/shopify_app/session/session_repository.rb @@ -23,6 +23,14 @@ def retrieve_user_session_by_shopify_user_id(user_id) user_storage.retrieve_by_shopify_user_id(user_id) end + def destroy_shop_session_by_domain(shopify_domain) + shop_storage.destroy_by_shopify_domain(shopify_domain) + end + + def destroy_user_session_by_shopify_user_id(user_id) + user_storage.destroy_by_shopify_user_id(user_id) + end + def store_shop_session(session) shop_storage.store(session) end @@ -73,18 +81,17 @@ def load_session(id) def delete_session(id) match = id.match(/^offline_(.*)/) - record = if match + if match domain = match[1] ShopifyApp::Logger.debug("Destroying session by domain - domain: #{domain}") - Shop.find_by(shopify_domain: match[1]) + destroy_shop_session_by_domain(domain) + else shopify_user_id = id.split("_").last ShopifyApp::Logger.debug("Destroying session by user - user_id: #{shopify_user_id}") - User.find_by(shopify_user_id: shopify_user_id) + destroy_user_session_by_shopify_user_id(shopify_user_id) end - record.destroy - true end diff --git a/lib/shopify_app/session/shop_session_storage.rb b/lib/shopify_app/session/shop_session_storage.rb index a1cc27ef7..f3cc08171 100644 --- a/lib/shopify_app/session/shop_session_storage.rb +++ b/lib/shopify_app/session/shop_session_storage.rb @@ -27,6 +27,10 @@ def retrieve_by_shopify_domain(domain) construct_session(shop) end + def destroy_by_shopify_domain(domain) + destroy_by(shopify_domain: domain) + end + private def construct_session(shop) diff --git a/lib/shopify_app/session/shop_session_storage_with_scopes.rb b/lib/shopify_app/session/shop_session_storage_with_scopes.rb index 5312608e5..24458e421 100644 --- a/lib/shopify_app/session/shop_session_storage_with_scopes.rb +++ b/lib/shopify_app/session/shop_session_storage_with_scopes.rb @@ -29,6 +29,10 @@ def retrieve_by_shopify_domain(domain) construct_session(shop) end + def destroy_by_shopify_domain(domain) + destroy_by(shopify_domain: domain) + end + private def construct_session(shop) diff --git a/lib/shopify_app/session/user_session_storage.rb b/lib/shopify_app/session/user_session_storage.rb index 78a77d1af..1cba9b2bf 100644 --- a/lib/shopify_app/session/user_session_storage.rb +++ b/lib/shopify_app/session/user_session_storage.rb @@ -28,6 +28,10 @@ def retrieve_by_shopify_user_id(user_id) construct_session(user) end + def destroy_by_shopify_user_id(user_id) + destroy_by(shopify_user_id: user_id) + end + private def construct_session(user) diff --git a/lib/shopify_app/session/user_session_storage_with_scopes.rb b/lib/shopify_app/session/user_session_storage_with_scopes.rb index 440226862..cf19a166d 100644 --- a/lib/shopify_app/session/user_session_storage_with_scopes.rb +++ b/lib/shopify_app/session/user_session_storage_with_scopes.rb @@ -15,6 +15,7 @@ def store(auth_session, user) user.shopify_token = auth_session.access_token user.shopify_domain = auth_session.shop user.access_scopes = auth_session.scope.to_s + user.expires_at = auth_session.expires user.save! user.id @@ -30,6 +31,10 @@ def retrieve_by_shopify_user_id(user_id) construct_session(user) end + def destroy_by_shopify_user_id(user_id) + destroy_by(shopify_user_id: user_id) + end + private def construct_session(user) @@ -52,6 +57,7 @@ def construct_session(user) scope: user.access_scopes, associated_user_scope: user.access_scopes, associated_user: associated_user, + expires: user.expires_at, ) end end @@ -67,5 +73,24 @@ def access_scopes rescue NotImplementedError, NoMethodError raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes" end + + def expires_at=(expires_at) + super + rescue NotImplementedError, NoMethodError + if ShopifyApp.configuration.check_session_expiry_date + raise NotImplementedError, + "#expires_at= must be defined to handle storing the session expiry date" + end + end + + def expires_at + super + rescue NotImplementedError, NoMethodError + if ShopifyApp.configuration.check_session_expiry_date + raise NotImplementedError, "#expires_at must be defined to check the session expiry date" + end + + nil + end end end diff --git a/lib/shopify_app/version.rb b/lib/shopify_app/version.rb index 44ae5656b..1d0df6966 100644 --- a/lib/shopify_app/version.rb +++ b/lib/shopify_app/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module ShopifyApp - VERSION = "21.9.0" + VERSION = "21.10.0" end diff --git a/package.json b/package.json index 55c9ac900..b2bc3cb36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "shopify_app", - "version": "21.9.0", + "version": "21.10.0", "repository": "git@github.com:Shopify/shopify_app.git", "author": "Shopify", "license": "MIT", diff --git a/test/generators/user_model_generator_test.rb b/test/generators/user_model_generator_test.rb index b90ef3c02..35896f37e 100644 --- a/test/generators/user_model_generator_test.rb +++ b/test/generators/user_model_generator_test.rb @@ -35,7 +35,14 @@ class UserModelGeneratorTest < Rails::Generators::TestCase end end - test "create User with access_scopes migration with --new-shopify-cli-app flag provided" do + test "create expires_at migration for User model" do + run_generator + assert_migration "db/migrate/add_user_expires_at_column.rb" do |migration| + assert_match "add_column :users, :expires_at, :datetime", migration + end + end + + test "create User with all migrations with --new-shopify-cli-app flag provided" do Rails.env = "mock_environment" run_generator ["--new-shopify-cli-app"] @@ -44,6 +51,9 @@ class UserModelGeneratorTest < Rails::Generators::TestCase assert_migration "db/migrate/add_user_access_scopes_column.rb" do |migration| assert_match "add_column :users, :access_scopes, :string", migration end + assert_migration "db/migrate/add_user_expires_at_column.rb" do |migration| + assert_match "add_column :users, :expires_at, :datetime", migration + end end test "updates the shopify_app initializer to use User to store session" do diff --git a/test/shopify_app/configuration_test.rb b/test/shopify_app/configuration_test.rb index 9c6bd9d97..aaaddb9ce 100644 --- a/test/shopify_app/configuration_test.rb +++ b/test/shopify_app/configuration_test.rb @@ -228,4 +228,30 @@ class ConfigurationTest < ActiveSupport::TestCase end assert_equal "Invalid user access scopes strategy - expected a string", error.message end + + test "#use_new_embedded_auth_strategy? is true when wip_new_embedded_auth_strategy is on for embedded apps" do + ShopifyApp.configure do |config| + config.embedded_app = true + config.wip_new_embedded_auth_strategy = true + end + + assert ShopifyApp.configuration.use_new_embedded_auth_strategy? + end + + test "#use_new_embedded_auth_strategy? is false for non-embedded apps even if wip_new_embedded_auth_strategy is configured" do + ShopifyApp.configure do |config| + config.embedded_app = false + config.wip_new_embedded_auth_strategy = true + end + + refute ShopifyApp.configuration.use_new_embedded_auth_strategy? + end + + test "#use_new_embedded_auth_strategy? is false when wip_new_embedded_auth_strategy is off" do + ShopifyApp.configure do |config| + config.wip_new_embedded_auth_strategy = false + end + + refute ShopifyApp.configuration.use_new_embedded_auth_strategy? + end end diff --git a/test/shopify_app/controller_concerns/login_protection_test.rb b/test/shopify_app/controller_concerns/login_protection_test.rb index fbac19018..7f11ac758 100644 --- a/test/shopify_app/controller_concerns/login_protection_test.rb +++ b/test/shopify_app/controller_concerns/login_protection_test.rb @@ -426,6 +426,53 @@ class LoginProtectionControllerTest < ActionController::TestCase end end + test "#activate_shopify_session with an expired Shopify session redirects to the login url when check_session_expiry_date enabled" do + ShopifyApp.configuration.check_session_expiry_date = true + + with_application_test_routes do + cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie" + ShopifyApp::SessionRepository.expects(:load_session) + .returns(ShopifyAPI::Auth::Session.new(shop: "shop.myshopify.com", expires: 1.minute.ago)) + + get :index, params: { shop: "foobar" } + + assert_redirected_to "/login?shop=foobar.myshopify.com" + assert_nil cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] + end + end + + test "#activate_shopify_session with an expired Shopify session, when the request is an XHR, returns an HTTP 401 when check_session_expiry_date enabled" do + ShopifyApp.configuration.check_session_expiry_date = true + + with_application_test_routes do + cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie" + ShopifyApp::SessionRepository.expects(:load_session) + .returns(ShopifyAPI::Auth::Session.new(shop: "shop.myshopify.com", expires: 1.minute.ago)) + + get :index, params: { shop: "foobar" }, xhr: true + + assert_equal 401, response.status + assert_match "1", response.headers["X-Shopify-API-Request-Failure-Reauthorize"] + assert_match "/login?shop=foobar", response.headers["X-Shopify-API-Request-Failure-Reauthorize-Url"] + assert_nil cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] + end + end + + test "#activate_shopify_session with an expired Shopify session does not redirect to the login url when check_session_expiry_date disabled" do + ShopifyApp.configuration.check_session_expiry_date = false + + with_application_test_routes do + cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME] = "cookie" + ShopifyApp::SessionRepository.expects(:load_session) + .returns(ShopifyAPI::Auth::Session.new(shop: "shop.myshopify.com", expires: 1.minute.ago)) + ::ShopifyAPI::Context.expects(:activate_session) + + get :index, params: { shop: "foobar" } + + assert_response :ok + end + end + test "#fullpage_redirect_to sends a post message to that shop in the shop param" do with_application_test_routes do example_shop = "shop.myshopify.com" diff --git a/test/shopify_app/session/session_repository_test.rb b/test/shopify_app/session/session_repository_test.rb index 1c18bce48..2ed23a37f 100644 --- a/test/shopify_app/session/session_repository_test.rb +++ b/test/shopify_app/session/session_repository_test.rb @@ -140,21 +140,21 @@ class SessionRepositoryTest < ActiveSupport::TestCase end test(".delete_session destroys a shop record") do - shop = MockShopInstance.new(shopify_domain: "shop", shopify_token: "token") + SessionRepository.shop_storage = InMemoryShopSessionStore + mock_session_id = "offline_abra-shop" - Shop.expects(:find_by).with(shopify_domain: "shop").returns(shop) - shop.expects(:destroy) + InMemoryShopSessionStore.expects(:destroy_by_shopify_domain).with("abra-shop") - SessionRepository.delete_session("offline_shop") + SessionRepository.delete_session(mock_session_id) end test(".delete_session destroys a user record") do - user = MockUserInstance.new(shopify_domain: "shop", shopify_token: "token") + SessionRepository.user_storage = InMemoryUserSessionStore + mock_session_id = "test_shop.myshopify.com_1234" - User.expects(:find_by).with(shopify_user_id: "1234").returns(user) - user.expects(:destroy) + InMemoryUserSessionStore.expects(:destroy_by_shopify_user_id).with("1234") - SessionRepository.delete_session("shop_1234") + SessionRepository.delete_session(mock_session_id) end private diff --git a/test/shopify_app/session/shop_session_storage_test.rb b/test/shopify_app/session/shop_session_storage_test.rb index cecad0696..02bfc6524 100644 --- a/test/shopify_app/session/shop_session_storage_test.rb +++ b/test/shopify_app/session/shop_session_storage_test.rb @@ -40,6 +40,12 @@ class ShopSessionStorageTest < ActiveSupport::TestCase assert_equal expected_session.access_token, session.access_token end + test ".destroy_by_shopify_domain destroys shop session records by JWT" do + ShopMockSessionStore.expects(:destroy_by).with(shopify_domain: TEST_SHOPIFY_DOMAIN) + + ShopMockSessionStore.destroy_by_shopify_domain(TEST_SHOPIFY_DOMAIN) + end + test ".store can store shop session records" do mock_shop_instance = MockShopInstance.new(id: 12345) mock_shop_instance.stubs(:save!).returns(true) diff --git a/test/shopify_app/session/shop_session_storage_with_scopes_test.rb b/test/shopify_app/session/shop_session_storage_with_scopes_test.rb index 87f1e08a2..663b1a9aa 100644 --- a/test/shopify_app/session/shop_session_storage_with_scopes_test.rb +++ b/test/shopify_app/session/shop_session_storage_with_scopes_test.rb @@ -46,6 +46,12 @@ class ShopSessionStorageWithScopesTest < ActiveSupport::TestCase assert_equal expected_session.scope, session.scope end + test ".destroy_by_shopify_domain destroys shop session records by JWT" do + ShopMockSessionStoreWithScopes.expects(:destroy_by).with(shopify_domain: TEST_SHOPIFY_DOMAIN) + + ShopMockSessionStoreWithScopes.destroy_by_shopify_domain(TEST_SHOPIFY_DOMAIN) + end + test ".store can store shop session records" do mock_shop_instance = MockShopInstance.new(id: 12345) mock_shop_instance.stubs(:save!).returns(true) diff --git a/test/shopify_app/session/user_session_storage_test.rb b/test/shopify_app/session/user_session_storage_test.rb index 7f399f361..6f1037209 100644 --- a/test/shopify_app/session/user_session_storage_test.rb +++ b/test/shopify_app/session/user_session_storage_test.rb @@ -46,6 +46,12 @@ class UserSessionStorageTest < ActiveSupport::TestCase assert_equal expected_session.access_token, session.access_token end + test ".destroy_by_shopify_user_id destroys user session by shopify_user_id" do + UserMockSessionStore.expects(:destroy_by).with(shopify_user_id: TEST_SHOPIFY_USER_ID) + + UserMockSessionStore.destroy_by_shopify_user_id(TEST_SHOPIFY_USER_ID) + end + test ".store can store user session record" do mock_user_instance = MockUserInstance.new(shopify_user_id: 100) mock_user_instance.stubs(:save!).returns(true) diff --git a/test/shopify_app/session/user_session_storage_with_scopes_test.rb b/test/shopify_app/session/user_session_storage_with_scopes_test.rb index 1bb47188b..6cf6a09c1 100644 --- a/test/shopify_app/session/user_session_storage_with_scopes_test.rb +++ b/test/shopify_app/session/user_session_storage_with_scopes_test.rb @@ -12,6 +12,7 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase TEST_SHOPIFY_DOMAIN = "example.myshopify.com" TEST_SHOPIFY_USER_TOKEN = "some-user-token-42" TEST_MERCHANT_SCOPES = "read_orders, write_products" + TEST_EXPIRES_AT = Time.now test ".retrieve returns user session by id" do UserMockSessionStoreWithScopes.stubs(:find_by).returns(MockUserInstance.new( @@ -19,6 +20,7 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase shopify_domain: TEST_SHOPIFY_DOMAIN, shopify_token: TEST_SHOPIFY_USER_TOKEN, scopes: TEST_MERCHANT_SCOPES, + expires_at: TEST_EXPIRES_AT, )) session = UserMockSessionStoreWithScopes.retrieve(shopify_user_id: TEST_SHOPIFY_USER_ID) @@ -26,6 +28,7 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase assert_equal TEST_SHOPIFY_DOMAIN, session.shop assert_equal TEST_SHOPIFY_USER_TOKEN, session.access_token assert_equal ShopifyAPI::Auth::AuthScopes.new(TEST_MERCHANT_SCOPES), session.scope + assert_equal TEST_EXPIRES_AT, session.expires end test ".retrieve_by_shopify_user_id returns user session by shopify_user_id" do @@ -35,6 +38,7 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase shopify_token: TEST_SHOPIFY_USER_TOKEN, api_version: ShopifyApp.configuration.api_version, scopes: TEST_MERCHANT_SCOPES, + expires_at: TEST_EXPIRES_AT, ) UserMockSessionStoreWithScopes.stubs(:find_by).with(shopify_user_id: TEST_SHOPIFY_USER_ID).returns(instance) @@ -42,6 +46,7 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase shop: instance.shopify_domain, access_token: instance.shopify_token, scope: TEST_MERCHANT_SCOPES, + expires: TEST_EXPIRES_AT, ) user_id = TEST_SHOPIFY_USER_ID @@ -49,6 +54,13 @@ class UserSessionStorageWithScopesTest < ActiveSupport::TestCase assert_equal expected_session.shop, session.shop assert_equal expected_session.access_token, session.access_token assert_equal expected_session.scope, session.scope + assert_equal expected_session.expires, session.expires + end + + test ".destroy_by_shopify_user_id destroys user session by shopify_user_id" do + UserMockSessionStoreWithScopes.expects(:destroy_by).with(shopify_user_id: TEST_SHOPIFY_USER_ID) + + UserMockSessionStoreWithScopes.destroy_by_shopify_user_id(TEST_SHOPIFY_USER_ID) end test ".store can store user session record" do diff --git a/test/support/session_store_strategy_test_helpers.rb b/test/support/session_store_strategy_test_helpers.rb index 622fd176c..ceebe937d 100644 --- a/test/support/session_store_strategy_test_helpers.rb +++ b/test/support/session_store_strategy_test_helpers.rb @@ -21,8 +21,8 @@ def initialize( end class MockUserInstance - attr_reader :id, :shopify_user_id, :shopify_domain, :shopify_token, :api_version, :access_scopes - attr_writer :shopify_token, :shopify_domain, :access_scopes + attr_reader :id, :shopify_user_id, :shopify_domain, :shopify_token, :api_version, :access_scopes, :expires_at + attr_writer :shopify_token, :shopify_domain, :access_scopes, :expires_at def initialize( id: 1, @@ -30,7 +30,8 @@ def initialize( shopify_domain: "example.myshopify.com", shopify_token: "1234-user-token", api_version: ShopifyApp.configuration.api_version, - scopes: "read_products" + scopes: "read_products", + expires_at: nil ) @id = id @shopify_user_id = shopify_user_id @@ -38,6 +39,7 @@ def initialize( @shopify_token = shopify_token @api_version = api_version @access_scopes = scopes + @expires_at = expires_at end end end diff --git a/test/test_helper.rb b/test/test_helper.rb index ce758d969..31e5e7834 100644 --- a/test/test_helper.rb +++ b/test/test_helper.rb @@ -57,6 +57,8 @@ def mock_session(shop: "my-shop.myshopify.com", scope: ShopifyApp.configuration. mock_session.stubs(:access_token).returns("a-new-user_token!") mock_session.stubs(:scope).returns(ShopifyAPI::Auth::AuthScopes.new(scope)) mock_session.stubs(:shopify_session_id).returns(1) + mock_session.stubs(:expires).returns(nil) + mock_session.stubs(:expired?).returns(false) mock_session end