@shopify/shopify-app-remix 4.1.1 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. package/dist/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
  2. package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
  3. package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
  4. package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
  5. package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
  6. package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
  7. package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
  8. package/dist/cjs/server/version.js +1 -1
  9. package/dist/cjs/server/version.js.map +1 -1
  10. package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
  11. package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
  12. package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
  13. package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
  14. package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
  15. package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
  16. package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
  17. package/dist/esm/server/version.mjs +1 -1
  18. package/dist/esm/server/version.mjs.map +1 -1
  19. package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
  20. package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
  21. package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
  22. package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
  23. package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
  24. package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
  25. package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
  26. package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
  27. package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
  28. package/dist/ts/server/authenticate/webhooks/types.d.ts +34 -2
  29. package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
  30. package/dist/ts/server/config-types.d.ts.map +1 -1
  31. package/dist/ts/server/version.d.ts +1 -1
  32. package/dist/tsconfig.build.tsbuildinfo +1 -1
  33. package/package.json +18 -14
  34. package/src/react/components/AppProvider/AppProvider.doc.ts +35 -0
  35. package/src/react/components/AppProvider/AppProvider.tsx +147 -0
  36. package/src/react/components/AppProvider/index.ts +1 -0
  37. package/src/react/components/AppProxyForm/AppProxyForm.doc.ts +28 -0
  38. package/src/react/components/AppProxyForm/AppProxyForm.tsx +82 -0
  39. package/src/react/components/AppProxyForm/index.ts +1 -0
  40. package/src/react/components/AppProxyLink/AppProxyLink.doc.ts +28 -0
  41. package/src/react/components/AppProxyLink/AppProxyLink.tsx +58 -0
  42. package/src/react/components/AppProxyLink/index.ts +1 -0
  43. package/src/react/components/AppProxyProvider/AppProxyProvider.doc.ts +40 -0
  44. package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +100 -0
  45. package/src/react/components/AppProxyProvider/index.ts +5 -0
  46. package/src/react/components/RemixPolarisLink.tsx +14 -0
  47. package/src/react/components/index.ts +4 -0
  48. package/src/react/const.ts +2 -0
  49. package/src/react/index.ts +1 -0
  50. package/src/server/__test-helpers/const.ts +14 -0
  51. package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
  52. package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
  53. package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
  54. package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
  55. package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
  56. package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
  57. package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
  58. package/src/server/__test-helpers/get-hmac.ts +7 -0
  59. package/src/server/__test-helpers/get-jwt.ts +10 -0
  60. package/src/server/__test-helpers/get-thrown-error.ts +19 -0
  61. package/src/server/__test-helpers/get-thrown-response.ts +17 -0
  62. package/src/server/__test-helpers/index.ts +21 -0
  63. package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
  64. package/src/server/__test-helpers/request-mock.ts +169 -0
  65. package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
  66. package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
  67. package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
  68. package/src/server/__test-helpers/setup-valid-request.ts +78 -0
  69. package/src/server/__test-helpers/setup-valid-session.ts +28 -0
  70. package/src/server/__test-helpers/sign-request-cookie.ts +21 -0
  71. package/src/server/__test-helpers/test-config.ts +56 -0
  72. package/src/server/adapters/node/index.ts +13 -0
  73. package/src/server/adapters/vercel/index.ts +13 -0
  74. package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
  75. package/src/server/authenticate/admin/authenticate.admin.doc.ts +51 -0
  76. package/src/server/authenticate/admin/authenticate.ts +232 -0
  77. package/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts +29 -0
  78. package/src/server/authenticate/admin/billing/cancel.ts +38 -0
  79. package/src/server/authenticate/admin/billing/check.ts +41 -0
  80. package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
  81. package/src/server/authenticate/admin/billing/helpers.ts +42 -0
  82. package/src/server/authenticate/admin/billing/index.ts +6 -0
  83. package/src/server/authenticate/admin/billing/request.ts +64 -0
  84. package/src/server/authenticate/admin/billing/require.ts +58 -0
  85. package/src/server/authenticate/admin/billing/types.ts +613 -0
  86. package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
  87. package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
  88. package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
  89. package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
  90. package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
  91. package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
  92. package/src/server/authenticate/admin/helpers/index.ts +14 -0
  93. package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
  94. package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
  95. package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
  96. package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
  97. package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
  98. package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
  99. package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
  100. package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
  101. package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
  102. package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
  103. package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
  104. package/src/server/authenticate/admin/scope/authenticate.admin.scopes.doc.ts +28 -0
  105. package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
  106. package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
  107. package/src/server/authenticate/admin/scope/factory.ts +21 -0
  108. package/src/server/authenticate/admin/scope/query.ts +51 -0
  109. package/src/server/authenticate/admin/scope/request.ts +32 -0
  110. package/src/server/authenticate/admin/scope/revoke.ts +48 -0
  111. package/src/server/authenticate/admin/scope/types.ts +126 -0
  112. package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
  113. package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
  114. package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
  115. package/src/server/authenticate/admin/strategies/types.ts +29 -0
  116. package/src/server/authenticate/admin/types.ts +216 -0
  117. package/src/server/authenticate/const.ts +15 -0
  118. package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
  119. package/src/server/authenticate/flow/authenticate.flow.doc.ts +47 -0
  120. package/src/server/authenticate/flow/authenticate.ts +68 -0
  121. package/src/server/authenticate/flow/types.ts +85 -0
  122. package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
  123. package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.ts +48 -0
  124. package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
  125. package/src/server/authenticate/fulfillment-service/types.ts +82 -0
  126. package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
  127. package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
  128. package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
  129. package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
  130. package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
  131. package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
  132. package/src/server/authenticate/helpers/index.ts +10 -0
  133. package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
  134. package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
  135. package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
  136. package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
  137. package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
  138. package/src/server/authenticate/login/login.ts +53 -0
  139. package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
  140. package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +55 -0
  141. package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
  142. package/src/server/authenticate/public/appProxy/types.ts +215 -0
  143. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
  144. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
  145. package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.ts +49 -0
  146. package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
  147. package/src/server/authenticate/public/checkout/types.ts +65 -0
  148. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
  149. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
  150. package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts +49 -0
  151. package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
  152. package/src/server/authenticate/public/customer-account/types.ts +65 -0
  153. package/src/server/authenticate/public/extension/authenticate.ts +54 -0
  154. package/src/server/authenticate/public/extension/types.ts +11 -0
  155. package/src/server/authenticate/public/factory.ts +24 -0
  156. package/src/server/authenticate/public/index.ts +1 -0
  157. package/src/server/authenticate/public/pos/authenticate.ts +8 -0
  158. package/src/server/authenticate/public/pos/types.ts +65 -0
  159. package/src/server/authenticate/public/types.ts +96 -0
  160. package/src/server/authenticate/webhooks/authenticate.ts +105 -0
  161. package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
  162. package/src/server/authenticate/webhooks/authenticate.webhooks.doc.ts +39 -0
  163. package/src/server/authenticate/webhooks/index.ts +1 -0
  164. package/src/server/authenticate/webhooks/register.ts +48 -0
  165. package/src/server/authenticate/webhooks/types.ts +266 -0
  166. package/src/server/boundary/error.tsx +14 -0
  167. package/src/server/boundary/headers.ts +15 -0
  168. package/src/server/boundary/index.ts +39 -0
  169. package/src/server/boundary/types.ts +4 -0
  170. package/src/server/clients/admin/authenticate.admin.api.doc.ts +34 -0
  171. package/src/server/clients/admin/factory.ts +22 -0
  172. package/src/server/clients/admin/graphql.ts +39 -0
  173. package/src/server/clients/admin/index.ts +2 -0
  174. package/src/server/clients/admin/types.ts +120 -0
  175. package/src/server/clients/index.ts +2 -0
  176. package/src/server/clients/storefront/authenticate.storefront.api.doc.ts +34 -0
  177. package/src/server/clients/storefront/factory.ts +32 -0
  178. package/src/server/clients/storefront/index.ts +2 -0
  179. package/src/server/clients/storefront/types.ts +78 -0
  180. package/src/server/clients/types.ts +47 -0
  181. package/src/server/config-types.ts +309 -0
  182. package/src/server/errors.ts +3 -0
  183. package/src/server/future/flags.ts +62 -0
  184. package/src/server/helpers/create-or-load-offline-session.ts +19 -0
  185. package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
  186. package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
  187. package/src/server/helpers/index.ts +4 -0
  188. package/src/server/helpers/refresh-token.ts +35 -0
  189. package/src/server/index.ts +24 -0
  190. package/src/server/override-logger.ts +42 -0
  191. package/src/server/shopify-app.doc.ts +45 -0
  192. package/src/server/shopify-app.ts +215 -0
  193. package/src/server/test-helpers/index.ts +1 -0
  194. package/src/server/test-helpers/test-config.ts +79 -0
  195. package/src/server/types-contexts.ts +44 -0
  196. package/src/server/types.ts +507 -0
  197. package/src/server/unauthenticated/admin/factory.ts +23 -0
  198. package/src/server/unauthenticated/admin/index.ts +1 -0
  199. package/src/server/unauthenticated/admin/types.ts +79 -0
  200. package/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +40 -0
  201. package/src/server/unauthenticated/storefront/factory.ts +28 -0
  202. package/src/server/unauthenticated/storefront/index.ts +1 -0
  203. package/src/server/unauthenticated/storefront/types.ts +105 -0
  204. package/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts +40 -0
  205. package/src/server/unauthenticated/types.ts +65 -0
  206. package/src/server/version.ts +1 -0
@@ -0,0 +1,51 @@
1
+ import {ApiVersion, Session} from '@shopify/shopify-api';
2
+
3
+ import type {AdminApiContext} from '../clients';
4
+
5
+ import {TEST_SHOP} from './const';
6
+ import {mockExternalRequest} from './request-mock';
7
+
8
+ export function expectAdminApiClient(
9
+ factory: () => Promise<{
10
+ admin: AdminApiContext;
11
+ expectedSession: Session;
12
+ actualSession: Session;
13
+ }>,
14
+ ) {
15
+ describe('Graphql client', () => {
16
+ it('can perform requests', async () => {
17
+ // GIVEN
18
+ const {admin, actualSession} = await factory();
19
+ await mockExternalRequest({
20
+ request: new Request(
21
+ `https://${TEST_SHOP}/admin/api/${ApiVersion.July25}/graphql.json`,
22
+ {
23
+ method: 'POST',
24
+ headers: {'X-Shopify-Access-Token': actualSession.accessToken!},
25
+ },
26
+ ),
27
+ response: new Response(
28
+ JSON.stringify({data: {shop: {name: 'Test shop'}}}),
29
+ ),
30
+ });
31
+
32
+ // WHEN
33
+ const response = await admin.graphql('{ shop { name } }');
34
+
35
+ // THEN
36
+ expect(response.status).toEqual(200);
37
+ expect(await response.json()).toEqual({
38
+ data: {shop: {name: 'Test shop'}},
39
+ headers: {'Content-Type': ['application/json']},
40
+ });
41
+ });
42
+
43
+ it('returns a session object as part of the context', async () => {
44
+ // GIVEN
45
+ const {expectedSession, actualSession} = await factory();
46
+
47
+ // THEN
48
+ expect(expectedSession).toEqual(actualSession);
49
+ });
50
+ });
51
+ }
@@ -0,0 +1,22 @@
1
+ import {TEST_SHOP} from './const';
2
+ import {testConfig} from './test-config';
3
+
4
+ export function expectBeginAuthRedirect(
5
+ config: ReturnType<typeof testConfig>,
6
+ response: Response,
7
+ ) {
8
+ expect(response.status).toEqual(302);
9
+
10
+ const {hostname, pathname, searchParams} = new URL(
11
+ response.headers.get('location')!,
12
+ );
13
+
14
+ expect(hostname).toBe(TEST_SHOP);
15
+ expect(pathname).toBe('/admin/oauth/authorize');
16
+ expect(searchParams.get('client_id')).toBe(config.apiKey);
17
+ expect(searchParams.get('scope')).toBe(config.scopes!.toString());
18
+ expect(searchParams.get('redirect_uri')).toBe(
19
+ `${config.appUrl}/auth/callback`,
20
+ );
21
+ expect(searchParams.get('state')).toStrictEqual(expect.any(String));
22
+ }
@@ -0,0 +1,25 @@
1
+ import {APP_BRIDGE_URL} from '../authenticate/const';
2
+
3
+ import {TEST_SHOP} from './const';
4
+
5
+ export function expectDocumentRequestHeaders(
6
+ response: Response,
7
+ isEmbeddedApp = true,
8
+ ) {
9
+ const headers = response.headers;
10
+
11
+ if (isEmbeddedApp) {
12
+ expect(headers.get('Content-Security-Policy')).toEqual(
13
+ `frame-ancestors https://${encodeURIComponent(
14
+ TEST_SHOP,
15
+ )} https://admin.shopify.com https://*.spin.dev https://admin.myshopify.io https://admin.shop.dev;`,
16
+ );
17
+ expect(headers.get('Link')).toEqual(
18
+ `<${APP_BRIDGE_URL}>; rel="preload"; as="script";`,
19
+ );
20
+ } else {
21
+ expect(headers.get('Content-Security-Policy')).toEqual(
22
+ `frame-ancestors 'none';`,
23
+ );
24
+ }
25
+ }
@@ -0,0 +1,37 @@
1
+ import {APP_URL, BASE64_HOST, TEST_SHOP} from './const';
2
+
3
+ interface ExpectExitIframeRedirectOptions {
4
+ shop?: string;
5
+ host?: string | null;
6
+ destination?: string;
7
+ addHostToExitIframePath?: boolean;
8
+ }
9
+
10
+ export function expectExitIframeRedirect(
11
+ response: Response,
12
+ {
13
+ shop = TEST_SHOP,
14
+ host = BASE64_HOST,
15
+ destination = `/auth?shop=${shop}`,
16
+ addHostToExitIframePath = true,
17
+ }: ExpectExitIframeRedirectOptions = {},
18
+ ) {
19
+ expect(response.status).toBe(302);
20
+
21
+ const {pathname, searchParams} = new URL(
22
+ response.headers.get('location')!,
23
+ APP_URL,
24
+ );
25
+ expect(pathname).toBe('/auth/exit-iframe');
26
+
27
+ expect(searchParams.get('shop')).toBe(shop);
28
+
29
+ let expectedDestination = destination;
30
+ if (host) {
31
+ expect(searchParams.get('host')).toBe(host);
32
+ if (addHostToExitIframePath) {
33
+ expectedDestination = destination.concat(`&host=${host}`);
34
+ }
35
+ }
36
+ expect(searchParams.get('exitIframe')).toBe(expectedDestination);
37
+ }
@@ -0,0 +1,8 @@
1
+ import {APP_URL} from './const';
2
+
3
+ export function expectLoginRedirect(response: Response) {
4
+ expect(response.status).toBe(302);
5
+
6
+ const {pathname} = new URL(response.headers.get('location')!, APP_URL);
7
+ expect(pathname).toBe('/auth/login');
8
+ }
@@ -0,0 +1,49 @@
1
+ import {ApiVersion, Session} from '@shopify/shopify-api';
2
+
3
+ import type {StorefrontContext} from '../clients';
4
+
5
+ import {mockExternalRequest} from './request-mock';
6
+ import {TEST_SHOP} from './const';
7
+
8
+ export function expectStorefrontApiClient(
9
+ factory: () => Promise<{
10
+ storefront: StorefrontContext;
11
+ expectedSession: Session;
12
+ actualSession: Session;
13
+ }>,
14
+ ) {
15
+ it('Storefront client can perform GraphQL Requests', async () => {
16
+ // GIVEN
17
+ const {storefront, actualSession} = await factory();
18
+ const apiResponse = {data: {blogs: {nodes: [{id: 1}]}}, headers: {}};
19
+ await mockExternalRequest({
20
+ request: new Request(
21
+ `https://${TEST_SHOP}/api/${ApiVersion.July25}/graphql.json`,
22
+ {
23
+ method: 'POST',
24
+ headers: {
25
+ 'Shopify-Storefront-Private-Token': actualSession.accessToken!,
26
+ },
27
+ },
28
+ ),
29
+ response: new Response(JSON.stringify(apiResponse)),
30
+ });
31
+
32
+ // WHEN
33
+ const response = await storefront.graphql(
34
+ 'blogs(first: 1) { nodes { id }}',
35
+ );
36
+
37
+ // THEN
38
+ expect(response.status).toEqual(200);
39
+ expect(await response.json()).toEqual(apiResponse);
40
+ });
41
+
42
+ it('Storefront client uses the correct session', async () => {
43
+ // GIVEN
44
+ const {expectedSession, actualSession} = await factory();
45
+
46
+ // THEN
47
+ expect(expectedSession).toEqual(actualSession);
48
+ });
49
+ }
@@ -0,0 +1,188 @@
1
+ import {Session} from '@shopify/shopify-api';
2
+ import {SessionStorage} from '@shopify/shopify-app-session-storage';
3
+ import {MemorySessionStorage} from '@shopify/shopify-app-session-storage-memory';
4
+
5
+ import {TestOverridesArg} from '../test-helpers/test-config';
6
+ import {AppDistribution} from '../types';
7
+
8
+ import {API_KEY, API_SECRET_KEY, TEST_SHOP} from './const';
9
+ import {mockExternalRequest} from './request-mock';
10
+ import {
11
+ setUpValidSession,
12
+ setupValidCustomAppSession,
13
+ } from './setup-valid-session';
14
+
15
+ export function expectTokenRefresh(
16
+ runAuth: (
17
+ sessionStorage: SessionStorage,
18
+ session: Session,
19
+ configOverrides?: TestOverridesArg,
20
+ ) => Promise<Session>,
21
+ ) {
22
+ describe('token refresh for expired offline sessions', () => {
23
+ it('does not refresh token when session is not expired', async () => {
24
+ // GIVEN
25
+ const sessionStorage = new MemorySessionStorage();
26
+ const oneHourFromNow = new Date(Date.now() + 1000 * 3600);
27
+ const session = await setUpValidSession(sessionStorage, {
28
+ expires: oneHourFromNow,
29
+ refreshToken: 'test-refresh-token',
30
+ });
31
+
32
+ // WHEN
33
+ const actualSession = await runAuth(sessionStorage, session);
34
+
35
+ // THEN
36
+ expect(actualSession.accessToken).toBe(session.accessToken);
37
+ expect(actualSession.expires?.getTime()).toBe(oneHourFromNow.getTime());
38
+ });
39
+
40
+ it('refreshes token when session is expired and feature flag is enabled', async () => {
41
+ // GIVEN
42
+ const sessionStorage = new MemorySessionStorage();
43
+ const oneSecondAgo = new Date(Date.now() - 1000);
44
+ const thirtyDaysFromNow = new Date(Date.now() + 1000 * 3600 * 24 * 30);
45
+
46
+ const session = await setUpValidSession(sessionStorage, {
47
+ expires: oneSecondAgo,
48
+ refreshToken: 'test-refresh-token',
49
+ refreshTokenExpires: thirtyDaysFromNow,
50
+ });
51
+
52
+ const refreshResponse = {
53
+ access_token: 'new-access-token',
54
+ scope: 'testScope',
55
+ expires_in: 3600,
56
+ refresh_token: 'new-refresh-token',
57
+ refresh_token_expires_in: 2592000,
58
+ };
59
+
60
+ await mockExternalRequest({
61
+ request: new Request(`https://${TEST_SHOP}/admin/oauth/access_token`, {
62
+ method: 'POST',
63
+ headers: {'Content-Type': 'application/json'},
64
+ body: JSON.stringify({
65
+ client_id: API_KEY,
66
+ client_secret: API_SECRET_KEY,
67
+ refresh_token: 'test-refresh-token',
68
+ grant_type: 'refresh_token',
69
+ }),
70
+ }),
71
+ response: new Response(JSON.stringify(refreshResponse), {
72
+ status: 200,
73
+ headers: {'Content-Type': 'application/json'},
74
+ }),
75
+ });
76
+
77
+ // WHEN
78
+ const actualSession = await runAuth(sessionStorage, session);
79
+
80
+ // THEN
81
+ expect(actualSession.accessToken).toBe('new-access-token');
82
+ expect(actualSession.refreshToken).toBe('new-refresh-token');
83
+ expect(actualSession.id).toBe(session.id);
84
+ expect(actualSession.shop).toBe(TEST_SHOP);
85
+ expect(actualSession.expires?.getTime()).toBeGreaterThan(Date.now());
86
+ });
87
+
88
+ it('refreshes token when session is within milliseconds of expiry', async () => {
89
+ // GIVEN
90
+ const sessionStorage = new MemorySessionStorage();
91
+ const halfSecondFromNow = new Date(Date.now() + 500);
92
+ const thirtyDaysFromNow = new Date(Date.now() + 1000 * 3600 * 24 * 30);
93
+
94
+ const session = await setUpValidSession(sessionStorage, {
95
+ expires: halfSecondFromNow,
96
+ refreshToken: 'test-refresh-token',
97
+ refreshTokenExpires: thirtyDaysFromNow,
98
+ });
99
+
100
+ const refreshResponse = {
101
+ access_token: 'new-access-token-2',
102
+ scope: 'testScope',
103
+ expires_in: 3600,
104
+ refresh_token: 'new-refresh-token-2',
105
+ refresh_token_expires_in: 2592000,
106
+ };
107
+
108
+ await mockExternalRequest({
109
+ request: new Request(`https://${TEST_SHOP}/admin/oauth/access_token`, {
110
+ method: 'POST',
111
+ headers: {'Content-Type': 'application/json'},
112
+ body: JSON.stringify({
113
+ client_id: API_KEY,
114
+ client_secret: API_SECRET_KEY,
115
+ refresh_token: 'test-refresh-token',
116
+ grant_type: 'refresh_token',
117
+ }),
118
+ }),
119
+ response: new Response(JSON.stringify(refreshResponse), {
120
+ status: 200,
121
+ headers: {'Content-Type': 'application/json'},
122
+ }),
123
+ });
124
+
125
+ // WHEN
126
+ const actualSession = await runAuth(sessionStorage, session);
127
+
128
+ // THEN
129
+ expect(actualSession.accessToken).toBe('new-access-token-2');
130
+ expect(actualSession.refreshToken).toBe('new-refresh-token-2');
131
+ });
132
+
133
+ it('does not refresh token when session has no expiry', async () => {
134
+ // GIVEN
135
+ const sessionStorage = new MemorySessionStorage();
136
+ const session = await setUpValidSession(sessionStorage, {
137
+ expires: undefined,
138
+ refreshToken: 'test-refresh-token',
139
+ });
140
+
141
+ // WHEN
142
+ const actualSession = await runAuth(sessionStorage, session);
143
+
144
+ // THEN
145
+ expect(actualSession.accessToken).toBe(session.accessToken);
146
+ expect(actualSession.expires).toBeUndefined();
147
+ });
148
+
149
+ it('does not refresh token when feature flag is disabled even if session is expired', async () => {
150
+ // GIVEN
151
+ const sessionStorage = new MemorySessionStorage();
152
+ const oneSecondAgo = new Date(Date.now() - 1000);
153
+ const session = await setUpValidSession(sessionStorage, {
154
+ expires: oneSecondAgo,
155
+ refreshToken: 'test-refresh-token',
156
+ });
157
+
158
+ // WHEN
159
+ const actualSession = await runAuth(sessionStorage, session, {
160
+ future: {
161
+ expiringOfflineAccessTokens: false,
162
+ },
163
+ });
164
+
165
+ // THEN
166
+ expect(actualSession.accessToken).toBe(session.accessToken);
167
+ expect(actualSession.expires?.getTime()).toBe(oneSecondAgo.getTime());
168
+ expect(actualSession.refreshToken).toBe('test-refresh-token');
169
+ });
170
+
171
+ it('does not refresh token when distribution is ShopifyAdmin', async () => {
172
+ // GIVEN
173
+ const sessionStorage = new MemorySessionStorage();
174
+ const session = setupValidCustomAppSession(TEST_SHOP);
175
+
176
+ // WHEN
177
+ const actualSession = await runAuth(sessionStorage, session, {
178
+ distribution: AppDistribution.ShopifyAdmin,
179
+ });
180
+
181
+ // THEN
182
+ expect(actualSession).toBeDefined();
183
+ expect(actualSession.shop).toBe(TEST_SHOP);
184
+ expect(actualSession.expires).toBeUndefined();
185
+ expect(actualSession.refreshToken).toBeUndefined();
186
+ });
187
+ });
188
+ }
@@ -0,0 +1,7 @@
1
+ import {getHmac as getHmacImport} from '@shopify/shopify-api/test-helpers';
2
+
3
+ import {API_SECRET_KEY} from './const';
4
+
5
+ export function getHmac(body: string): string {
6
+ return getHmacImport(body, API_SECRET_KEY);
7
+ }
@@ -0,0 +1,10 @@
1
+ import {JwtPayload} from '@shopify/shopify-api';
2
+ import {getJwt as getJwtImport} from '@shopify/shopify-api/test-helpers';
3
+
4
+ import {API_KEY, API_SECRET_KEY, TEST_SHOP_NAME} from './const';
5
+
6
+ export async function getJwt(
7
+ overrides: Partial<JwtPayload> = {},
8
+ ): Promise<ReturnType<typeof getJwtImport>> {
9
+ return getJwtImport(TEST_SHOP_NAME, API_KEY, API_SECRET_KEY, overrides);
10
+ }
@@ -0,0 +1,19 @@
1
+ import {ShopifyError} from '@shopify/shopify-api';
2
+
3
+ export async function getThrownError(
4
+ callback: (request: Request) => Promise<any>,
5
+ request: Request,
6
+ ): Promise<ShopifyError> {
7
+ try {
8
+ await callback(request);
9
+ } catch (error) {
10
+ if (!(error instanceof ShopifyError)) {
11
+ throw new Error(
12
+ `${request.method} request to ${request.url} did not throw a ShopifyError.`,
13
+ );
14
+ }
15
+ return error;
16
+ }
17
+
18
+ throw new Error(`${request.method} request to ${request.url} did not throw`);
19
+ }
@@ -0,0 +1,17 @@
1
+ export async function getThrownResponse(
2
+ callback: (request: Request) => Promise<any>,
3
+ request: Request,
4
+ ): Promise<Response> {
5
+ try {
6
+ await callback(request);
7
+ } catch (response) {
8
+ if (!(response instanceof Response)) {
9
+ throw new Error(
10
+ `${request.method} request to ${request.url} threw an error instead of a response: ${response}`,
11
+ );
12
+ }
13
+ return response;
14
+ }
15
+
16
+ throw new Error(`${request.method} request to ${request.url} did not throw`);
17
+ }
@@ -0,0 +1,21 @@
1
+ export * from './const';
2
+ export * from './expect-admin-api-client';
3
+ export * from './expect-begin-auth-redirect';
4
+ export * from './expect-document-request-headers';
5
+ export * from './expect-exit-iframe';
6
+ export * from './expect-login-redirect';
7
+ export * from './expect-storefront-api-client';
8
+ export * from './get-hmac';
9
+ export * from './get-jwt';
10
+ export * from './get-thrown-response';
11
+ export * from './request-mock';
12
+ export * from './setup-valid-session';
13
+ export * from './setup-valid-request';
14
+ export * from './sign-request-cookie';
15
+ export * from './test-config';
16
+ export * from './get-thrown-error';
17
+ export * from './setup-embedded-flow';
18
+ export * from './setup-fetch-flow';
19
+ export * from './mock-graphql-request';
20
+ export * from './setup-non-embedded-flow';
21
+ export * from './expect-token-refresh';
@@ -0,0 +1,60 @@
1
+ import {ApiVersion} from '@shopify/shopify-api';
2
+
3
+ import {TEST_SHOP} from './const';
4
+ import {mockExternalRequest, mockExternalRequests} from './request-mock';
5
+
6
+ export interface MockGraphqlRequestArg {
7
+ status?: number;
8
+ responseContent?: string;
9
+ body?: string;
10
+ }
11
+
12
+ export function mockGraphqlRequest(
13
+ apiVersion = ApiVersion.July25,
14
+ shopUrl = TEST_SHOP,
15
+ ) {
16
+ return async function ({
17
+ status = 401,
18
+ responseContent,
19
+ }: MockGraphqlRequestArg = {}) {
20
+ const requestMock = new Request(
21
+ `https://${shopUrl}/admin/api/${apiVersion}/graphql.json`,
22
+ {method: 'POST'},
23
+ );
24
+
25
+ await mockExternalRequest({
26
+ request: requestMock,
27
+ response: new Response(responseContent, {status}),
28
+ });
29
+
30
+ return requestMock;
31
+ };
32
+ }
33
+
34
+ export function mockGraphqlRequests(
35
+ apiVersion = ApiVersion.July25,
36
+ shopUrl = TEST_SHOP,
37
+ ) {
38
+ return async function (...mocks: MockGraphqlRequestArg[]) {
39
+ const mockedRequests: Request[] = [];
40
+ const externalRequests = mocks.map(
41
+ ({body, responseContent, status = 200}) => {
42
+ const requestMock = new Request(
43
+ `https://${shopUrl}/admin/api/${apiVersion}/graphql.json`,
44
+ {method: 'POST', ...(body ? {body} : {})},
45
+ );
46
+
47
+ mockedRequests.push(requestMock);
48
+
49
+ return {
50
+ request: requestMock,
51
+ response: new Response(responseContent, {status}),
52
+ };
53
+ },
54
+ );
55
+
56
+ await mockExternalRequests(...externalRequests);
57
+
58
+ return mockedRequests;
59
+ };
60
+ }
@@ -0,0 +1,169 @@
1
+ import fetchMock, {MockParams} from 'jest-fetch-mock';
2
+
3
+ interface MockExternalRequestArg {
4
+ request?: Request;
5
+ response: Response;
6
+ }
7
+
8
+ interface ResponseParams {
9
+ body: string;
10
+ init: MockParams;
11
+ }
12
+
13
+ let requestMocks: MockExternalRequestArg[] = [];
14
+
15
+ let skipMockChecksFlag = false;
16
+
17
+ export async function mockExternalRequest({
18
+ request,
19
+ response,
20
+ }: MockExternalRequestArg) {
21
+ requestMocks.push({request, response});
22
+
23
+ const {body, init} = await mockParams(response);
24
+ fetchMock.mockResponse(body, init);
25
+ }
26
+
27
+ export async function mockExternalRequests(...mocks: MockExternalRequestArg[]) {
28
+ const parsedResponses: [string, MockParams][] = [];
29
+ for (const mock of mocks) {
30
+ const {request, response} = mock;
31
+
32
+ requestMocks.push({request, response});
33
+
34
+ const {body, init} = await mockParams(response);
35
+ parsedResponses.push([body, init]);
36
+ }
37
+
38
+ fetchMock.mockResponses(...parsedResponses);
39
+ }
40
+
41
+ async function mockParams(response: Response): Promise<ResponseParams> {
42
+ return {
43
+ body: await response.text(),
44
+ init: {
45
+ status: response.status,
46
+ statusText: response.statusText,
47
+ url: response.url,
48
+ headers: {
49
+ ...Object.fromEntries(response.headers.entries()),
50
+ 'content-type': 'application/json',
51
+ },
52
+ },
53
+ };
54
+ }
55
+
56
+ export async function validateMocks() {
57
+ if (requestMocks.length === 0 && fetchMock.mock.calls.length === 0) {
58
+ return;
59
+ }
60
+
61
+ let matchedRequests = 0;
62
+
63
+ for (const [index, requestMock] of requestMocks.entries()) {
64
+ const {request} = requestMock;
65
+
66
+ if (fetchMock.mock.calls.length === 0) {
67
+ continue;
68
+ }
69
+
70
+ matchedRequests++;
71
+ const [url, init] = fetchMock.mock.calls[index];
72
+
73
+ const expected: Record<string, any> = {};
74
+ const actual: Record<string, any> = {};
75
+
76
+ if (request?.url) {
77
+ expected.url = new URL(request.url);
78
+ actual.url = new URL(url as string);
79
+ }
80
+
81
+ if (request?.method) {
82
+ expected.method = request.method;
83
+ actual.method = init?.method || 'GET';
84
+ }
85
+
86
+ if (request?.body) {
87
+ const bodyBuffer = await stream2buffer(request.body);
88
+ const bodyString = new TextDecoder('utf-8').decode(bodyBuffer);
89
+
90
+ expected.body = expect.stringContaining(bodyString);
91
+ actual.body = init?.body?.toString();
92
+ }
93
+
94
+ if (request?.headers) {
95
+ expected.headers = {};
96
+ actual.headers = {};
97
+
98
+ // eslint-disable-next-line no-warning-comments
99
+ // TODO: we're currently not checking the headers properly. We should fix this.
100
+ Object.entries(request.headers).forEach(([key, value]) => {
101
+ expected.headers[key] = value;
102
+ actual.headers[key] = (init?.headers as any)[key];
103
+ });
104
+ }
105
+
106
+ try {
107
+ expect(actual).toEqual(expected);
108
+ } catch (error) {
109
+ error.message = `${init?.method} request made to ${url} does not match expectation:\n\n${error.message}`;
110
+ throw error;
111
+ }
112
+ }
113
+
114
+ if (requestMocks.length > matchedRequests) {
115
+ throw new Error(
116
+ `Expected ${
117
+ requestMocks.length
118
+ } request(s) to be made but they were not:\n\n${JSON.stringify(
119
+ requestMocks,
120
+ null,
121
+ 2,
122
+ )}`,
123
+ );
124
+ }
125
+
126
+ if (fetchMock.mock.calls.length > matchedRequests) {
127
+ throw new Error(
128
+ `${
129
+ fetchMock.mock.calls.length
130
+ } unexpected request(s) were made, make sure to mock all expected requests:\n\n${JSON.stringify(
131
+ fetchMock.mock.calls,
132
+ null,
133
+ 2,
134
+ )}`,
135
+ );
136
+ }
137
+ }
138
+
139
+ async function stream2buffer(
140
+ stream: ReadableStream<Uint8Array>,
141
+ ): Promise<Buffer> {
142
+ const buffers = [];
143
+
144
+ for await (const data of stream as any) {
145
+ if (typeof data === 'number') {
146
+ buffers.push(Uint8Array.from([data]));
147
+ } else {
148
+ buffers.push(data);
149
+ }
150
+ }
151
+
152
+ return Buffer.concat(buffers);
153
+ }
154
+
155
+ export function skipMockChecks(value: boolean) {
156
+ skipMockChecksFlag = value;
157
+ }
158
+
159
+ beforeEach(() => {
160
+ skipMockChecksFlag = false;
161
+ requestMocks = [];
162
+ fetchMock.resetMocks();
163
+ });
164
+
165
+ afterEach(async () => {
166
+ if (!skipMockChecksFlag) {
167
+ await validateMocks();
168
+ }
169
+ });