Files
portal/app/tests/Frontend/auth-api.spec.ts
T
Дмитрий 95f5f94a6b 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>
2026-05-12 22:14:51 +03:00

143 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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);
});
});