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

228 lines
10 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { mount, flushPromises } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
// Мокаем api/admin до import'а компонента — иначе реальный axios вызывает
// сетевые запросы в jsdom-runtime.
vi.mock('../../resources/js/api/admin', () => ({
impersonationInit: vi.fn(),
impersonationVerify: vi.fn(),
impersonationEnd: vi.fn(),
}));
// extractValidationErrors / extractErrorMessage используют axios.isAxiosError,
// который в jsdom возвращает false для plain Error → мокаем helper'ы тоже
// (паттерн из auth-store.spec.ts).
vi.mock('../../resources/js/api/client', () => ({
extractValidationErrors: vi.fn(() => null),
extractErrorMessage: vi.fn((_e, fallback?: string) => fallback ?? 'err'),
apiClient: {},
ensureCsrfCookie: vi.fn(),
}));
import * as adminApi from '../../resources/js/api/admin';
import ImpersonationDialog from '../../resources/js/components/admin/ImpersonationDialog.vue';
import { MOCK_TENANTS } from '../../resources/js/composables/mockTenants';
/**
* v-dialog требует layout-injection (v-app/v-layout) — в Vitest без auto-import
* это невозможно. Stub'им VDialog как passthrough div, чтобы slot рендерился.
*/
const factory = (props: { modelValue: boolean; tenant: (typeof MOCK_TENANTS)[number] | null }) =>
mount(ImpersonationDialog, {
props: { ...props, requestedBy: 1 },
global: {
plugins: [createVuetify()],
stubs: {
VDialog: {
template: '<div class="dialog-stub" v-if="modelValue"><slot /></div>',
props: ['modelValue'],
},
VTooltip: {
template: '<div><slot name="activator" :props="{}" /></div>',
},
},
},
});
const sampleTenant = MOCK_TENANTS[0]; // Окна Москва ООО
describe('ImpersonationDialog.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('не рендерит content когда modelValue=false', () => {
const wrapper = factory({ modelValue: false, tenant: sampleTenant });
expect(wrapper.find('.dialog-stub').exists()).toBe(false);
});
it('step 1 — заголовок «Войти как клиент» + tenant info + textarea reason', () => {
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
const text = wrapper.text();
expect(text).toContain('Войти как клиент');
expect(text).toContain(sampleTenant.name);
expect(text).toContain(sampleTenant.code);
expect(text).toContain('ИНН ' + sampleTenant.inn);
expect(wrapper.find('[data-testid="reason-input"]').exists()).toBe(true);
// submit-init disabled при пустом reason
expect(wrapper.find('[data-testid="submit-init-btn"]').exists()).toBe(true);
});
it('reason < 30 chars — submit disabled, отображается remaining counter', async () => {
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
const textarea = wrapper.find('[data-testid="reason-input"] textarea');
await textarea.setValue('коротко');
await wrapper.vm.$nextTick();
// Hint показывает сколько осталось до 30 chars
expect(wrapper.text()).toMatch(/Ещё\s+\d+\s+символов/);
});
it('успешный init → step verify → показывает email + dev-code banner', async () => {
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@okna-moscow.ru',
_dev_plain_code: '123456',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
const textarea = wrapper.find('[data-testid="reason-input"] textarea');
await textarea.setValue('Тикет SUP-12453: клиент сообщил, что в карточке сделки не сохраняется коммент.');
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
expect(adminApi.impersonationInit).toHaveBeenCalledWith({
tenant_id: sampleTenant.id,
requested_by: 1,
reason: expect.stringContaining('Тикет SUP-12453'),
});
// step 2 → email + dev-code visible
const text = wrapper.text();
expect(text).toContain('Код отправлен на email клиента');
expect(text).toContain('admin@okna-moscow.ru');
expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(true);
expect(wrapper.find('[data-testid="dev-code-banner"]').text()).toContain('123456');
expect(wrapper.find('[data-testid="code-input"]').exists()).toBe(true);
});
it('verify success → step active → кнопка «Завершить сессию»', async () => {
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@okna-moscow.ru',
_dev_plain_code: '111222',
});
vi.mocked(adminApi.impersonationVerify).mockResolvedValue({
token_id: 42,
tenant_id: sampleTenant.id,
used_at: '2026-05-09T11:30:00Z',
message: 'OK',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
// Step 1 → 2
await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35));
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
// Step 2 → 3
await wrapper.find('[data-testid="code-input"] input').setValue('111222');
await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click');
await flushPromises();
expect(adminApi.impersonationVerify).toHaveBeenCalledWith({ token_id: 42, code: '111222' });
expect(wrapper.text()).toContain('Impersonation активен');
expect(wrapper.find('[data-testid="submit-end-btn"]').exists()).toBe(true);
});
it('verify с не-6-цифрами не вызывает API + показывает ошибку', async () => {
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@example.ru',
_dev_plain_code: '999999',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35));
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
await wrapper.find('[data-testid="code-input"] input').setValue('12345'); // 5 цифр
await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click');
await flushPromises();
expect(adminApi.impersonationVerify).not.toHaveBeenCalled();
expect(wrapper.text()).toContain('Введите 6 цифр');
});
it('end success → step done → кнопка «Закрыть»', async () => {
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@example.ru',
_dev_plain_code: '777777',
});
vi.mocked(adminApi.impersonationVerify).mockResolvedValue({
token_id: 42,
tenant_id: sampleTenant.id,
used_at: '2026-05-09T11:30:00Z',
message: 'OK',
});
vi.mocked(adminApi.impersonationEnd).mockResolvedValue({
token_id: 42,
session_ended_at: '2026-05-09T12:00:00Z',
message: 'Завершён',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
await wrapper.find('[data-testid="reason-input"] textarea').setValue('A'.repeat(35));
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
await wrapper.find('[data-testid="code-input"] input').setValue('777777');
await wrapper.find('[data-testid="submit-verify-btn"]').trigger('click');
await flushPromises();
await wrapper.find('[data-testid="submit-end-btn"]').trigger('click');
await flushPromises();
expect(adminApi.impersonationEnd).toHaveBeenCalledWith(42);
expect(wrapper.text()).toContain('Сессия impersonation завершена');
expect(wrapper.find('[data-testid="close-btn"]').exists()).toBe(true);
});
it('Отмена на step reason эмитит update:modelValue=false', async () => {
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
await wrapper.find('[data-testid="cancel-btn"]').trigger('click');
const events = wrapper.emitted('update:modelValue');
expect(events).toBeDefined();
expect(events?.[0]).toEqual([false]);
});
it('I4 — dev-code-banner НЕ рендерится когда import.meta.env.DEV=false (prod)', async () => {
vi.stubEnv('DEV', false);
vi.mocked(adminApi.impersonationInit).mockResolvedValue({
token_id: 42,
expires_at: '2026-05-09T12:00:00Z',
sent_to_email: 'admin@okna-moscow.ru',
_dev_plain_code: '123456',
});
const wrapper = factory({ modelValue: true, tenant: sampleTenant });
await wrapper.find('[data-testid="reason-input"] textarea').setValue(
'Тикет SUP-12453: клиент сообщил, что в карточке сделки не сохраняется коммент.',
);
await wrapper.find('[data-testid="submit-init-btn"]').trigger('click');
await flushPromises();
// step verify достигнут — devPlainCode='123456' есть, но DEV=false → баннер не рендерится
expect(wrapper.text()).toContain('Код отправлен на email клиента');
expect(wrapper.find('[data-testid="dev-code-banner"]').exists()).toBe(false);
});
});