c693d03a75
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>
153 lines
6.4 KiB
TypeScript
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();
|
|
});
|
|
});
|