-
Notifications
You must be signed in to change notification settings - Fork 81
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add tests for exported context types
- Loading branch information
Showing
5 changed files
with
328 additions
and
169 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
119 changes: 119 additions & 0 deletions
119
packages/apps/shopify-app-remix/src/server/__test-helpers/setup-valid-request.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |
203 changes: 203 additions & 0 deletions
203
packages/apps/shopify-app-remix/src/server/__tests__/types-contexts.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); | ||
}); |
Oops, something went wrong.