SurfSense/surfsense_web/components/editConnector/EditSlackConnectorConfigForm.test.tsx
google-labs-jules[bot] 4bf09b8bde I've added comprehensive unit and integration tests for the recently implemented Slack connector features. This includes tests for backend logic (SlackHistory, indexing tasks, API routes) and frontend components and hooks.
Backend Tests (`surfsense_backend`):
- `test_slack_history.py`:
    - Tests for `SlackHistory` class methods including `get_all_channels`,
      `get_conversation_history`, timestamp conversions, and message
      formatting. Covers various API response scenarios, pagination,
      and error handling.
- `test_connectors_indexing_tasks.py`:
    - Unit tests for `index_slack_messages` task.
    - Scenarios include initial indexing, periodic indexing, on-demand
      re-indexing (targeted channels, force re-index, custom dates),
      channel filtering, and error handling.
    - Mocks `SlackHistory` and database interactions.
- `test_search_source_connectors_routes.py`:
    - Tests for `/slack/{connector_id}/discover-channels` endpoint.
    - Tests for `/slack/{connector_id}/reindex-channels` endpoint,
      verifying parameter passing to the background task.
    - Tests for the main `/search-source-connectors/{connector_id}/index`
      endpoint for Slack connectors, ensuring the `force_full_reindex`
      flag is handled.

Frontend Tests (`surfsense_web`):
- `components/editConnector/EditSlackConnectorConfigForm.test.tsx`:
    - Tests rendering of all Slack configuration fields.
    - Verifies `onConfigChange` callback functionality.
    - Tests conditional rendering of periodic indexing settings.
    - Checks disabled state of the form.
- `app/dashboard/[search_space_id]/connectors/[connector_id]/edit/page.test.tsx`:
    - Tests for the "Channel Management" tab specific to Slack connectors.
    - Verifies UI for discovering channels (mocking API calls).
    - Tests selection of channels and updating the connector configuration.
    - Tests UI for triggering on-demand re-indexing with various options.
- `hooks/useSearchSourceConnectors.test.ts`:
    - Unit tests for the `discoverSlackChannels` function, mocking `fetch`.
    - Unit tests for the `reindexSlackChannels` function, mocking `fetch`
      and verifying correct request body construction.
    - Includes testing of the `fetchWithAuth` helper function.
2025-05-28 10:48:28 +00:00

226 lines
11 KiB
TypeScript

import React from 'react';
import { render, screen, fireEvent, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; // For more realistic interactions with Select
import EditSlackConnectorConfigForm from './EditSlackConnectorConfigForm';
import { SearchSourceConnector } from '@/hooks/useSearchSourceConnectors'; // Adjust path as needed
// Mock UI components that are not part of the core logic being tested, if necessary
// For ShadCN components, often direct interaction is fine, but sometimes they need setup.
// For this test, we'll assume direct interaction works.
// jest.mock("@/components/ui/input", () => (props: any) => <input {...props} data-testid={props.id || 'input-mock'} />);
// jest.mock("@/components/ui/checkbox", () => (props: any) => <input type="checkbox" {...props} data-testid={props.id || 'checkbox-mock'} />);
// jest.mock("@/components/ui/select", () => ({
// Select: (props: any) => <div data-testid={props.id || 'select-mock'}>{props.children}</div>,
// SelectTrigger: (props: any) => <button {...props}>{props.children}</button>,
// SelectValue: (props: any) => <div {...props}>{props.placeholder}</div>,
// SelectContent: (props: any) => <div {...props}>{props.children}</div>,
// SelectItem: (props: any) => <option value={props.value} {...props}>{props.children}</option>,
// }));
const initialMockConfig = {
SLACK_BOT_TOKEN: 'xoxb-initial-token',
slack_membership_filter_type: 'all_member_channels',
slack_selected_channel_ids: ['C123', 'C456'],
slack_initial_indexing_days: 30,
slack_initial_max_messages_per_channel: 1000,
slack_periodic_indexing_enabled: false,
slack_periodic_indexing_frequency: 'daily',
slack_max_messages_per_channel_periodic: 100,
};
const mockConnector: SearchSourceConnector = {
id: 1,
name: 'Slack Test Connector',
connector_type: 'SLACK_CONNECTOR',
is_indexable: true,
last_indexed_at: null,
config: { ...initialMockConfig },
user_id: 'user1',
created_at: new Date().toISOString(),
};
describe('EditSlackConnectorConfigForm', () => {
let mockOnConfigChange: jest.Mock;
beforeEach(() => {
mockOnConfigChange = jest.fn();
});
test('renders all form fields correctly with initial values', () => {
render(
<EditSlackConnectorConfigForm
connector={mockConnector}
onConfigChange={mockOnConfigChange}
disabled={false}
/>
);
// Authentication
expect(screen.getByLabelText(/Slack Bot Token/i)).toHaveValue(initialMockConfig.SLACK_BOT_TOKEN);
// Initial Indexing Settings
expect(screen.getByText('Index All Channels Where Bot is Member')).toBeInTheDocument(); // For Select value display
expect(screen.getByText(/Channel selection is managed in the 'Channels' tab/i)).toBeInTheDocument();
expect(screen.getByLabelText(/Initial Indexing Period \(days\)/i)).toHaveValue(initialMockConfig.slack_initial_indexing_days);
expect(screen.getByLabelText(/Max Messages Per Channel \(Initial Sync\)/i)).toHaveValue(initialMockConfig.slack_initial_max_messages_per_channel);
// Periodic Indexing Settings
const periodicCheckbox = screen.getByLabelText(/Enable Periodic Indexing/i) as HTMLInputElement;
expect(periodicCheckbox.checked).toBe(initialMockConfig.slack_periodic_indexing_enabled);
// Periodic fields should initially be hidden if checkbox is false
if (!initialMockConfig.slack_periodic_indexing_enabled) {
expect(screen.queryByLabelText(/Periodic Indexing Frequency/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i)).not.toBeInTheDocument();
}
});
test('calls onConfigChange with updated config for SLACK_BOT_TOKEN', () => {
render(
<EditSlackConnectorConfigForm
connector={mockConnector}
onConfigChange={mockOnConfigChange}
disabled={false}
/>
);
const tokenInput = screen.getByLabelText(/Slack Bot Token/i);
fireEvent.change(tokenInput, { target: { value: 'new-token' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ SLACK_BOT_TOKEN: 'new-token' })
);
});
test('calls onConfigChange for slack_initial_indexing_days', () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
const input = screen.getByLabelText(/Initial Indexing Period \(days\)/i);
fireEvent.change(input, { target: { value: '10' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_indexing_days: 10 }));
fireEvent.change(input, { target: { value: '-1' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_indexing_days: -1 }));
fireEvent.change(input, { target: { value: '' } }); // Empty value
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_indexing_days: null }));
});
test('calls onConfigChange for slack_initial_max_messages_per_channel', () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
const input = screen.getByLabelText(/Max Messages Per Channel \(Initial Sync\)/i);
fireEvent.change(input, { target: { value: '500' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_max_messages_per_channel: 500 }));
});
test('calls onConfigChange for slack_membership_filter_type', async () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
// For ShadCN Select, we need to click the trigger, then the item.
// `userEvent` is better for this, but `fireEvent` can also work.
const selectTrigger = screen.getByRole('combobox', { name: /Channel Indexing Behavior/i });
await userEvent.click(selectTrigger);
// Assuming SelectItem roles are 'option' or similar, and they are now in the document
const option = await screen.findByText('Index Only Selected Channels'); // Wait for option to appear
await userEvent.click(option);
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ slack_membership_filter_type: 'selected_member_channels' })
);
});
test('shows/hides periodic indexing fields and calls onConfigChange', async () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
const periodicCheckbox = screen.getByLabelText(/Enable Periodic Indexing/i);
// Initially periodic fields are not visible (as per initialMockConfig)
expect(screen.queryByLabelText(/Periodic Indexing Frequency/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i)).not.toBeInTheDocument();
// Check the checkbox
fireEvent.click(periodicCheckbox);
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ slack_periodic_indexing_enabled: true })
);
// Now the fields should be visible
const frequencySelect = await screen.findByLabelText(/Periodic Indexing Frequency/i); // Wait for it
const maxMessagesInput = screen.getByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i);
expect(frequencySelect).toBeInTheDocument();
expect(maxMessagesInput).toBeInTheDocument();
// Change frequency
const freqSelectTrigger = screen.getByRole('combobox', { name: /Periodic Indexing Frequency/i });
await userEvent.click(freqSelectTrigger);
const weeklyOption = await screen.findByText('Weekly');
await userEvent.click(weeklyOption);
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ slack_periodic_indexing_frequency: 'weekly', slack_periodic_indexing_enabled: true })
);
// Change max messages periodic
fireEvent.change(maxMessagesInput, { target: { value: '75' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ slack_max_messages_per_channel_periodic: 75, slack_periodic_indexing_enabled: true })
);
// Uncheck the checkbox
fireEvent.click(periodicCheckbox);
expect(mockOnConfigChange).toHaveBeenCalledWith(
expect.objectContaining({ slack_periodic_indexing_enabled: false })
);
// Fields should be hidden again
expect(screen.queryByLabelText(/Periodic Indexing Frequency/i)).not.toBeInTheDocument();
expect(screen.queryByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i)).not.toBeInTheDocument();
});
test('preserves other config values when one changes', () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
const tokenInput = screen.getByLabelText(/Slack Bot Token/i);
fireEvent.change(tokenInput, { target: { value: 'new-xoxb-token' } });
expect(mockOnConfigChange).toHaveBeenCalledWith({
...initialMockConfig, // All original values
SLACK_BOT_TOKEN: 'new-xoxb-token', // Only this one changed
});
});
test('disables all form fields when disabled prop is true', () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={true} />);
expect(screen.getByLabelText(/Slack Bot Token/i)).toBeDisabled();
expect(screen.getByRole('combobox', { name: /Channel Indexing Behavior/i })).toBeDisabled();
expect(screen.getByLabelText(/Initial Indexing Period \(days\)/i)).toBeDisabled();
expect(screen.getByLabelText(/Max Messages Per Channel \(Initial Sync\)/i)).toBeDisabled();
expect(screen.getByLabelText(/Enable Periodic Indexing/i)).toBeDisabled();
// If periodic indexing was enabled by default in mock, check those too
const connectorWithPeriodicEnabled = {
...mockConnector,
config: { ...mockConnector.config, slack_periodic_indexing_enabled: true }
};
render(<EditSlackConnectorConfigForm connector={connectorWithPeriodicEnabled} onConfigChange={mockOnConfigChange} disabled={true} />);
expect(screen.getByRole('combobox', { name: /Periodic Indexing Frequency/i })).toBeDisabled();
expect(screen.getByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i)).toBeDisabled();
});
test('handles empty string for number inputs correctly by converting to null', () => {
render(<EditSlackConnectorConfigForm connector={mockConnector} onConfigChange={mockOnConfigChange} disabled={false} />);
const initialDaysInput = screen.getByLabelText(/Initial Indexing Period \(days\)/i);
fireEvent.change(initialDaysInput, { target: { value: '' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_indexing_days: null }));
const initialMaxMessagesInput = screen.getByLabelText(/Max Messages Per Channel \(Initial Sync\)/i);
fireEvent.change(initialMaxMessagesInput, { target: { value: '' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_initial_max_messages_per_channel: null }));
// Enable periodic to test that field
const periodicCheckbox = screen.getByLabelText(/Enable Periodic Indexing/i);
fireEvent.click(periodicCheckbox); // Enable
const periodicMaxMessagesInput = screen.getByLabelText(/Max Messages Per Channel \(Periodic Sync\)/i);
fireEvent.change(periodicMaxMessagesInput, { target: { value: '' } });
expect(mockOnConfigChange).toHaveBeenCalledWith(expect.objectContaining({ slack_max_messages_per_channel_periodic: null }));
});
});