diff --git a/client/src/pages/DomainsPage.tsx b/client/src/pages/DomainsPage.tsx
index 744e11f..36c8ed6 100644
--- a/client/src/pages/DomainsPage.tsx
+++ b/client/src/pages/DomainsPage.tsx
@@ -758,6 +758,7 @@ export default function DomainsPage() {
({
- Delete: () => null,
- Add: () => null,
- UploadFile: () => null,
- Remove: () => null,
- ExpandMore: () => null,
- Download: () => null,
-}))
-
const mockGet = vi.fn()
const mockPost = vi.fn()
const mockDelete = vi.fn()
@@ -27,18 +17,13 @@ vi.mock('../../src/api', () => ({
},
}))
-const mockClipboardWriteText = vi.fn()
-Object.assign(navigator, { clipboard: { writeText: mockClipboardWriteText } })
-
beforeEach(() => {
vi.clearAllMocks()
- mockClipboardWriteText.mockClear()
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'debug').mockImplementation(() => {})
vi.spyOn(console, 'warn').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
-
- // Mock localStorage
+
Object.defineProperty(window, 'localStorage', {
value: {
getItem: vi.fn(() => null),
@@ -51,7 +36,6 @@ beforeEach(() => {
})
afterEach(() => {
- vi.clearAllMocks()
vi.restoreAllMocks()
})
@@ -66,7 +50,7 @@ const setupMockGet = (overrides?: {
return Promise.resolve({ data: overrides?.domains || { data: [], total: 0 } })
}
if (url === '/domains/scan/capabilities') {
- return Promise.resolve({ data: overrides?.capabilities || { scannerAvailable: false, timeoutAvailable: false } })
+ return Promise.resolve({ data: overrides?.capabilities || { scannerAvailable: false } })
}
if (url === '/domains/scan/status') {
return Promise.resolve({ data: overrides?.status || { running: false } })
@@ -176,15 +160,12 @@ describe('DomainsPage', () => {
})
})
- it('должен отображать домены из API', async () => {
- setupMockGet({
- domains: { data: [{ id: 1, name: 'test.com' }, { id: 2, name: 'example.org' }], total: 2 }
- })
+ it('должен загружать настройки', async () => {
+ setupMockGet()
renderDomainsPage()
await waitFor(() => {
- expect(screen.getByText('test.com')).toBeInTheDocument()
- expect(screen.getByText('example.org')).toBeInTheDocument()
+ expect(mockGet).toHaveBeenCalledWith('/settings')
})
})
})
@@ -195,7 +176,7 @@ describe('DomainsPage', () => {
renderDomainsPage()
await waitFor(() => {
- expect(screen.getByText('0–0 из 0')).toBeInTheDocument()
+ expect(screen.getByRole('combobox')).toBeInTheDocument()
})
})
@@ -291,12 +272,94 @@ describe('DomainsPage', () => {
})
describe('Удаление домена', () => {
- it('должен иметь функцию удаления в API', async () => {
- setupMockGet()
+ it('должен отображать кнопку удаления для домена', async () => {
+ setupMockGet({
+ domains: { data: [{ id: 1, name: 'test.com' }], total: 1 }
+ })
renderDomainsPage()
- // Просто проверяем что компонент рендерится
- expect(screen.getByText('Белый список доменов (SNI)')).toBeInTheDocument()
+ 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()
+ })
})
})
@@ -314,8 +377,72 @@ describe('DomainsPage', () => {
setupMockGet()
renderDomainsPage()
- const fileInput = await screen.findByRole('button', { name: /Из файла/i })
+ 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),
+ }))
+ })
+ })
})
})
diff --git a/client/test/pages/TunnelsPage.spec.tsx b/client/test/pages/TunnelsPage.spec.tsx
index ad49e08..040dde5 100644
--- a/client/test/pages/TunnelsPage.spec.tsx
+++ b/client/test/pages/TunnelsPage.spec.tsx
@@ -1,209 +1,764 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
-import { MemoryRouter } from 'react-router-dom'
+import { BrowserRouter } from 'react-router-dom'
import TunnelsPage from '../../src/pages/TunnelsPage'
-import api from '../../src/api'
+import { ThemeProvider } from '../../src/ThemeContext'
+import { AuthProvider } from '../../src/auth/AuthContext'
-vi.mock('../../src/api')
+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(() => {})
+})
+
+afterEach(() => {
+ vi.restoreAllMocks()
+})
+
+const setupMockGet = (overrides?: {
+ tunnels?: unknown[]
+}) => {
+ mockGet.mockImplementation((url: string) => {
+ if (url === '/tunnels') return Promise.resolve({ data: overrides?.tunnels || [] })
+ return Promise.resolve({ data: {} })
+ })
+}
+
+const renderTunnelsPage = () => {
+ return render(
+
+
+
+
+
+
+
+ )
+}
describe('TunnelsPage', () => {
beforeEach(() => {
vi.clearAllMocks()
- vi.mocked(api.get).mockResolvedValue({ data: [] })
- vi.mocked(api.post).mockResolvedValue({ data: {} })
- vi.mocked(api.delete).mockResolvedValue({ data: {} })
})
afterEach(() => {
vi.clearAllMocks()
})
- const renderPage = async () => {
- const result = render(
-
-
-
- )
- // Ждём завершения первоначальной загрузки данных
- await waitFor(() => {
- expect(screen.getByText('Relay серверы')).toBeInTheDocument()
- })
- return result
- }
+ describe('Рендеринг', () => {
+ it('должен рендериться с заголовком', async () => {
+ setupMockGet()
+ renderTunnelsPage()
- it('должен рендериться с заголовком', async () => {
- await renderPage()
- expect(screen.getByText('Relay серверы')).toBeInTheDocument()
- })
-
- it('должен отображать кнопку "Добавить"', async () => {
- await renderPage()
- expect(screen.getByText('Добавить')).toBeInTheDocument()
- })
-
- it('должен открывать диалог создания при клике на "Добавить"', async () => {
- await renderPage()
- const addButton = screen.getByText('Добавить')
- fireEvent.click(addButton)
-
- expect(screen.getByText('Новый редирект сервер')).toBeInTheDocument()
- })
-
- it('должен закрывать диалог создания при клике на отмену', async () => {
- await renderPage()
- const addButton = screen.getByText('Добавить')
- fireEvent.click(addButton)
-
- await waitFor(() => {
- expect(screen.getByText('Новый редирект сервер')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Relay серверы')).toBeInTheDocument()
+ })
})
- const cancelButton = screen.getByText('Отмена')
- fireEvent.click(cancelButton)
+ it('должен отображать кнопку "Добавить"', async () => {
+ setupMockGet()
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.queryByText('Новый редирект сервер')).not.toBeInTheDocument()
- })
- })
-
- it('должен отображать поля формы в диалоге создания', async () => {
- await renderPage()
- const addButton = screen.getByText('Добавить')
- fireEvent.click(addButton)
-
- await waitFor(() => {
- expect(screen.getByLabelText('Название')).toBeInTheDocument()
- })
- expect(screen.getByLabelText('IP адрес')).toBeInTheDocument()
- expect(screen.getByLabelText('SSH Порт')).toBeInTheDocument()
- expect(screen.getByLabelText('SSH User')).toBeInTheDocument()
- })
-
- it('должен переключать режим аутентификации между паролем и ключом', async () => {
- await renderPage()
- const addButton = screen.getByText('Добавить')
- fireEvent.click(addButton)
-
- await waitFor(() => {
- expect(screen.getByLabelText('SSH Пароль')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Добавить')).toBeInTheDocument()
+ })
})
- const keyRadio = screen.getByLabelText('По SSH ключу')
- fireEvent.click(keyRadio)
+ it('должен отображать сообщение при отсутствии серверов', async () => {
+ setupMockGet({ tunnels: [] })
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByLabelText(/SSH Private Key/i)).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Нет серверов')).toBeInTheDocument()
+ })
})
- })
- it('должен показывать сообщение "Нет серверов" при пустом списке', async () => {
- await renderPage()
- expect(screen.getByText('Нет серверов')).toBeInTheDocument()
- })
-
- it('должен отображать список туннелей', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Test Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ it('должен отображать список серверов', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ await waitFor(() => {
+ expect(screen.getByText('Test Server')).toBeInTheDocument()
+ expect(screen.getByText('192.168.1.1')).toBeInTheDocument()
+ })
})
- await renderPage()
-
- await waitFor(() => {
- expect(screen.getByText('Test Tunnel')).toBeInTheDocument()
- })
- expect(screen.getByText('192.168.1.1')).toBeInTheDocument()
- })
-
- it('должен отображать статус "Активен" для установленного туннеля', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Active Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ it('должен отображать статус "Активен" для установленного сервера', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Active Server', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ await waitFor(() => {
+ expect(screen.getByText('Активен')).toBeInTheDocument()
+ })
})
- await renderPage()
+ it('должен отображать статус "Не настроен" для неустановленного сервера', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Inactive Server', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Активен')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Не настроен')).toBeInTheDocument()
+ })
})
})
- it('должен отображать статус "Не настроен" для неустановленного туннеля', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Inactive Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: false }
- ]
+ describe('Загрузка данных', () => {
+ it('должен загружать туннели при монтировании', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ await waitFor(() => {
+ expect(mockGet).toHaveBeenCalledWith('/tunnels')
+ })
})
- await renderPage()
+ it('должен отображать несколько серверов', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Server 1', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: true },
+ { id: 2, name: 'Server 2', ip: '2.2.2.2', sshPort: 2222, username: 'admin', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Не настроен')).toBeInTheDocument()
+ await waitFor(() => {
+ expect(screen.getByText('Server 1')).toBeInTheDocument()
+ expect(screen.getByText('Server 2')).toBeInTheDocument()
+ })
})
})
- it('должен отображать кнопку "Установить" для неустановленного туннеля', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: false }
- ]
+ describe('Диалог создания сервера', () => {
+ it('должен открывать диалог при клике на "Добавить"', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Новый редирект сервер')).toBeInTheDocument()
+ })
})
- await renderPage()
+ it('должен закрывать диалог при клике на "Отмена"', async () => {
+ setupMockGet()
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Установить')).toBeInTheDocument()
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const cancelButton = await screen.findByText('Отмена')
+ fireEvent.click(cancelButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Новый редирект сервер')).not.toBeInTheDocument()
+ })
+ })
+
+ it('должен отображать поля формы', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ await waitFor(() => {
+ expect(screen.getByLabelText('Название')).toBeInTheDocument()
+ expect(screen.getByLabelText('IP адрес')).toBeInTheDocument()
+ expect(screen.getByLabelText('SSH Порт')).toBeInTheDocument()
+ expect(screen.getByLabelText('SSH User')).toBeInTheDocument()
+ expect(screen.getByLabelText('SSH Пароль')).toBeInTheDocument()
+ })
+ })
+
+ it('должен отображать переключатель метода аутентификации', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ await waitFor(() => {
+ expect(screen.getByLabelText('По паролю')).toBeInTheDocument()
+ expect(screen.getByLabelText('По SSH ключу')).toBeInTheDocument()
+ })
+ })
+
+ it('должен переключаться на ввод SSH ключа', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const keyRadio = await screen.findByLabelText('По SSH ключу')
+ fireEvent.click(keyRadio)
+
+ await waitFor(() => {
+ expect(screen.getByLabelText(/SSH Private Key/i)).toBeInTheDocument()
+ expect(screen.queryByLabelText('SSH Пароль')).not.toBeInTheDocument()
+ })
+ })
+
+ it('должен позволять вводить название сервера', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'My Server' } })
+
+ expect(nameField).toHaveValue('My Server')
+ })
+
+ it('должен позволять вводить IP адрес', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '10.0.0.1' } })
+
+ expect(ipField).toHaveValue('10.0.0.1')
+ })
+
+ it('должен позволять вводить SSH порт', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '2222' } })
+
+ expect(portField).toHaveValue(2222)
+ })
+
+ it('должен позволять вводить SSH пользователя', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const userField = await screen.findByLabelText('SSH User')
+ fireEvent.change(userField, { target: { value: 'admin' } })
+
+ expect(userField).toHaveValue('admin')
+ })
+
+ it('должен позволять вводить SSH пароль', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const passwordField = await screen.findByLabelText('SSH Пароль')
+ fireEvent.change(passwordField, { target: { value: 'secret123' } })
+
+ expect(passwordField).toHaveValue('secret123')
})
})
- it('должен открывать диалог подтверждения при удалении туннеля', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
- ]
+ describe('Валидация формы', () => {
+ it('должен показывать ошибку при пустом названии', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const saveButton = await screen.findByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Введите название сервера')).toBeInTheDocument()
+ })
})
- await renderPage()
+ it('должен показывать ошибку при пустом IP', async () => {
+ setupMockGet()
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Tunnel')).toBeInTheDocument()
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Введите IP адрес')).toBeInTheDocument()
+ })
})
- const deleteButton = screen.getAllByTestId('icon-Delete')[0]
- fireEvent.click(deleteButton)
+ it('должен показывать ошибку при неверном формате IP', async () => {
+ setupMockGet()
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Удалить сервер из списка?')).toBeInTheDocument()
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: 'invalid-ip' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Неверный формат IP адреса')).toBeInTheDocument()
+ })
+ })
+
+ it('должен показывать ошибку при неверном порте', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.1.1.1' } })
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '99999' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Порт должен быть от 1 до 65535')).toBeInTheDocument()
+ })
+ })
+
+ it('должен показывать ошибку при пустом пароле', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.1.1.1' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Введите SSH пароль')).toBeInTheDocument()
+ })
+ })
+
+ it('должен показывать ошибку при пустом SSH ключе', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const keyRadio = await screen.findByLabelText('По SSH ключу')
+ fireEvent.click(keyRadio)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.1.1.1' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Введите SSH ключ')).toBeInTheDocument()
+ })
+ })
+
+ it('должен показывать ошибку при неверном формате SSH ключа', async () => {
+ setupMockGet()
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const keyRadio = await screen.findByLabelText('По SSH ключу')
+ fireEvent.click(keyRadio)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'Test' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.1.1.1' } })
+
+ const keyField = await screen.findByLabelText(/SSH Private Key/i)
+ fireEvent.change(keyField, { target: { value: 'invalid-key' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Неверный формат SSH ключа')).toBeInTheDocument()
+ })
})
})
- it('должен закрывать диалог подтверждения при клике на "Отмена"', async () => {
- vi.mocked(api.get).mockResolvedValue({
- data: [
- { id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ describe('Создание сервера', () => {
+ it('должен создавать сервер с паролем', async () => {
+ setupMockGet()
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'New Server' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.2.3.4' } })
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '22' } })
+
+ const userField = await screen.findByLabelText('SSH User')
+ fireEvent.change(userField, { target: { value: 'root' } })
+
+ const passwordField = await screen.findByLabelText('SSH Пароль')
+ fireEvent.change(passwordField, { target: { value: 'secret123' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith('/tunnels', expect.objectContaining({
+ name: 'New Server',
+ ip: '1.2.3.4',
+ }))
+ })
+ })
+
+ it('должен показывать успех после создания', async () => {
+ setupMockGet()
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'New Server' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.2.3.4' } })
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '22' } })
+
+ const userField = await screen.findByLabelText('SSH User')
+ fireEvent.change(userField, { target: { value: 'root' } })
+
+ const passwordField = await screen.findByLabelText('SSH Пароль')
+ fireEvent.change(passwordField, { target: { value: 'secret123' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Сервер добавлен')).toBeInTheDocument()
+ })
+ })
+
+ it('должен закрывать диалог после создания', async () => {
+ setupMockGet()
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'New Server' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.2.3.4' } })
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '22' } })
+
+ const userField = await screen.findByLabelText('SSH User')
+ fireEvent.change(userField, { target: { value: 'root' } })
+
+ const passwordField = await screen.findByLabelText('SSH Пароль')
+ fireEvent.change(passwordField, { target: { value: 'secret123' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Новый редирект сервер')).not.toBeInTheDocument()
+ })
+ })
+
+ it('должен очищать форму после создания', async () => {
+ setupMockGet()
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const addButton = await screen.findByText('Добавить')
+ fireEvent.click(addButton)
+
+ const nameField = await screen.findByLabelText('Название')
+ fireEvent.change(nameField, { target: { value: 'New Server' } })
+
+ const ipField = await screen.findByLabelText('IP адрес')
+ fireEvent.change(ipField, { target: { value: '1.2.3.4' } })
+
+ const portField = await screen.findByLabelText('SSH Порт')
+ fireEvent.change(portField, { target: { value: '22' } })
+
+ const userField = await screen.findByLabelText('SSH User')
+ fireEvent.change(userField, { target: { value: 'root' } })
+
+ const passwordField = await screen.findByLabelText('SSH Пароль')
+ fireEvent.change(passwordField, { target: { value: 'secret123' } })
+
+ const saveButton = screen.getByText('Сохранить')
+ fireEvent.click(saveButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Новый редирект сервер')).not.toBeInTheDocument()
+ })
+
+ fireEvent.click(addButton)
+ await waitFor(() => {
+ expect(screen.getByLabelText('Название')).toHaveValue('')
+ expect(screen.getByLabelText('IP адрес')).toHaveValue('')
+ })
+ })
+ })
+
+ describe('Удаление сервера', () => {
+ it('должен открывать диалог подтверждения удаления', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: true }
]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ const deleteButton = await screen.findByTestId('icon-Delete')
+ fireEvent.click(deleteButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Удалить сервер из списка?')).toBeInTheDocument()
+ })
})
- await renderPage()
+ it('должен закрывать диалог при клике на "Отмена"', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
- await waitFor(() => {
- expect(screen.getByText('Tunnel')).toBeInTheDocument()
+ const deleteButton = await screen.findByTestId('icon-Delete')
+ fireEvent.click(deleteButton)
+
+ const cancelButton = await screen.findByText('Отмена')
+ fireEvent.click(cancelButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Удалить сервер из списка?')).not.toBeInTheDocument()
+ })
})
- const deleteButton = screen.getAllByTestId('icon-Delete')[0]
- fireEvent.click(deleteButton)
+ it('должен удалять сервер при подтверждении', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ mockDelete.mockResolvedValue({})
- await waitFor(() => {
- expect(screen.getByText('Удалить сервер из списка?')).toBeInTheDocument()
+ renderTunnelsPage()
+
+ const deleteButton = await screen.findByTestId('icon-Delete')
+ fireEvent.click(deleteButton)
+
+ const confirmButton = screen.getByText('Подтвердить')
+ fireEvent.click(confirmButton)
+
+ await waitFor(() => {
+ expect(mockDelete).toHaveBeenCalledWith('/tunnels/1')
+ })
})
- const cancelButton = screen.getByText('Отмена')
- fireEvent.click(cancelButton)
+ it('должен показывать успех после удаления', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: true }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ mockDelete.mockResolvedValue({})
- await waitFor(() => {
- expect(screen.queryByText('Удалить сервер из списка?')).not.toBeInTheDocument()
+ renderTunnelsPage()
+
+ const deleteButton = await screen.findByTestId('icon-Delete')
+ fireEvent.click(deleteButton)
+
+ const confirmButton = screen.getByText('Подтвердить')
+ fireEvent.click(confirmButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Сервер удалён')).toBeInTheDocument()
+ })
+ })
+ })
+
+ describe('Установка перенаправления', () => {
+ it('должен отображать кнопку "Установить" для неустановленного сервера', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ await waitFor(() => {
+ expect(screen.getByText('Установить')).toBeInTheDocument()
+ })
+ })
+
+ it('должен открывать диалог подтверждения установки', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ const installButton = await screen.findByText('Установить')
+ fireEvent.click(installButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Начать установку перенаправления на этот сервер?')).toBeInTheDocument()
+ })
+ })
+
+ it('должен закрывать диалог установки при клике на "Отмена"', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ renderTunnelsPage()
+
+ const installButton = await screen.findByText('Установить')
+ fireEvent.click(installButton)
+
+ const cancelButton = await screen.findByText('Отмена')
+ fireEvent.click(cancelButton)
+
+ await waitFor(() => {
+ expect(screen.queryByText('Начать установку перенаправления')).not.toBeInTheDocument()
+ })
+ })
+
+ it('должен устанавливать перенаправление при подтверждении', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const installButton = await screen.findByText('Установить')
+ fireEvent.click(installButton)
+
+ const confirmButton = screen.getByText('Подтвердить')
+ fireEvent.click(confirmButton)
+
+ await waitFor(() => {
+ expect(mockPost).toHaveBeenCalledWith('/tunnels/1/install')
+ })
+ })
+
+ it('должен показывать успех после установки', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ mockPost.mockResolvedValue({})
+
+ renderTunnelsPage()
+
+ const installButton = await screen.findByText('Установить')
+ fireEvent.click(installButton)
+
+ const confirmButton = screen.getByText('Подтвердить')
+ fireEvent.click(confirmButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Скрипт успешно установлен! Трафик перенаправляется.')).toBeInTheDocument()
+ })
+ })
+
+ it('должен показывать ошибку при неудачной установке', async () => {
+ const mockTunnels = [
+ { id: 1, name: 'Test Server', ip: '1.1.1.1', sshPort: 22, username: 'root', isInstalled: false }
+ ]
+ setupMockGet({ tunnels: mockTunnels })
+ mockPost.mockRejectedValue({ response: { data: { message: 'Ошибка подключения' } } })
+
+ renderTunnelsPage()
+
+ const installButton = await screen.findByText('Установить')
+ fireEvent.click(installButton)
+
+ const confirmButton = screen.getByText('Подтвердить')
+ fireEvent.click(confirmButton)
+
+ await waitFor(() => {
+ expect(screen.getByText('Ошибка: Ошибка подключения')).toBeInTheDocument()
+ })
})
})
})
diff --git a/client/test/setup.ts b/client/test/setup.ts
index cd8341f..f62dfa0 100644
--- a/client/test/setup.ts
+++ b/client/test/setup.ts
@@ -112,7 +112,7 @@ vi.mock('@mui/icons-material', async () => {
'Circle', 'NavigateNext', 'NavigateBefore', 'ChevronRight', 'ChevronLeft',
'ExpandMore', 'ExpandLess', 'UnfoldMore', 'UnfoldLess', 'ArrowRight', 'ArrowLeft',
'ArrowBack', 'ArrowForward', 'ArrowDropDown', 'ArrowDropUp', 'Expand',
- 'FileDownload', 'FileUpload', 'Attachment', 'Link', 'InsertLink', 'Photo',
+ 'FileDownload', 'FileUpload', 'UploadFile', 'Download', 'Attachment', 'Link', 'InsertLink', 'Photo',
'Image', 'PictureAsPdf', 'ImageIcon', 'CameraAlt', 'Videocam', 'Movie',
'MusicNote', 'Mic', 'VolumeUp', 'VolumeOff', 'Headset', 'Headphones', 'Speaker',
'Radio', 'Podcasts', 'Tv', 'DesktopWindows', 'Laptop', 'Computer', 'Tablet',