Skip to content
Merged
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
44 changes: 40 additions & 4 deletions apps/sim/lib/webhooks/deploy.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
/**
* @vitest-environment node
*/
import { credential } from '@sim/db/schema'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { SubBlockConfig } from '@/blocks/types'
import type { BlockState } from '@/stores/workflows/workflow/types'

const { mockEq, mockFrom, mockLimit, mockSelect, mockWhere } = vi.hoisted(() => ({
mockEq: vi.fn((left: unknown, right: unknown) => ({ left, right })),
mockFrom: vi.fn(),
mockLimit: vi.fn(),
mockSelect: vi.fn(),
mockWhere: vi.fn(),
}))

vi.mock('@sim/db', () => ({ db: { select: mockSelect } }))
vi.mock('drizzle-orm', () => ({
and: vi.fn((...conditions: unknown[]) => ({ conditions })),
eq: mockEq,
inArray: vi.fn((...args: unknown[]) => ({ args })),
isNull: vi.fn((value: unknown) => ({ value })),
or: vi.fn((...conditions: unknown[]) => ({ conditions })),
}))

// deploy.ts pulls in the trigger/block/provider registries at module load; none are exercised by
// buildProviderConfig (a pure function), so stub them to keep this unit test fast and isolated.
vi.mock('@/blocks', () => ({ getBlock: vi.fn() }))
Expand All @@ -22,7 +40,7 @@ vi.mock('@/lib/webhooks/pending-verification', () => ({
PendingWebhookVerificationTracker: vi.fn(),
}))

import { buildProviderConfig } from '@/lib/webhooks/deploy'
import { buildProviderConfig, resolveTriggerCredentialId } from '@/lib/webhooks/deploy'

const trigger = (subBlocks: Partial<SubBlockConfig>[]): { subBlocks: SubBlockConfig[] } => ({
subBlocks: subBlocks as SubBlockConfig[],
Expand Down Expand Up @@ -59,16 +77,22 @@ function makeBlock(
} as unknown as BlockState
}

describe('buildProviderConfig canonical collapse', () => {
beforeEach(() => vi.clearAllMocks())
beforeEach(() => {
vi.clearAllMocks()
mockSelect.mockReturnValue({ from: mockFrom })
mockFrom.mockReturnValue({ where: mockWhere })
mockWhere.mockReturnValue({ limit: mockLimit })
mockLimit.mockResolvedValue([{ id: 'credential-1' }])
})

describe('buildProviderConfig canonical collapse', () => {
it('writes the basic value under the canonical key in basic mode', () => {
const block = makeBlock('google_drive_poller', { folderId: 'BASIC' })
const { providerConfig } = buildProviderConfig(block, 'google_drive_poller', driveTrigger)
expect(providerConfig.folderId).toBe('BASIC')
})

it('returns the credential reference and canonical OAuth service for deploy validation', () => {
it('returns the credential reference and OAuth service for deploy validation', () => {
const block = makeBlock('google_drive_poller', { triggerCredentials: 'credential-1' })
const result = buildProviderConfig(block, 'google_drive_poller', driveTrigger)

Expand Down Expand Up @@ -132,3 +156,15 @@ describe('buildProviderConfig canonical collapse', () => {
expect(providerConfig.tableId).toBe('ACTIVE')
})
})

describe('resolveTriggerCredentialId', () => {
it('canonicalizes an OAuth service alias at the credential lookup boundary', async () => {
await resolveTriggerCredentialId('credential-1', 'workspace-1', 'gmail')

expect(mockEq).toHaveBeenCalledWith(credential.workspaceId, 'workspace-1')
expect(mockEq).toHaveBeenCalledWith(credential.type, 'oauth')
expect(mockEq).toHaveBeenCalledWith(credential.providerId, 'google-email')
expect(mockEq).toHaveBeenCalledWith(credential.id, 'credential-1')
expect(mockEq).toHaveBeenCalledWith(credential.accountId, 'credential-1')
})
})
10 changes: 7 additions & 3 deletions apps/sim/lib/webhooks/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getErrorMessage } from '@sim/utils/errors'
import { generateShortId } from '@sim/utils/id'
import { and, eq, inArray, isNull, or } from 'drizzle-orm'
import type { NextRequest } from 'next/server'
import { getProviderIdFromServiceId } from '@/lib/oauth'
import { WebhookPathClaimConflictError } from '@/lib/webhooks/path-claims'
import { PendingWebhookVerificationTracker } from '@/lib/webhooks/pending-verification'
import {
Expand Down Expand Up @@ -334,21 +335,24 @@ export function buildProviderConfig(

/**
* Resolves a trigger credential reference to its canonical platform credential ID while enforcing
* that the credential belongs to the deployed workflow's workspace and OAuth service.
* that the credential belongs to the deployed workflow's workspace and OAuth provider.
*
* Exported for unit testing the service-to-provider boundary; not part of the public deploy API.
*/
async function resolveTriggerCredentialId(
export async function resolveTriggerCredentialId(
credentialReference: string,
workspaceId: string,
serviceId: string
): Promise<string | null> {
const providerId = getProviderIdFromServiceId(serviceId)
const [resolvedCredential] = await db
.select({ id: credential.id })
.from(credential)
.where(
and(
eq(credential.workspaceId, workspaceId),
eq(credential.type, 'oauth'),
eq(credential.providerId, serviceId),
eq(credential.providerId, providerId),
or(eq(credential.id, credentialReference), eq(credential.accountId, credentialReference))
)
)
Expand Down
Loading