Files
portal/app/tests/Frontend/AdminSystemView.spec.ts
T

151 lines
6.4 KiB
TypeScript
Raw Normal View History

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
import { createRouter, createMemoryHistory } from 'vue-router';
// Мокаем api/admin до import'а view (loadSettings вызывается на mount).
vi.mock('../../resources/js/api/admin', () => ({
listSystemSettings: vi.fn(() => Promise.resolve([])), // default — пустой массив
updateSystemSetting: vi.fn(),
}));
vi.mock('../../resources/js/api/client', () => ({
extractErrorMessage: vi.fn((_e, fb?: string) => fb ?? 'err'),
extractValidationErrors: vi.fn(() => null),
apiClient: {},
ensureCsrfCookie: vi.fn(),
}));
import * as adminApi from '../../resources/js/api/admin';
import AdminSystemView from '../../resources/js/views/admin/AdminSystemView.vue';
import { ADMIN_SYSTEM_SETTINGS } from '../../resources/js/composables/mockAdmin';
const mountView = async () => {
const router = createRouter({
history: createMemoryHistory(),
routes: [{ path: '/admin/system', component: AdminSystemView }],
});
await router.push('/admin/system');
await router.isReady();
return mount(AdminSystemView, {
global: {
plugins: [createVuetify(), router],
stubs: { SystemSettingEditDialog: true },
},
});
};
describe('AdminSystemView.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
// По умолчанию backend возвращает те же 7 mock-настроек — view replace
// settingsState на ту же форму, тесты структуры остаются валидными.
vi.mocked(adminApi.listSystemSettings).mockResolvedValue(
ADMIN_SYSTEM_SETTINGS.map((s) => ({
key: s.key,
value: s.value,
type: s.type,
description: s.description,
updated_at: s.updated_at,
updated_by: null,
})),
);
});
it('монтируется и содержит заголовок «Система»', async () => {
const wrapper = await mountView();
expect(wrapper.text()).toContain('Система');
});
it('показывает информационный alert про edit-flow + audit-log', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('Edit-flow');
expect(text).toContain('saas_admin_audit_log');
});
it('перечисляет ключевые system_settings (rate-limit, retention, login_max_attempts)', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('webhook_rate_limit_rps');
expect(text).toContain('login_max_attempts');
expect(text).toContain('password_min_length');
expect(text).toContain('webhook_log_retention_days');
expect(text).toContain('maintenance_mode');
});
it('содержит type-chip для каждой строки (int/string/bool/json)', async () => {
const wrapper = await mountView();
const text = wrapper.text();
expect(text).toContain('int');
expect(text).toContain('bool');
});
it('число строк settings = 7 (mock count)', async () => {
const wrapper = await mountView();
const rows = wrapper.findAll('[data-testid="setting-row"]');
expect(rows.length).toBe(7);
});
it('каждая строка имеет «Изменить» кнопку с уникальным data-testid', async () => {
const wrapper = await mountView();
const editBtns = wrapper.findAll('[data-testid$="-btn"]').filter((b) => b.text().includes('Изменить'));
expect(editBtns.length).toBe(7);
});
it('click на Изменить открывает edit-dialog с правильным setting', async () => {
const wrapper = await mountView();
const vm = wrapper.vm as unknown as {
editOpen: boolean;
editSetting: { key: string } | null;
};
expect(vm.editOpen).toBe(false);
await wrapper.find('[data-testid="edit-login_max_attempts-btn"]').trigger('click');
await wrapper.vm.$nextTick();
expect(vm.editOpen).toBe(true);
expect(vm.editSetting?.key).toBe('login_max_attempts');
});
it('на mount вызывает listSystemSettings (реальный fetch с backend)', async () => {
await mountView();
await flushPromises();
expect(adminApi.listSystemSettings).toHaveBeenCalledTimes(1);
});
it('кнопка Обновить триггерит loadSettings', async () => {
const wrapper = await mountView();
await flushPromises();
vi.mocked(adminApi.listSystemSettings).mockClear();
await wrapper.find('[data-testid="reload-btn"]').trigger('click');
await flushPromises();
expect(adminApi.listSystemSettings).toHaveBeenCalledTimes(1);
});
it('при сетевой ошибке показывает warning-banner + сохраняет mock-данные', async () => {
vi.mocked(adminApi.listSystemSettings).mockRejectedValueOnce(new Error('Network down'));
const wrapper = await mountView();
await flushPromises();
const banner = wrapper.find('[data-testid="fetch-error-alert"]');
expect(banner.exists()).toBe(true);
// Mock-настройки остались (fallback)
const rows = wrapper.findAll('[data-testid="setting-row"]');
expect(rows.length).toBe(7);
});
it('onSettingUpdated обновляет value и updated_at в settingsState', async () => {
const wrapper = await mountView();
const vm = wrapper.vm as unknown as {
settingsState: Array<{ key: string; value: string; updated_at: string }>;
onSettingUpdated: (p: { key: string; value: string; updated_at: string }) => void;
};
vm.onSettingUpdated({
key: 'login_max_attempts',
value: '7',
updated_at: '2026-05-09T11:30:00',
});
await wrapper.vm.$nextTick();
const row = vm.settingsState.find((s) => s.key === 'login_max_attempts');
expect(row?.value).toBe('7');
expect(row?.updated_at).toBe('2026-05-09T11:30:00');
});
});