Skip to content

Commit

Permalink
Add tests for exported context types
Browse files Browse the repository at this point in the history
  • Loading branch information
paulomarg committed May 14, 2024
1 parent f852d89 commit 8475ae7
Show file tree
Hide file tree
Showing 5 changed files with 328 additions and 169 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-doors-speak.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@shopify/shopify-app-remix": minor
---

Made it possible to create types for the context objects returned by the various `authenticate` methods from the actual `shopifyApp` object. With this, apps can pass the contexts and their components as function arguments much more easily.
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ export * from './get-jwt';
export * from './get-thrown-response';
export * from './request-mock';
export * from './setup-valid-session';
export * from './setup-valid-request';
export * from './sign-request-cookie';
export * from './test-config';
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import {HashFormat, createSHA256HMAC} from '@shopify/shopify-api/runtime';

import {ShopifyApp} from '../types';

import {API_SECRET_KEY, APP_URL, BASE64_HOST, TEST_SHOP} from './const';
import {setUpValidSession} from './setup-valid-session';
import {getJwt} from './get-jwt';
import {getHmac} from './get-hmac';

export enum RequestType {
Admin,
Bearer,
Extension,
Public,
}

interface ValidBaseRequestOptions {
type: RequestType.Admin | RequestType.Bearer | RequestType.Public;
}

interface ValidExtensionRequestOptions {
type: RequestType.Extension;
body?: any;
headers?: Record<string, string>;
}

type ValidRequestOptions =
| ValidBaseRequestOptions
| ValidExtensionRequestOptions;

export async function setupValidRequest(
shopify: ShopifyApp<any>,
options: ValidRequestOptions,
) {
const session = await setUpValidSession(shopify.sessionStorage, {
isOnline: false,
});

const url = new URL(APP_URL);
const init: RequestInit = {};

let request: Request;
switch (options.type) {
case RequestType.Admin:
request = adminRequest(url, init);
break;
case RequestType.Bearer:
request = bearerRequest(url, init);
break;
case RequestType.Extension:
request = extensionRequest(url, init, options.body, options.headers);
break;
case RequestType.Public:
request = await publicRequest(url, init);
break;
}

return {shopify, session, request};
}

function adminRequest(url: URL, init: RequestInit) {
const {token} = getJwt();

url.search = new URLSearchParams({
embedded: '1',
shop: TEST_SHOP,
host: BASE64_HOST,
id_token: token,
}).toString();
return new Request(url.href, init);
}

function bearerRequest(url: URL, init: RequestInit) {
const {token} = getJwt();

init.headers = {
authorization: `Bearer ${token}`,
};

return new Request(url.href, init);
}

function extensionRequest(
url: URL,
init: RequestInit,
body: any,
headers?: Record<string, string>,
) {
const bodyString = JSON.stringify(body);

init.method = 'POST';
init.body = bodyString;
init.headers = {
'X-Shopify-Hmac-Sha256': getHmac(bodyString),
'X-Shopify-Shop-Domain': TEST_SHOP,
...headers,
};

return new Request(url.href, init);
}

async function publicRequest(url: URL, init: RequestInit) {
url.searchParams.set('shop', TEST_SHOP);
url.searchParams.set('timestamp', String(Math.trunc(Date.now() / 1000) - 1));

const params = Object.fromEntries(url.searchParams.entries());
const string = Object.entries(params)
.sort(([val1], [val2]) => val1.localeCompare(val2))
.reduce((acc, [key, value]) => {
return `${acc}${key}=${value}`;
}, '');

url.searchParams.set(
'signature',
await createSHA256HMAC(API_SECRET_KEY, string, HashFormat.Hex),
);

return new Request(url.href, init);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import {
RequestType,
TEST_SHOP,
setUpValidSession,
setupValidRequest,
testConfig,
} from '../__test-helpers';
import {shopifyApp} from '../shopify-app';
import {
AdminApiContext,
AdminContext,
AdminGraphqlClient,
AppProxyContext,
CheckoutContext,
CustomerAccountContext,
FlowContext,
FulfillmentServiceContext,
StorefrontApiContext,
StorefrontGraphqlClient,
UnauthenticatedAdminContext,
UnauthenticatedStorefrontContext,
WebhookContext,
} from '../types-contexts';

// These tests aren't asserting anything useful, but if the types are incorrect they'll still cause failures
describe('assign authentication contexts to variables', () => {
it('unauthenticated.admin', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const session = await setUpValidSession(shopify.sessionStorage);

// WHEN
const realContext = await shopify.unauthenticated.admin(session.shop);
const context: UnauthenticatedAdminContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('unauthenticated.storefront', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const session = await setUpValidSession(shopify.sessionStorage);

// WHEN
const realContext = await shopify.unauthenticated.storefront(session.shop);
const context: UnauthenticatedStorefrontContext<typeof shopify> =
realContext;
const apiContext: StorefrontApiContext<typeof shopify> =
realContext.storefront;
const graphqlClient: StorefrontGraphqlClient<typeof shopify> =
realContext.storefront.graphql;

// THEN
expect(context.storefront).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.admin', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Admin,
});

// WHEN
const realContext = await shopify.authenticate.admin(request);
const context: AdminContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.flow', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Extension,
body: {shopify_domain: TEST_SHOP},
});

// WHEN
const realContext = await shopify.authenticate.flow(request);
const context: FlowContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.fulfillmentService', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Extension,
body: {kind: 'FULFILLMENT_REQUEST'},
});

// WHEN
const realContext = await shopify.authenticate.fulfillmentService(request);
const context: FulfillmentServiceContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.webhook', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Extension,
body: {payload: 'test'},
headers: {
'X-Shopify-Topic': 'app/uninstalled',
'X-Shopify-API-Version': '2023-01',
'X-Shopify-Webhook-Id': '1234567890',
},
});

// WHEN
const realContext = await shopify.authenticate.webhook(request);
const context: WebhookContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin!;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin!.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.public.appProxy', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Public,
});

// WHEN
const realContext = await shopify.authenticate.public.appProxy(request);
const context: AppProxyContext<typeof shopify> = realContext;
const apiContext: AdminApiContext<typeof shopify> = realContext.admin!;
const graphqlClient: AdminGraphqlClient<typeof shopify> =
realContext.admin!.graphql;

// THEN
expect(context.admin).toBeDefined();
expect(apiContext.graphql).toBeDefined();
expect(graphqlClient).toBeDefined();
});

it('authenticate.public.checkout', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Bearer,
});

// WHEN
const realContext = await shopify.authenticate.public.checkout(request);
const context: CheckoutContext<typeof shopify> = realContext;

// THEN
expect(context.cors).toBeDefined();
});

it('authenticate.public.customerAccount', async () => {
// GIVEN
const shopify = shopifyApp(testConfig());
const {request} = await setupValidRequest(shopify, {
type: RequestType.Bearer,
});

// WHEN
const realContext =
await shopify.authenticate.public.customerAccount(request);
const context: CustomerAccountContext<typeof shopify> = realContext;

// THEN
expect(context.cors).toBeDefined();
});
});

0 comments on commit 8475ae7

Please sign in to comment.