|
| 1 | +/** @vitest-environment jsdom */ |
| 2 | +import { act } from 'react' |
| 3 | +import { createRoot, type Root } from 'react-dom/client' |
| 4 | +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' |
| 5 | + |
| 6 | +const mocks = vi.hoisted(() => ({ |
| 7 | + inventory: vi.fn(), |
| 8 | + policies: vi.fn(), |
| 9 | + connect: vi.fn(), |
| 10 | + reconnect: vi.fn(), |
| 11 | + refetch: vi.fn(), |
| 12 | +})) |
| 13 | +vi.mock('@/hooks/queries/organization-accounts', () => ({ |
| 14 | + useOrganizationAccounts: mocks.inventory, |
| 15 | + useConnectOrganizationAccount: () => ({ mutate: mocks.connect }), |
| 16 | + useReconnectPersonalOrganizationAccount: () => ({ mutate: mocks.reconnect }), |
| 17 | +})) |
| 18 | +vi.mock('@/hooks/queries/search-integrations', () => ({ useSearchIntegrations: mocks.policies })) |
| 19 | +vi.mock('@/app/o/[organizationId]/integrations/disconnect-account-menu', () => ({ |
| 20 | + DisconnectAccountMenu: ({ accounts }: { accounts: { displayName: string }[] }) => ( |
| 21 | + <span>{accounts.map((account) => account.displayName).join(', ')}</span> |
| 22 | + ), |
| 23 | +})) |
| 24 | +vi.mock('@/app/workspace/[workspaceId]/integrations/components/integrations-showcase', () => ({ |
| 25 | + IntegrationTile: () => null, |
| 26 | +})) |
| 27 | + |
| 28 | +import { LiveMemberIntegrations } from '@/app/o/[organizationId]/integrations/live-member-integrations' |
| 29 | + |
| 30 | +let root: Root |
| 31 | +let container: HTMLDivElement |
| 32 | +const group = { |
| 33 | + status: 'active', |
| 34 | + options: [ |
| 35 | + { |
| 36 | + id: 'github-option', |
| 37 | + provider: 'github-repositories', |
| 38 | + status: 'active', |
| 39 | + configurationStatus: 'ready', |
| 40 | + }, |
| 41 | + ], |
| 42 | + mcpServers: [], |
| 43 | +} |
| 44 | +const inventory = (overrides = {}) => ({ |
| 45 | + credentialGroup: group, |
| 46 | + viewerAccounts: [], |
| 47 | + viewerMcpAccounts: [], |
| 48 | + canManage: false, |
| 49 | + ...overrides, |
| 50 | +}) |
| 51 | +beforeEach(() => { |
| 52 | + vi.clearAllMocks() |
| 53 | + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) |
| 54 | + mocks.inventory.mockReturnValue({ data: inventory(), refetch: mocks.refetch }) |
| 55 | + mocks.policies.mockReturnValue({ |
| 56 | + data: [{ connectorType: 'github', approved: true }], |
| 57 | + refetch: mocks.refetch, |
| 58 | + }) |
| 59 | + container = document.createElement('div') |
| 60 | + document.body.appendChild(container) |
| 61 | + root = createRoot(container) |
| 62 | +}) |
| 63 | +afterEach(() => { |
| 64 | + act(() => root.unmount()) |
| 65 | + container.remove() |
| 66 | + vi.unstubAllGlobals() |
| 67 | +}) |
| 68 | +const render = async (search = '') => { |
| 69 | + await act(async () => |
| 70 | + root.render(<LiveMemberIntegrations organizationId='org' search={search} />) |
| 71 | + ) |
| 72 | +} |
| 73 | +const button = (label: string) => |
| 74 | + [...container.querySelectorAll('button')].find((button) => button.textContent === label) |
| 75 | +describe('live member connection states', () => { |
| 76 | + it('keeps GitLab organization-managed with no personal token or connect action', async () => { |
| 77 | + mocks.policies.mockReturnValue({ data: [{ connectorType: 'gitlab', approved: true }] }) |
| 78 | + await render() |
| 79 | + expect(container.textContent).toContain('Organization managed') |
| 80 | + expect(container.textContent).not.toContain('personal access token') |
| 81 | + expect(button('Connect')).toBeUndefined() |
| 82 | + }) |
| 83 | + it('connects the active provider option in this organization', async () => { |
| 84 | + await render() |
| 85 | + await act(async () => button('Connect')!.click()) |
| 86 | + expect(mocks.connect).toHaveBeenCalledWith( |
| 87 | + { organizationId: 'org', optionId: 'github-option' }, |
| 88 | + expect.any(Object) |
| 89 | + ) |
| 90 | + }) |
| 91 | + it.each([ |
| 92 | + ['disabled', { credentialGroup: { ...group, status: 'disabled' } }], |
| 93 | + ['unconfigured', { credentialGroup: null }], |
| 94 | + ])('does not offer a connection when %s', async (_name, overrides) => { |
| 95 | + mocks.inventory.mockReturnValue({ data: inventory(overrides) }) |
| 96 | + await render() |
| 97 | + expect(button('Connect')).toBeUndefined() |
| 98 | + }) |
| 99 | + it('shows the current account and reconnect action without enabling a disabled integration', async () => { |
| 100 | + mocks.inventory.mockReturnValue({ |
| 101 | + data: inventory({ |
| 102 | + viewerAccounts: [ |
| 103 | + { |
| 104 | + credentialId: 'own', |
| 105 | + providerId: 'github-repositories', |
| 106 | + displayName: 'reader', |
| 107 | + status: 'needs_reauth', |
| 108 | + }, |
| 109 | + ], |
| 110 | + }), |
| 111 | + }) |
| 112 | + await render() |
| 113 | + await act(async () => button('Reconnect')!.click()) |
| 114 | + expect(mocks.reconnect).toHaveBeenCalledWith('own', expect.any(Object)) |
| 115 | + mocks.policies.mockReturnValue({ data: [{ connectorType: 'github', approved: false }] }) |
| 116 | + await render() |
| 117 | + expect(button('Reconnect')?.disabled).toBe(true) |
| 118 | + expect(button('Add account')).toBeUndefined() |
| 119 | + }) |
| 120 | + it('never presents another MCP provider as a Coda connection', async () => { |
| 121 | + mocks.policies.mockReturnValue({ data: [{ connectorType: 'coda', approved: true }] }) |
| 122 | + mocks.inventory.mockReturnValue({ |
| 123 | + data: inventory({ |
| 124 | + credentialGroup: { |
| 125 | + ...group, |
| 126 | + mcpServers: [{ id: 'coda-server', managedConnectorId: 'coda', enabled: true }], |
| 127 | + }, |
| 128 | + viewerMcpAccounts: [ |
| 129 | + { |
| 130 | + credentialId: 'other', |
| 131 | + mcpServerId: 'other-server', |
| 132 | + displayName: 'Other private account', |
| 133 | + status: 'active', |
| 134 | + }, |
| 135 | + ], |
| 136 | + }), |
| 137 | + }) |
| 138 | + await render() |
| 139 | + expect(container.textContent).not.toContain('Other private account') |
| 140 | + await act(async () => button('Connect')!.click()) |
| 141 | + expect(mocks.connect).toHaveBeenCalledWith( |
| 142 | + { organizationId: 'org', mcpServerId: 'coda-server' }, |
| 143 | + expect.any(Object) |
| 144 | + ) |
| 145 | + }) |
| 146 | + it('distinguishes filtered empty, loading, and failed states', async () => { |
| 147 | + await render('nothing') |
| 148 | + expect(container.textContent).toContain('No integrations match') |
| 149 | + mocks.inventory.mockReturnValue({}) |
| 150 | + await render() |
| 151 | + expect(container.textContent).toContain('Loading your connections') |
| 152 | + mocks.inventory.mockReturnValue({ |
| 153 | + error: new Error('Network unavailable'), |
| 154 | + refetch: mocks.refetch, |
| 155 | + }) |
| 156 | + await render() |
| 157 | + expect(container.textContent).toContain('Network unavailable') |
| 158 | + }) |
| 159 | +}) |
0 commit comments