Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Metafields to webhooks #5167

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/app/src/cli/models/app/app.test-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,7 @@ export async function testSingleWebhookSubscriptionExtension({
topic,
api_version: '2024-01',
uri: 'https://my-app.com/webhooks',
metafields: [{namespace: 'custom', key: 'test'}],
},
}: {
emptyConfig?: boolean
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import spec from './app_config_webhook.js'
import {webhookValidator} from './validation/app_config_webhook.js'
import {WebhookSubscriptionSchema} from './app_config_webhook_schemas/webhook_subscription_schema.js'
import {placeholderAppConfiguration} from '../../app/app.test-data.js'
import {describe, expect, test} from 'vitest'
import {zod} from '@shopify/cli-kit/node/schema'

describe('webhooks', () => {
describe('transform', () => {
Expand Down Expand Up @@ -61,4 +64,216 @@ describe('webhooks', () => {
})
})
})

describe('validation', () => {
interface TestWebhookConfig {
api_version: string
subscriptions: unknown[]
}

function validateWebhooks(webhookConfig: TestWebhookConfig) {
const ctx = {
addIssue: (issue: zod.ZodIssue) => {
throw new Error(issue.message)
},
path: [],
} as zod.RefinementCtx

// First validate the schema for each subscription
for (const subscription of webhookConfig.subscriptions) {
const schemaResult = WebhookSubscriptionSchema.safeParse(subscription)
if (!schemaResult.success) {
return {
success: false,
error: new Error(schemaResult.error.issues[0]?.message ?? 'Invalid webhook subscription'),
}
}
}

// Then validate business rules
try {
webhookValidator(webhookConfig, ctx)
return {success: true, error: undefined}
} catch (error) {
if (error instanceof Error) {
return {success: false, error}
}
throw error
}
}

test('allows metafields when API version is 2025-04', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2025-04',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [{namespace: 'custom', key: 'test'}],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(true)
})

test('allows metafields when API version is unstable', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: 'unstable',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [{namespace: 'custom', key: 'test'}],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(true)
})

test('rejects metafields when API version is earlier than 2025-04', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2024-01',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [{namespace: 'custom', key: 'test'}],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(false)
expect(result.error?.message).toBe(
'Webhook metafields are only supported in API version 2025-04 or later, or with version "unstable"',
)
})

test('validates metafields namespace and key are strings', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2025-04',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [{namespace: 123, key: 'test'}],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(false)
expect(result.error?.message).toBe('Metafield namespace must be a string')
})

test('allows configuration without metafields in older API versions', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2024-01',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(true)
})

test('allows empty metafields array in supported API versions', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2025-04',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(true)
})

test('rejects metafields with invalid property types', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2025-04',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [
{
namespace: 123,
key: 'valid',
},
],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(false)
expect(result.error?.message).toBe('Metafield namespace must be a string')
})

test('rejects malformed metafields missing a required property', () => {
// Given
const webhookConfig: TestWebhookConfig = {
api_version: '2025-04',
subscriptions: [
{
topics: ['orders/create'],
uri: 'https://example.com/webhooks',
metafields: [
{
namespace: 'custom',
},
],
},
],
}

// When
const result = validateWebhooks(webhookConfig)

// Then
expect(result.success).toBe(false)
expect(result.error?.message).toBe('Required')
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,15 @@ export const WebhookSubscriptionSchema = zod.object({
}),
include_fields: zod.array(zod.string({invalid_type_error: 'Value must be a string'})).optional(),
filter: zod.string({invalid_type_error: 'Value must be a string'}).optional(),
metafields: zod
.array(
zod.object({
namespace: zod.string({invalid_type_error: 'Metafield namespace must be a string'}),
key: zod.string({invalid_type_error: 'Metafield key must be a string'}),
}),
{invalid_type_error: 'Metafields must be an array of objects with namespace and key'},
)
.optional(),
compliance_topics: zod
.array(
zod.enum([ComplianceTopic.CustomersRedact, ComplianceTopic.CustomersDataRequest, ComplianceTopic.ShopRedact]),
Expand Down
Loading
Loading