163 lines
6.9 KiB
TypeScript
163 lines
6.9 KiB
TypeScript
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();
|
||
const wrapper = mount(AdminSystemView, {
|
||
global: {
|
||
plugins: [createVuetify(), router],
|
||
stubs: { SystemSettingEditDialog: true },
|
||
},
|
||
});
|
||
await flushPromises();
|
||
return wrapper;
|
||
};
|
||
|
||
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 + settingsState пустой', 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(0);
|
||
});
|
||
|
||
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');
|
||
});
|
||
|
||
it('G9: edit-кнопки имеют aria-label с ключом настройки', async () => {
|
||
const wrapper = await mountView();
|
||
const editBtns = wrapper.findAll('[data-testid^="edit-"]');
|
||
expect(editBtns.length).toBeGreaterThan(0);
|
||
for (const btn of editBtns) {
|
||
const label = btn.attributes('aria-label') ?? '';
|
||
expect(label).toMatch(/^Изменить настройку .+/);
|
||
}
|
||
});
|
||
});
|