SurfSense/surfsense_web/hooks/useSearchSourceConnectors.test.ts
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

247 lines
9 KiB
TypeScript

import { renderHook, act, waitFor } from '@testing-library/react';
import { useSearchSourceConnectors, SlackChannelInfo } from './useSearchSourceConnectors'; // Adjust path as needed
// Helper to create a mock response for fetch
const mockFetchResponse = (data: any, ok: boolean = true, status: number = 200) => {
return Promise.resolve({
ok,
status,
json: () => Promise.resolve(data),
text: () => Promise.resolve(JSON.stringify(data)), // For error messages
} as Response);
};
const mockFetchResponseNoContent = (ok: boolean = true, status: number = 202) => {
return Promise.resolve({
ok,
status,
json: () => Promise.reject(new Error("No JSON content")), // Should not be called for 202/204
text: () => Promise.resolve(""),
} as Response);
};
describe('useSearchSourceConnectors Hook - Slack Operations', () => {
let mockFetch: jest.SpyInstance;
let mockLocalStorageGetItem: jest.SpyInstance;
const mockApiUrl = 'http://localhost:8000/api/v1'; // Example, ensure it matches env
process.env.NEXT_PUBLIC_FASTAPI_BACKEND_URL = mockApiUrl;
beforeEach(() => {
// Spy on global fetch
mockFetch = jest.spyOn(window, 'fetch');
// Mock localStorage
mockLocalStorageGetItem = jest.spyOn(Storage.prototype, 'getItem');
mockLocalStorageGetItem.mockReturnValue('test-token'); // Default to having a token
});
afterEach(() => {
mockFetch.mockRestore();
mockLocalStorageGetItem.mockRestore();
});
describe('discoverSlackChannels', () => {
test('successful API call returns channels and calls fetch correctly', async () => {
const mockChannelsData: { channels: SlackChannelInfo[] } = {
channels: [
{ id: 'C1', name: 'General', is_private: false, is_member: true },
{ id: 'C2', name: 'Random', is_private: true, is_member: true },
],
};
mockFetch.mockReturnValueOnce(mockFetchResponse(mockChannelsData));
const { result } = renderHook(() => useSearchSourceConnectors());
// Wait for initial connectors fetch to complete if any (though not directly tested here)
await waitFor(() => expect(result.current.isLoading).toBe(false));
let discoveredChannels: SlackChannelInfo[] = [];
await act(async () => {
discoveredChannels = await result.current.discoverSlackChannels(1);
});
expect(discoveredChannels).toEqual(mockChannelsData.channels);
expect(mockFetch).toHaveBeenCalledWith(
`${mockApiUrl}/slack/1/discover-channels`,
expect.objectContaining({
method: 'GET',
headers: expect.objectContaining({
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json',
}),
})
);
});
test('API error throws an error', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponse({ detail: 'API Error' }, false, 500));
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await expect(result.current.discoverSlackChannels(1)).rejects.toThrow(
/API request failed: Internal Server Error - {"detail":"API Error"}/
);
});
test('no token throws an error', async () => {
mockLocalStorageGetItem.mockReturnValueOnce(null); // No token
const { result } = renderHook(() => useSearchSourceConnectors());
// Initial fetch will fail here, which is fine for this test's focus
await waitFor(() => expect(result.current.isLoading).toBe(false));
await expect(result.current.discoverSlackChannels(1)).rejects.toThrow(
'No authentication token found'
);
expect(mockFetch).not.toHaveBeenCalled(); // fetchWithAuth should prevent call
});
});
describe('reindexSlackChannels', () => {
test('successful API call (basic) calls fetch correctly', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponseNoContent(true, 202)); // 202 Accepted
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.reindexSlackChannels(1, ['C1', 'C2']);
});
expect(mockFetch).toHaveBeenCalledWith(
`${mockApiUrl}/slack/1/reindex-channels`,
expect.objectContaining({
method: 'POST',
headers: expect.objectContaining({
'Authorization': 'Bearer test-token',
'Content-Type': 'application/json',
}),
body: JSON.stringify({
channel_ids: ['C1', 'C2'],
force_reindex_all_messages: undefined, // Explicitly undefined if not passed
reindex_start_date: null, // Defaulted to null if not passed
reindex_latest_date: null, // Defaulted to null if not passed
}),
})
);
});
test('successful API call with all optional parameters', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponseNoContent(true, 202));
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.reindexSlackChannels(
1,
['C1'],
true,
'2023-01-01',
'2023-01-31'
);
});
expect(mockFetch).toHaveBeenCalledWith(
`${mockApiUrl}/slack/1/reindex-channels`,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
channel_ids: ['C1'],
force_reindex_all_messages: true,
reindex_start_date: '2023-01-01',
reindex_latest_date: '2023-01-31',
}),
})
);
});
test('handles empty date strings by passing null', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponseNoContent(true, 202));
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.reindexSlackChannels(
1,
['C1'],
true,
'', // Empty start date
'' // Empty latest date
);
});
expect(mockFetch).toHaveBeenCalledWith(
`${mockApiUrl}/slack/1/reindex-channels`,
expect.objectContaining({
method: 'POST',
body: JSON.stringify({
channel_ids: ['C1'],
force_reindex_all_messages: true,
reindex_start_date: null, // Should be null
reindex_latest_date: null, // Should be null
}),
})
);
});
test('API error throws an error during reindex', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponse({ detail: 'Reindex Failed' }, false, 400));
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await expect(result.current.reindexSlackChannels(1, ['C1'])).rejects.toThrow(
/API request failed: Bad Request - {"detail":"Reindex Failed"}/
);
});
test('no token throws an error during reindex', async () => {
mockLocalStorageGetItem.mockReturnValueOnce(null); // No token
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await expect(result.current.reindexSlackChannels(1, ['C1'])).rejects.toThrow(
'No authentication token found'
);
expect(mockFetch).not.toHaveBeenCalled();
});
});
// Basic test for fetchWithAuth behavior (implicitly tested by others, but can be explicit)
describe('fetchWithAuth internal behavior', () => {
test('fetchWithAuth throws if no token', async () => {
mockLocalStorageGetItem.mockReturnValueOnce(null);
const { result } = renderHook(() => useSearchSourceConnectors());
// The hook itself might make initial calls, let them pass/fail
await waitFor(() => expect(result.current.isLoading).toBe(false));
// Attempting any operation that uses fetchWithAuth internally should fail early
// For example, discoverSlackChannels
await expect(result.current.discoverSlackChannels(1)).rejects.toThrow('No authentication token found');
expect(mockFetch).not.toHaveBeenCalled();
});
test('fetchWithAuth includes Authorization header', async () => {
mockFetch.mockReturnValueOnce(mockFetchResponse({ channels: [] })); // For discoverSlackChannels
const { result } = renderHook(() => useSearchSourceConnectors());
await waitFor(() => expect(result.current.isLoading).toBe(false));
await act(async () => {
await result.current.discoverSlackChannels(1);
});
expect(mockFetch).toHaveBeenCalledWith(
expect.any(String),
expect.objectContaining({
headers: expect.objectContaining({ Authorization: 'Bearer test-token' }),
})
);
});
});
});