228 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|