diff --git a/.changeset/exact-domain-store-lookup.md b/.changeset/exact-domain-store-lookup.md new file mode 100644 index 00000000000..b8aaa62238b --- /dev/null +++ b/.changeset/exact-domain-store-lookup.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix store lookup by domain to require an exact domain match. diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts index ce225c38957..81eaf08cf7e 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.test.ts @@ -1609,6 +1609,102 @@ describe('storeByDomain', () => { }, }) }) + + test('returns the exact-domain match when the fuzzy search also returns substring matches', async () => { + // Given + // The Business Platform `accessibleShops` query treats `domain` as a fuzzy `search` argument, so + // a request for `example.myshopify.com` also returns `turbo-example.myshopify.com`, and the + // backend may rank the wrong (substring superset) store first. + const orgGid = 'gid://shopify/Organization/123' + const shopDomain = 'example.myshopify.com' + const token = 'business-platform-token' + const response: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: { + edges: [ + { + node: { + id: 'gid://BusinessPlatform/Shop/1', + externalId: encodedGidFromShopId('1'), + name: 'Turbo Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: 'turbo-example.myshopify.com', + shortName: 'turbo-example', + url: 'https://turbo-example.myshopify.com', + }, + }, + { + node: { + id: 'gid://BusinessPlatform/Shop/2', + externalId: encodedGidFromShopId('2'), + name: 'Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: shopDomain, + shortName: 'example', + url: `https://${shopDomain}`, + }, + }, + ], + }, + currentUser: {organizationPermissions: ['ondemand_access_to_stores']}, + }, + } + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce(response) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve(token) + + // When + const result = await client.storeByDomain(orgGid, shopDomain, ['APP_DEVELOPMENT']) + + // Then + expect(result).toMatchObject({ + shopDomain, + shopName: 'Example', + storeType: 'APP_DEVELOPMENT', + }) + }) + + test('returns undefined when the fuzzy search only returns substring (non-exact) matches', async () => { + // Given + const orgGid = 'gid://shopify/Organization/123' + const shopDomain = 'example.myshopify.com' + const token = 'business-platform-token' + const response: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: { + edges: [ + { + node: { + id: 'gid://BusinessPlatform/Shop/1', + externalId: encodedGidFromShopId('1'), + name: 'Turbo Example', + storeType: 'APP_DEVELOPMENT', + primaryDomain: 'turbo-example.myshopify.com', + shortName: 'turbo-example', + url: 'https://turbo-example.myshopify.com', + }, + }, + ], + }, + currentUser: {organizationPermissions: []}, + }, + } + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce(response) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve(token) + + // When + const result = await client.storeByDomain(orgGid, shopDomain, ['APP_DEVELOPMENT']) + + // Then + expect(result).toBeUndefined() + }) }) describe('ensureUserAccessToStore', () => { diff --git a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts index 07b32c020bd..32457c5c47f 100644 --- a/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts +++ b/packages/app/src/cli/utilities/developer-platform-client/app-management-client.ts @@ -863,7 +863,13 @@ export class AppManagementClient implements DeveloperPlatformClient { const provisionable = isStoreProvisionable(org.currentUser?.organizationPermissions ?? []) return mapBusinessPlatformStoresToOrganizationStores(nodes, provisionable) }) - return stores[0] + + // `accessibleShops` treats `domain` as a fuzzy, free-text `search` argument, so a domain that is + // a substring of another store's domain (e.g. `example.myshopify.com` vs + // `turbo-example.myshopify.com`) can return multiple hits. Return only the store whose domain + // matches exactly, otherwise we risk connecting to the wrong store. + const normalizedDomain = normalizeStoreFqdn(shopDomain) + return stores.find((store) => store.shopDomain === normalizedDomain) } async ensureUserAccessToStore(orgId: string, store: OrganizationStore): Promise {