From 5b8104bb5f7ff11e27defa1c92e76c7d464a013d Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Mon, 11 Dec 2023 12:09:03 -0700 Subject: [PATCH 01/10] Add utility helper to convert between unified admin url and legacy url --- .../unified-admin-url-helper.test.ts | 34 +++++++++++++++ .../lib/utils/unified-admin-url-helper.ts | 41 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts create mode 100644 packages/shopify-api/lib/utils/unified-admin-url-helper.ts diff --git a/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts new file mode 100644 index 000000000..5b3093b89 --- /dev/null +++ b/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts @@ -0,0 +1,34 @@ +import {shopifyApi} from '../..'; +import {testConfig} from '../../__tests__/test-config'; + +const VALID_UNIFIED_ADMIN_URLS = [ + { + unifiedAdminUrl: 'admin.shopify.com/store/my-shop', + legacyAdminUrl: 'my-shop.myshopify.com', + }, + { + unifiedAdminUrl: 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop', + legacyAdminUrl: 'my-shop.shopify.abc.def-gh.ij.spin.dev', + }, +]; + +VALID_UNIFIED_ADMIN_URLS.forEach(({unifiedAdminUrl, legacyAdminUrl}) => { + test('converts unified admin URL to legacy URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.unifiedAdminUrlToLegacyUrl(unifiedAdminUrl); + expect(actual).toEqual(legacyAdminUrl); + }); + + test('converts legacy URL to unified admin URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.legacyUrlToUnifiedAdminUrl(legacyAdminUrl); + expect(actual).toEqual(unifiedAdminUrl); + }); +}); + +test('returns null when trying to convert from unified admin url to legacy url', () => { + const invalid_url = 'not-admin.shopify.com/store/my-shop'; + const shopify = shopifyApi(testConfig()); + + expect(shopify.utils.unifiedAdminUrlToLegacyUrl(invalid_url)).toBe(null); +}); diff --git a/packages/shopify-api/lib/utils/unified-admin-url-helper.ts b/packages/shopify-api/lib/utils/unified-admin-url-helper.ts new file mode 100644 index 000000000..4e3a77760 --- /dev/null +++ b/packages/shopify-api/lib/utils/unified-admin-url-helper.ts @@ -0,0 +1,41 @@ +// This class assumes the legacy url has been sanitized already +// Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com +export function unifiedAdminUrlToLegacyUrl() { + return (unifiedAdminUrl: string): string | null => { + const isUnifiedAdminUrl = unifiedAdminUrl.split('.')[0] === 'admin'; + + if (!isUnifiedAdminUrl) { + return null; + } + + const urlComponents = unifiedAdminUrl.split('/'); + const shopName = urlComponents[urlComponents.length - 1]; + + const isSpinUrl = unifiedAdminUrl.includes('spin.dev/store/'); + if (isSpinUrl) { + const spinUrlComponents = unifiedAdminUrl + .split('.') + .slice(2, 5) + .join('.'); + + return `${shopName}.shopify.${spinUrlComponents}.spin.dev`; + } else { + return `${shopName}.myshopify.com`; + } + }; +} + +// Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop +export function legacyUrlToUnifiedAdminUrl() { + return (legacyAdminUrl: string): string | null => { + const shopName = legacyAdminUrl.split('.')[0]; + + const isSpinUrl = legacyAdminUrl.endsWith('spin.dev'); + if (isSpinUrl) { + const spinUrlComponents = legacyAdminUrl.split('.').slice(2, 5).join('.'); + return `admin.web.${spinUrlComponents}.spin.dev/store/${shopName}`; + } else { + return `admin.shopify.com/store/${shopName}`; + } + }; +} From 12a36213177572ae20685c062631df02acd4b717 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Mon, 11 Dec 2023 14:31:04 -0700 Subject: [PATCH 02/10] Try to convert shop from unified admin to legacy url before sanitizing it --- .../utils/__tests__/shop-validator.test.ts | 31 ++++++++++++++++--- packages/shopify-api/lib/utils/index.ts | 6 ++++ .../shopify-api/lib/utils/shop-validator.ts | 15 ++++++++- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts index 0a32f4fae..1181e7bb9 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts @@ -9,6 +9,8 @@ const VALID_SHOP_URL_4 = 'dev-shop-.myshopify.io'; const INVALID_SHOP_URL_1 = 'notshopify.com'; const INVALID_SHOP_URL_2 = '-invalid.myshopify.io'; +const VALID_SHOPIFY_HOST_BUT_NOT_VALID_UNIFIED_ADMIN = + 'not-admin.shopify.com/store/my-shop'; const CUSTOM_DOMAIN = 'my-custom-domain.com'; const VALID_SHOP_WITH_CUSTOM_DOMAIN = `my-shop.${CUSTOM_DOMAIN}`; @@ -28,6 +30,13 @@ const VALID_HOSTS = [ return {testhost, base64host: Buffer.from(testhost).toString('base64')}; }); +const VALID_UNIFIED_ADMIN_URLS = [ + { + unifiedAdminUrl: 'admin.shopify.com/store/my-shop', + legacyAdminUrl: 'my-shop.myshopify.com', + }, +]; + const INVALID_HOSTS = [ { testhost: 'plain-string-is-not-base64', @@ -65,11 +74,16 @@ describe('sanitizeShop', () => { ); }); - test('returns null for invalid URLs', () => { - const shopify = shopifyApi(testConfig()); + [ + INVALID_SHOP_URL_1, + INVALID_SHOP_URL_2, + VALID_SHOPIFY_HOST_BUT_NOT_VALID_UNIFIED_ADMIN, + ].forEach((invalidUrl) => { + test('returns null for invalid URLs', () => { + const shopify = shopifyApi(testConfig()); - expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_1)).toBe(null); - expect(shopify.utils.sanitizeShop(INVALID_SHOP_URL_2)).toBe(null); + expect(shopify.utils.sanitizeShop(invalidUrl)).toBe(null); + }); }); test('throws for invalid URLs if set to', () => { @@ -104,6 +118,15 @@ describe('sanitizeShop', () => { shopify.utils.sanitizeShop(INVALID_SHOP_WITH_CUSTOM_DOMAIN_REGEX), ).toBe(null); }); + + VALID_UNIFIED_ADMIN_URLS.forEach(({unifiedAdminUrl, legacyAdminUrl}) => { + test('accepts unified admin URLs and converts to legacy admin URLs', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.sanitizeShop(unifiedAdminUrl); + + expect(actual).toEqual(legacyAdminUrl); + }); + }); }); describe('sanitizeHost', () => { diff --git a/packages/shopify-api/lib/utils/index.ts b/packages/shopify-api/lib/utils/index.ts index 8eb4279a3..1032081a1 100644 --- a/packages/shopify-api/lib/utils/index.ts +++ b/packages/shopify-api/lib/utils/index.ts @@ -3,6 +3,10 @@ import {ConfigInterface} from '../base-types'; import {sanitizeShop, sanitizeHost} from './shop-validator'; import {validateHmac} from './hmac-validator'; import {versionCompatible, versionPriorTo} from './version-compatible'; +import { + unifiedAdminUrlToLegacyUrl, + legacyUrlToUnifiedAdminUrl, +} from './unified-admin-url-helper'; export function shopifyUtils(config: ConfigInterface) { return { @@ -11,6 +15,8 @@ export function shopifyUtils(config: ConfigInterface) { validateHmac: validateHmac(config), versionCompatible: versionCompatible(config), versionPriorTo: versionPriorTo(config), + unifiedAdminUrlToLegacyUrl: unifiedAdminUrlToLegacyUrl(), + legacyUrlToUnifiedAdminUrl: legacyUrlToUnifiedAdminUrl(), }; } diff --git a/packages/shopify-api/lib/utils/shop-validator.ts b/packages/shopify-api/lib/utils/shop-validator.ts index 931dc5a33..2a4363a99 100644 --- a/packages/shopify-api/lib/utils/shop-validator.ts +++ b/packages/shopify-api/lib/utils/shop-validator.ts @@ -2,8 +2,11 @@ import {ConfigInterface} from '../base-types'; import {InvalidHostError, InvalidShopError} from '../error'; import {decodeHost} from '../auth/decode-host'; +import {unifiedAdminUrlToLegacyUrl} from './unified-admin-url-helper'; + export function sanitizeShop(config: ConfigInterface) { return (shop: string, throwOnInvalid = false): string | null => { + let shopUrl = shop; const domainsRegex = ['myshopify\\.com', 'shopify\\.com', 'myshopify\\.io']; if (config.customShopDomains) { domainsRegex.push( @@ -17,7 +20,17 @@ export function sanitizeShop(config: ConfigInterface) { `^[a-zA-Z0-9][a-zA-Z0-9-_]*\\.(${domainsRegex.join('|')})[/]*$`, ); - const sanitizedShop = shopUrlRegex.test(shop) ? shop : null; + const unifiedAdminRegex = new RegExp( + `^admin.shopify.com/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`, + ); + + const isUnifiedAdminUrl = unifiedAdminRegex.test(shopUrl); + if (isUnifiedAdminUrl) { + const unifiedAdminUrlToLegacyUrlUtil = unifiedAdminUrlToLegacyUrl(); + shopUrl = unifiedAdminUrlToLegacyUrlUtil(shopUrl) || ''; + } + + const sanitizedShop = shopUrlRegex.test(shopUrl) ? shopUrl : null; if (!sanitizedShop && throwOnInvalid) { throw new InvalidShopError('Received invalid shop argument'); } From 64cda80ace25c9fd1f49dee81686f55ab03b17e7 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Mon, 11 Dec 2023 15:01:23 -0700 Subject: [PATCH 03/10] Update changelog --- .changeset/young-news-fail.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/young-news-fail.md diff --git a/.changeset/young-news-fail.md b/.changeset/young-news-fail.md new file mode 100644 index 000000000..095466314 --- /dev/null +++ b/.changeset/young-news-fail.md @@ -0,0 +1,5 @@ +--- +"@shopify/shopify-api": minor +--- + +Add helpers to convert unified admin URLs, and sanitizeShop util method can now support unified admin URLs From 6444a51b8b24e211970a88e7582b6ae9cff020cb Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Tue, 12 Dec 2023 09:44:31 -0700 Subject: [PATCH 04/10] Rename utility methods to be shopAdminUrl --- .../__tests__/shop-admin-url-helper.test.ts | 34 +++++++++++++++++++ .../utils/__tests__/shop-validator.test.ts | 14 ++++---- .../unified-admin-url-helper.test.ts | 34 ------------------- packages/shopify-api/lib/utils/index.ts | 10 +++--- ...url-helper.ts => shop-admin-url-helper.ts} | 21 +++++------- .../shopify-api/lib/utils/shop-validator.ts | 12 +++---- 6 files changed, 61 insertions(+), 64 deletions(-) create mode 100644 packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts delete mode 100644 packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts rename packages/shopify-api/lib/utils/{unified-admin-url-helper.ts => shop-admin-url-helper.ts} (59%) diff --git a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts new file mode 100644 index 000000000..f7cea369c --- /dev/null +++ b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts @@ -0,0 +1,34 @@ +import {shopifyApi} from '../..'; +import {testConfig} from '../../__tests__/test-config'; + +const VALID_URLS = [ + { + adminUrl: 'admin.shopify.com/store/my-shop', + legacyAdminUrl: 'my-shop.myshopify.com', + }, + { + adminUrl: 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop', + legacyAdminUrl: 'my-shop.shopify.abc.def-gh.ij.spin.dev', + }, +]; + +VALID_URLS.forEach(({adminUrl, legacyAdminUrl}) => { + test('converts shop admin URL to legacy URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl); + expect(actual).toEqual(legacyAdminUrl); + }); + + test('converts legacy URL to shop admin URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl); + expect(actual).toEqual(adminUrl); + }); +}); + +test('returns null when trying to convert from shop admin url to legacy url', () => { + const invalid_url = 'not-admin.shopify.com/store/my-shop'; + const shopify = shopifyApi(testConfig()); + + expect(shopify.utils.shopAdminUrlToLegacyUrl(invalid_url)).toBe(null); +}); diff --git a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts index 1181e7bb9..557c99101 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts @@ -9,7 +9,7 @@ const VALID_SHOP_URL_4 = 'dev-shop-.myshopify.io'; const INVALID_SHOP_URL_1 = 'notshopify.com'; const INVALID_SHOP_URL_2 = '-invalid.myshopify.io'; -const VALID_SHOPIFY_HOST_BUT_NOT_VALID_UNIFIED_ADMIN = +const VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL = 'not-admin.shopify.com/store/my-shop'; const CUSTOM_DOMAIN = 'my-custom-domain.com'; @@ -30,9 +30,9 @@ const VALID_HOSTS = [ return {testhost, base64host: Buffer.from(testhost).toString('base64')}; }); -const VALID_UNIFIED_ADMIN_URLS = [ +const VALID_SHOP_ADMIN_URLS = [ { - unifiedAdminUrl: 'admin.shopify.com/store/my-shop', + adminUrl: 'admin.shopify.com/store/my-shop', legacyAdminUrl: 'my-shop.myshopify.com', }, ]; @@ -77,7 +77,7 @@ describe('sanitizeShop', () => { [ INVALID_SHOP_URL_1, INVALID_SHOP_URL_2, - VALID_SHOPIFY_HOST_BUT_NOT_VALID_UNIFIED_ADMIN, + VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL, ].forEach((invalidUrl) => { test('returns null for invalid URLs', () => { const shopify = shopifyApi(testConfig()); @@ -119,10 +119,10 @@ describe('sanitizeShop', () => { ).toBe(null); }); - VALID_UNIFIED_ADMIN_URLS.forEach(({unifiedAdminUrl, legacyAdminUrl}) => { - test('accepts unified admin URLs and converts to legacy admin URLs', () => { + VALID_SHOP_ADMIN_URLS.forEach(({adminUrl, legacyAdminUrl}) => { + test('accepts new format of shop admin URLs and converts to legacy admin URLs', () => { const shopify = shopifyApi(testConfig()); - const actual = shopify.utils.sanitizeShop(unifiedAdminUrl); + const actual = shopify.utils.sanitizeShop(adminUrl); expect(actual).toEqual(legacyAdminUrl); }); diff --git a/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts deleted file mode 100644 index 5b3093b89..000000000 --- a/packages/shopify-api/lib/utils/__tests__/unified-admin-url-helper.test.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {shopifyApi} from '../..'; -import {testConfig} from '../../__tests__/test-config'; - -const VALID_UNIFIED_ADMIN_URLS = [ - { - unifiedAdminUrl: 'admin.shopify.com/store/my-shop', - legacyAdminUrl: 'my-shop.myshopify.com', - }, - { - unifiedAdminUrl: 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop', - legacyAdminUrl: 'my-shop.shopify.abc.def-gh.ij.spin.dev', - }, -]; - -VALID_UNIFIED_ADMIN_URLS.forEach(({unifiedAdminUrl, legacyAdminUrl}) => { - test('converts unified admin URL to legacy URL', () => { - const shopify = shopifyApi(testConfig()); - const actual = shopify.utils.unifiedAdminUrlToLegacyUrl(unifiedAdminUrl); - expect(actual).toEqual(legacyAdminUrl); - }); - - test('converts legacy URL to unified admin URL', () => { - const shopify = shopifyApi(testConfig()); - const actual = shopify.utils.legacyUrlToUnifiedAdminUrl(legacyAdminUrl); - expect(actual).toEqual(unifiedAdminUrl); - }); -}); - -test('returns null when trying to convert from unified admin url to legacy url', () => { - const invalid_url = 'not-admin.shopify.com/store/my-shop'; - const shopify = shopifyApi(testConfig()); - - expect(shopify.utils.unifiedAdminUrlToLegacyUrl(invalid_url)).toBe(null); -}); diff --git a/packages/shopify-api/lib/utils/index.ts b/packages/shopify-api/lib/utils/index.ts index 1032081a1..10d22e932 100644 --- a/packages/shopify-api/lib/utils/index.ts +++ b/packages/shopify-api/lib/utils/index.ts @@ -4,9 +4,9 @@ import {sanitizeShop, sanitizeHost} from './shop-validator'; import {validateHmac} from './hmac-validator'; import {versionCompatible, versionPriorTo} from './version-compatible'; import { - unifiedAdminUrlToLegacyUrl, - legacyUrlToUnifiedAdminUrl, -} from './unified-admin-url-helper'; + shopAdminUrlToLegacyUrl, + legacyUrlToShopAdminUrl, +} from './shop-admin-url-helper'; export function shopifyUtils(config: ConfigInterface) { return { @@ -15,8 +15,8 @@ export function shopifyUtils(config: ConfigInterface) { validateHmac: validateHmac(config), versionCompatible: versionCompatible(config), versionPriorTo: versionPriorTo(config), - unifiedAdminUrlToLegacyUrl: unifiedAdminUrlToLegacyUrl(), - legacyUrlToUnifiedAdminUrl: legacyUrlToUnifiedAdminUrl(), + shopAdminUrlToLegacyUrl: shopAdminUrlToLegacyUrl(), + legacyUrlToShopAdminUrl: legacyUrlToShopAdminUrl(), }; } diff --git a/packages/shopify-api/lib/utils/unified-admin-url-helper.ts b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts similarity index 59% rename from packages/shopify-api/lib/utils/unified-admin-url-helper.ts rename to packages/shopify-api/lib/utils/shop-admin-url-helper.ts index 4e3a77760..a4cbc2549 100644 --- a/packages/shopify-api/lib/utils/unified-admin-url-helper.ts +++ b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts @@ -1,22 +1,19 @@ -// This class assumes the legacy url has been sanitized already +// This class assumes the legacy url has protocol stripped already // Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com -export function unifiedAdminUrlToLegacyUrl() { - return (unifiedAdminUrl: string): string | null => { - const isUnifiedAdminUrl = unifiedAdminUrl.split('.')[0] === 'admin'; +export function shopAdminUrlToLegacyUrl() { + return (shopAdminUrl: string): string | null => { + const isShopAdminUrl = shopAdminUrl.split('.')[0] === 'admin'; - if (!isUnifiedAdminUrl) { + if (!isShopAdminUrl) { return null; } - const urlComponents = unifiedAdminUrl.split('/'); + const urlComponents = shopAdminUrl.split('/'); const shopName = urlComponents[urlComponents.length - 1]; - const isSpinUrl = unifiedAdminUrl.includes('spin.dev/store/'); + const isSpinUrl = shopAdminUrl.includes('spin.dev/store/'); if (isSpinUrl) { - const spinUrlComponents = unifiedAdminUrl - .split('.') - .slice(2, 5) - .join('.'); + const spinUrlComponents = shopAdminUrl.split('.').slice(2, 5).join('.'); return `${shopName}.shopify.${spinUrlComponents}.spin.dev`; } else { @@ -26,7 +23,7 @@ export function unifiedAdminUrlToLegacyUrl() { } // Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop -export function legacyUrlToUnifiedAdminUrl() { +export function legacyUrlToShopAdminUrl() { return (legacyAdminUrl: string): string | null => { const shopName = legacyAdminUrl.split('.')[0]; diff --git a/packages/shopify-api/lib/utils/shop-validator.ts b/packages/shopify-api/lib/utils/shop-validator.ts index 2a4363a99..a0222440a 100644 --- a/packages/shopify-api/lib/utils/shop-validator.ts +++ b/packages/shopify-api/lib/utils/shop-validator.ts @@ -2,7 +2,7 @@ import {ConfigInterface} from '../base-types'; import {InvalidHostError, InvalidShopError} from '../error'; import {decodeHost} from '../auth/decode-host'; -import {unifiedAdminUrlToLegacyUrl} from './unified-admin-url-helper'; +import {shopAdminUrlToLegacyUrl} from './shop-admin-url-helper'; export function sanitizeShop(config: ConfigInterface) { return (shop: string, throwOnInvalid = false): string | null => { @@ -20,14 +20,14 @@ export function sanitizeShop(config: ConfigInterface) { `^[a-zA-Z0-9][a-zA-Z0-9-_]*\\.(${domainsRegex.join('|')})[/]*$`, ); - const unifiedAdminRegex = new RegExp( + const shopAdminRegex = new RegExp( `^admin.shopify.com/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`, ); - const isUnifiedAdminUrl = unifiedAdminRegex.test(shopUrl); - if (isUnifiedAdminUrl) { - const unifiedAdminUrlToLegacyUrlUtil = unifiedAdminUrlToLegacyUrl(); - shopUrl = unifiedAdminUrlToLegacyUrlUtil(shopUrl) || ''; + const isShopAdminUrl = shopAdminRegex.test(shopUrl); + if (isShopAdminUrl) { + const shopAdminUrlToLegacyUrlUtil = shopAdminUrlToLegacyUrl(); + shopUrl = shopAdminUrlToLegacyUrlUtil(shopUrl) || ''; } const sanitizedShop = shopUrlRegex.test(shopUrl) ? shopUrl : null; From 82fb57b1406e55111cbf2904cb42586cca1a5ac5 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Wed, 13 Dec 2023 09:08:54 -0700 Subject: [PATCH 05/10] reformat tests --- .../__tests__/shop-admin-url-helper.test.ts | 29 ++++++++++--------- .../utils/__tests__/shop-validator.test.ts | 19 ++++++------ 2 files changed, 25 insertions(+), 23 deletions(-) diff --git a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts index f7cea369c..44aaa6047 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts @@ -12,21 +12,24 @@ const VALID_URLS = [ }, ]; -VALID_URLS.forEach(({adminUrl, legacyAdminUrl}) => { - test('converts shop admin URL to legacy URL', () => { - const shopify = shopifyApi(testConfig()); - const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl); - expect(actual).toEqual(legacyAdminUrl); - }); +describe.each(VALID_URLS)( + 'For valid shop URLs: %s', + ({adminUrl, legacyAdminUrl}) => { + it('can convert from shop admin URL to legacy URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl); + expect(actual).toEqual(actual); + }); - test('converts legacy URL to shop admin URL', () => { - const shopify = shopifyApi(testConfig()); - const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl); - expect(actual).toEqual(adminUrl); - }); -}); + it('can convert from legacy URL to shop admin URL', () => { + const shopify = shopifyApi(testConfig()); + const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl); + expect(actual).toEqual(adminUrl); + }); + }, +); -test('returns null when trying to convert from shop admin url to legacy url', () => { +it('returns null when trying to convert from shop admin url to legacy url', () => { const invalid_url = 'not-admin.shopify.com/store/my-shop'; const shopify = shopifyApi(testConfig()); diff --git a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts index 557c99101..a19d409ae 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts @@ -74,16 +74,14 @@ describe('sanitizeShop', () => { ); }); - [ + test.each([ INVALID_SHOP_URL_1, INVALID_SHOP_URL_2, VALID_SHOPIFY_HOST_BUT_NOT_VALID_ADMIN_URL, - ].forEach((invalidUrl) => { - test('returns null for invalid URLs', () => { - const shopify = shopifyApi(testConfig()); + ])('returns null for invalid URL - %s', (invalidUrl) => { + const shopify = shopifyApi(testConfig()); - expect(shopify.utils.sanitizeShop(invalidUrl)).toBe(null); - }); + expect(shopify.utils.sanitizeShop(invalidUrl)).toBe(null); }); test('throws for invalid URLs if set to', () => { @@ -119,14 +117,15 @@ describe('sanitizeShop', () => { ).toBe(null); }); - VALID_SHOP_ADMIN_URLS.forEach(({adminUrl, legacyAdminUrl}) => { - test('accepts new format of shop admin URLs and converts to legacy admin URLs', () => { + test.each(VALID_SHOP_ADMIN_URLS)( + 'accepts new format of shop admin URLs and converts to legacy admin URLs - %s', + ({adminUrl, legacyAdminUrl}) => { const shopify = shopifyApi(testConfig()); const actual = shopify.utils.sanitizeShop(adminUrl); expect(actual).toEqual(legacyAdminUrl); - }); - }); + }, + ); }); describe('sanitizeHost', () => { From c62fb3c55080ee2aa72c1462e246ea9d209ef535 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Thu, 14 Dec 2023 15:23:41 -0700 Subject: [PATCH 06/10] Refactor logic to convert between shop admin URL and shop URL and add more test cases --- .../__tests__/shop-admin-url-helper.test.ts | 42 ++++++++++--- .../utils/__tests__/shop-validator.test.ts | 17 +++++ .../lib/utils/shop-admin-url-helper.ts | 62 +++++++++++++++---- .../shopify-api/lib/utils/shop-validator.ts | 2 +- 4 files changed, 102 insertions(+), 21 deletions(-) diff --git a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts index 44aaa6047..b35c1c009 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts @@ -12,13 +12,26 @@ const VALID_URLS = [ }, ]; +const INVALID_ADMIN_URLS = [ + 'not-admin.shopify.com/store/my-shop', + 'adminisnotthis.shopify.com/store/my-shop', + 'adminisnot.web.abc.def-gh.ij.spin.dev/store/my-shop', + 'admin.what.abc.def-gh.ij.spin.dev/store/my-shop', +]; + +const INVALID_LEGACY_URLS = [ + 'notshopify.com', + 'my-shop.myshopify.com.nope', + 'my-shop.myshopify.com/admin', +]; + describe.each(VALID_URLS)( - 'For valid shop URLs: %s', + 'For valid shop URL: %s', ({adminUrl, legacyAdminUrl}) => { it('can convert from shop admin URL to legacy URL', () => { const shopify = shopifyApi(testConfig()); const actual = shopify.utils.shopAdminUrlToLegacyUrl(adminUrl); - expect(actual).toEqual(actual); + expect(actual).toEqual(legacyAdminUrl); }); it('can convert from legacy URL to shop admin URL', () => { @@ -29,9 +42,24 @@ describe.each(VALID_URLS)( }, ); -it('returns null when trying to convert from shop admin url to legacy url', () => { - const invalid_url = 'not-admin.shopify.com/store/my-shop'; - const shopify = shopifyApi(testConfig()); +describe.each(INVALID_ADMIN_URLS)( + 'For invalid shop admin URL: %s', + (invalidUrl) => { + it('returns null when trying to convert from shop admin url to legacy url', () => { + const shopify = shopifyApi(testConfig()); + + expect(shopify.utils.shopAdminUrlToLegacyUrl(invalidUrl)).toBe(null); + }); + }, +); + +describe.each(INVALID_LEGACY_URLS)( + 'For invalid legacy shop URL: %s', + (invalidUrl) => { + it('returns null when trying to convert from legacy url to shop admin url', () => { + const shopify = shopifyApi(testConfig()); - expect(shopify.utils.shopAdminUrlToLegacyUrl(invalid_url)).toBe(null); -}); + expect(shopify.utils.legacyUrlToShopAdminUrl(invalidUrl)).toBe(null); + }); + }, +); diff --git a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts index a19d409ae..10278b575 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-validator.test.ts @@ -126,6 +126,23 @@ describe('sanitizeShop', () => { expect(actual).toEqual(legacyAdminUrl); }, ); + + test('Accepts new format of spin admin URL and converts to legacy admin URL', () => { + const expectedLegacyAdminUrl = 'my-shop.shopify.abc.def-gh.ij.spin.dev'; + const spinAdminUrl = 'admin.web.abc.def-gh.ij.spin.dev/store/my-shop'; + + const shopify = shopifyApi( + testConfig({ + customShopDomains: [ + 'web\\.abc\\.def-gh\\.ij\\.spin\\.dev', + 'shopify\\.abc\\.def-gh\\.ij\\.spin\\.dev', + ], + }), + ); + const actual = shopify.utils.sanitizeShop(spinAdminUrl); + + expect(actual).toEqual(expectedLegacyAdminUrl); + }); }); describe('sanitizeHost', () => { diff --git a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts index a4cbc2549..b965d1136 100644 --- a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts +++ b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts @@ -8,16 +8,20 @@ export function shopAdminUrlToLegacyUrl() { return null; } - const urlComponents = shopAdminUrl.split('/'); - const shopName = urlComponents[urlComponents.length - 1]; + const regex = new RegExp(`admin\\..+/store/(.+)`); + const matches = shopAdminUrl.match(regex); - const isSpinUrl = shopAdminUrl.includes('spin.dev/store/'); - if (isSpinUrl) { - const spinUrlComponents = shopAdminUrl.split('.').slice(2, 5).join('.'); + if (matches && matches.length === 2) { + const shopName = matches[1]; + const isSpinUrl = shopAdminUrl.includes('spin.dev/store/'); - return `${shopName}.shopify.${spinUrlComponents}.spin.dev`; + if (isSpinUrl) { + return spinAdminUrlToLegacyUrl(shopAdminUrl); + } else { + return `${shopName}.myshopify.com`; + } } else { - return `${shopName}.myshopify.com`; + return null; } }; } @@ -25,14 +29,46 @@ export function shopAdminUrlToLegacyUrl() { // Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop export function legacyUrlToShopAdminUrl() { return (legacyAdminUrl: string): string | null => { - const shopName = legacyAdminUrl.split('.')[0]; + const regex = new RegExp(`(.+)\\.myshopify\\.com$`); + const matches = legacyAdminUrl.match(regex); - const isSpinUrl = legacyAdminUrl.endsWith('spin.dev'); - if (isSpinUrl) { - const spinUrlComponents = legacyAdminUrl.split('.').slice(2, 5).join('.'); - return `admin.web.${spinUrlComponents}.spin.dev/store/${shopName}`; - } else { + if (matches && matches.length === 2) { + const shopName = matches[1]; return `admin.shopify.com/store/${shopName}`; + } else { + const isSpinUrl = legacyAdminUrl.endsWith('spin.dev'); + + if (isSpinUrl) { + return spinLegacyUrlToAdminUrl(legacyAdminUrl); + } else { + return null; + } } }; } + +function spinAdminUrlToLegacyUrl(shopAdminUrl: string) { + const spinRegex = new RegExp(`admin\\.web\\.(.+\\.spin\\.dev)/store/(.+)`); + const spinMatches = shopAdminUrl.match(spinRegex); + + if (spinMatches && spinMatches.length === 3) { + const spinUrl = spinMatches[1]; + const shopName = spinMatches[2]; + return `${shopName}.shopify.${spinUrl}`; + } else { + return null; + } +} + +function spinLegacyUrlToAdminUrl(legacyAdminUrl: string) { + const spinRegex = new RegExp(`(.+)\\.shopify\\.(.+\\.spin\\.dev)`); + const spinMatches = legacyAdminUrl.match(spinRegex); + + if (spinMatches && spinMatches.length === 3) { + const shopName = spinMatches[1]; + const spinUrl = spinMatches[2]; + return `admin.web.${spinUrl}/store/${shopName}`; + } else { + return null; + } +} diff --git a/packages/shopify-api/lib/utils/shop-validator.ts b/packages/shopify-api/lib/utils/shop-validator.ts index a0222440a..cbeca10b3 100644 --- a/packages/shopify-api/lib/utils/shop-validator.ts +++ b/packages/shopify-api/lib/utils/shop-validator.ts @@ -21,7 +21,7 @@ export function sanitizeShop(config: ConfigInterface) { ); const shopAdminRegex = new RegExp( - `^admin.shopify.com/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`, + `^admin\\.(${domainsRegex.join('|')})/store/([a-zA-Z0-9][a-zA-Z0-9-_]*)$`, ); const isShopAdminUrl = shopAdminRegex.test(shopUrl); From bd3198d592bc1ef9e61e5e0d581ce8397702ff2f Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Thu, 14 Dec 2023 15:26:33 -0700 Subject: [PATCH 07/10] Update readme message --- .changeset/young-news-fail.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/young-news-fail.md b/.changeset/young-news-fail.md index 095466314..d87773648 100644 --- a/.changeset/young-news-fail.md +++ b/.changeset/young-news-fail.md @@ -2,4 +2,4 @@ "@shopify/shopify-api": minor --- -Add helpers to convert unified admin URLs, and sanitizeShop util method can now support unified admin URLs +Add helpers to convert between shop admin URLs and legacy URLs. `sanitizeShop` utility method can now support shop admin URLs. From bafd6d484eb16d3d741751a79ce630f76ef2cebe Mon Sep 17 00:00:00 2001 From: Zoey Lan <102243935+zzooeeyy@users.noreply.github.com> Date: Fri, 15 Dec 2023 09:15:19 -0700 Subject: [PATCH 08/10] Update packages/shopify-api/lib/utils/shop-admin-url-helper.ts Co-authored-by: Paulo Margarido <64600052+paulomarg@users.noreply.github.com> --- packages/shopify-api/lib/utils/shop-admin-url-helper.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts index b965d1136..6b7777814 100644 --- a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts +++ b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts @@ -8,7 +8,7 @@ export function shopAdminUrlToLegacyUrl() { return null; } - const regex = new RegExp(`admin\\..+/store/(.+)`); + const regex = new RegExp(`admin\\..+/store/([^/]+)`); const matches = shopAdminUrl.match(regex); if (matches && matches.length === 2) { From 6f9c462dce4e6689110ed3dc92a39bb28ddb0d16 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Fri, 15 Dec 2023 09:45:50 -0700 Subject: [PATCH 09/10] Strips protocol before converting between legacy and admin URLS --- .../__tests__/shop-admin-url-helper.test.ts | 14 ++++++++++++ .../lib/utils/shop-admin-url-helper.ts | 22 ++++++++++++------- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts index b35c1c009..1111ee3ae 100644 --- a/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts +++ b/packages/shopify-api/lib/utils/__tests__/shop-admin-url-helper.test.ts @@ -39,6 +39,20 @@ describe.each(VALID_URLS)( const actual = shopify.utils.legacyUrlToShopAdminUrl(legacyAdminUrl); expect(actual).toEqual(adminUrl); }); + + it('can strip protocol before converting from shop admin URL to legacy URL', () => { + const shopify = shopifyApi(testConfig()); + const urlWithProtocol = `https://${adminUrl}`; + const actual = shopify.utils.shopAdminUrlToLegacyUrl(urlWithProtocol); + expect(actual).toEqual(legacyAdminUrl); + }); + + it('can strip protocol before converting from legacy URL to shop admin URL', () => { + const shopify = shopifyApi(testConfig()); + const urlWithProtocol = `https://${legacyAdminUrl}`; + const actual = shopify.utils.legacyUrlToShopAdminUrl(urlWithProtocol); + expect(actual).toEqual(adminUrl); + }); }, ); diff --git a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts index 6b7777814..a051cf3ec 100644 --- a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts +++ b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts @@ -1,22 +1,23 @@ -// This class assumes the legacy url has protocol stripped already // Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com export function shopAdminUrlToLegacyUrl() { return (shopAdminUrl: string): string | null => { - const isShopAdminUrl = shopAdminUrl.split('.')[0] === 'admin'; + const shopUrl = removeProtocol(shopAdminUrl); + + const isShopAdminUrl = shopUrl.split('.')[0] === 'admin'; if (!isShopAdminUrl) { return null; } const regex = new RegExp(`admin\\..+/store/([^/]+)`); - const matches = shopAdminUrl.match(regex); + const matches = shopUrl.match(regex); if (matches && matches.length === 2) { const shopName = matches[1]; - const isSpinUrl = shopAdminUrl.includes('spin.dev/store/'); + const isSpinUrl = shopUrl.includes('spin.dev/store/'); if (isSpinUrl) { - return spinAdminUrlToLegacyUrl(shopAdminUrl); + return spinAdminUrlToLegacyUrl(shopUrl); } else { return `${shopName}.myshopify.com`; } @@ -29,17 +30,18 @@ export function shopAdminUrlToLegacyUrl() { // Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop export function legacyUrlToShopAdminUrl() { return (legacyAdminUrl: string): string | null => { + const shopUrl = removeProtocol(legacyAdminUrl); const regex = new RegExp(`(.+)\\.myshopify\\.com$`); - const matches = legacyAdminUrl.match(regex); + const matches = shopUrl.match(regex); if (matches && matches.length === 2) { const shopName = matches[1]; return `admin.shopify.com/store/${shopName}`; } else { - const isSpinUrl = legacyAdminUrl.endsWith('spin.dev'); + const isSpinUrl = shopUrl.endsWith('spin.dev'); if (isSpinUrl) { - return spinLegacyUrlToAdminUrl(legacyAdminUrl); + return spinLegacyUrlToAdminUrl(shopUrl); } else { return null; } @@ -72,3 +74,7 @@ function spinLegacyUrlToAdminUrl(legacyAdminUrl: string) { return null; } } + +function removeProtocol(url: string): string { + return url.replace(/^https?:\/\//, '').replace(/\/$/, ''); +} From b49453e31927b596c1e8fdda8868ac38a2741612 Mon Sep 17 00:00:00 2001 From: Zoey Lan Date: Fri, 15 Dec 2023 09:56:12 -0700 Subject: [PATCH 10/10] Return simple function instead --- packages/shopify-api/lib/utils/index.ts | 4 +- .../lib/utils/shop-admin-url-helper.ts | 66 +++++++++---------- .../shopify-api/lib/utils/shop-validator.ts | 3 +- 3 files changed, 34 insertions(+), 39 deletions(-) diff --git a/packages/shopify-api/lib/utils/index.ts b/packages/shopify-api/lib/utils/index.ts index 10d22e932..9123bfcdb 100644 --- a/packages/shopify-api/lib/utils/index.ts +++ b/packages/shopify-api/lib/utils/index.ts @@ -15,8 +15,8 @@ export function shopifyUtils(config: ConfigInterface) { validateHmac: validateHmac(config), versionCompatible: versionCompatible(config), versionPriorTo: versionPriorTo(config), - shopAdminUrlToLegacyUrl: shopAdminUrlToLegacyUrl(), - legacyUrlToShopAdminUrl: legacyUrlToShopAdminUrl(), + shopAdminUrlToLegacyUrl, + legacyUrlToShopAdminUrl, }; } diff --git a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts index a051cf3ec..0fd63f0e7 100644 --- a/packages/shopify-api/lib/utils/shop-admin-url-helper.ts +++ b/packages/shopify-api/lib/utils/shop-admin-url-helper.ts @@ -1,52 +1,48 @@ // Converts admin.shopify.com/store/my-shop to my-shop.myshopify.com -export function shopAdminUrlToLegacyUrl() { - return (shopAdminUrl: string): string | null => { - const shopUrl = removeProtocol(shopAdminUrl); +export function shopAdminUrlToLegacyUrl(shopAdminUrl: string): string | null { + const shopUrl = removeProtocol(shopAdminUrl); - const isShopAdminUrl = shopUrl.split('.')[0] === 'admin'; + const isShopAdminUrl = shopUrl.split('.')[0] === 'admin'; - if (!isShopAdminUrl) { - return null; - } + if (!isShopAdminUrl) { + return null; + } - const regex = new RegExp(`admin\\..+/store/([^/]+)`); - const matches = shopUrl.match(regex); + const regex = new RegExp(`admin\\..+/store/([^/]+)`); + const matches = shopUrl.match(regex); - if (matches && matches.length === 2) { - const shopName = matches[1]; - const isSpinUrl = shopUrl.includes('spin.dev/store/'); + if (matches && matches.length === 2) { + const shopName = matches[1]; + const isSpinUrl = shopUrl.includes('spin.dev/store/'); - if (isSpinUrl) { - return spinAdminUrlToLegacyUrl(shopUrl); - } else { - return `${shopName}.myshopify.com`; - } + if (isSpinUrl) { + return spinAdminUrlToLegacyUrl(shopUrl); } else { - return null; + return `${shopName}.myshopify.com`; } - }; + } else { + return null; + } } // Converts my-shop.myshopify.com to admin.shopify.com/store/my-shop -export function legacyUrlToShopAdminUrl() { - return (legacyAdminUrl: string): string | null => { - const shopUrl = removeProtocol(legacyAdminUrl); - const regex = new RegExp(`(.+)\\.myshopify\\.com$`); - const matches = shopUrl.match(regex); +export function legacyUrlToShopAdminUrl(legacyAdminUrl: string): string | null { + const shopUrl = removeProtocol(legacyAdminUrl); + const regex = new RegExp(`(.+)\\.myshopify\\.com$`); + const matches = shopUrl.match(regex); - if (matches && matches.length === 2) { - const shopName = matches[1]; - return `admin.shopify.com/store/${shopName}`; - } else { - const isSpinUrl = shopUrl.endsWith('spin.dev'); + if (matches && matches.length === 2) { + const shopName = matches[1]; + return `admin.shopify.com/store/${shopName}`; + } else { + const isSpinUrl = shopUrl.endsWith('spin.dev'); - if (isSpinUrl) { - return spinLegacyUrlToAdminUrl(shopUrl); - } else { - return null; - } + if (isSpinUrl) { + return spinLegacyUrlToAdminUrl(shopUrl); + } else { + return null; } - }; + } } function spinAdminUrlToLegacyUrl(shopAdminUrl: string) { diff --git a/packages/shopify-api/lib/utils/shop-validator.ts b/packages/shopify-api/lib/utils/shop-validator.ts index cbeca10b3..f9027dad7 100644 --- a/packages/shopify-api/lib/utils/shop-validator.ts +++ b/packages/shopify-api/lib/utils/shop-validator.ts @@ -26,8 +26,7 @@ export function sanitizeShop(config: ConfigInterface) { const isShopAdminUrl = shopAdminRegex.test(shopUrl); if (isShopAdminUrl) { - const shopAdminUrlToLegacyUrlUtil = shopAdminUrlToLegacyUrl(); - shopUrl = shopAdminUrlToLegacyUrlUtil(shopUrl) || ''; + shopUrl = shopAdminUrlToLegacyUrl(shopUrl) || ''; } const sanitizedShop = shopUrlRegex.test(shopUrl) ? shopUrl : null;