-
Notifications
You must be signed in to change notification settings - Fork 392
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #908 from Shopify/kos/switch_auth_to_offline_tokens
Switch to offline session tokens
- Loading branch information
Showing
8 changed files
with
328 additions
and
49 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 |
---|---|---|
@@ -1,5 +1,9 @@ | ||
# Release history | ||
|
||
## Unreleased | ||
|
||
- ⚠️ [Breaking] Use offline access tokens to interact with the Admin API, remove GraphQL proxy since it doesn't work with offline tokens. ([#908](https://github.com/Shopify/shopify-app-template-node/pull/908)) | ||
|
||
## 1.0.0 (June 20, 2022) | ||
|
||
- New Shopify App Template for Node, with React frontend, for use with CLI 3 |
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
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
Submodule frontend
updated
10 files
+3 −3 | App.jsx | |
+18 −112 | components/ProductsCard.jsx | |
+24 −6 | components/providers/AppBridgeProvider.jsx | |
+1 −1 | components/providers/QueryProvider.jsx | |
+1 −1 | components/providers/index.js | |
+0 −2 | hooks/index.js | |
+0 −25 | hooks/useShopifyMutation.js | |
+0 −21 | hooks/useShopifyQuery.js | |
+0 −2 | package.json | |
+2 −2 | test/mount.jsx |
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
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,186 @@ | ||
import { Shopify } from "@shopify/shopify-api"; | ||
import { describe, expect, test, vi } from "vitest"; | ||
|
||
import productCreator, { DEFAULT_PRODUCTS_COUNT } from "../product-creator"; | ||
|
||
|
||
describe("productCreator", async () => { | ||
const session = new Shopify.Session.Session("1", "test-shop", "state", false); | ||
session.scope = Shopify.Context.SCOPES; | ||
session.accessToken = "access-token"; | ||
session.expires = null; | ||
|
||
describe("defaults", async () => { | ||
test(`successfully creates ${DEFAULT_PRODUCTS_COUNT} products`, async () => { | ||
const spy = mockGraphQLMutations( | ||
[...Array(DEFAULT_PRODUCTS_COUNT).keys()].map(() => { | ||
return generateProductCreateSuccessResponse(); | ||
} | ||
)); | ||
await productCreator(session); | ||
|
||
expect(spy).toHaveBeenCalledTimes(DEFAULT_PRODUCTS_COUNT); | ||
for (let i = 0; i < DEFAULT_PRODUCTS_COUNT; i++) { | ||
expect(spy).toHaveBeenNthCalledWith( | ||
i+1, | ||
expect.objectContaining({ | ||
data: expect.objectContaining({ | ||
query: expect.stringContaining("mutation populateProduct"), | ||
}), | ||
}) | ||
); | ||
} | ||
}); | ||
|
||
test(`fails to create ${DEFAULT_PRODUCTS_COUNT} products`, async () => { | ||
const spy = mockGraphQLMutations([ | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
]); | ||
await productCreator(session); | ||
|
||
expect(spy).toHaveBeenCalledTimes(DEFAULT_PRODUCTS_COUNT); | ||
for (let i = 0; i < DEFAULT_PRODUCTS_COUNT; i++) { | ||
expect(spy).toHaveBeenNthCalledWith( | ||
i+1, | ||
expect.objectContaining({ | ||
data: expect.objectContaining({ | ||
query: expect.stringContaining("mutation populateProduct"), | ||
}), | ||
}) | ||
); | ||
} | ||
}); | ||
}); | ||
|
||
describe("count = 2", async () => { | ||
const count = 2; | ||
|
||
test(`successfully creates ${count} products`, async () => { | ||
const spy = mockGraphQLMutations( | ||
[...Array(count).keys()].map(() => { | ||
return generateProductCreateSuccessResponse(); | ||
} | ||
)); | ||
await productCreator(session, count); | ||
|
||
expect(spy).toHaveBeenCalledTimes(count); | ||
for (let i = 0; i < count; i++) { | ||
expect(spy).toHaveBeenNthCalledWith( | ||
i+1, | ||
expect.objectContaining({ | ||
data: expect.objectContaining({ | ||
query: expect.stringContaining("mutation populateProduct"), | ||
}), | ||
}) | ||
); | ||
} | ||
}); | ||
|
||
test(`fails to create ${count} products`, async () => { | ||
const spy = mockGraphQLMutations([ | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
PRODUCT_CREATE_ERROR_RESPONSE, | ||
]); | ||
await productCreator(session, count); | ||
|
||
expect(spy).toHaveBeenCalledTimes(count); | ||
for (let i = 0; i < count; i++) { | ||
expect(spy).toHaveBeenNthCalledWith( | ||
i+1, | ||
expect.objectContaining({ | ||
data: expect.objectContaining({ | ||
query: expect.stringContaining("mutation populateProduct"), | ||
}), | ||
}) | ||
); | ||
} | ||
}); | ||
}); | ||
}); | ||
|
||
function mockGraphQLMutations(queryResponses) { | ||
let queryIndex = 0; | ||
const querySpy = vi.fn().mockImplementation(() => { | ||
return queryResponses[queryIndex++]; | ||
}); | ||
|
||
vi.spyOn(Shopify.Clients, "Graphql").mockImplementation(() => { | ||
return { | ||
query: querySpy, | ||
}; | ||
}); | ||
|
||
return querySpy; | ||
} | ||
|
||
function generateProductCreateSuccessResponse() { | ||
const gid = [...Array(13).keys()].map(() => { return Math.floor(Math.random() * 10) }).join(""); | ||
let response = PRODUCT_CREATE_SUCCESS_RESPONSE; | ||
response.body.data.productCreate.product.id = `gid://shopify/Product/${gid}`; | ||
return response; | ||
} | ||
|
||
const PRODUCT_CREATE_SUCCESS_RESPONSE = { | ||
body: { | ||
data: { | ||
productCreate: { | ||
product: { | ||
id: "gid://shopify/Product/7422520950945" | ||
} | ||
} | ||
}, | ||
extensions: { | ||
cost: { | ||
requestedQueryCost: 10, | ||
actualQueryCost: 10, | ||
throttleStatus: { | ||
maximumAvailable: 1000, | ||
currentlyAvailable: 990, | ||
restoreRate: 50 | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
const PRODUCT_CREATE_ERROR_RESPONSE = { | ||
body: { | ||
data: { | ||
productCreate: null | ||
}, | ||
errors: [ | ||
{ | ||
message: "Access denied for productCreate field. Required access: `write_products` access scope.", | ||
locations: [ | ||
{ | ||
line: 2, | ||
column: 3 | ||
} | ||
], | ||
path: [ | ||
"productCreate" | ||
], | ||
extensions: { | ||
code: "ACCESS_DENIED", | ||
documentation: "https://shopify.dev/api/usage/access-scopes", | ||
requiredAccess: "`write_products` access scope." | ||
} | ||
} | ||
], | ||
extensions: { | ||
cost: { | ||
requestedQueryCost: 10, | ||
actualQueryCost: 10, | ||
throttleStatus: { | ||
maximumAvailable: 1000, | ||
currentlyAvailable: 990, | ||
restoreRate: 50 | ||
} | ||
} | ||
} | ||
} | ||
} |
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,108 @@ | ||
import { Shopify } from "@shopify/shopify-api"; | ||
|
||
const ADJECTIVES = [ | ||
"autumn", | ||
"hidden", | ||
"bitter", | ||
"misty", | ||
"silent", | ||
"empty", | ||
"dry", | ||
"dark", | ||
"summer", | ||
"icy", | ||
"delicate", | ||
"quiet", | ||
"white", | ||
"cool", | ||
"spring", | ||
"winter", | ||
"patient", | ||
"twilight", | ||
"dawn", | ||
"crimson", | ||
"wispy", | ||
"weathered", | ||
"blue", | ||
"billowing", | ||
"broken", | ||
"cold", | ||
"damp", | ||
"falling", | ||
"frosty", | ||
"green", | ||
"long", | ||
] | ||
|
||
const NOUNS = [ | ||
"waterfall", | ||
"river", | ||
"breeze", | ||
"moon", | ||
"rain", | ||
"wind", | ||
"sea", | ||
"morning", | ||
"snow", | ||
"lake", | ||
"sunset", | ||
"pine", | ||
"shadow", | ||
"leaf", | ||
"dawn", | ||
"glitter", | ||
"forest", | ||
"hill", | ||
"cloud", | ||
"meadow", | ||
"sun", | ||
"glade", | ||
"bird", | ||
"brook", | ||
"butterfly", | ||
"bush", | ||
"dew", | ||
"dust", | ||
"field", | ||
"fire", | ||
"flower", | ||
] | ||
|
||
export const DEFAULT_PRODUCTS_COUNT = 5; | ||
const CREATE_PRODUCTS_MUTATION = ` | ||
mutation populateProduct($input: ProductInput!) { | ||
productCreate(input: $input) { | ||
product { | ||
id | ||
} | ||
} | ||
} | ||
` | ||
|
||
export default async function productCreator(session, count = DEFAULT_PRODUCTS_COUNT) { | ||
const client = new Shopify.Clients.Graphql(session.shop, session.accessToken); | ||
|
||
for (let i = 0; i < count; i++) { | ||
await client.query({ | ||
data: { | ||
query: CREATE_PRODUCTS_MUTATION, | ||
variables: { | ||
input: { | ||
title: `${randomTitle()}`, | ||
variants: [{ price: randomPrice() }], | ||
}, | ||
}, | ||
}, | ||
}); | ||
} | ||
} | ||
|
||
function randomTitle() { | ||
const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; | ||
const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; | ||
return `${adjective} ${noun}`; | ||
} | ||
|
||
function randomPrice() { | ||
return Math.round((Math.random() * 10 + Number.EPSILON) * 100) / 100; | ||
} |
Oops, something went wrong.