diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index b5704a8692..a84b78c56e 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -31,10 +31,11 @@ import { import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile} from '@shopify/cli-kit/node/fs' import {joinPath, dirname, cwd, normalizePath} from '@shopify/cli-kit/node/path' import {platformAndArch} from '@shopify/cli-kit/node/os' -import {outputContent} from '@shopify/cli-kit/node/output' +import {outputContent, outputToken} from '@shopify/cli-kit/node/output' import {zod} from '@shopify/cli-kit/node/schema' import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' import {currentProcessIsGlobal} from '@shopify/cli-kit/node/is-global' +import colors from '@shopify/cli-kit/node/colors' // eslint-disable-next-line no-restricted-imports import {resolve} from 'path' @@ -417,7 +418,7 @@ wrong = "property" }) // When - await expect(loadTestingApp()).rejects.toThrow(/Validation errors in/) + await expect(loadTestingApp()).rejects.toThrow(/Validation errors/) }) test('throws an error if the extension type is invalid', async () => { @@ -2483,9 +2484,9 @@ describe('parseConfigurationObject', () => { message: 'Boolean is required', }, ] - const expectedFormatted = outputContent`App configuration is not valid\nValidation errors in tmp:\n\n${parseHumanReadableError( - errorObject, - )}` + const expectedFormatted = outputContent`\n${outputToken.errorText( + 'Validation errors', + )} in tmp:\n\n${parseHumanReadableError(errorObject)}` const abortOrReport = vi.fn() @@ -2510,9 +2511,9 @@ describe('parseConfigurationObject', () => { message: 'Expected string, received array', }, ] - const expectedFormatted = outputContent`App configuration is not valid\nValidation errors in tmp:\n\n${parseHumanReadableError( - errorObject, - )}` + const expectedFormatted = outputContent`\n${outputToken.errorText( + 'Validation errors', + )} in tmp:\n\n${parseHumanReadableError(errorObject)}` const abortOrReport = vi.fn() await parseConfigurationObject(LegacyAppSchema, 'tmp', configurationObject, abortOrReport) @@ -2559,9 +2560,9 @@ describe('parseConfigurationObject', () => { message: 'Invalid input', }, ] - const expectedFormatted = outputContent`App configuration is not valid\nValidation errors in tmp:\n\n${parseHumanReadableError( - errorObject, - )}` + const expectedFormatted = outputContent`\n${outputToken.errorText( + 'Validation errors', + )} in tmp:\n\n${parseHumanReadableError(errorObject)}` const abortOrReport = vi.fn() await parseConfigurationObject(WebConfigurationSchema, 'tmp', configurationObject, abortOrReport) @@ -2712,16 +2713,14 @@ describe('WebhooksSchema', () => { test('throws an error if we have duplicate subscriptions in same topics array', async () => { const webhookConfig: WebhooksConfig = { api_version: '2021-07', - subscriptions: [ - {uri: 'https://example.com', topics: ['products/create', 'products/create']}, - {uri: 'https://example.com', topics: ['products/create']}, - ], + subscriptions: [{uri: 'https://example.com', topics: ['products/create', 'products/create']}], } + const webhookFields = colors.dim(`\n\ntopic: products/create\nuri: https://example.com`) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/create'], + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2736,11 +2735,12 @@ describe('WebhooksSchema', () => { {uri: 'https://example.com', topics: ['products/create', 'products/update']}, ], } + const webhookFields = colors.dim(`\n\ntopic: products/create\nuri: https://example.com`) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/create'], + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2814,11 +2814,12 @@ describe('WebhooksSchema', () => { }, ], } + const webhookFields = colors.dim(`\n\ntopic: products/create\nuri: https://example.com`) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/create'], + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2839,11 +2840,12 @@ describe('WebhooksSchema', () => { }, ], } + const webhookFields = colors.dim(`\n\ntopic: products/create\nuri: pubsub://my-project-123:my-topic`) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/create'], + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2864,11 +2866,14 @@ describe('WebhooksSchema', () => { }, ], } + const webhookFields = colors.dim( + `\n\ntopic: products/create\nuri: arn:aws:events:us-west-2::event-source/aws.partner/shopify.com/123/my_webhook_path`, + ) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/create'], + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2891,11 +2896,50 @@ describe('WebhooksSchema', () => { }, ], } + const webhookFields = colors.dim(`\n\ntopic: products/update\nuri: https://example.com\nfilter: title:shoes`) const errorObj = { code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, fatal: true, - path: ['webhooks', 'subscriptions', 1, 'topics', 0, 'products/update'], + path: ['webhooks', 'subscriptions'], + } + + const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) + expect(abortOrReport).toHaveBeenCalledWith(expectedFormatted, {}, 'tmp') + }) + + test('shows multiple duplicate subscriptions in error message', async () => { + const webhookConfig: WebhooksConfig = { + api_version: '2021-07', + subscriptions: [ + { + topics: ['products/update'], + uri: 'https://example.com', + filter: 'title:shoes', + }, + { + topics: ['products/update'], + uri: 'https://example.com', + filter: 'title:shoes', + }, + { + topics: ['products/create'], + uri: 'https://example.com', + }, + { + topics: ['products/create'], + uri: 'https://example.com', + }, + ], + } + const webhookFields = + colors.dim(`\n\ntopic: products/update\nuri: https://example.com\nfilter: title:shoes`) + + colors.dim(`\n\ntopic: products/create\nuri: https://example.com`) + const errorObj = { + code: zod.ZodIssueCode.custom, + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${webhookFields}`, + fatal: true, + path: ['webhooks', 'subscriptions'], } const {abortOrReport, expectedFormatted} = await setupParsing(errorObj, webhookConfig) @@ -2993,9 +3037,9 @@ describe('WebhooksSchema', () => { async function setupParsing(errorObj: zod.ZodIssue | {}, webhookConfigOverrides: WebhooksConfig) { const err = Array.isArray(errorObj) ? errorObj : [errorObj] - const expectedFormatted = outputContent`App configuration is not valid\nValidation errors in tmp:\n\n${parseHumanReadableError( - err, - )}` + const expectedFormatted = outputContent`\n${outputToken.errorText( + 'Validation errors', + )} in tmp:\n\n${parseHumanReadableError(err)}` const abortOrReport = vi.fn() const {path, ...toParse} = getWebhookConfig(webhookConfigOverrides) diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 36c1350b81..3db353645b 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -137,7 +137,7 @@ export function parseConfigurationObject( const parseResult = schema.safeParse(configurationObject) if (!parseResult.success) { return abortOrReport( - outputContent`App configuration is not valid\nValidation errors in ${outputToken.path( + outputContent`\n${outputToken.errorText('Validation errors')} in ${outputToken.path( filepath, )}:\n\n${parseHumanReadableError(parseResult.error.issues)}`, fallbackOutput, diff --git a/packages/app/src/cli/models/extensions/specifications/validation/app_config_webhook.ts b/packages/app/src/cli/models/extensions/specifications/validation/app_config_webhook.ts index e379ca2ccd..247274ead7 100644 --- a/packages/app/src/cli/models/extensions/specifications/validation/app_config_webhook.ts +++ b/packages/app/src/cli/models/extensions/specifications/validation/app_config_webhook.ts @@ -1,5 +1,6 @@ import {zod} from '@shopify/cli-kit/node/schema' import {uniq} from '@shopify/cli-kit/common/array' +import colors from '@shopify/cli-kit/node/colors' import type {WebhooksConfig} from '../types/app_config_webhook.js' export function webhookValidator(schema: object, ctx: zod.RefinementCtx) { @@ -14,6 +15,7 @@ export function webhookValidator(schema: object, ctx: zod.RefinementCtx) { function validateSubscriptions(webhookConfig: WebhooksConfig) { const {subscriptions = []} = webhookConfig const uniqueSubscriptionSet = new Set() + const duplicatedSubscriptionsFields: string[] = [] if (!subscriptions.length) return @@ -49,19 +51,28 @@ function validateSubscriptions(webhookConfig: WebhooksConfig) { } } - for (const [j, topic] of topics.entries()) { + topics.forEach((topic) => { const key = `${topic}::${uri}::${filter}` if (uniqueSubscriptionSet.has(key)) { - return { - code: zod.ZodIssueCode.custom, - message: 'You can’t have duplicate subscriptions with the exact same `topic`, `uri` and `filter`', - fatal: true, - path: [...path, 'topics', j, topic], - } + const subscriptionFieldsString = filter + ? colors.dim(`\n\ntopic: ${topic}\nuri: ${uri}\nfilter: ${filter}`) + : colors.dim(`\n\ntopic: ${topic}\nuri: ${uri}`) + + duplicatedSubscriptionsFields.push(subscriptionFieldsString) } uniqueSubscriptionSet.add(key) + }) + } + + if (duplicatedSubscriptionsFields.length > 0) { + const fieldsArrToString = duplicatedSubscriptionsFields.join('') + + return { + code: zod.ZodIssueCode.custom, + message: `Multiple subscriptions with the exact same topic, uri, and filter. To resolve, remove or edit the duplicates ${fieldsArrToString}`, + path: ['subscriptions'], } } }