diff --git a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx index b128245018e..3c91acc8103 100644 --- a/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx +++ b/apps/sim/app/o/[organizationId]/settings/components/integrations/organization-integrations-settings.test.tsx @@ -170,7 +170,6 @@ describe('organization integration invitations', () => { label: 'Slack', required: false, slackBotCredentialId: 'slack-bot', - requiredScopes: ['search:read'], }, ], }, diff --git a/apps/sim/ee/credential-groups/components/organization-account-providers.test.tsx b/apps/sim/ee/credential-groups/components/organization-account-providers.test.tsx index 5bb56a7e043..0610a6bc7ea 100644 --- a/apps/sim/ee/credential-groups/components/organization-account-providers.test.tsx +++ b/apps/sim/ee/credential-groups/components/organization-account-providers.test.tsx @@ -259,7 +259,6 @@ describe('organization provider configuration UI', () => { label: slack.label, required: slack.required, slackBotCredentialId: slack.slackBotCredentialId, - requiredScopes: slack.requiredScopes, }, ], }, diff --git a/apps/sim/lib/credential-groups/organization-account-options.test.ts b/apps/sim/lib/credential-groups/organization-account-options.test.ts new file mode 100644 index 00000000000..d8b8d81e212 --- /dev/null +++ b/apps/sim/lib/credential-groups/organization-account-options.test.ts @@ -0,0 +1,64 @@ +/** @vitest-environment node */ +import { describe, expect, it } from 'vitest' +import { + type OrganizationAccountsSettings, + updateOrganizationAccountsContract, +} from '@/lib/api/contracts/organization-accounts' +import { getOrganizationAccountUpdateOptions } from '@/lib/credential-groups/organization-account-options' +import { CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS } from '@/lib/credential-groups/providers' + +describe('organization account update options', () => { + it.each([undefined, '12345678-1234-4123-8123-123456789012'])( + 'satisfies the update contract with stored Slack scopes and bot %s', + (slackBotCredentialId) => { + const group: NonNullable = { + id: 'group-1', + workspaceId: null, + organizationId: 'org-1', + name: 'Connected accounts', + description: null, + mcpServers: [], + status: 'active', + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + options: [ + ...CREDENTIAL_GROUP_STANDARD_OAUTH_PROVIDER_IDS.map((provider) => ({ + id: `${provider}-option`, + provider, + label: provider, + required: false, + status: 'active' as const, + configurationStatus: 'ready' as const, + })), + { + id: 'slack-option', + provider: 'slack', + label: 'Slack', + required: true, + status: 'active', + configurationStatus: 'ready', + slackBotCredentialId, + requiredScopes: ['search:read', 'channels:history'], + }, + ], + } + + const options = getOrganizationAccountUpdateOptions(group) + const result = updateOrganizationAccountsContract.body.parse({ options }) + + expect(result.options).toEqual(options) + expect(result.options?.map(({ id }) => id)).toEqual(group.options.map(({ id }) => id)) + expect(result.options?.at(-1)).toEqual({ + id: 'slack-option', + provider: 'slack', + label: 'Slack', + required: true, + slackBotCredentialId, + }) + expect(group.options.at(-1)).toHaveProperty('requiredScopes', [ + 'search:read', + 'channels:history', + ]) + } + ) +}) diff --git a/apps/sim/lib/credential-groups/organization-account-options.ts b/apps/sim/lib/credential-groups/organization-account-options.ts index 8716bdbc7d8..f8bf3091c18 100644 --- a/apps/sim/lib/credential-groups/organization-account-options.ts +++ b/apps/sim/lib/credential-groups/organization-account-options.ts @@ -3,7 +3,7 @@ import type { UpdateOrganizationAccountsBody, } from '@/lib/api/contracts/organization-accounts' -/** Preserves option identities and custom Slack scopes while the server refreshes managed OAuth policies. */ +/** Keeps option IDs so the server preserves saved scopes when refreshing provider policies. */ export function getOrganizationAccountUpdateOptions( group: NonNullable ): NonNullable { @@ -14,7 +14,6 @@ export function getOrganizationAccountUpdateOptions( ...common, provider: 'slack', slackBotCredentialId: option.slackBotCredentialId, - requiredScopes: option.requiredScopes, } : { ...common, provider: option.provider } }) diff --git a/apps/sim/lib/credential-groups/service.test.ts b/apps/sim/lib/credential-groups/service.test.ts index b648f861ec5..7c956d2015c 100644 --- a/apps/sim/lib/credential-groups/service.test.ts +++ b/apps/sim/lib/credential-groups/service.test.ts @@ -105,7 +105,7 @@ describe('Credential Group service', () => { } ) - it('validates provider policy through the active update transaction', async () => { + it('preserves stored scopes while validating provider policy in the update transaction', async () => { const option = { id: 'option-1', provider: 'slack' as const, @@ -156,8 +156,12 @@ describe('Credential Group service', () => { }) ).resolves.toMatchObject({ id: 'group-1' }) + expect(dbChainMockFns.set).toHaveBeenCalledWith(expect.objectContaining({ options: [option] })) expect(mockGetPolicy).toHaveBeenCalledWith( - expect.objectContaining({ slackBotCredentialId: 'bot-1' }), + expect.objectContaining({ + slackBotCredentialId: 'bot-1', + requiredScopes: option.requiredScopes, + }), { workspaceId: 'workspace-1', credentialGroupId: 'group-1',