95f5f94a6b
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>
143 lines
6.1 KiB
TypeScript
143 lines
6.1 KiB
TypeScript
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);
|
||
});
|
||
});
|