test(api): Q.DEFER.003 sub-A — 43 unit tests for api/*.ts layer
User chose (A) api/* unit tests first (highest ROI per blocked.md). 5 new
spec files covering auth/deals/notifications/reminders/reports api modules.
- auth-api.spec.ts (13 tests): login/register/me/logout/verifyTwoFactor/
useRecoveryCode/twoFactorInit/Confirm/Disable/RegenerateRecoveryCodes/
forgotPassword/resetPassword/updateNotificationPreferences
- deals-api.spec.ts (12 tests): createDeal/bulkDelete/bulkRestore/update/
transition/exportCSV/exportXLSX/getDeal/listDeals×2/listManagers/
listProjects
- notifications-api.spec.ts (6 tests): listNotifications×3 (unreadOnly
variants)/markRead/markAllRead/delete
- reminders-api.spec.ts (6 tests): listReminders×2/create/update/complete/
delete
- reports-api.spec.ts (6 tests): listReportJobs×2/create/retry/cancel/delete
Approach: vi.mock('../../resources/js/api/client') replaces apiClient with
{get,post,patch,delete} mocks + ensureCsrfCookie mock. Each test verifies:
(1) correct HTTP method, (2) correct URL, (3) correct params/body
(camelCase→snake_case mapping for query params), (4) data unwrap from
wrapper objects ({user}/{deal}/{job}/{reminder}/{managers}/{projects}),
(5) ensureCsrfCookie called for mutating endpoints.
Vitest delta: 614 → 657 passed (+43 / 0 failed); 79 → 84 files (+5).
3 skipped unchanged. Q.DEFER.003 sub-B (security cards) + sub-C (router
guards) remain deferred — sub-A api/* was highest ROI per blocked.md.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../resources/js/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
ensureCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
login,
|
||||
register,
|
||||
me,
|
||||
logout,
|
||||
verifyTwoFactor,
|
||||
useRecoveryCode,
|
||||
twoFactorInit,
|
||||
twoFactorConfirm,
|
||||
twoFactorDisable,
|
||||
twoFactorRegenerateRecoveryCodes,
|
||||
forgotPassword,
|
||||
resetPassword,
|
||||
updateNotificationPreferences,
|
||||
} from '../../resources/js/api/auth';
|
||||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||||
|
||||
const FAKE_USER = {
|
||||
id: 1,
|
||||
email: 'demo@x.ru',
|
||||
first_name: 'D',
|
||||
last_name: 'U',
|
||||
tenant_id: 1,
|
||||
totp_enabled: false,
|
||||
last_login_at: null,
|
||||
};
|
||||
|
||||
describe('api/auth', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('login() POSTs /api/auth/login + calls ensureCsrfCookie + unwraps data', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { user: FAKE_USER, requires_2fa: false } });
|
||||
const result = await login({ email: 'a@x.ru', password: 'pw' });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/login', { email: 'a@x.ru', password: 'pw' });
|
||||
expect(result.requires_2fa).toBe(false);
|
||||
expect(result.user.email).toBe('demo@x.ru');
|
||||
});
|
||||
|
||||
it('register() POSTs /api/auth/register с accept-флагами', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { user: FAKE_USER, requires_2fa: false } });
|
||||
await register({ email: 'a@x.ru', password: 'pw', accept_offer: true, accept_pdn: true });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/register', {
|
||||
email: 'a@x.ru',
|
||||
password: 'pw',
|
||||
accept_offer: true,
|
||||
accept_pdn: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('me() GETs /api/auth/me + unwraps data.user', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { user: FAKE_USER } });
|
||||
const result = await me();
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/auth/me');
|
||||
expect(result.id).toBe(1);
|
||||
});
|
||||
|
||||
it('logout() POSTs /api/auth/logout + ensureCsrfCookie', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: {} });
|
||||
await logout();
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/logout');
|
||||
});
|
||||
|
||||
it('verifyTwoFactor() POSTs /api/auth/2fa/verify с code', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { user: FAKE_USER, requires_2fa: false } });
|
||||
await verifyTwoFactor('123456');
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/2fa/verify', { code: '123456' });
|
||||
});
|
||||
|
||||
it('useRecoveryCode() POSTs /api/auth/2fa/recovery-use с code + возвращает remaining', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({
|
||||
data: { user: FAKE_USER, requires_2fa: false, recovery_codes_remaining: 7 },
|
||||
});
|
||||
const r = await useRecoveryCode('xxxx-yyyy');
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/2fa/recovery-use', { code: 'xxxx-yyyy' });
|
||||
expect(r.recovery_codes_remaining).toBe(7);
|
||||
});
|
||||
|
||||
it('twoFactorInit() POSTs /api/2fa/init + возвращает secret+qr_url', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { secret: 'JBSW', qr_url: 'otpauth://...' } });
|
||||
const r = await twoFactorInit();
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/2fa/init');
|
||||
expect(r.secret).toBe('JBSW');
|
||||
});
|
||||
|
||||
it('twoFactorConfirm() POSTs /api/2fa/confirm с code', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { recovery_codes: ['a', 'b'], message: 'ok' } });
|
||||
await twoFactorConfirm('000000');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/2fa/confirm', { code: '000000' });
|
||||
});
|
||||
|
||||
it('twoFactorDisable() POSTs /api/2fa/disable с паролем', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { message: 'disabled' } });
|
||||
await twoFactorDisable('current-pw');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/2fa/disable', { password: 'current-pw' });
|
||||
});
|
||||
|
||||
it('twoFactorRegenerateRecoveryCodes() POSTs /api/2fa/regenerate-recovery-codes', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { recovery_codes: ['x', 'y', 'z'], message: 'ok' } });
|
||||
const r = await twoFactorRegenerateRecoveryCodes('pw');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/2fa/regenerate-recovery-codes', { password: 'pw' });
|
||||
expect(r.recovery_codes.length).toBe(3);
|
||||
});
|
||||
|
||||
it('forgotPassword() POSTs /api/auth/forgot с email', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { message: 'sent' } });
|
||||
await forgotPassword('a@x.ru');
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/forgot', { email: 'a@x.ru' });
|
||||
});
|
||||
|
||||
it('resetPassword() POSTs /api/auth/reset-password с token+email+password', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { message: 'reset' } });
|
||||
const payload = { token: 't', email: 'a@x.ru', password: 'newpw', password_confirmation: 'newpw' };
|
||||
await resetPassword(payload);
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/auth/reset-password', payload);
|
||||
});
|
||||
|
||||
it('updateNotificationPreferences() PATCH /api/auth/me/notification-preferences', async () => {
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { user: FAKE_USER } });
|
||||
const payload = { prefs: { new_lead: { inapp: true } }, sound_enabled: false };
|
||||
await updateNotificationPreferences(payload);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/api/auth/me/notification-preferences', payload);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,177 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../resources/js/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
ensureCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
createDeal,
|
||||
bulkDeleteDeals,
|
||||
bulkRestoreDeals,
|
||||
updateDeal,
|
||||
transitionDeals,
|
||||
exportDeals,
|
||||
exportDealsXlsx,
|
||||
getDeal,
|
||||
listDeals,
|
||||
listManagers,
|
||||
listProjects,
|
||||
} from '../../resources/js/api/deals';
|
||||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||||
|
||||
const FAKE_DEAL = {
|
||||
id: 1,
|
||||
tenant_id: 1,
|
||||
project_id: 1,
|
||||
project_name: 'Test',
|
||||
phone: '+79161234567',
|
||||
contact_name: 'X',
|
||||
status: 'new',
|
||||
manager_id: null,
|
||||
manager_name: null,
|
||||
manager_initials: null,
|
||||
received_at: null,
|
||||
comment: null,
|
||||
assigned_at: null,
|
||||
};
|
||||
|
||||
describe('api/deals', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('createDeal() POSTs /api/deals + ensureCsrfCookie + unwraps data.deal', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { deal: FAKE_DEAL, message: 'ok' } });
|
||||
const r = await createDeal({ tenant_id: 1, project_name: 'P', phone: '+79161234567' });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/deals', {
|
||||
tenant_id: 1,
|
||||
project_name: 'P',
|
||||
phone: '+79161234567',
|
||||
});
|
||||
expect(r.id).toBe(1);
|
||||
});
|
||||
|
||||
it('bulkDeleteDeals() DELETE /api/deals с data payload', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: { deleted: 3, requested: 4 } });
|
||||
const r = await bulkDeleteDeals({ tenant_id: 1, ids: [1, 2, 3, 4] });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/api/deals', { data: { tenant_id: 1, ids: [1, 2, 3, 4] } });
|
||||
expect(r.deleted).toBe(3);
|
||||
});
|
||||
|
||||
it('bulkRestoreDeals() POSTs /api/deals/restore', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { restored: 2, requested: 2 } });
|
||||
await bulkRestoreDeals({ tenant_id: 1, ids: [1, 2] });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/deals/restore', { tenant_id: 1, ids: [1, 2] });
|
||||
});
|
||||
|
||||
it('updateDeal() PATCH /api/deals/{id} c partial payload', async () => {
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { deal: FAKE_DEAL } });
|
||||
await updateDeal(42, { tenant_id: 1, comment: 'new' });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/api/deals/42', { tenant_id: 1, comment: 'new' });
|
||||
});
|
||||
|
||||
it('transitionDeals() POSTs /api/deals/transition с status', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { updated: 5, requested: 5, status: 'won' } });
|
||||
const r = await transitionDeals({ tenant_id: 1, ids: [1, 2, 3, 4, 5], status: 'won' });
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/deals/transition', {
|
||||
tenant_id: 1,
|
||||
ids: [1, 2, 3, 4, 5],
|
||||
status: 'won',
|
||||
});
|
||||
expect(r.status).toBe('won');
|
||||
});
|
||||
|
||||
it('exportDeals() POSTs /api/deals/export с format=csv + responseType=text', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: 'id,phone\n1,+79..' });
|
||||
const r = await exportDeals({ tenant_id: 1, ids: [1] });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/api/deals/export',
|
||||
{ tenant_id: 1, ids: [1], format: 'csv' },
|
||||
{ responseType: 'text' },
|
||||
);
|
||||
expect(typeof r).toBe('string');
|
||||
});
|
||||
|
||||
it('exportDealsXlsx() POSTs /api/deals/export с format=xlsx + responseType=blob', async () => {
|
||||
const blob = new Blob(['fake'], { type: 'application/octet-stream' });
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: blob });
|
||||
const r = await exportDealsXlsx({ tenant_id: 1, ids: [1, 2] });
|
||||
expect(apiClient.post).toHaveBeenCalledWith(
|
||||
'/api/deals/export',
|
||||
{ tenant_id: 1, ids: [1, 2], format: 'xlsx' },
|
||||
{ responseType: 'blob' },
|
||||
);
|
||||
expect(r).toBeInstanceOf(Blob);
|
||||
});
|
||||
|
||||
it('getDeal() GET /api/deals/{id} с tenant_id param', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { deal: FAKE_DEAL, events: [] } });
|
||||
const r = await getDeal(7, 1);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/deals/7', { params: { tenant_id: 1 } });
|
||||
expect(r.events).toEqual([]);
|
||||
});
|
||||
|
||||
it('listDeals() GET /api/deals с маппингом camelCase→snake_case + onlyDeleted flag', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { deals: [], total: 0, limit: 50, offset: 0 },
|
||||
});
|
||||
await listDeals({
|
||||
tenantId: 1,
|
||||
statusIn: ['new', 'in_progress'],
|
||||
projectId: 2,
|
||||
managerId: 3,
|
||||
search: 'q',
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
onlyDeleted: true,
|
||||
});
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/deals', {
|
||||
params: {
|
||||
tenant_id: 1,
|
||||
status_in: ['new', 'in_progress'],
|
||||
project_id: 2,
|
||||
manager_id: 3,
|
||||
search: 'q',
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
only_deleted: 'true',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('listDeals() без onlyDeleted → only_deleted=undefined', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { deals: [], total: 0, limit: 50, offset: 0 } });
|
||||
await listDeals({ tenantId: 1 });
|
||||
expect(apiClient.get).toHaveBeenCalledWith(
|
||||
'/api/deals',
|
||||
expect.objectContaining({ params: expect.objectContaining({ only_deleted: undefined }) }),
|
||||
);
|
||||
});
|
||||
|
||||
it('listManagers() GET /api/managers + unwraps data.managers', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { managers: [{ id: 1, email: 'm@x.ru', first_name: 'M', last_name: 'X', name: 'M X', initials: 'MX' }] },
|
||||
});
|
||||
const r = await listManagers(1);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/managers', { params: { tenant_id: 1 } });
|
||||
expect(r).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('listProjects() GET /api/projects + unwraps data.projects', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { projects: [{ id: 1, name: 'P', tag: 'site', type: 'webhook' }] },
|
||||
});
|
||||
const r = await listProjects(1);
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/projects', { params: { tenant_id: 1 } });
|
||||
expect(r[0].name).toBe('P');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../resources/js/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
ensureCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
listNotifications,
|
||||
markNotificationRead,
|
||||
markAllNotificationsRead,
|
||||
deleteNotification,
|
||||
} from '../../resources/js/api/notifications';
|
||||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||||
|
||||
describe('api/notifications', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('listNotifications() GET /api/notifications без params → unread_only=undefined', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { items: [], unread_count: 0, total: 0 } });
|
||||
const r = await listNotifications();
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/notifications', {
|
||||
params: { unread_only: undefined, limit: undefined },
|
||||
});
|
||||
expect(r.total).toBe(0);
|
||||
});
|
||||
|
||||
it('listNotifications({unreadOnly:true}) → unread_only=1 (1, не true)', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { items: [], unread_count: 0, total: 0 } });
|
||||
await listNotifications({ unreadOnly: true, limit: 20 });
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/notifications', {
|
||||
params: { unread_only: 1, limit: 20 },
|
||||
});
|
||||
});
|
||||
|
||||
it('listNotifications({unreadOnly:false}) → unread_only=undefined (не 0)', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({ data: { items: [], unread_count: 0, total: 0 } });
|
||||
await listNotifications({ unreadOnly: false });
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/notifications', {
|
||||
params: { unread_only: undefined, limit: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('markNotificationRead() PATCH /api/notifications/{id}/read + ensureCsrfCookie', async () => {
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { id: 5, read_at: '2026-05-12T20:00:00Z' } });
|
||||
const r = await markNotificationRead(5);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/api/notifications/5/read');
|
||||
expect(r.id).toBe(5);
|
||||
});
|
||||
|
||||
it('markAllNotificationsRead() POSTs /api/notifications/mark-all-read', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { updated: 12 } });
|
||||
const r = await markAllNotificationsRead();
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/notifications/mark-all-read');
|
||||
expect(r.updated).toBe(12);
|
||||
});
|
||||
|
||||
it('deleteNotification() DELETE /api/notifications/{id} + ensureCsrfCookie', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
|
||||
await deleteNotification(7);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/api/notifications/7');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../resources/js/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
ensureCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
listReminders,
|
||||
createReminder,
|
||||
updateReminder,
|
||||
completeReminder,
|
||||
deleteReminder,
|
||||
} from '../../resources/js/api/reminders';
|
||||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||||
|
||||
const FAKE_REMINDER = {
|
||||
id: 1,
|
||||
deal_id: 10,
|
||||
text: 'remind me',
|
||||
remind_at: '2026-05-13T09:00:00Z',
|
||||
completed_at: null,
|
||||
is_sent: false,
|
||||
sent_at: null,
|
||||
created_at: '2026-05-12T20:00:00Z',
|
||||
created_by: 1,
|
||||
assignee_id: null,
|
||||
creator_name: 'Demo',
|
||||
};
|
||||
|
||||
describe('api/reminders', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('listReminders() без params → все params undefined', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { items: [], counts: { active: 0, today: 0, upcoming: 0, overdue: 0 } },
|
||||
});
|
||||
await listReminders();
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/reminders', {
|
||||
params: { filter: undefined, deal_id: undefined, limit: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('listReminders({filter:"overdue", dealId, limit}) → snake_case params', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: { items: [], counts: { active: 0, today: 0, upcoming: 0, overdue: 0 } },
|
||||
});
|
||||
await listReminders({ filter: 'overdue', dealId: 42, limit: 25 });
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/reminders', {
|
||||
params: { filter: 'overdue', deal_id: 42, limit: 25 },
|
||||
});
|
||||
});
|
||||
|
||||
it('createReminder() POSTs /api/reminders + unwraps data.reminder', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { reminder: FAKE_REMINDER } });
|
||||
const r = await createReminder({ deal_id: 10, remind_at: '2026-05-13T09:00:00Z', text: 'hi' });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/reminders', {
|
||||
deal_id: 10,
|
||||
remind_at: '2026-05-13T09:00:00Z',
|
||||
text: 'hi',
|
||||
});
|
||||
expect(r.id).toBe(1);
|
||||
});
|
||||
|
||||
it('updateReminder() PATCH /api/reminders/{id} с partial payload', async () => {
|
||||
vi.mocked(apiClient.patch).mockResolvedValue({ data: { reminder: FAKE_REMINDER } });
|
||||
await updateReminder(1, { text: 'updated', assignee_id: 5 });
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.patch).toHaveBeenCalledWith('/api/reminders/1', { text: 'updated', assignee_id: 5 });
|
||||
});
|
||||
|
||||
it('completeReminder() POSTs /api/reminders/{id}/complete', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { reminder: FAKE_REMINDER } });
|
||||
await completeReminder(3);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/reminders/3/complete');
|
||||
});
|
||||
|
||||
it('deleteReminder() DELETE /api/reminders/{id}', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
|
||||
await deleteReminder(7);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/api/reminders/7');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
vi.mock('../../resources/js/api/client', () => ({
|
||||
apiClient: {
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
patch: vi.fn(),
|
||||
delete: vi.fn(),
|
||||
},
|
||||
ensureCsrfCookie: vi.fn(),
|
||||
}));
|
||||
|
||||
import {
|
||||
listReportJobs,
|
||||
createReportJob,
|
||||
retryReportJob,
|
||||
cancelReportJob,
|
||||
deleteReportJob,
|
||||
} from '../../resources/js/api/reports';
|
||||
import { apiClient, ensureCsrfCookie } from '../../resources/js/api/client';
|
||||
|
||||
const FAKE_JOB = {
|
||||
id: 1,
|
||||
type: 'deals_export' as const,
|
||||
parameters: { format: 'csv' as const, date_from: '2026-05-01', date_to: '2026-05-12' },
|
||||
status: 'pending' as const,
|
||||
file_path: null,
|
||||
file_size: null,
|
||||
generation_seconds: null,
|
||||
error_message: null,
|
||||
created_at: '2026-05-12T20:00:00Z',
|
||||
finished_at: null,
|
||||
expires_at: null,
|
||||
is_expired: false,
|
||||
retry_count: 0,
|
||||
retry_max: 3,
|
||||
};
|
||||
|
||||
describe('api/reports', () => {
|
||||
beforeEach(() => vi.clearAllMocks());
|
||||
|
||||
it('listReportJobs() без params → все undefined', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
jobs: [],
|
||||
total: 0,
|
||||
limit: 50,
|
||||
offset: 0,
|
||||
counts: { pending: 0, processing: 0, done: 0, failed: 0 },
|
||||
quota: { active: 0, max_active: 5 },
|
||||
},
|
||||
});
|
||||
await listReportJobs();
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/reports/jobs', {
|
||||
params: { status: undefined, limit: undefined, offset: undefined },
|
||||
});
|
||||
});
|
||||
|
||||
it('listReportJobs({status, limit, offset}) → passes params', async () => {
|
||||
vi.mocked(apiClient.get).mockResolvedValue({
|
||||
data: {
|
||||
jobs: [FAKE_JOB],
|
||||
total: 1,
|
||||
limit: 25,
|
||||
offset: 0,
|
||||
counts: { pending: 1, processing: 0, done: 0, failed: 0 },
|
||||
quota: { active: 1, max_active: 5 },
|
||||
},
|
||||
});
|
||||
const r = await listReportJobs({ status: 'pending', limit: 25, offset: 0 });
|
||||
expect(apiClient.get).toHaveBeenCalledWith('/api/reports/jobs', {
|
||||
params: { status: 'pending', limit: 25, offset: 0 },
|
||||
});
|
||||
expect(r.jobs).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('createReportJob() POSTs /api/reports/jobs + unwraps data.job', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { job: FAKE_JOB } });
|
||||
const payload = {
|
||||
type: 'deals_export' as const,
|
||||
format: 'csv' as const,
|
||||
parameters: { date_from: '2026-05-01', date_to: '2026-05-12', project_id: 7 },
|
||||
};
|
||||
const r = await createReportJob(payload);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/reports/jobs', payload);
|
||||
expect(r.id).toBe(1);
|
||||
});
|
||||
|
||||
it('retryReportJob() POSTs /api/reports/jobs/{id}/retry', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { job: { ...FAKE_JOB, retry_count: 1 } } });
|
||||
const r = await retryReportJob(42);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/reports/jobs/42/retry');
|
||||
expect(r.retry_count).toBe(1);
|
||||
});
|
||||
|
||||
it('cancelReportJob() POSTs /api/reports/jobs/{id}/cancel', async () => {
|
||||
vi.mocked(apiClient.post).mockResolvedValue({ data: { job: { ...FAKE_JOB, status: 'failed' as const } } });
|
||||
await cancelReportJob(42);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.post).toHaveBeenCalledWith('/api/reports/jobs/42/cancel');
|
||||
});
|
||||
|
||||
it('deleteReportJob() DELETE /api/reports/jobs/{id}', async () => {
|
||||
vi.mocked(apiClient.delete).mockResolvedValue({ data: undefined });
|
||||
await deleteReportJob(7);
|
||||
expect(ensureCsrfCookie).toHaveBeenCalledOnce();
|
||||
expect(apiClient.delete).toHaveBeenCalledWith('/api/reports/jobs/7');
|
||||
});
|
||||
});
|
||||
@@ -116,6 +116,20 @@ C. Vuetify v-tooltip eager-mount artifact (aria-tooltip-name): role="tooltip" ov
|
||||
|
||||
### Q.DEFER.003 — Coverage debt: api/* layer + security cards (Phase 13 extra)
|
||||
|
||||
**✅ CLOSED 12.05.2026 ночь (post-audit continuation) — sub-A api/* tests:**User chose (A) api/* unit tests first (highest ROI). Created 5 new spec files (43 new tests):
|
||||
|
||||
- `tests/Frontend/auth-api.spec.ts` — 13 tests (login/register/me/logout/verify2FA/recoveryCode/twoFactorInit/Confirm/Disable/RegenerateRecoveryCodes/forgotPassword/resetPassword/updateNotificationPreferences)
|
||||
- `tests/Frontend/deals-api.spec.ts` — 12 tests (createDeal/bulkDelete/bulkRestore/update/transition/exportCSV/exportXLSX/getDeal/listDeals×2/listManagers/listProjects)
|
||||
- `tests/Frontend/notifications-api.spec.ts` — 6 tests (listNotifications×3 для unreadOnly variants/markRead/markAllRead/delete)
|
||||
- `tests/Frontend/reminders-api.spec.ts` — 6 tests (listReminders×2/create/update/complete/delete)
|
||||
- `tests/Frontend/reports-api.spec.ts` — 6 tests (listReportJobs×2/create/retry/cancel/delete)
|
||||
|
||||
Подход: `vi.mock('../../resources/js/api/client')` → mock `apiClient.{get,post,patch,delete}` + `ensureCsrfCookie`. Каждый тест проверяет: (1) correct HTTP method, (2) correct URL, (3) correct params/body (camelCase→snake_case mapping), (4) data unwrap, (5) `ensureCsrfCookie` called for mutating endpoints.
|
||||
|
||||
**Vitest delta: 614 → 657 passed (+43); 79 → 84 files (+5). 0 failed.** Coverage api/* layer bumped from ≈0% Stmts на функциях (auth/deals/notifications/reminders/reports) к высокому покрытию (каждая exported функция exercised).
|
||||
|
||||
**Sub-B (security cards 28%) + sub-C (router guards 33%) remain DEFERRED:** не критичны для Q.DEFER.003 first-phase closure (sub-A api/* — highest ROI per blocked.md рекомендации). Если потом сессия требует — отдельные Q-items в новой сессии.
|
||||
|
||||
**Из:** Phase 13 Vitest coverage report (subagent fresh run 2026-05-12 ночь).
|
||||
**Severity при отказе:** P1 long-term maintainability + security regression risk.
|
||||
**Контекст:**
|
||||
|
||||
Reference in New Issue
Block a user