@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.
- package/dist/cjs/react/components/AppProvider/AppProvider.js.map +1 -1
- package/dist/cjs/react/components/AppProxyLink/AppProxyLink.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/auth-code-flow.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/merchant-custom-app.js.map +1 -1
- package/dist/cjs/server/authenticate/admin/strategies/token-exchange.js.map +1 -1
- package/dist/cjs/server/authenticate/webhooks/authenticate.js +34 -10
- package/dist/cjs/server/authenticate/webhooks/authenticate.js.map +1 -1
- package/dist/cjs/server/version.js +1 -1
- package/dist/cjs/server/version.js.map +1 -1
- package/dist/esm/react/components/AppProvider/AppProvider.mjs.map +1 -1
- package/dist/esm/react/components/AppProxyLink/AppProxyLink.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/auth-code-flow.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/merchant-custom-app.mjs.map +1 -1
- package/dist/esm/server/authenticate/admin/strategies/token-exchange.mjs.map +1 -1
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs +35 -11
- package/dist/esm/server/authenticate/webhooks/authenticate.mjs.map +1 -1
- package/dist/esm/server/version.mjs +1 -1
- package/dist/esm/server/version.mjs.map +1 -1
- package/dist/ts/react/components/AppProvider/AppProvider.d.ts.map +1 -1
- package/dist/ts/react/components/AppProxyLink/AppProxyLink.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/billing/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/auth-code-flow.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/merchant-custom-app.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/strategies/token-exchange.d.ts.map +1 -1
- package/dist/ts/server/authenticate/admin/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/public/extension/types.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/authenticate.d.ts.map +1 -1
- package/dist/ts/server/authenticate/webhooks/types.d.ts +34 -2
- package/dist/ts/server/authenticate/webhooks/types.d.ts.map +1 -1
- package/dist/ts/server/config-types.d.ts.map +1 -1
- package/dist/ts/server/version.d.ts +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +18 -14
- package/src/react/components/AppProvider/AppProvider.doc.ts +35 -0
- package/src/react/components/AppProvider/AppProvider.tsx +147 -0
- package/src/react/components/AppProvider/index.ts +1 -0
- package/src/react/components/AppProxyForm/AppProxyForm.doc.ts +28 -0
- package/src/react/components/AppProxyForm/AppProxyForm.tsx +82 -0
- package/src/react/components/AppProxyForm/index.ts +1 -0
- package/src/react/components/AppProxyLink/AppProxyLink.doc.ts +28 -0
- package/src/react/components/AppProxyLink/AppProxyLink.tsx +58 -0
- package/src/react/components/AppProxyLink/index.ts +1 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.doc.ts +40 -0
- package/src/react/components/AppProxyProvider/AppProxyProvider.tsx +100 -0
- package/src/react/components/AppProxyProvider/index.ts +5 -0
- package/src/react/components/RemixPolarisLink.tsx +14 -0
- package/src/react/components/index.ts +4 -0
- package/src/react/const.ts +2 -0
- package/src/react/index.ts +1 -0
- package/src/server/__test-helpers/const.ts +14 -0
- package/src/server/__test-helpers/expect-admin-api-client.ts +51 -0
- package/src/server/__test-helpers/expect-begin-auth-redirect.ts +22 -0
- package/src/server/__test-helpers/expect-document-request-headers.ts +25 -0
- package/src/server/__test-helpers/expect-exit-iframe.ts +37 -0
- package/src/server/__test-helpers/expect-login-redirect.ts +8 -0
- package/src/server/__test-helpers/expect-storefront-api-client.ts +49 -0
- package/src/server/__test-helpers/expect-token-refresh.ts +188 -0
- package/src/server/__test-helpers/get-hmac.ts +7 -0
- package/src/server/__test-helpers/get-jwt.ts +10 -0
- package/src/server/__test-helpers/get-thrown-error.ts +19 -0
- package/src/server/__test-helpers/get-thrown-response.ts +17 -0
- package/src/server/__test-helpers/index.ts +21 -0
- package/src/server/__test-helpers/mock-graphql-request.ts +60 -0
- package/src/server/__test-helpers/request-mock.ts +169 -0
- package/src/server/__test-helpers/setup-embedded-flow.ts +30 -0
- package/src/server/__test-helpers/setup-fetch-flow.ts +29 -0
- package/src/server/__test-helpers/setup-non-embedded-flow.ts +31 -0
- package/src/server/__test-helpers/setup-valid-request.ts +78 -0
- package/src/server/__test-helpers/setup-valid-session.ts +28 -0
- package/src/server/__test-helpers/sign-request-cookie.ts +21 -0
- package/src/server/__test-helpers/test-config.ts +56 -0
- package/src/server/adapters/node/index.ts +13 -0
- package/src/server/adapters/vercel/index.ts +13 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.example.ts +34 -0
- package/src/server/authenticate/admin/authenticate.admin.doc.ts +51 -0
- package/src/server/authenticate/admin/authenticate.ts +232 -0
- package/src/server/authenticate/admin/billing/authenticate.admin.billing.doc.ts +29 -0
- package/src/server/authenticate/admin/billing/cancel.ts +38 -0
- package/src/server/authenticate/admin/billing/check.ts +41 -0
- package/src/server/authenticate/admin/billing/create-usage-record.ts +36 -0
- package/src/server/authenticate/admin/billing/helpers.ts +42 -0
- package/src/server/authenticate/admin/billing/index.ts +6 -0
- package/src/server/authenticate/admin/billing/request.ts +64 -0
- package/src/server/authenticate/admin/billing/require.ts +58 -0
- package/src/server/authenticate/admin/billing/types.ts +613 -0
- package/src/server/authenticate/admin/billing/update-usage-subscription-capped-amount.ts +55 -0
- package/src/server/authenticate/admin/helpers/begin-auth.ts +17 -0
- package/src/server/authenticate/admin/helpers/create-admin-api-context.ts +20 -0
- package/src/server/authenticate/admin/helpers/ensure-app-is-embedded-if-required.ts +18 -0
- package/src/server/authenticate/admin/helpers/ensure-session-token-search-param-if-required.ts +25 -0
- package/src/server/authenticate/admin/helpers/handle-client-error.ts +45 -0
- package/src/server/authenticate/admin/helpers/index.ts +14 -0
- package/src/server/authenticate/admin/helpers/redirect-to-auth-page.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect-to-bounce-page.ts +23 -0
- package/src/server/authenticate/admin/helpers/redirect-to-install-page.ts +53 -0
- package/src/server/authenticate/admin/helpers/redirect-to-shopify-or-app-root.ts +21 -0
- package/src/server/authenticate/admin/helpers/redirect-with-app-bridge-headers.ts +13 -0
- package/src/server/authenticate/admin/helpers/redirect-with-exitiframe.ts +28 -0
- package/src/server/authenticate/admin/helpers/redirect.ts +164 -0
- package/src/server/authenticate/admin/helpers/render-app-bridge.ts +46 -0
- package/src/server/authenticate/admin/helpers/trigger-after-auth-hook.ts +29 -0
- package/src/server/authenticate/admin/helpers/validate-redirect-url.ts +67 -0
- package/src/server/authenticate/admin/helpers/validate-shop-and-host-params.ts +46 -0
- package/src/server/authenticate/admin/scope/authenticate.admin.scopes.doc.ts +28 -0
- package/src/server/authenticate/admin/scope/client/fetch-scopes-details.ts +43 -0
- package/src/server/authenticate/admin/scope/client/revoke-scopes.ts +38 -0
- package/src/server/authenticate/admin/scope/factory.ts +21 -0
- package/src/server/authenticate/admin/scope/query.ts +51 -0
- package/src/server/authenticate/admin/scope/request.ts +32 -0
- package/src/server/authenticate/admin/scope/revoke.ts +48 -0
- package/src/server/authenticate/admin/scope/types.ts +126 -0
- package/src/server/authenticate/admin/strategies/auth-code-flow.ts +332 -0
- package/src/server/authenticate/admin/strategies/merchant-custom-app.ts +65 -0
- package/src/server/authenticate/admin/strategies/token-exchange.ts +188 -0
- package/src/server/authenticate/admin/strategies/types.ts +29 -0
- package/src/server/authenticate/admin/types.ts +216 -0
- package/src/server/authenticate/const.ts +15 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.example.ts +39 -0
- package/src/server/authenticate/flow/authenticate.flow.doc.ts +47 -0
- package/src/server/authenticate/flow/authenticate.ts +68 -0
- package/src/server/authenticate/flow/types.ts +85 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.example.ts +52 -0
- package/src/server/authenticate/fulfillment-service/authenticate.fulfillment-service.doc.ts +48 -0
- package/src/server/authenticate/fulfillment-service/authenticate.ts +82 -0
- package/src/server/authenticate/fulfillment-service/types.ts +82 -0
- package/src/server/authenticate/helpers/add-response-headers.ts +43 -0
- package/src/server/authenticate/helpers/app-bridge-url.ts +10 -0
- package/src/server/authenticate/helpers/ensure-cors-headers.ts +38 -0
- package/src/server/authenticate/helpers/get-session-token-header.ts +11 -0
- package/src/server/authenticate/helpers/get-shop-from-request.ts +4 -0
- package/src/server/authenticate/helpers/idempotent-promise-handler.ts +45 -0
- package/src/server/authenticate/helpers/index.ts +10 -0
- package/src/server/authenticate/helpers/invalidate-access-token.ts +17 -0
- package/src/server/authenticate/helpers/reject-bot-request.ts +27 -0
- package/src/server/authenticate/helpers/respond-to-invalid-session-token.ts +28 -0
- package/src/server/authenticate/helpers/respond-to-options-request.ts +26 -0
- package/src/server/authenticate/helpers/validate-session-token.ts +40 -0
- package/src/server/authenticate/login/login.ts +53 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.example.ts +27 -0
- package/src/server/authenticate/public/appProxy/authenticate.public.app-proxy.doc.ts +55 -0
- package/src/server/authenticate/public/appProxy/authenticate.ts +150 -0
- package/src/server/authenticate/public/appProxy/types.ts +215 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.offers.ts +21 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.example.ts +17 -0
- package/src/server/authenticate/public/checkout/authenticate.public.checkout.doc.ts +49 -0
- package/src/server/authenticate/public/checkout/authenticate.ts +13 -0
- package/src/server/authenticate/public/checkout/types.ts +65 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.offers.ts +36 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.example.ts +18 -0
- package/src/server/authenticate/public/customer-account/authenticate.public.customerAccount.doc.ts +49 -0
- package/src/server/authenticate/public/customer-account/authenticate.ts +13 -0
- package/src/server/authenticate/public/customer-account/types.ts +65 -0
- package/src/server/authenticate/public/extension/authenticate.ts +54 -0
- package/src/server/authenticate/public/extension/types.ts +11 -0
- package/src/server/authenticate/public/factory.ts +24 -0
- package/src/server/authenticate/public/index.ts +1 -0
- package/src/server/authenticate/public/pos/authenticate.ts +8 -0
- package/src/server/authenticate/public/pos/types.ts +65 -0
- package/src/server/authenticate/public/types.ts +96 -0
- package/src/server/authenticate/webhooks/authenticate.ts +105 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.example.ts +43 -0
- package/src/server/authenticate/webhooks/authenticate.webhooks.doc.ts +39 -0
- package/src/server/authenticate/webhooks/index.ts +1 -0
- package/src/server/authenticate/webhooks/register.ts +48 -0
- package/src/server/authenticate/webhooks/types.ts +266 -0
- package/src/server/boundary/error.tsx +14 -0
- package/src/server/boundary/headers.ts +15 -0
- package/src/server/boundary/index.ts +39 -0
- package/src/server/boundary/types.ts +4 -0
- package/src/server/clients/admin/authenticate.admin.api.doc.ts +34 -0
- package/src/server/clients/admin/factory.ts +22 -0
- package/src/server/clients/admin/graphql.ts +39 -0
- package/src/server/clients/admin/index.ts +2 -0
- package/src/server/clients/admin/types.ts +120 -0
- package/src/server/clients/index.ts +2 -0
- package/src/server/clients/storefront/authenticate.storefront.api.doc.ts +34 -0
- package/src/server/clients/storefront/factory.ts +32 -0
- package/src/server/clients/storefront/index.ts +2 -0
- package/src/server/clients/storefront/types.ts +78 -0
- package/src/server/clients/types.ts +47 -0
- package/src/server/config-types.ts +309 -0
- package/src/server/errors.ts +3 -0
- package/src/server/future/flags.ts +62 -0
- package/src/server/helpers/create-or-load-offline-session.ts +19 -0
- package/src/server/helpers/ensure-offline-token-is-not-expired.ts +32 -0
- package/src/server/helpers/ensure-valid-offline-session.ts +15 -0
- package/src/server/helpers/index.ts +4 -0
- package/src/server/helpers/refresh-token.ts +35 -0
- package/src/server/index.ts +24 -0
- package/src/server/override-logger.ts +42 -0
- package/src/server/shopify-app.doc.ts +45 -0
- package/src/server/shopify-app.ts +215 -0
- package/src/server/test-helpers/index.ts +1 -0
- package/src/server/test-helpers/test-config.ts +79 -0
- package/src/server/types-contexts.ts +44 -0
- package/src/server/types.ts +507 -0
- package/src/server/unauthenticated/admin/factory.ts +23 -0
- package/src/server/unauthenticated/admin/index.ts +1 -0
- package/src/server/unauthenticated/admin/types.ts +79 -0
- package/src/server/unauthenticated/admin/unauthenticated.admin.doc.ts +40 -0
- package/src/server/unauthenticated/storefront/factory.ts +28 -0
- package/src/server/unauthenticated/storefront/index.ts +1 -0
- package/src/server/unauthenticated/storefront/types.ts +105 -0
- package/src/server/unauthenticated/storefront/unauthenticated.storefront.doc.ts +40 -0
- package/src/server/unauthenticated/types.ts +65 -0
- 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,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,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
|
+
});
|