35% front tests
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, fireEvent, waitFor } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import LoginPage from '../../src/pages/LoginPage'
|
||||
import { ThemeProvider } from '../../src/ThemeContext'
|
||||
import { AuthProvider } from '../../src/auth/AuthContext'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
const mockLogin = vi.fn()
|
||||
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom')
|
||||
return {
|
||||
...(actual as object),
|
||||
useNavigate: () => mockNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../src/auth/AuthContext', async () => {
|
||||
const actual = await vi.importActual('../../src/auth/AuthContext')
|
||||
return {
|
||||
...(actual as object),
|
||||
useAuth: () => ({ login: mockLogin }),
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../../src/api', () => ({
|
||||
default: {
|
||||
post: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
const renderLoginPage = () => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<LoginPage />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('LoginPage', () => {
|
||||
beforeEach(() => {
|
||||
mockNavigate.mockClear()
|
||||
mockLogin.mockClear()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('должен отображать заголовок с версией', () => {
|
||||
renderLoginPage()
|
||||
expect(screen.getByText(/Вход в 3DP-MANAGER/)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен отображать поля ввода логина и пароля', () => {
|
||||
renderLoginPage()
|
||||
expect(screen.getByLabelText('Логин')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Пароль')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен отображать кнопку "Войти"', () => {
|
||||
renderLoginPage()
|
||||
expect(screen.getByText('Войти')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен позволять вводить текст в поля', () => {
|
||||
renderLoginPage()
|
||||
const loginField = screen.getByLabelText('Логин')
|
||||
const passwordField = screen.getByLabelText('Пароль')
|
||||
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'password123' } })
|
||||
|
||||
expect(loginField).toHaveValue('admin')
|
||||
expect(passwordField).toHaveValue('password123')
|
||||
})
|
||||
|
||||
it('должен показывать ошибку при неверных учетных данных', async () => {
|
||||
const api = await import('../../src/api')
|
||||
vi.mocked(api.default.post).mockRejectedValue({ response: { status: 401 } })
|
||||
|
||||
renderLoginPage()
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Логин'), { target: { value: 'admin' } })
|
||||
fireEvent.change(screen.getByLabelText('Пароль'), { target: { value: 'wrong' } })
|
||||
fireEvent.click(screen.getByText('Войти'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Неверный логин или пароль')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен выполнять навигацию на главную при успешном входе', async () => {
|
||||
const api = await import('../../src/api')
|
||||
vi.mocked(api.default.post).mockResolvedValue({ data: { access_token: 'fake-token' } })
|
||||
|
||||
renderLoginPage()
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Логин'), { target: { value: 'admin' } })
|
||||
fireEvent.change(screen.getByLabelText('Пароль'), { target: { value: 'password' } })
|
||||
fireEvent.click(screen.getByText('Войти'))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockLogin).toHaveBeenCalledWith('fake-token')
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,44 @@
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
import { render, screen, fireEvent } from '@testing-library/react'
|
||||
import { BrowserRouter } from 'react-router-dom'
|
||||
import NotFoundPage from '../../src/pages/NotFoundPage'
|
||||
|
||||
const mockNavigate = vi.fn()
|
||||
vi.mock('react-router-dom', async () => {
|
||||
const actual = await vi.importActual('react-router-dom')
|
||||
return {
|
||||
...(actual as object),
|
||||
useNavigate: () => mockNavigate,
|
||||
}
|
||||
})
|
||||
|
||||
const renderNotFoundPage = () => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<NotFoundPage />
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('NotFoundPage', () => {
|
||||
it('должен отображать код ошибки 404', () => {
|
||||
renderNotFoundPage()
|
||||
expect(screen.getByText('404')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен отображать сообщение "Страница не найдена"', () => {
|
||||
renderNotFoundPage()
|
||||
expect(screen.getByText('Страница не найдена')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен иметь кнопку "На главную"', () => {
|
||||
renderNotFoundPage()
|
||||
expect(screen.getByText('На главную')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('должен выполнять навигацию на главную при клике на кнопку', () => {
|
||||
renderNotFoundPage()
|
||||
fireEvent.click(screen.getByText('На главную'))
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,651 @@
|
||||
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 SettingsPage from '../../src/pages/SettingsPage'
|
||||
import { ThemeProvider } from '../../src/ThemeContext'
|
||||
import { AuthProvider } from '../../src/auth/AuthContext'
|
||||
|
||||
const mockGet = vi.fn()
|
||||
const mockPost = vi.fn()
|
||||
const mockPut = vi.fn()
|
||||
|
||||
vi.mock('../../src/api', () => ({
|
||||
default: {
|
||||
get: (...args: unknown[]) => mockGet(...args),
|
||||
post: (...args: unknown[]) => mockPost(...args),
|
||||
put: (...args: unknown[]) => mockPut(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
// Хелпер для настройки мока get по умолчанию
|
||||
const setupMockGet = (overrides?: { settings?: Record<string, unknown>, subscriptions?: unknown[] }) => {
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/settings') return Promise.resolve({ data: overrides?.settings || {} })
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: overrides?.subscriptions || [] })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
}
|
||||
|
||||
const renderSettingsPage = () => {
|
||||
return render(
|
||||
<BrowserRouter>
|
||||
<ThemeProvider>
|
||||
<AuthProvider>
|
||||
<SettingsPage />
|
||||
</AuthProvider>
|
||||
</ThemeProvider>
|
||||
</BrowserRouter>
|
||||
)
|
||||
}
|
||||
|
||||
describe('SettingsPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('Рендеринг', () => {
|
||||
it('должен рендериться с заголовком', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Настройки утилиты')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать секцию панели 3x-ui', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Панель 3x-ui')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать секцию генерации инбаундов', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Генерация инбаундов')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать поля ввода для 3x-ui', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('URL панели')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Логин 3x-ui')).toBeInTheDocument()
|
||||
expect(screen.getByLabelText('Пароль 3x-ui')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать поле интервала генерации', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText('Интервал генерации')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать пресеты интервалов', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Сутки')).toBeInTheDocument()
|
||||
expect(screen.getByText('3 дня')).toBeInTheDocument()
|
||||
expect(screen.getByText('Неделя')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Загрузка настроек', () => {
|
||||
it('должен загружать настройки при монтировании', async () => {
|
||||
const mockSettings = {
|
||||
xui_url: 'https://test.com:2053',
|
||||
xui_login: 'admin',
|
||||
xui_password: 'password',
|
||||
rotation_interval: '60',
|
||||
rotation_status: 'active',
|
||||
last_rotation_timestamp: '1234567890',
|
||||
}
|
||||
setupMockGet({ settings: mockSettings })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGet).toHaveBeenCalledWith('/settings')
|
||||
})
|
||||
})
|
||||
|
||||
it('должен загружать подписки при монтировании', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: true }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockGet).toHaveBeenCalledWith('/subscriptions')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Изменение полей', () => {
|
||||
it('должен позволять изменять URL панели', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
fireEvent.change(urlField, { target: { value: 'https://new-url.com:2053' } })
|
||||
|
||||
expect(urlField).toHaveValue('https://new-url.com:2053')
|
||||
})
|
||||
|
||||
it('должен позволять изменять логин 3x-ui', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const loginField = await screen.findByLabelText('Логин 3x-ui')
|
||||
fireEvent.change(loginField, { target: { value: 'newadmin' } })
|
||||
|
||||
expect(loginField).toHaveValue('newadmin')
|
||||
})
|
||||
|
||||
it('должен позволять изменять пароль 3x-ui', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const passwordField = await screen.findByLabelText('Пароль 3x-ui')
|
||||
fireEvent.change(passwordField, { target: { value: 'newpassword' } })
|
||||
|
||||
expect(passwordField).toHaveValue('newpassword')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Пресеты интервалов', () => {
|
||||
it('должен отображать пресеты интервалов', async () => {
|
||||
setupMockGet({ settings: { rotation_interval: '30' } })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Сутки')).toBeInTheDocument()
|
||||
expect(screen.getByText('3 дня')).toBeInTheDocument()
|
||||
expect(screen.getByText('Неделя')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Сохранение настроек подключения', () => {
|
||||
it('должен показывать ошибку при пустых полях', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const saveButton = await screen.findByText('Сохранить подключение')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Заполните все поля/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен сохранять настройки при валидных данных', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
const loginField = screen.getByLabelText('Логин 3x-ui')
|
||||
const passwordField = screen.getByLabelText('Пароль 3x-ui')
|
||||
|
||||
fireEvent.change(urlField, { target: { value: 'https://test.com:2053' } })
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'password' } })
|
||||
|
||||
const saveButton = screen.getByText('Сохранить подключение')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/settings', expect.objectContaining({
|
||||
xui_url: 'https://test.com:2053',
|
||||
xui_login: 'admin',
|
||||
xui_password: 'password',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать сообщение об успехе после сохранения', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
const loginField = screen.getByLabelText('Логин 3x-ui')
|
||||
const passwordField = screen.getByLabelText('Пароль 3x-ui')
|
||||
|
||||
fireEvent.change(urlField, { target: { value: 'https://test.com:2053' } })
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'password' } })
|
||||
|
||||
const saveButton = screen.getByText('Сохранить подключение')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Настройки сохранены!')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Проверка подключения', () => {
|
||||
it('должен проверять подключение при клике на кнопку "Проверить"', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: { success: true } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
const loginField = screen.getByLabelText('Логин 3x-ui')
|
||||
const passwordField = screen.getByLabelText('Пароль 3x-ui')
|
||||
|
||||
fireEvent.change(urlField, { target: { value: 'https://test.com:2053' } })
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'password' } })
|
||||
|
||||
const checkButton = screen.getByText('Проверить')
|
||||
fireEvent.click(checkButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/settings/check', expect.objectContaining({
|
||||
xui_url: 'https://test.com:2053',
|
||||
xui_login: 'admin',
|
||||
xui_password: 'password',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать успех при успешной проверке', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: { success: true } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
const loginField = screen.getByLabelText('Логин 3x-ui')
|
||||
const passwordField = screen.getByLabelText('Пароль 3x-ui')
|
||||
|
||||
fireEvent.change(urlField, { target: { value: 'https://test.com:2053' } })
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'password' } })
|
||||
|
||||
const checkButton = screen.getByText('Проверить')
|
||||
fireEvent.click(checkButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Подключение успешно!')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать ошибку при неудачной проверке', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: { success: false } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const urlField = await screen.findByLabelText('URL панели')
|
||||
const loginField = screen.getByLabelText('Логин 3x-ui')
|
||||
const passwordField = screen.getByLabelText('Пароль 3x-ui')
|
||||
|
||||
fireEvent.change(urlField, { target: { value: 'https://test.com:2053' } })
|
||||
fireEvent.change(loginField, { target: { value: 'admin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'wrong' } })
|
||||
|
||||
const checkButton = screen.getByText('Проверить')
|
||||
fireEvent.click(checkButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/Ошибка/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Сохранение интервала', () => {
|
||||
it('должен сохранять интервал при клике на кнопку', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const intervalField = await screen.findByLabelText('Интервал генерации')
|
||||
fireEvent.change(intervalField, { target: { value: '120' } })
|
||||
|
||||
const saveButton = screen.getByText('Применить интервал')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/settings', {
|
||||
rotation_interval: '120',
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать сообщение об успехе после сохранения интервала', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const intervalField = await screen.findByLabelText('Интервал генерации')
|
||||
fireEvent.change(intervalField, { target: { value: '120' } })
|
||||
|
||||
const saveButton = screen.getByText('Применить интервал')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Интервал генерации применён!')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Принудительная ротация', () => {
|
||||
it('должен показывать диалог подтверждения при клике на "Сгенерировать сейчас"', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const rotateButton = await screen.findByText('Сгенерировать сейчас')
|
||||
fireEvent.click(rotateButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/ВНИМАНИЕ/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен выполнять ротацию при подтверждении', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: { success: true, message: 'Ротация выполнена' } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const rotateButton = await screen.findByText('Сгенерировать сейчас')
|
||||
fireEvent.click(rotateButton)
|
||||
|
||||
const confirmButton = await screen.findByText('Продолжить')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/rotation/rotate-all')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Управление авторотацией подписок', () => {
|
||||
it('должен отображать список подписок с чекбоксами', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: true }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Test Sub')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен переключать авторотацию при клике на чекбокс', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: true }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mockPut.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const checkbox = await screen.findByRole('checkbox')
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPut).toHaveBeenCalledWith('/subscriptions/bulk-auto-rotation', expect.anything())
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать сообщение при включении авторотации', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: false }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mockPut.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const checkbox = await screen.findByRole('checkbox')
|
||||
fireEvent.click(checkbox)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Авторотация включена')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен выполнять ручную ротацию при клике на кнопку обновления', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: true }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mockPost.mockResolvedValue({ data: { message: 'Ротация выполнена' } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const refreshButton = await screen.findByRole('button', { name: /Обновить подписку вручную/i })
|
||||
fireEvent.click(refreshButton)
|
||||
|
||||
const confirmButton = await screen.findByText('Продолжить')
|
||||
fireEvent.click(confirmButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/rotation/rotate-one/1')
|
||||
})
|
||||
})
|
||||
|
||||
it('должен выполнять массовое включение авторотации', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: false }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mockPut.mockResolvedValue({ data: { message: 'Настройки обновлены' } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const enableAllButton = await screen.findByText('Включить для всех')
|
||||
fireEvent.click(enableAllButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPut).toHaveBeenCalledWith('/subscriptions/bulk-auto-rotation', expect.objectContaining({
|
||||
enabled: true,
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('должен выполнять массовое выключение авторотации', async () => {
|
||||
const mockSubs = [
|
||||
{ id: '1', name: 'Test Sub', uuid: 'abc-123', isAutoRotationEnabled: true }
|
||||
]
|
||||
mockGet.mockImplementation((url: string) => {
|
||||
if (url === '/subscriptions') return Promise.resolve({ data: mockSubs })
|
||||
return Promise.resolve({ data: {} })
|
||||
})
|
||||
mockPut.mockResolvedValue({ data: { message: 'Настройки обновлены' } })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const disableAllButton = await screen.findByText('Выключить для всех')
|
||||
fireEvent.click(disableAllButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPut).toHaveBeenCalledWith('/subscriptions/bulk-auto-rotation', expect.objectContaining({
|
||||
enabled: false,
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Обновление профиля администратора', () => {
|
||||
it('должен позволять изменять логин администратора', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const loginField = await screen.findByLabelText('Логин администратора')
|
||||
fireEvent.change(loginField, { target: { value: 'newadmin' } })
|
||||
|
||||
expect(loginField).toHaveValue('newadmin')
|
||||
})
|
||||
|
||||
it('должен позволять изменять пароль администратора', async () => {
|
||||
setupMockGet()
|
||||
renderSettingsPage()
|
||||
|
||||
const passwordField = await screen.findByLabelText('Новый пароль')
|
||||
fireEvent.change(passwordField, { target: { value: 'newpassword' } })
|
||||
|
||||
expect(passwordField).toHaveValue('newpassword')
|
||||
})
|
||||
|
||||
it('должен сохранять профиль администратора', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const loginField = await screen.findByLabelText('Логин администратора')
|
||||
const passwordField = screen.getByLabelText('Новый пароль')
|
||||
|
||||
fireEvent.change(loginField, { target: { value: 'newadmin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'newpassword' } })
|
||||
|
||||
const saveButton = screen.getByText('Обновить профиль')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/auth/update-profile', expect.objectContaining({
|
||||
login: 'newadmin',
|
||||
password: 'newpassword',
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
it('должен показывать сообщение об успехе после обновления профиля', async () => {
|
||||
setupMockGet()
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const loginField = await screen.findByLabelText('Логин администратора')
|
||||
const passwordField = screen.getByLabelText('Новый пароль')
|
||||
|
||||
fireEvent.change(loginField, { target: { value: 'newadmin' } })
|
||||
fireEvent.change(passwordField, { target: { value: 'newpassword' } })
|
||||
|
||||
const saveButton = screen.getByText('Обновить профиль')
|
||||
fireEvent.click(saveButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Профиль администратора обновлен!')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Пауза/возобновление ротации', () => {
|
||||
it('должен отображать статус "Активен" при active статусе', async () => {
|
||||
setupMockGet({ settings: { rotation_status: 'active' } })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Активен')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать статус "Остановлен" при stopped статусе', async () => {
|
||||
setupMockGet({ settings: { rotation_status: 'stopped' } })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Остановлен')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен переключать статус при клике на кнопку паузы', async () => {
|
||||
setupMockGet({ settings: { rotation_status: 'active' } })
|
||||
mockPost.mockResolvedValue({ data: {} })
|
||||
|
||||
renderSettingsPage()
|
||||
|
||||
const pauseButton = await screen.findByRole('button', { name: 'Поставить на паузу' })
|
||||
fireEvent.click(pauseButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockPost).toHaveBeenCalledWith('/settings', expect.objectContaining({
|
||||
rotation_status: 'stopped',
|
||||
}))
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Отображение дат ротации', () => {
|
||||
it('должен отображать дату последней ротации', async () => {
|
||||
setupMockGet({ settings: { last_rotation_timestamp: '1234567890' } })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Последняя генерация')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать дату следующей ротации', async () => {
|
||||
setupMockGet({ settings: {
|
||||
rotation_status: 'active',
|
||||
last_rotation_timestamp: '1234567890',
|
||||
rotation_interval: '60'
|
||||
} })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Следующая генерация')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать "Пауза" для следующей ротации при stopped статусе', async () => {
|
||||
setupMockGet({ settings: { rotation_status: 'stopped' } })
|
||||
renderSettingsPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Пауза')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,209 @@
|
||||
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 TunnelsPage from '../../src/pages/TunnelsPage'
|
||||
import api from '../../src/api'
|
||||
|
||||
vi.mock('../../src/api')
|
||||
|
||||
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(
|
||||
<MemoryRouter>
|
||||
<TunnelsPage />
|
||||
</MemoryRouter>
|
||||
)
|
||||
// Ждём завершения первоначальной загрузки данных
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Relay серверы')).toBeInTheDocument()
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
const cancelButton = screen.getByText('Отмена')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
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()
|
||||
})
|
||||
|
||||
const keyRadio = screen.getByLabelText('По SSH ключу')
|
||||
fireEvent.click(keyRadio)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText(/SSH Private Key/i)).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 }
|
||||
]
|
||||
})
|
||||
|
||||
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 }
|
||||
]
|
||||
})
|
||||
|
||||
await renderPage()
|
||||
|
||||
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 }
|
||||
]
|
||||
})
|
||||
|
||||
await renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Не настроен')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен отображать кнопку "Установить" для неустановленного туннеля', async () => {
|
||||
vi.mocked(api.get).mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: false }
|
||||
]
|
||||
})
|
||||
|
||||
await renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Установить')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен открывать диалог подтверждения при удалении туннеля', async () => {
|
||||
vi.mocked(api.get).mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
|
||||
]
|
||||
})
|
||||
|
||||
await renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tunnel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const deleteButton = screen.getAllByTestId('icon-Delete')[0]
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Удалить сервер из списка?')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('должен закрывать диалог подтверждения при клике на "Отмена"', async () => {
|
||||
vi.mocked(api.get).mockResolvedValue({
|
||||
data: [
|
||||
{ id: 1, name: 'Tunnel', ip: '192.168.1.1', sshPort: 22, username: 'root', isInstalled: true }
|
||||
]
|
||||
})
|
||||
|
||||
await renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Tunnel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const deleteButton = screen.getAllByTestId('icon-Delete')[0]
|
||||
fireEvent.click(deleteButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Удалить сервер из списка?')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const cancelButton = screen.getByText('Отмена')
|
||||
fireEvent.click(cancelButton)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Удалить сервер из списка?')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user