Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -288,4 +288,53 @@ describe('/api/u/[slug]/connectors/[id]/tool-permissions', () => {
expect(res.status).toBe(404)
await expect(res.json()).resolves.toEqual({ error: 'connector_not_found' })
})

describe('zendesk connectors', () => {
const ZENDESK_CONNECTOR = { id: 'c1', type: 'zendesk', config: 'encrypted', enabled: true }

beforeEach(() => {
mocks.connectorService.findByIdAndUserId.mockResolvedValue(ZENDESK_CONNECTOR)
mocks.decryptConfig.mockReturnValue({
subdomain: 'test',
email: 'a@b.com',
apiToken: 'tok',
permissions: {
allowRead: true,
allowCreateTickets: true,
allowUpdateTickets: true,
allowPublicComments: false,
allowInternalComments: true,
},
})
})

it('GET projects the normalized canonical actions instead of stored tool permissions', async () => {
const res = await GET(makeGetRequest(), params())
const body = await res.json()

expect(res.status).toBe(200)
expect(body.policyConfigured).toBe(true)
expect(body.tools).toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'create_ticket_public', permission: 'deny' }),
expect.objectContaining({ name: 'create_ticket_internal', permission: 'allow' }),
expect.objectContaining({ name: 'search_tickets', permission: 'allow' }),
])
)
expect(body.tools).toHaveLength(8)
})

it('PATCH rejects writes so no independent Zendesk policy state can be created', async () => {
const res = await PATCH(
makePatchRequest({ permissions: { create_ticket_public: 'allow' } }),
params(),
)
const body = await res.json()

expect(res.status).toBe(409)
expect(body.error).toBe('unsupported_connector')
expect(mocks.encryptConfig).not.toHaveBeenCalled()
expect(mocks.connectorService.updateManyByIdAndUserId).not.toHaveBeenCalled()
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ import {
} from '@/lib/connectors/tool-permissions'
import type { ConnectorType } from '@/lib/connectors/types'
import { validateConnectorConfig, validateConnectorType } from '@/lib/connectors/validators'
import {
normalizeZendeskActionPermissions,
} from '@/lib/connectors/zendesk'
import {
ZENDESK_ACTION_KEYS,
type ZendeskActionName,
} from '@/lib/connectors/zendesk-types'
import { requireCapability } from '@/lib/runtime/require-capability'
import { withAuth } from '@/lib/runtime/with-auth'
import { connectorService, userService } from '@/lib/services'
Expand Down Expand Up @@ -45,6 +52,28 @@ function fallbackToolsFromStoredPermissions(
}))
}

function toZendeskActionTitle(name: string): string {
const formatted = name.replace(/_/g, ' ').trim()
return formatted ? formatted.charAt(0).toUpperCase() + formatted.slice(1) : name
}

// Zendesk policies are canonical action state, not generic tool-permission
// state: the read path projects the normalized actions so the response can
// never disagree with Zendesk settings, and writes are rejected so a second,
// independently editable policy surface cannot exist.
function buildZendeskToolPermissionsResponse(
config: Record<string, unknown>
): ConnectorToolPermissionsResponse {
const actions = normalizeZendeskActionPermissions(config)
const tools: ConnectorToolPermissionEntry[] = ZENDESK_ACTION_KEYS.map((action: ZendeskActionName) => ({
name: action,
title: toZendeskActionTitle(action),
permission: actions[action],
}))

return { tools, policyConfigured: true }
}

async function buildToolPermissionsResponse(input: {
connectorType: ConnectorType
config: Record<string, unknown>
Expand Down Expand Up @@ -135,6 +164,10 @@ export const GET = withAuth<
const context = await getConnectorContext(slug, id)
if (!context.ok) return context.response

if (context.connectorType === 'zendesk') {
return NextResponse.json(buildZendeskToolPermissionsResponse(context.config))
}

return NextResponse.json(
await buildToolPermissionsResponse({
connectorType: context.connectorType,
Expand All @@ -153,6 +186,16 @@ export const PATCH = withAuth<
const context = await getConnectorContext(slug, id)
if (!context.ok) return context.response

if (context.connectorType === 'zendesk') {
return NextResponse.json(
{
error: 'unsupported_connector',
message: 'Zendesk tool permissions are managed through Zendesk settings.',
},
{ status: 409 },
)
}

let body: UpdateConnectorToolPermissionsRequest
try {
body = await request.json()
Expand Down
Loading
Loading