chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,332 @@
|
||||
/**
|
||||
* @vitest-environment node
|
||||
*/
|
||||
|
||||
import type { webhook, workflow } from '@sim/db/schema'
|
||||
import {
|
||||
createMockRequest,
|
||||
envFlagsMock,
|
||||
executionPreprocessingMock,
|
||||
executionPreprocessingMockFns,
|
||||
} from '@sim/testing'
|
||||
import type { NextRequest } from 'next/server'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const {
|
||||
mockGenerateId,
|
||||
mockEnqueue,
|
||||
mockGetJobQueue,
|
||||
mockProviderHandler,
|
||||
mockShouldExecuteInline,
|
||||
mockWebhookLookupResult,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGenerateId: vi.fn(),
|
||||
mockEnqueue: vi.fn(),
|
||||
mockGetJobQueue: vi.fn(),
|
||||
mockProviderHandler: { current: {} as Record<string, unknown> },
|
||||
mockShouldExecuteInline: vi.fn(),
|
||||
mockWebhookLookupResult: { rows: [] as Array<{ webhook: any; workflow: any }> },
|
||||
}))
|
||||
|
||||
const mockPreprocessExecution = executionPreprocessingMockFns.mockPreprocessExecution
|
||||
|
||||
vi.mock('@sim/db', () => {
|
||||
const selectChain = {
|
||||
from: () => selectChain,
|
||||
innerJoin: () => selectChain,
|
||||
leftJoin: () => selectChain,
|
||||
where: () => Promise.resolve(mockWebhookLookupResult.rows),
|
||||
}
|
||||
return {
|
||||
db: { select: () => selectChain },
|
||||
webhook: {},
|
||||
workflow: {},
|
||||
workflowDeploymentVersion: {},
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('drizzle-orm', () => ({
|
||||
and: vi.fn(),
|
||||
eq: vi.fn(),
|
||||
isNull: vi.fn(),
|
||||
or: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@sim/utils/id', () => ({
|
||||
generateId: mockGenerateId,
|
||||
generateShortId: vi.fn(() => 'mock-short-id'),
|
||||
isValidUuid: vi.fn((v: string) =>
|
||||
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v)
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/billing/subscriptions/utils', () => ({
|
||||
checkEnterprisePlan: vi.fn().mockReturnValue(true),
|
||||
checkTeamPlan: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/async-jobs', () => ({
|
||||
getInlineJobQueue: vi.fn(),
|
||||
getJobQueue: mockGetJobQueue,
|
||||
shouldExecuteInline: mockShouldExecuteInline,
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/core/config/env-flags', () => envFlagsMock)
|
||||
|
||||
vi.mock('@sim/security/compare', () => ({
|
||||
safeCompare: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/environment/utils', () => ({
|
||||
getEffectiveDecryptedEnv: vi.fn().mockResolvedValue({}),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/execution/preprocessing', () => executionPreprocessingMock)
|
||||
|
||||
vi.mock('@/lib/webhooks/pending-verification', () => ({
|
||||
getPendingWebhookVerification: vi.fn(),
|
||||
matchesPendingWebhookVerificationProbe: vi.fn().mockReturnValue(false),
|
||||
requiresPendingWebhookVerification: vi.fn().mockReturnValue(false),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/webhooks/utils', () => ({
|
||||
convertSquareBracketsToTwiML: vi.fn((value: string) => value),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/webhooks/utils.server', () => ({
|
||||
handleSlackChallenge: vi.fn().mockReturnValue(null),
|
||||
handleWhatsAppVerification: vi.fn().mockResolvedValue(null),
|
||||
}))
|
||||
|
||||
vi.mock('@/lib/webhooks/providers', () => ({
|
||||
getProviderHandler: vi.fn(() => mockProviderHandler.current),
|
||||
}))
|
||||
|
||||
vi.mock('@/background/webhook-execution', () => ({
|
||||
executeWebhookJob: vi.fn().mockResolvedValue({ success: true }),
|
||||
}))
|
||||
|
||||
vi.mock('@/executor/utils/reference-validation', () => ({
|
||||
resolveEnvVarReferences: vi.fn((value: string) => value),
|
||||
}))
|
||||
|
||||
vi.mock('@/triggers/confluence/utils', () => ({
|
||||
isConfluencePayloadMatch: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
vi.mock('@/triggers/constants', () => ({
|
||||
isPollingWebhookProvider: vi.fn((provider: string) => provider === 'gmail'),
|
||||
}))
|
||||
|
||||
vi.mock('@/triggers/github/utils', () => ({
|
||||
isGitHubEventMatch: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
vi.mock('@/triggers/jira/utils', () => ({
|
||||
isJiraEventMatch: vi.fn().mockReturnValue(true),
|
||||
}))
|
||||
|
||||
import { dispatchResolvedWebhookTarget, findAllWebhooksForPath } from '@/lib/webhooks/processor'
|
||||
|
||||
type WebhookRecord = typeof webhook.$inferSelect
|
||||
type WorkflowRecord = typeof workflow.$inferSelect
|
||||
|
||||
function makeWebhookRecord(overrides: Partial<WebhookRecord>): WebhookRecord {
|
||||
const now = new Date('2026-01-01T00:00:00.000Z')
|
||||
return {
|
||||
id: 'webhook-1',
|
||||
workflowId: 'workflow-1',
|
||||
deploymentVersionId: null,
|
||||
blockId: null,
|
||||
path: 'incoming/test',
|
||||
provider: 'generic',
|
||||
providerConfig: {},
|
||||
isActive: true,
|
||||
failedCount: 0,
|
||||
lastFailedAt: null,
|
||||
archivedAt: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function makeWorkflowRecord(overrides: Partial<WorkflowRecord>): WorkflowRecord {
|
||||
const now = new Date('2026-01-01T00:00:00.000Z')
|
||||
return {
|
||||
id: 'workflow-1',
|
||||
userId: 'owner-1',
|
||||
workspaceId: 'workspace-1',
|
||||
folderId: null,
|
||||
sortOrder: 0,
|
||||
name: 'Webhook workflow',
|
||||
description: null,
|
||||
lastSynced: now,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
isDeployed: true,
|
||||
deployedAt: now,
|
||||
isPublicApi: false,
|
||||
locked: false,
|
||||
runCount: 0,
|
||||
lastRunAt: null,
|
||||
variables: {},
|
||||
archivedAt: null,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
describe('findAllWebhooksForPath cross-tenant collision', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockWebhookLookupResult.rows = []
|
||||
})
|
||||
|
||||
const makeRow = (workflowId: string, webhookId: string, createdAt: Date) => ({
|
||||
webhook: { id: webhookId, workflowId, path: 'shared-path', createdAt },
|
||||
workflow: { id: workflowId },
|
||||
})
|
||||
|
||||
it('returns all rows when they belong to a single workflow', async () => {
|
||||
mockWebhookLookupResult.rows = [
|
||||
makeRow('workflow-1', 'wh-a', new Date('2026-01-01')),
|
||||
makeRow('workflow-1', 'wh-b', new Date('2026-01-02')),
|
||||
]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-1', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results.map((r) => r.webhook.id)).toEqual(['wh-a', 'wh-b'])
|
||||
})
|
||||
|
||||
it('drops foreign rows when a path collides across workflows, keeping the earliest owner', async () => {
|
||||
const victim = makeRow('victim-workflow', 'victim-wh', new Date('2026-01-01'))
|
||||
const attacker = makeRow('attacker-workflow', 'attacker-wh', new Date('2026-05-01'))
|
||||
mockWebhookLookupResult.rows = [attacker, victim]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-2', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].webhook.id).toBe('victim-wh')
|
||||
expect(results[0].webhook.workflowId).toBe('victim-workflow')
|
||||
})
|
||||
|
||||
it("preserves the owner's full multi-webhook match while dropping a foreign row", async () => {
|
||||
const victimA = makeRow('victim-workflow', 'victim-wh-a', new Date('2026-01-01'))
|
||||
const victimB = makeRow('victim-workflow', 'victim-wh-b', new Date('2026-01-03'))
|
||||
const attacker = makeRow('attacker-workflow', 'attacker-wh', new Date('2026-05-01'))
|
||||
mockWebhookLookupResult.rows = [victimB, attacker, victimA]
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-5', path: 'shared-path' })
|
||||
|
||||
expect(results).toHaveLength(2)
|
||||
expect(results.every((r) => r.webhook.workflowId === 'victim-workflow')).toBe(true)
|
||||
expect(results.map((r) => r.webhook.id).sort()).toEqual(['victim-wh-a', 'victim-wh-b'])
|
||||
})
|
||||
|
||||
it('returns an empty array when no webhooks match', async () => {
|
||||
mockWebhookLookupResult.rows = []
|
||||
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-3', path: 'missing' })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
|
||||
it('returns an empty array when path is not provided', async () => {
|
||||
const results = await findAllWebhooksForPath({ requestId: 'req-4' })
|
||||
|
||||
expect(results).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('webhook processor execution identity', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockPreprocessExecution.mockResolvedValue({
|
||||
success: true,
|
||||
actorUserId: 'actor-user-1',
|
||||
})
|
||||
mockEnqueue.mockResolvedValue('job-1')
|
||||
mockGetJobQueue.mockResolvedValue({ enqueue: mockEnqueue })
|
||||
mockProviderHandler.current = {}
|
||||
mockShouldExecuteInline.mockReturnValue(false)
|
||||
mockGenerateId.mockReturnValue('generated-execution-id')
|
||||
})
|
||||
|
||||
it('reuses preprocessing execution identity when queueing a polling webhook', async () => {
|
||||
const expectedCorrelation = {
|
||||
executionId: 'generated-execution-id',
|
||||
requestId: 'request-1',
|
||||
source: 'webhook',
|
||||
workflowId: 'workflow-1',
|
||||
webhookId: 'webhook-1',
|
||||
path: 'incoming/gmail',
|
||||
provider: 'gmail',
|
||||
triggerType: 'webhook',
|
||||
}
|
||||
|
||||
const result = await dispatchResolvedWebhookTarget(
|
||||
makeWebhookRecord({
|
||||
path: 'incoming/gmail',
|
||||
provider: 'gmail',
|
||||
}),
|
||||
makeWorkflowRecord({}),
|
||||
{ event: 'message.received' },
|
||||
createMockRequest('POST', { event: 'message.received' }) as NextRequest,
|
||||
{
|
||||
requestId: 'request-1',
|
||||
path: 'incoming/gmail',
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.outcome).toBe('queued')
|
||||
expect(mockGenerateId).toHaveBeenCalledTimes(1)
|
||||
expect(mockEnqueue).toHaveBeenCalledWith(
|
||||
'webhook-execution',
|
||||
expect.objectContaining({
|
||||
workflowId: 'workflow-1',
|
||||
provider: 'gmail',
|
||||
}),
|
||||
expect.objectContaining({
|
||||
metadata: expect.objectContaining({
|
||||
workflowId: 'workflow-1',
|
||||
workspaceId: 'workspace-1',
|
||||
userId: 'actor-user-1',
|
||||
correlation: expectedCorrelation,
|
||||
}),
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
it('routes queue-mode providers through the durable job backend', async () => {
|
||||
mockProviderHandler.current = { executionMode: 'queue' }
|
||||
|
||||
const result = await dispatchResolvedWebhookTarget(
|
||||
makeWebhookRecord({
|
||||
id: 'webhook-2',
|
||||
path: 'tiktok',
|
||||
provider: 'tiktok',
|
||||
}),
|
||||
makeWorkflowRecord({
|
||||
id: 'workflow-2',
|
||||
workspaceId: 'workspace-2',
|
||||
}),
|
||||
{ event: 'post.publish.complete' },
|
||||
createMockRequest('POST', { event: 'post.publish.complete' }) as NextRequest,
|
||||
{
|
||||
requestId: 'request-2',
|
||||
}
|
||||
)
|
||||
|
||||
expect(result.outcome).toBe('queued')
|
||||
expect(result.response.status).toBe(200)
|
||||
expect(mockEnqueue).toHaveBeenCalledWith(
|
||||
'webhook-execution',
|
||||
expect.objectContaining({
|
||||
provider: 'tiktok',
|
||||
workflowId: 'workflow-2',
|
||||
}),
|
||||
expect.any(Object)
|
||||
)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user