From e3c4376ea3c198c203e9f20e96c8f0895710bef0 Mon Sep 17 00:00:00 2001 From: Donald Merand Date: Mon, 11 May 2026 15:07:36 -0400 Subject: [PATCH] Filter explicit store lookups to active stores --- .../app-management-client.test.ts | 131 ++++++++++++++++++ .../app-management-client.ts | 8 +- 2 files changed, 135 insertions(+), 4 deletions(-) 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 ba99e689cb..40e187c221 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 @@ -19,6 +19,10 @@ import { } from '../../models/app/app.test-data.js' import {ExtensionInstance} from '../../models/extensions/extension-instance.js' import {ListApps} from '../../api/graphql/app-management/generated/apps.js' +import { + FetchStoreByDomain, + FetchStoreByDomainQuery, +} from '../../api/graphql/business-platform-organizations/generated/fetch_store_by_domain.js' import { ListAppDevStores, ListAppDevStoresQuery, @@ -1452,6 +1456,133 @@ describe('AppManagementClient', () => { }) }) +describe('storeByDomain', () => { + test('queries Business Platform with STORE_STATUS=ACTIVE for each requested store type', async () => { + // Given + const orgGid = 'gid://shopify/Organization/123' + const shopDomain = 'my-production-store.myshopify.com' + const token = 'business-platform-token' + const emptyResponse: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: {edges: []}, + currentUser: {organizationPermissions: []}, + }, + } + const productionResponse: FetchStoreByDomainQuery = { + organization: { + id: orgGid, + name: 'Org 123', + accessibleShops: { + edges: [ + { + node: { + id: 'gid://BusinessPlatform/Shop/2', + externalId: encodedGidFromShopId('2'), + name: 'My Production Store', + storeType: 'PRODUCTION', + primaryDomain: shopDomain, + shortName: 'my-production-store', + url: `https://${shopDomain}`, + }, + }, + ], + }, + currentUser: {organizationPermissions: ['ondemand_access_to_stores']}, + }, + } + vi.mocked(businessPlatformOrganizationsRequestDoc) + .mockResolvedValueOnce(emptyResponse) + .mockResolvedValueOnce(productionResponse) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve(token) + + // When + const result = await client.storeByDomain(orgGid, shopDomain, ['APP_DEVELOPMENT', 'PRODUCTION']) + + // Then + expect(vi.mocked(businessPlatformOrganizationsRequestDoc)).toHaveBeenNthCalledWith(1, { + query: FetchStoreByDomain, + token, + organizationId: '123', + variables: { + domain: shopDomain, + filters: [ + {field: 'STORE_TYPE', operator: 'EQUALS', value: 'app_development'}, + {field: 'STORE_STATUS', operator: 'EQUALS', value: 'ACTIVE'}, + ], + }, + unauthorizedHandler: { + type: 'token_refresh', + handler: expect.any(Function), + }, + }) + expect(vi.mocked(businessPlatformOrganizationsRequestDoc)).toHaveBeenNthCalledWith(2, { + query: FetchStoreByDomain, + token, + organizationId: '123', + variables: { + domain: shopDomain, + filters: [ + {field: 'STORE_TYPE', operator: 'EQUALS', value: 'production'}, + {field: 'STORE_STATUS', operator: 'EQUALS', value: 'ACTIVE'}, + ], + }, + unauthorizedHandler: { + type: 'token_refresh', + handler: expect.any(Function), + }, + }) + expect(result).toMatchObject({ + shopDomain, + shopName: 'My Production Store', + provisionable: true, + storeType: 'PRODUCTION', + }) + }) + + test('returns undefined when no active stores match the requested filters', async () => { + // Given + vi.mocked(businessPlatformOrganizationsRequestDoc).mockResolvedValueOnce({ + organization: { + id: 'gid://shopify/Organization/123', + name: 'Org 123', + accessibleShops: {edges: []}, + currentUser: {organizationPermissions: []}, + }, + }) + + const client = AppManagementClient.getInstance() + client.businessPlatformToken = () => Promise.resolve('business-platform-token') + + // When + const result = await client.storeByDomain('gid://shopify/Organization/123', 'missing-store.myshopify.com', [ + 'APP_DEVELOPMENT', + ]) + + // Then + expect(result).toBeUndefined() + expect(vi.mocked(businessPlatformOrganizationsRequestDoc)).toHaveBeenCalledWith({ + query: FetchStoreByDomain, + token: 'business-platform-token', + organizationId: '123', + variables: { + domain: 'missing-store.myshopify.com', + filters: [ + {field: 'STORE_TYPE', operator: 'EQUALS', value: 'app_development'}, + {field: 'STORE_STATUS', operator: 'EQUALS', value: 'ACTIVE'}, + ], + }, + unauthorizedHandler: { + type: 'token_refresh', + handler: expect.any(Function), + }, + }) + }) +}) + describe('ensureUserAccessToStore', () => { test('ensures user access to store', async () => { // Given 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 6a410d2222..25d9770d3e 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 @@ -1486,11 +1486,11 @@ function toUserError(err: CreateAppVersionMutation['appVersionCreate']['userErro return {...err, details} } -// Keep explicit domain lookup broader than ListAppDevStores for now. -// If APP_DEVELOPMENT lookups also need to exclude deleted/inactive stores here, -// add STORE_STATUS=ACTIVE only for that store type and cover mixed storeTypes callers. function storeByDomainFilters(storeType: Store) { - return [{field: 'STORE_TYPE' as const, operator: 'EQUALS' as const, value: storeType.toLowerCase()}] + return [ + {field: 'STORE_TYPE' as const, operator: 'EQUALS' as const, value: storeType.toLowerCase()}, + {field: 'STORE_STATUS' as const, operator: 'EQUALS' as const, value: 'ACTIVE'}, + ] } function isStoreProvisionable(permissions: string[]) {