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
82 changes: 82 additions & 0 deletions apps/sim/blocks/blocks/confluence.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/**
* @vitest-environment node
*/
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { ConfluenceV2Block } from '@/blocks/blocks/confluence'

const { mockGetBlock } = vi.hoisted(() => ({ mockGetBlock: vi.fn() }))

vi.mock('@/blocks/registry', () => ({
getBlock: mockGetBlock,
getAllBlocks: vi.fn(() => []),
getLatestBlock: vi.fn(() => undefined),
getBlockRegistry: vi.fn(() => ({})),
getBlockByToolName: vi.fn(() => undefined),
getBlocksByCategory: vi.fn(() => []),
}))

import { migrateSubblockIds } from '@/lib/workflows/migrations/subblock-migrations'
import { extractBlockParams } from '@/serializer'
import type { BlockState } from '@/stores/workflows/workflow/types'

function legacySearchBlock(field: string, value: string, advancedMode: boolean): BlockState {
const values = { operation: 'search_in_space', [field]: value }
return {
id: 'block-1',
type: 'confluence_v2',
name: 'Confluence 1',
position: { x: 0, y: 0 },
advancedMode,
subBlocks: Object.fromEntries(
Object.entries(values).map(([id, fieldValue]) => [
id,
{ id, type: 'short-input', value: fieldValue },
])
),
outputs: {},
enabled: true,
} as unknown as BlockState
}

function mappedSearchParams(state: BlockState): {
blocks: Record<string, BlockState>
params: Record<string, unknown>
} {
const { blocks } = migrateSubblockIds({ 'block-1': state })
const params = extractBlockParams(blocks['block-1'])
const transform = ConfluenceV2Block.tools.config?.params
if (!transform) throw new Error('Confluence V2 block has no params transform')
return { blocks, params: { ...params, ...transform(params) } }
}

describe('Confluence search-in-space values saved before the selector split', () => {
beforeEach(() => {
vi.clearAllMocks()
mockGetBlock.mockReturnValue(ConfluenceV2Block)
})

it.each([
{
mode: 'basic',
source: 'spaceSelector',
target: 'spaceKeySelector',
value: 'ENG',
advancedMode: false,
},
{
mode: 'advanced',
source: 'spaceId',
target: 'manualSpaceKey',
value: '12345',
advancedMode: true,
},
])('migrates the $mode value and sends it as spaceKey', (testCase) => {
const { blocks, params } = mappedSearchParams(
legacySearchBlock(testCase.source, testCase.value, testCase.advancedMode)
)

expect(blocks['block-1'].subBlocks[testCase.target]?.value).toBe(testCase.value)
expect(params).toMatchObject({ operation: 'search_in_space', spaceKey: testCase.value })
expect(params.spaceId).toBeUndefined()
})
})
43 changes: 39 additions & 4 deletions apps/sim/blocks/blocks/confluence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const PAGE_FIELD = ['pageId', 'manualPageId'] as const
*/
const SPACE_FIELD = ['spaceSelector', 'spaceId'] as const

/** Canonical basic/advanced pair for V1 operations that require a space key. */
const SPACE_KEY_FIELD = ['spaceKeySelector', 'manualSpaceKey'] as const

/** Canonical upload/reference pair for an attachment's file. V2 only. */
const ATTACHMENT_FILE_FIELD = ['attachmentFileUpload', 'attachmentFileReference'] as const

Expand Down Expand Up @@ -485,7 +488,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
{ text: ', up to', field: 'limit', after: 'results' },
],
search_in_space: [
{ text: 'Search', field: SPACE_FIELD, core: true },
{ text: 'Search', field: SPACE_KEY_FIELD, core: true },
{ text: 'for', field: 'query' },
],
list_blogposts: ['List blog posts', { text: ', up to', field: 'limit', after: 'results' }],
Expand Down Expand Up @@ -834,7 +837,6 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'update_space',
'delete_space',
'list_pages_in_space',
'search_in_space',
'create_blogpost',
'list_blogposts_in_space',
'list_space_labels',
Expand All @@ -861,7 +863,6 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
'update_space',
'delete_space',
'list_pages_in_space',
'search_in_space',
'create_blogpost',
'list_blogposts_in_space',
'list_space_labels',
Expand All @@ -872,6 +873,29 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
],
},
},
{
id: 'spaceKeySelector',
title: 'Space',
type: 'project-selector',
canonicalParamId: 'selectedSpaceKey',
serviceId: 'confluence',
selectorKey: 'confluence.spaces',
placeholder: 'Select Confluence space',
dependsOn: ['credential', 'domain'],
mode: 'basic',
required: true,
condition: { field: 'operation', value: 'search_in_space' },
},
{
id: 'manualSpaceKey',
title: 'Space Key',
type: 'short-input',
canonicalParamId: 'selectedSpaceKey',
placeholder: 'Enter Confluence space key',
mode: 'advanced',
required: true,
condition: { field: 'operation', value: 'search_in_space' },
},
{
id: 'blogPostId',
title: 'Blog Post ID',
Expand Down Expand Up @@ -1461,6 +1485,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
taskAssignedTo,
spaceName,
spaceKey,
selectedSpaceKey,
spaceDescription,
spacePropertyKey,
spacePropertyValue,
Expand Down Expand Up @@ -1626,6 +1651,15 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
}
}

if (operation === 'search_in_space') {
return {
credential: oauthCredential,
operation,
spaceKey: selectedSpaceKey,
...rest,
}
}

if (operation === 'update_space') {
return {
credential: oauthCredential,
Expand Down Expand Up @@ -1730,6 +1764,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
oauthCredential: { type: 'string', description: 'Confluence access token' },
pageId: { type: 'string', description: 'Page identifier' },
spaceId: { type: 'string', description: 'Space identifier' },
selectedSpaceKey: { type: 'string', description: 'Selected space key' },
blogPostId: { type: 'string', description: 'Blog post identifier' },
versionNumber: { type: 'number', description: 'Page version number' },
accountId: { type: 'string', description: 'Atlassian account ID' },
Expand Down Expand Up @@ -1758,7 +1793,7 @@ export const ConfluenceV2Block: BlockConfig<ConfluenceResponse> = {
taskStatus: { type: 'string', description: 'Task status (complete or incomplete)' },
taskAssignedTo: { type: 'string', description: 'Filter tasks by assignee account ID' },
spaceName: { type: 'string', description: 'Space name for create/update' },
spaceKey: { type: 'string', description: 'Space key for create' },
spaceKey: { type: 'string', description: 'Space key for create or scoped search' },
spaceDescription: { type: 'string', description: 'Space description' },
spacePropertyKey: { type: 'string', description: 'Space property key' },
spacePropertyValue: { type: 'json', description: 'Space property value' },
Expand Down
59 changes: 59 additions & 0 deletions apps/sim/lib/internal/confluence/operations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ vi.mock('@/lib/uploads/utils/file-utils.server', () => ({
import { ConfluenceOperationError } from '@/lib/internal/confluence/errors'
import {
executeConfluenceListLabels,
executeConfluenceListPagesInSpace,
executeConfluenceSearchInSpace,
executeConfluenceUploadAttachment,
} from '@/lib/internal/confluence/operations'

Expand Down Expand Up @@ -83,6 +85,63 @@ describe('Confluence operations', () => {
expect(response.bodyUsed).toBe(true)
})

it.each([
{ selectedValue: 'ENG', expectedCalls: 2 },
{ selectedValue: '12345', expectedCalls: 1 },
])(
'uses numeric space IDs for V2 requests when the selected value is $selectedValue',
async ({ selectedValue, expectedCalls }) => {
const fetchMock = vi.fn(async (request: string | URL | Request) => {
const url = String(request)
if (url.includes('/spaces?')) {
return Response.json({
results: [{ id: '12345', key: 'ENG', name: 'Engineering', status: 'current' }],
})
}
return Response.json({ results: [] })
})
vi.stubGlobal('fetch', fetchMock)

await expect(
executeConfluenceListPagesInSpace(
{ ...CONNECTION, spaceId: selectedValue, limit: 25 },
{ headers: new Headers(), requestId: 'request-1' }
)
).resolves.toEqual({ pages: [], nextCursor: null })

expect(fetchMock).toHaveBeenCalledTimes(expectedCalls)
const urls = fetchMock.mock.calls.map(([request]) => String(request))
expect(urls.at(-1)).toContain('/spaces/12345/pages?limit=25')
if (selectedValue === 'ENG') {
expect(urls[0]).toContain('/spaces?keys=ENG&limit=1&status=current')
}
}
)

it('resolves a legacy numeric space value before constructing key-based CQL', async () => {
const fetchMock = vi.fn(async (request: string | URL | Request) => {
const url = String(request)
if (url.includes('/api/v2/spaces/12345')) {
return Response.json({ id: '12345', key: 'ENG', name: 'Engineering' })
}
return Response.json({ results: [], totalSize: 0 })
})
vi.stubGlobal('fetch', fetchMock)

await expect(
executeConfluenceSearchInSpace(
{ ...CONNECTION, spaceKey: '12345', query: 'release notes', limit: 25 },
{ headers: new Headers(), requestId: 'request-1' }
)
).resolves.toEqual({ results: [], spaceKey: 'ENG', totalSize: 0 })

expect(fetchMock).toHaveBeenCalledTimes(2)
const searchUrl = String(fetchMock.mock.calls[1][0])
expect(new URL(searchUrl).searchParams.get('cql')).toBe(
'space = "ENG" AND text ~ "release notes"'
)
})

it('fails closed before downloading a stored file without an acting user', async () => {
let caught: unknown
try {
Expand Down
Loading
Loading