diff --git a/.changeset/chilly-glasses-fetch.md b/.changeset/chilly-glasses-fetch.md new file mode 100644 index 0000000000..7e1b85c8f7 --- /dev/null +++ b/.changeset/chilly-glasses-fetch.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': patch +--- + +Fix crash "config2.map is not a function" in `rewriteConfiguration` when writing app configuration with unvalidated data (e.g., from third-party templates without `client_id`) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts index 685e53f959..44f129c073 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.test.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.test.ts @@ -1,8 +1,9 @@ -import {writeAppConfigurationFile} from './write-app-configuration-file.js' +import {rewriteConfiguration, writeAppConfigurationFile} from './write-app-configuration-file.js' import {DEFAULT_CONFIG, buildVersionedAppSchema} from '../../models/app/app.test-data.js' import {CurrentAppConfiguration} from '../../models/app/app.js' import {inTemporaryDirectory, readFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' +import {zod} from '@shopify/cli-kit/node/schema' import {describe, expect, test} from 'vitest' const FULL_CONFIGURATION = { @@ -151,3 +152,59 @@ url = "https://example.com/prefs" }) }) }) + +describe('rewriteConfiguration', () => { + test('handles undefined config for an optional array schema wrapped in effects', () => { + const schema = zod.array(zod.string()).optional().transform((val) => val) + + expect(rewriteConfiguration(schema, undefined)).toBeUndefined() + }) + + test('handles null config for an array schema', () => { + const schema = zod.array(zod.string()) + + expect(rewriteConfiguration(schema, null)).toBeNull() + }) + + test('handles undefined config for an object schema', () => { + const schema = zod.object({name: zod.string()}).optional() + + expect(rewriteConfiguration(schema, undefined)).toBeUndefined() + }) + + test('handles null config for a nullable object schema', () => { + const schema = zod.object({name: zod.string()}).nullable() + + expect(rewriteConfiguration(schema, null)).toBeNull() + }) + + test('passes through non-array value when schema expects an array', () => { + const schema = zod.array(zod.string()) + + expect(rewriteConfiguration(schema, 'not-an-array')).toBe('not-an-array') + }) + + test('passes through non-object value when schema expects an object', () => { + const schema = zod.object({name: zod.string()}) + + expect(rewriteConfiguration(schema, 'not-an-object')).toBe('not-an-object') + }) + + test('passes through array value when schema expects an object', () => { + const schema = zod.object({name: zod.string()}) + + const input = ['a', 'b'] + expect(rewriteConfiguration(schema, input)).toBe(input) + }) + + test('does not crash with type-mismatched config against the real app schema', async () => { + const {schema} = await buildVersionedAppSchema() + const malformedConfig = { + ...DEFAULT_CONFIG, + auth: {redirect_urls: 'not-an-array'}, + webhooks: {api_version: '2023-07', subscriptions: 'also-not-an-array'}, + } + + expect(() => rewriteConfiguration(schema, malformedConfig)).not.toThrow() + }) +}) diff --git a/packages/app/src/cli/services/app/write-app-configuration-file.ts b/packages/app/src/cli/services/app/write-app-configuration-file.ts index c2f8201240..7dbcc53236 100644 --- a/packages/app/src/cli/services/app/write-app-configuration-file.ts +++ b/packages/app/src/cli/services/app/write-app-configuration-file.ts @@ -27,15 +27,18 @@ export async function writeAppConfigurationFile( export const rewriteConfiguration = (schema: T, config: unknown): unknown => { if (schema === null || schema === undefined) return null + if (config === null || config === undefined) return config if (schema instanceof zod.ZodNullable || schema instanceof zod.ZodOptional) return rewriteConfiguration(schema.unwrap(), config) if (schema instanceof zod.ZodArray) { - return (config as unknown[]).map((item) => rewriteConfiguration(schema.element, item)) + if (!Array.isArray(config)) return config + return config.map((item) => rewriteConfiguration(schema.element, item)) } if (schema instanceof zod.ZodEffects) { return rewriteConfiguration(schema._def.schema, config) } if (schema instanceof zod.ZodObject) { + if (typeof config !== 'object' || Array.isArray(config)) return config const entries = Object.entries(schema.shape) const confObj = config as {[key: string]: unknown} let result: {[key: string]: unknown} = {}