|
| 1 | +/** @vitest-environment node */ |
| 2 | +import { setupGlobalFetchMock } from '@sim/testing' |
| 3 | +import { beforeEach, describe, expect, it, vi } from 'vitest' |
| 4 | + |
| 5 | +vi.mock('@tanstack/react-query', () => ({ |
| 6 | + useQuery: vi.fn(), |
| 7 | + useQueryClient: () => ({ invalidateQueries: vi.fn() }), |
| 8 | + useMutation: <TInput, TOutput>(options: { mutationFn: (input: TInput) => Promise<TOutput> }) => ({ |
| 9 | + mutateAsync: options.mutationFn, |
| 10 | + }), |
| 11 | +})) |
| 12 | +vi.mock('@/hooks/queries/oauth/oauth-credentials', () => ({ |
| 13 | + oauthCredentialKeys: { lists: () => ['oauth-credentials', 'list'] }, |
| 14 | +})) |
| 15 | + |
| 16 | +import { updateOrganizationCredentialBodySchema } from '@/lib/api/contracts/organization-credentials' |
| 17 | +import { useUpdateScopedCredential } from '@/hooks/queries/scoped-credentials' |
| 18 | + |
| 19 | +const WORKSPACE_ID = 'workspace-1' |
| 20 | +const ORGANIZATION_ID = 'organization-1' |
| 21 | +const CREDENTIAL_ID = 'slack-bot-1' |
| 22 | +const reconnectFields = { |
| 23 | + signingSecret: 'new-signing-secret', |
| 24 | + botToken: 'xoxb-new-bot-token', |
| 25 | + displayName: 'Support Bot', |
| 26 | + description: 'Reconnected Slack bot', |
| 27 | +} as const |
| 28 | +const credential = { |
| 29 | + id: CREDENTIAL_ID, |
| 30 | + workspaceId: WORKSPACE_ID, |
| 31 | + type: 'service_account', |
| 32 | + displayName: reconnectFields.displayName, |
| 33 | + description: reconnectFields.description, |
| 34 | + unredacted: false, |
| 35 | + providerId: 'slack-custom-bot', |
| 36 | + accountId: null, |
| 37 | + envKey: null, |
| 38 | + envOwnerUserId: null, |
| 39 | + createdBy: 'user-1', |
| 40 | + createdAt: '2026-01-01T00:00:00.000Z', |
| 41 | + updatedAt: '2026-01-02T00:00:00.000Z', |
| 42 | +} as const |
| 43 | + |
| 44 | +beforeEach(() => { |
| 45 | + vi.clearAllMocks() |
| 46 | +}) |
| 47 | + |
| 48 | +describe('scoped Slack bot reconnect requests', () => { |
| 49 | + it.each([WORKSPACE_ID, undefined])( |
| 50 | + 'sends workspace scope %s only in the query when reconnecting the existing credential', |
| 51 | + async (workspaceId) => { |
| 52 | + const fetch = setupGlobalFetchMock({ json: { credential } }) |
| 53 | + |
| 54 | + await expect( |
| 55 | + useUpdateScopedCredential().mutateAsync({ |
| 56 | + credentialId: CREDENTIAL_ID, |
| 57 | + workspaceId, |
| 58 | + ...reconnectFields, |
| 59 | + }) |
| 60 | + ).resolves.toEqual({ credential }) |
| 61 | + |
| 62 | + expect(fetch).toHaveBeenCalledExactlyOnceWith( |
| 63 | + `/api/credentials/${CREDENTIAL_ID}${workspaceId ? `?workspaceId=${workspaceId}` : ''}`, |
| 64 | + expect.objectContaining({ method: 'PUT' }) |
| 65 | + ) |
| 66 | + expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toEqual(reconnectFields) |
| 67 | + } |
| 68 | + ) |
| 69 | + |
| 70 | + it('includes organization scope in the body when reconnecting the existing credential', async () => { |
| 71 | + const organizationCredential = { |
| 72 | + ...credential, |
| 73 | + workspaceId: null, |
| 74 | + organizationId: ORGANIZATION_ID, |
| 75 | + } |
| 76 | + const fetch = setupGlobalFetchMock({ json: { credential: organizationCredential } }) |
| 77 | + |
| 78 | + await expect( |
| 79 | + useUpdateScopedCredential().mutateAsync({ |
| 80 | + credentialId: CREDENTIAL_ID, |
| 81 | + organizationId: ORGANIZATION_ID, |
| 82 | + ...reconnectFields, |
| 83 | + }) |
| 84 | + ).resolves.toEqual({ credential: organizationCredential }) |
| 85 | + |
| 86 | + expect(fetch).toHaveBeenCalledExactlyOnceWith( |
| 87 | + `/api/organization-credentials/${CREDENTIAL_ID}`, |
| 88 | + expect.objectContaining({ method: 'PATCH' }) |
| 89 | + ) |
| 90 | + expect(JSON.parse(String(fetch.mock.calls[0]?.[1]?.body))).toEqual({ |
| 91 | + ...reconnectFields, |
| 92 | + organizationId: ORGANIZATION_ID, |
| 93 | + }) |
| 94 | + }) |
| 95 | + |
| 96 | + it.each([ |
| 97 | + { organizationId: ORGANIZATION_ID }, |
| 98 | + { ...reconnectFields }, |
| 99 | + { organizationId: ORGANIZATION_ID, ...reconnectFields, workspaceId: WORKSPACE_ID }, |
| 100 | + { organizationId: ORGANIZATION_ID, ...reconnectFields, unexpected: true }, |
| 101 | + ])('rejects invalid organization updates: %j', (body) => { |
| 102 | + expect(updateOrganizationCredentialBodySchema.safeParse(body).success).toBe(false) |
| 103 | + }) |
| 104 | +}) |
0 commit comments