From f6908cd8908bf5ab1764071ac77fc6c0d36ed9f7 Mon Sep 17 00:00:00 2001 From: iqubik Date: Sun, 29 Mar 2026 23:24:52 +0300 Subject: [PATCH] testfront 70+ --- client/src/pages/DomainsPage.tsx | 1 + client/test/pages/DomainsPage.spec.tsx | 187 +++++- client/test/pages/TunnelsPage.spec.tsx | 839 ++++++++++++++++++++----- client/test/setup.ts | 2 +- 4 files changed, 856 insertions(+), 173 deletions(-) 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',