Files
portal/app/tests/Frontend/ApiTab.spec.ts
T
Дмитрий c693d03a75 test(settings): ApiTab — load error-path coverage + idiomatic disabled check (review M2/M3)
Code-quality review of Task 5: adds tests for the loadApiKey/loadWebhook
catch branches (apiKeyError/webhookError -> error v-alert) and changes
the Copy-button disabled check to the idiomatic falsy form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 22:35:49 +03:00

153 lines
6.4 KiB
TypeScript

import { describe, it, expect, beforeEach, vi } from 'vitest';
import { mount } from '@vue/test-utils';
import { createVuetify } from 'vuetify';
vi.mock('../../resources/js/api/apiKeys', () => ({
listApiKeys: vi.fn(),
regenerateApiKey: vi.fn(),
}));
vi.mock('../../resources/js/api/webhooks', () => ({
getWebhookSettings: vi.fn(),
saveWebhookSettings: vi.fn(),
testWebhook: vi.fn(),
}));
vi.mock('../../resources/js/api/client', () => ({
apiClient: {},
ensureCsrfCookie: vi.fn(),
extractValidationErrors: vi.fn(() => null),
extractErrorMessage: vi.fn((_e, fallback) => fallback ?? 'Произошла ошибка.'),
extractRateLimitRetry: vi.fn(() => null),
}));
import * as apiKeysApi from '../../resources/js/api/apiKeys';
import * as webhooksApi from '../../resources/js/api/webhooks';
import ApiTab from '../../resources/js/views/settings/ApiTab.vue';
const vuetify = createVuetify();
const flush = () => new Promise((r) => setTimeout(r, 30));
const mountTab = () => mount(ApiTab, { global: { plugins: [vuetify] } });
describe('ApiTab.vue', () => {
beforeEach(() => {
vi.clearAllMocks();
(apiKeysApi.listApiKeys as ReturnType<typeof vi.fn>).mockResolvedValue([]);
(webhooksApi.getWebhookSettings as ReturnType<typeof vi.fn>).mockResolvedValue(null);
Object.assign(navigator, { clipboard: { writeText: vi.fn().mockResolvedValue(undefined) } });
});
it('загружает и показывает префикс API-ключа', async () => {
(apiKeysApi.listApiKeys as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 1, name: 'API-ключ', key_prefix: 'lpkapi_abc', last_used_at: null, expires_at: null, created_at: null },
]);
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
expect(vm.apiKeyExists).toBe(true);
expect(vm.apiTokenDisplay).toBe('lpkapi_abc');
});
it('copyToken() пишет в буфер и открывает toast', async () => {
(apiKeysApi.listApiKeys as ReturnType<typeof vi.fn>).mockResolvedValue([
{ id: 1, name: 'API-ключ', key_prefix: 'lpkapi_abc', last_used_at: null, expires_at: null, created_at: null },
]);
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
await vm.copyToken();
expect(navigator.clipboard.writeText).toHaveBeenCalledWith('lpkapi_abc');
expect(vm.toastOpen).toBe(true);
});
it('confirmRegenerate() показывает полный новый ключ один раз', async () => {
(apiKeysApi.regenerateApiKey as ReturnType<typeof vi.fn>).mockResolvedValue({
id: 2,
name: 'API-ключ',
key: 'lpkapi_FULLNEWKEYVALUE0000000000',
key_prefix: 'lpkapi_FUL',
});
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
await vm.confirmRegenerate();
expect(apiKeysApi.regenerateApiKey).toHaveBeenCalled();
expect(vm.apiTokenDisplay).toBe('lpkapi_FULLNEWKEYVALUE0000000000');
expect(vm.fullKeyShown).toBe(true);
});
it('загружает настройки webhook', async () => {
(webhooksApi.getWebhookSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
target_url: 'https://crm.example.ru/hook',
secret_prefix: 'whsec_abc',
events: ['deal.created'],
is_active: true,
});
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
expect(vm.webhookUrl).toBe('https://crm.example.ru/hook');
expect(vm.secretDisplay).toBe('whsec_abc');
});
it('saveWebhook() вызывает saveWebhookSettings и показывает secret один раз', async () => {
(webhooksApi.saveWebhookSettings as ReturnType<typeof vi.fn>).mockResolvedValue({
target_url: 'https://crm.example.ru/hook',
secret_prefix: 'whsec_new',
events: ['deal.created'],
is_active: true,
secret: 'whsec_FULLSECRETVALUE0000',
});
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
vm.webhookUrl = 'https://crm.example.ru/hook';
await vm.saveWebhook();
expect(webhooksApi.saveWebhookSettings).toHaveBeenCalledWith({ target_url: 'https://crm.example.ru/hook' });
expect(vm.secretDisplay).toBe('whsec_FULLSECRETVALUE0000');
expect(vm.fullSecretShown).toBe(true);
expect(vm.webhookSuccess).toBeTruthy();
});
it('runWebhookTest() вызывает testWebhook и открывает toast с результатом', async () => {
(webhooksApi.testWebhook as ReturnType<typeof vi.fn>).mockResolvedValue({
ok: true,
status: 200,
message: 'Тестовый запрос доставлен (HTTP 200).',
});
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
await vm.runWebhookTest();
expect(webhooksApi.testWebhook).toHaveBeenCalled();
expect(vm.toastOpen).toBe(true);
expect(vm.toastText).toContain('HTTP 200');
});
it('loadApiKey() выставляет apiKeyError при reject', async () => {
(apiKeysApi.listApiKeys as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('boom'));
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
expect(vm.apiKeyError).toBeTruthy();
});
it('loadWebhook() выставляет webhookError при reject', async () => {
(webhooksApi.getWebhookSettings as ReturnType<typeof vi.fn>).mockRejectedValue(new Error('boom'));
const wrapper = mountTab();
await flush();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vm = wrapper.vm as any;
expect(vm.webhookError).toBeTruthy();
});
});