624 lines
20 KiB
TypeScript
624 lines
20 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
|
import { BrowserRouter } from 'react-router-dom'
|
|
import DomainsPage from '../../src/pages/DomainsPage'
|
|
import { ThemeProvider } from '../../src/ThemeContext'
|
|
import { AuthProvider } from '../../src/auth/AuthContext'
|
|
|
|
const mockGet = vi.fn()
|
|
const mockPost = vi.fn()
|
|
const mockDelete = vi.fn()
|
|
|
|
vi.mock('../../src/api', () => ({
|
|
default: {
|
|
get: (...args: unknown[]) => mockGet(...args),
|
|
post: (...args: unknown[]) => mockPost(...args),
|
|
delete: (...args: unknown[]) => mockDelete(...args),
|
|
},
|
|
}))
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
vi.spyOn(console, 'debug').mockImplementation(() => {})
|
|
vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
|
|
Object.defineProperty(window, 'localStorage', {
|
|
value: {
|
|
getItem: vi.fn(() => null),
|
|
setItem: vi.fn(),
|
|
removeItem: vi.fn(),
|
|
clear: vi.fn(),
|
|
},
|
|
writable: true,
|
|
})
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks()
|
|
})
|
|
|
|
const setupMockGet = (overrides?: {
|
|
domains?: { data: unknown[]; total: number }
|
|
capabilities?: unknown
|
|
status?: unknown
|
|
settings?: unknown
|
|
}) => {
|
|
mockGet.mockImplementation((url: string) => {
|
|
if (url.startsWith('/domains?page=')) {
|
|
return Promise.resolve({ data: overrides?.domains || { data: [], total: 0 } })
|
|
}
|
|
if (url === '/domains/scan/capabilities') {
|
|
return Promise.resolve({ data: overrides?.capabilities || { scannerAvailable: false } })
|
|
}
|
|
if (url === '/domains/scan/status') {
|
|
return Promise.resolve({ data: overrides?.status || { running: false } })
|
|
}
|
|
if (url === '/settings') {
|
|
return Promise.resolve({ data: overrides?.settings || {} })
|
|
}
|
|
return Promise.resolve({ data: {} })
|
|
})
|
|
}
|
|
|
|
const renderDomainsPage = () => {
|
|
return render(
|
|
<BrowserRouter>
|
|
<ThemeProvider>
|
|
<AuthProvider>
|
|
<DomainsPage />
|
|
</AuthProvider>
|
|
</ThemeProvider>
|
|
</BrowserRouter>
|
|
)
|
|
}
|
|
|
|
describe('DomainsPage', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.clearAllMocks()
|
|
})
|
|
|
|
describe('Рендеринг', () => {
|
|
it('должен рендериться с заголовком', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Белый список доменов (SNI)')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен отображать поле для добавления домена', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByLabelText('Доменное имя')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен отображать кнопку "Добавить"', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Добавить')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен отображать кнопку "Из файла"', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Из файла')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен отображать таблицу доменов', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'example.com' }], total: 1 }
|
|
})
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('example.com')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен отображать сообщение при отсутствии доменов', async () => {
|
|
setupMockGet({ domains: { data: [], total: 0 } })
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Нет доменов')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Загрузка данных', () => {
|
|
it('должен загружать домены при монтировании', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=10')
|
|
})
|
|
})
|
|
|
|
it('должен загружать возможности сканера', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains/scan/capabilities')
|
|
})
|
|
})
|
|
|
|
it('должен загружать настройки', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/settings')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Пагинация', () => {
|
|
it('должен отображать пагинацию', async () => {
|
|
setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } })
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('combobox')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен переключать страницу при клике на следующую', async () => {
|
|
setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } })
|
|
renderDomainsPage()
|
|
|
|
const nextPageButton = await screen.findByLabelText('Go to next page')
|
|
fireEvent.click(nextPageButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains?page=2&limit=10')
|
|
})
|
|
})
|
|
|
|
it('должен менять количество строк на странице', async () => {
|
|
setupMockGet({ domains: { data: [{ id: 1, name: 'test.com' }], total: 25 } })
|
|
renderDomainsPage()
|
|
|
|
const rowsPerPageSelect = await screen.findByLabelText('Доменов на странице:')
|
|
fireEvent.mouseDown(rowsPerPageSelect)
|
|
|
|
const option = await screen.findByText('25')
|
|
fireEvent.click(option)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=25')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Добавление домена', () => {
|
|
it('должен позволять вводить домен', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
const input = await screen.findByLabelText('Доменное имя')
|
|
fireEvent.change(input, { target: { value: 'newdomain.com' } })
|
|
|
|
expect(input).toHaveValue('newdomain.com')
|
|
})
|
|
|
|
it('должен добавлять домен при клике на кнопку', async () => {
|
|
setupMockGet()
|
|
mockPost.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const input = await screen.findByLabelText('Доменное имя')
|
|
fireEvent.change(input, { target: { value: 'newdomain.com' } })
|
|
|
|
const addButton = screen.getByText('Добавить')
|
|
fireEvent.click(addButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockPost).toHaveBeenCalledWith('/domains', { name: 'newdomain.com' })
|
|
})
|
|
})
|
|
|
|
it('должен очищать поле после добавления', async () => {
|
|
setupMockGet()
|
|
mockPost.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const input = await screen.findByLabelText('Доменное имя')
|
|
fireEvent.change(input, { target: { value: 'newdomain.com' } })
|
|
|
|
const addButton = screen.getByText('Добавить')
|
|
fireEvent.click(addButton)
|
|
|
|
await waitFor(() => {
|
|
expect(input).toHaveValue('')
|
|
})
|
|
})
|
|
|
|
it('должен перезагружать список после добавления', async () => {
|
|
setupMockGet()
|
|
mockPost.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const input = await screen.findByLabelText('Доменное имя')
|
|
fireEvent.change(input, { target: { value: 'newdomain.com' } })
|
|
|
|
const addButton = screen.getByText('Добавить')
|
|
fireEvent.click(addButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=10')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Удаление домена', () => {
|
|
it('должен отображать кнопку удаления для домена', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
renderDomainsPage()
|
|
|
|
const deleteButton = await screen.findByTestId('icon-Delete')
|
|
expect(deleteButton).toBeInTheDocument()
|
|
})
|
|
|
|
it('должен удалять домен при клике на кнопку удаления', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteButton = await screen.findByTestId('icon-Delete')
|
|
fireEvent.click(deleteButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockDelete).toHaveBeenCalledWith('/domains/1')
|
|
})
|
|
})
|
|
|
|
it('должен перезагружать список после удаления', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteButton = await screen.findByTestId('icon-Delete')
|
|
fireEvent.click(deleteButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalledWith('/domains?page=1&limit=10')
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Удаление всех доменов', () => {
|
|
it('должен отображать кнопку "Удалить все"', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Удалить все')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен открывать диалог подтверждения удаления всех', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteAllButton = await screen.findByText('Удалить все')
|
|
fireEvent.click(deleteAllButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole('dialog')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен закрывать диалог при клике на "Отмена"', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteAllButton = await screen.findByText('Удалить все')
|
|
fireEvent.click(deleteAllButton)
|
|
|
|
const cancelButton = await screen.findByText('Отмена')
|
|
fireEvent.click(cancelButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.queryByRole('dialog')).not.toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Загрузка из файла', () => {
|
|
it('должен отображать кнопку "Из файла"', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Из файла')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен иметь скрытый input type="file"', async () => {
|
|
setupMockGet()
|
|
renderDomainsPage()
|
|
|
|
const fileInput = screen.getByTestId('file-input')
|
|
expect(fileInput).toBeInTheDocument()
|
|
})
|
|
|
|
it('должен загружать файл при выборе', async () => {
|
|
setupMockGet()
|
|
mockPost.mockResolvedValue({ data: { count: 5 } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const fileInput = screen.getByTestId('file-input') as HTMLInputElement
|
|
const file = new File(['example.com\ntest.org'], 'domains.txt', { type: 'text/plain' })
|
|
|
|
fireEvent.change(fileInput, { target: { files: [file] } })
|
|
|
|
await waitFor(() => {
|
|
expect(mockPost).toHaveBeenCalledWith('/domains/upload', { domains: expect.any(Array) })
|
|
})
|
|
})
|
|
|
|
it('должен показывать успех после загрузки файла', async () => {
|
|
setupMockGet()
|
|
mockPost.mockResolvedValue({ data: { count: 5 } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const fileInput = screen.getByTestId('file-input') as HTMLInputElement
|
|
const file = new File(['example.com\ntest.org'], 'domains.txt', { type: 'text/plain' })
|
|
|
|
fireEvent.change(fileInput, { target: { files: [file] } })
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Успешно добавлено доменов: 5')).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('Сканер доменов', () => {
|
|
it('должен отображать кнопку "Сканировать" в аккордеоне', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true }
|
|
})
|
|
renderDomainsPage()
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Сканировать')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен запускать сканирование при валидных данных', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true },
|
|
settings: { xui_ip: '1.2.3.4' }
|
|
})
|
|
mockPost.mockResolvedValue({ data: { runId: '123', foundCount: 5, domains: ['a.com', 'b.com'] } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const scanButton = await screen.findByText('Сканировать')
|
|
fireEvent.click(scanButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockPost).toHaveBeenCalledWith('/domains/scan/start', expect.objectContaining({
|
|
addr: expect.any(String),
|
|
}))
|
|
})
|
|
})
|
|
|
|
it('должен очищать результаты сканирования', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true },
|
|
settings: { xui_ip: '1.2.3.4' }
|
|
})
|
|
mockPost.mockResolvedValue({ data: { runId: '123', foundCount: 2, domains: ['a.com', 'b.com'] } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const scanButton = await screen.findByText('Сканировать')
|
|
fireEvent.click(scanButton)
|
|
|
|
const clearButton = await screen.findByText('Очистить предварительный')
|
|
fireEvent.click(clearButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockGet).toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('должен изменять настройки сканирования', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true },
|
|
settings: { xui_ip: '1.2.3.4' }
|
|
})
|
|
|
|
renderDomainsPage()
|
|
|
|
const secondsInput = await screen.findByLabelText('Секунд скана')
|
|
fireEvent.change(secondsInput, { target: { value: '120' } })
|
|
expect(secondsInput).toHaveValue(120)
|
|
|
|
const threadInput = screen.getByLabelText('Потоков')
|
|
fireEvent.change(threadInput, { target: { value: '8' } })
|
|
expect(threadInput).toHaveValue(8)
|
|
|
|
const timeoutInput = screen.getByLabelText('Таймаут, сек')
|
|
fireEvent.change(timeoutInput, { target: { value: '10' } })
|
|
expect(timeoutInput).toHaveValue(10)
|
|
})
|
|
|
|
it('должен разворачивать/сворачивать аккордеон сканера', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true }
|
|
})
|
|
|
|
renderDomainsPage()
|
|
|
|
const accordionSummary = await screen.findByText('Автопоиск SNI (backend scanner)')
|
|
|
|
fireEvent.click(accordionSummary)
|
|
|
|
await waitFor(() => {
|
|
const secondsInput = screen.getByLabelText('Секунд скана')
|
|
expect(secondsInput).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен показывать сообщение при пустом списке кандидатов после сканирования', async () => {
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true },
|
|
settings: { xui_ip: '1.2.3.4' }
|
|
})
|
|
mockPost.mockResolvedValue({ data: { runId: '123', foundCount: 0, domains: [] } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const scanButton = await screen.findByText('Сканировать')
|
|
fireEvent.click(scanButton)
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Домены не найдены')).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен сохранять состояние сканера в localStorage', async () => {
|
|
const mockSetItem = vi.fn()
|
|
const mockLocalStorage = {
|
|
getItem: vi.fn().mockReturnValue(null),
|
|
setItem: mockSetItem,
|
|
removeItem: vi.fn(),
|
|
clear: vi.fn(),
|
|
}
|
|
Object.defineProperty(window, 'localStorage', { value: mockLocalStorage, writable: true })
|
|
|
|
setupMockGet({
|
|
capabilities: { scannerAvailable: true },
|
|
settings: { xui_ip: '1.2.3.4' }
|
|
})
|
|
|
|
renderDomainsPage()
|
|
|
|
const addrInput = await screen.findByLabelText('IP/домен VPS')
|
|
fireEvent.change(addrInput, { target: { value: 'new-addr.com' } })
|
|
|
|
await waitFor(() => {
|
|
expect(mockSetItem).toHaveBeenCalledWith(
|
|
'domains_scan_state_v1',
|
|
expect.any(String)
|
|
)
|
|
})
|
|
})
|
|
|
|
it('должен показывать кнопку удаления для домена в основном списке', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteButtons = await screen.findAllByTestId('icon-Delete')
|
|
expect(deleteButtons.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('должен подтверждать удаление всех доменов', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteAllButton = await screen.findByText('Удалить все')
|
|
fireEvent.click(deleteAllButton)
|
|
|
|
const confirmButton = await screen.findByText('Удалить')
|
|
fireEvent.click(confirmButton)
|
|
|
|
await waitFor(() => {
|
|
expect(mockDelete).toHaveBeenCalledWith('/domains/all')
|
|
})
|
|
})
|
|
|
|
it('должен показывать успех после удаления всех доменов', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockResolvedValue({})
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteAllButton = await screen.findByText('Удалить все')
|
|
fireEvent.click(deleteAllButton)
|
|
|
|
const confirmButton = await screen.findByText('Удалить')
|
|
fireEvent.click(confirmButton)
|
|
|
|
await waitFor(() => {
|
|
const alerts = screen.getAllByRole('alert')
|
|
const successAlert = alerts.find(alert => alert.textContent?.includes('Все домены удалены'))
|
|
expect(successAlert).toBeInTheDocument()
|
|
})
|
|
})
|
|
|
|
it('должен показывать ошибку при неудачном удалении всех доменов', async () => {
|
|
setupMockGet({
|
|
domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
|
|
})
|
|
mockDelete.mockRejectedValue({ response: { status: 500 } })
|
|
|
|
renderDomainsPage()
|
|
|
|
const deleteAllButton = await screen.findByText('Удалить все')
|
|
fireEvent.click(deleteAllButton)
|
|
|
|
const confirmButton = await screen.findByText('Удалить')
|
|
fireEvent.click(confirmButton)
|
|
|
|
await waitFor(() => {
|
|
const alerts = screen.getAllByRole('alert')
|
|
const errorAlert = alerts.find(alert => alert.textContent?.includes('Ошибка удаления'))
|
|
expect(errorAlert).toBeInTheDocument()
|
|
})
|
|
})
|
|
})
|
|
})
|