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
172 changes: 170 additions & 2 deletions frontend/src/v2/__tests__/V2ConnectorsPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,13 @@ const renderPage = () => render(
);

describe('V2ConnectorsPage', () => {
beforeEach(() => jest.clearAllMocks());
beforeEach(() => {
jest.clearAllMocks();
jest.spyOn(window, 'open').mockReturnValue(null);
window.history.replaceState({}, '', '/v2/connectors');
});

afterEach(() => jest.restoreAllMocks());

it('lists connectors with grouped code, copy command, and dot status', async () => {
mockGets();
Expand Down Expand Up @@ -168,6 +174,20 @@ describe('V2ConnectorsPage', () => {
));
});

it('installs Slack for the selected pod through the lifecycle verb', async () => {
mockGets([]);
axios.post.mockResolvedValue({ data: { integration: { _id: 'slack-new' } } });
renderPage();
await screen.findByText(/Link a channel/);
fireEvent.click(screen.getByRole('button', { name: /Slack/ }));
fireEvent.click(screen.getByText('New Slack connector'));
await waitFor(() => expect(axios.post).toHaveBeenCalledWith(
'/api/installables/slack/install',
{ podId: 'p1' },
expect.anything(),
));
});

it('keeps the add form open while an install claim is in progress', async () => {
mockGets([]);
axios.post.mockResolvedValue({ data: { status: 'installing' } });
Expand Down Expand Up @@ -245,6 +265,26 @@ describe('V2ConnectorsPage', () => {
));
});

it('disconnects an installable Slack connector through Slack\'s lifecycle verb', async () => {
mockGets([{
_id: 'i-slack',
installationId: 'install-slack-u1',
type: 'slack',
status: 'connected',
config: { chatTitle: 'Commonly DM', liveRelay: true },
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.delete.mockResolvedValue({ data: { status: 'uninstalled' } });
renderPage();
fireEvent.click(await screen.findByRole('button', { name: 'Manage' }));
fireEvent.click(screen.getByRole('button', { name: 'Disconnect' }));
fireEvent.click(screen.getByRole('button', { name: /Really disconnect/ }));
await waitFor(() => expect(axios.delete).toHaveBeenCalledWith(
'/api/installables/slack/install',
expect.anything(),
));
});

it('derives the installable lifecycle target from the connector row type', () => {
expect(installableLifecyclePath('slack')).toBe('/api/installables/slack/install');
});
Expand Down Expand Up @@ -274,7 +314,135 @@ describe('V2ConnectorsPage', () => {
mockGets([]);
renderPage();
await screen.findByText(/Link a channel/);
expect(screen.queryByRole('button', { name: /Slack/ })).toBeNull();
expect(screen.getByRole('button', { name: /Slack/ })).toBeInTheDocument();
expect(screen.getAllByText('SOON').length).toBeGreaterThanOrEqual(1);
});

it('shows owner confirmation controls for a pending Slack authorization', async () => {
mockGets([{
_id: 'i-slack-pending',
installationId: 'install-slack-u1',
type: 'slack',
status: 'pending',
config: {
pendingBind: { teamName: 'Commonly HQ', slackUserName: 'sam', expiresAt: new Date(Date.now() + 60_000).toISOString() },
},
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.post.mockResolvedValue({ data: { status: 'connected' } });
renderPage();
expect(await screen.findByText('Commonly HQ wants to connect as @sam.')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Confirm connection' }));
await waitFor(() => expect(axios.post).toHaveBeenCalledWith(
'/api/installables/slack/confirm',
{},
expect.anything(),
));
});

it('requests the server-built Slack authorization URL from the pending card', async () => {
mockGets([{
_id: 'i-slack-authorize',
installationId: 'install-slack-u1',
type: 'slack',
status: 'pending',
config: { connectCode: 'a'.repeat(32), connectCodeExpiresAt: new Date(Date.now() + 60_000).toISOString() },
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.post.mockRejectedValue({ response: { status: 503 } });
renderPage();
fireEvent.click(await screen.findByRole('button', { name: 'Authorize in Slack' }));
await waitFor(() => expect(axios.post).toHaveBeenCalledWith(
'/api/installables/slack/authorize-url',
{},
expect.objectContaining({ withCredentials: true }),
));
expect(await screen.findByText('Could not begin Slack authorization. Try again in a moment.')).toBeInTheDocument();
});

it('opens Slack in a separate tab so the pending confirmation stays on this page', async () => {
const slackWindow = {
location: { assign: jest.fn() },
close: jest.fn(),
opener: window,
};
jest.mocked(window.open).mockReturnValue(slackWindow as unknown as Window);
mockGets([{
_id: 'i-slack-authorize',
installationId: 'install-slack-u1',
type: 'slack',
status: 'pending',
config: { connectCode: 'a'.repeat(32), connectCodeExpiresAt: new Date(Date.now() + 60_000).toISOString() },
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.post.mockResolvedValue({ data: { authorizeUrl: 'https://slack.test/oauth?state=secret' } });
renderPage();
fireEvent.click(await screen.findByRole('button', { name: 'Authorize in Slack' }));

await waitFor(() => expect(slackWindow.location.assign).toHaveBeenCalledWith('https://slack.test/oauth?state=secret'));
expect(slackWindow.opener).toBeNull();
});

it('lets the owner reject a pending Slack authorization', async () => {
mockGets([{
_id: 'i-slack-pending',
installationId: 'install-slack-u1',
type: 'slack',
status: 'pending',
config: { pendingBind: { teamName: 'Commonly HQ', slackUserName: 'sam' } },
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.post.mockResolvedValue({ data: { status: 'rejected' } });
renderPage();
fireEvent.click(await screen.findByRole('button', { name: 'This is not me' }));
await waitFor(() => expect(axios.post).toHaveBeenCalledWith(
'/api/installables/slack/reject',
{},
expect.anything(),
));
});

it('shows a generic Slack callback failure and clears its opaque query code', async () => {
window.history.replaceState({}, '', '/v2/connectors?slack=error&code=invalid_state');
mockGets([]);

renderPage();

expect(await screen.findByRole('alert')).toHaveTextContent('Slack authorization didn’t complete — try again.');
expect(window.location.search).toBe('');
});

it('reloads connectors after a successful Slack callback and clears its query', async () => {
window.history.replaceState({}, '', '/v2/connectors?slack=pending');
mockGets([]);

renderPage();

await waitFor(() => expect(
axios.get.mock.calls.filter(([url]) => url === '/api/integrations/user/all').length,
).toBeGreaterThanOrEqual(2));
expect(window.location.search).toBe('');
});

it('keeps an error-gated Slack row manageable so the user can disconnect and reconnect', async () => {
mockGets([{
_id: 'i-slack-error',
installationId: 'install-slack-u1',
type: 'slack',
status: 'error',
config: { teamName: 'Commonly HQ', chatId: 'D1', chatType: 'im' },
podId: { _id: 'p1', name: 'Rewire Live Demo' },
}]);
axios.delete.mockResolvedValue({ data: { status: 'uninstalled' } });
renderPage();

expect(await screen.findByText('Connection error — reconnect')).toBeInTheDocument();
fireEvent.click(screen.getByRole('button', { name: 'Manage' }));
fireEvent.click(screen.getByRole('button', { name: 'Disconnect' }));
fireEvent.click(screen.getByRole('button', { name: /Really disconnect/ }));
await waitFor(() => expect(axios.delete).toHaveBeenCalledWith(
'/api/installables/slack/install',
expect.anything(),
));
});
});
Loading
Loading