import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createPinia, setActivePinia } from 'pinia'; // Мокаем api/auth до import'а auth-store. vi.mock('../../resources/js/api/auth', () => ({ login: vi.fn(), register: vi.fn(), me: vi.fn(), logout: vi.fn(), verifyTwoFactor: vi.fn(), forgotPassword: vi.fn(), resetPassword: vi.fn(), useRecoveryCode: vi.fn(), })); // Мокаем client (extractRateLimitRetry) — иначе axios.isAxiosError в jsdom возвращает false для plain Error'ов. vi.mock('../../resources/js/api/client', () => ({ extractRateLimitRetry: vi.fn(() => null), extractValidationErrors: vi.fn(() => null), extractErrorMessage: vi.fn(() => 'Произошла ошибка.'), apiClient: {}, ensureCsrfCookie: vi.fn(), })); import * as authApi from '../../resources/js/api/auth'; import * as apiClient from '../../resources/js/api/client'; import { useAuthStore } from '../../resources/js/stores/auth'; describe('useAuthStore', () => { beforeEach(() => { setActivePinia(createPinia()); vi.clearAllMocks(); }); it('initial state: user=null, isAuthenticated=false, requires2fa=false', () => { const auth = useAuthStore(); expect(auth.user).toBeNull(); expect(auth.isAuthenticated).toBe(false); expect(auth.requires2fa).toBe(false); expect(auth.loading).toBe(false); }); it('login() при requires_2fa=true НЕ ставит user (pending state) — isAuthenticated остаётся false', async () => { vi.mocked(authApi.login).mockResolvedValue({ user: { id: 1, email: 'test@example.ru', first_name: 'T', last_name: 'U', tenant_id: 1, totp_enabled: true, last_login_at: '2026-05-08T12:00:00Z', }, requires_2fa: true, }); const auth = useAuthStore(); await auth.login({ email: 'test@example.ru', password: 'pass1234' }); // user НЕ ставится при 2FA — backend ещё не сделал Auth::login. expect(auth.user).toBeNull(); expect(auth.isAuthenticated).toBe(false); expect(auth.requires2fa).toBe(true); }); it('login() при requires_2fa=false ставит user — isAuthenticated=true', async () => { vi.mocked(authApi.login).mockResolvedValue({ user: { id: 2, email: 'no2fa@example.ru', first_name: 'N', last_name: 'U', tenant_id: 1, totp_enabled: false, last_login_at: null, }, requires_2fa: false, }); const auth = useAuthStore(); await auth.login({ email: 'no2fa@example.ru', password: 'pass1234' }); expect(auth.user?.email).toBe('no2fa@example.ru'); expect(auth.isAuthenticated).toBe(true); expect(auth.requires2fa).toBe(false); }); it('verifyTwoFactor() с правильным кодом ставит user + сбрасывает requires2fa', async () => { vi.mocked(authApi.verifyTwoFactor).mockResolvedValue({ user: { id: 5, email: '2fa-done@example.ru', first_name: '2', last_name: 'F', tenant_id: 1, totp_enabled: true, last_login_at: '2026-05-08T12:00:00Z', }, requires_2fa: false, }); const auth = useAuthStore(); // Эмулируем pending-state после login. auth.requires2fa = true; await auth.verifyTwoFactor('123456'); expect(auth.user?.email).toBe('2fa-done@example.ru'); expect(auth.isAuthenticated).toBe(true); expect(auth.requires2fa).toBe(false); expect(authApi.verifyTwoFactor).toHaveBeenCalledWith('123456'); }); it('verifyTwoFactor() при reject пробрасывает + НЕ ставит user', async () => { vi.mocked(authApi.verifyTwoFactor).mockRejectedValue(new Error('Invalid code')); const auth = useAuthStore(); auth.requires2fa = true; await expect(auth.verifyTwoFactor('000000')).rejects.toThrow(); expect(auth.user).toBeNull(); expect(auth.requires2fa).toBe(true); }); it('login() → reject пробрасывает ошибку и оставляет user=null', async () => { vi.mocked(authApi.login).mockRejectedValue(new Error('Invalid credentials')); const auth = useAuthStore(); await expect(auth.login({ email: 'bad@example.ru', password: 'wrong' })).rejects.toThrow(); expect(auth.user).toBeNull(); expect(auth.isAuthenticated).toBe(false); }); it('register() → success ставит user', async () => { vi.mocked(authApi.register).mockResolvedValue({ user: { id: 2, email: 'new@example.ru', first_name: 'N', last_name: 'U', tenant_id: 1, totp_enabled: false, last_login_at: null, }, requires_2fa: false, }); const auth = useAuthStore(); await auth.register({ email: 'new@example.ru', password: 'pass1234', accept_offer: true, accept_pdn: true, }); expect(auth.user?.email).toBe('new@example.ru'); expect(auth.isAuthenticated).toBe(true); }); it('fetchMe() → success возвращает user + ставит state', async () => { vi.mocked(authApi.me).mockResolvedValue({ id: 5, email: 'me@example.ru', first_name: 'M', last_name: 'E', tenant_id: 1, totp_enabled: false, last_login_at: null, }); const auth = useAuthStore(); const result = await auth.fetchMe(); expect(result?.email).toBe('me@example.ru'); expect(auth.user?.email).toBe('me@example.ru'); }); it('fetchMe() → 401 НЕ throw, возвращает null + чистит user', async () => { vi.mocked(authApi.me).mockRejectedValue(new Error('Unauthenticated')); const auth = useAuthStore(); const result = await auth.fetchMe(); expect(result).toBeNull(); expect(auth.user).toBeNull(); }); it('login() при 429 кладёт retry_after в lockoutSeconds + пробрасывает ошибку', async () => { vi.mocked(authApi.login).mockRejectedValue({ response: { status: 429, data: { retry_after: 600 } } }); vi.mocked(apiClient.extractRateLimitRetry).mockReturnValueOnce(600); const auth = useAuthStore(); await expect(auth.login({ email: 'lock@example.ru', password: 'whatever-pass' })).rejects.toBeDefined(); expect(auth.lockoutSeconds).toBe(600); expect(auth.user).toBeNull(); }); it('verifyTwoFactor() при 429 кладёт retry_after в lockoutSeconds', async () => { vi.mocked(authApi.verifyTwoFactor).mockRejectedValue({ response: { status: 429 } }); vi.mocked(apiClient.extractRateLimitRetry).mockReturnValueOnce(900); const auth = useAuthStore(); auth.requires2fa = true; await expect(auth.verifyTwoFactor('000000')).rejects.toBeDefined(); expect(auth.lockoutSeconds).toBe(900); }); it('успешный login() сбрасывает lockoutSeconds', async () => { vi.mocked(authApi.login).mockResolvedValue({ user: { id: 1, email: 'ok@example.ru', first_name: 'O', last_name: 'K', tenant_id: 1, totp_enabled: false, last_login_at: null, }, requires_2fa: false, }); const auth = useAuthStore(); // Эмулируем предыдущий lockout. auth.lockoutSeconds = 300; await auth.login({ email: 'ok@example.ru', password: 'pass1234' }); expect(auth.lockoutSeconds).toBeNull(); }); it('requestPasswordReset() → success возвращает message без изменения user-state', async () => { vi.mocked(authApi.forgotPassword).mockResolvedValue({ message: 'Если такой email зарегистрирован — ссылка отправлена.', }); const auth = useAuthStore(); const result = await auth.requestPasswordReset('forgot@example.ru'); expect(result.message).toContain('ссылка отправлена'); expect(authApi.forgotPassword).toHaveBeenCalledWith('forgot@example.ru'); // user-state не меняется. expect(auth.user).toBeNull(); expect(auth.isAuthenticated).toBe(false); }); it('requestPasswordReset() при 429 кладёт retry_after в lockoutSeconds', async () => { vi.mocked(authApi.forgotPassword).mockRejectedValue({ response: { status: 429 } }); vi.mocked(apiClient.extractRateLimitRetry).mockReturnValueOnce(900); const auth = useAuthStore(); await expect(auth.requestPasswordReset('locked@example.ru')).rejects.toBeDefined(); expect(auth.lockoutSeconds).toBe(900); }); it('resetPassword() success возвращает message без auth-state', async () => { vi.mocked(authApi.resetPassword).mockResolvedValue({ message: 'Пароль успешно изменён.' }); const auth = useAuthStore(); const result = await auth.resetPassword({ token: 'abc', email: 'a@b.ru', password: 'new-strong-pass-123', password_confirmation: 'new-strong-pass-123', }); expect(result.message).toContain('успешно'); expect(auth.user).toBeNull(); }); it('resetPassword() при 429 кладёт retry_after в lockoutSeconds', async () => { vi.mocked(authApi.resetPassword).mockRejectedValue({ response: { status: 429 } }); vi.mocked(apiClient.extractRateLimitRetry).mockReturnValueOnce(900); const auth = useAuthStore(); await expect( auth.resetPassword({ token: 'bad', email: 'a@b.ru', password: 'new-strong-pass-123', password_confirmation: 'new-strong-pass-123', }), ).rejects.toBeDefined(); expect(auth.lockoutSeconds).toBe(900); }); it('useRecoveryCode() success ставит user + сбрасывает requires2fa', async () => { vi.mocked(authApi.useRecoveryCode).mockResolvedValue({ user: { id: 7, email: 'recovery@example.ru', first_name: 'R', last_name: 'C', tenant_id: 1, totp_enabled: true, last_login_at: '2026-05-09T12:00:00Z', }, requires_2fa: false, recovery_codes_remaining: 3, }); const auth = useAuthStore(); auth.requires2fa = true; const result = await auth.useRecoveryCode('ABCD-1234'); expect(result.recovery_codes_remaining).toBe(3); expect(auth.user?.email).toBe('recovery@example.ru'); expect(auth.requires2fa).toBe(false); }); it('useRecoveryCode() при reject пробрасывает + НЕ ставит user', async () => { vi.mocked(authApi.useRecoveryCode).mockRejectedValue(new Error('Invalid code')); const auth = useAuthStore(); auth.requires2fa = true; await expect(auth.useRecoveryCode('WRONG-1234')).rejects.toThrow(); expect(auth.user).toBeNull(); expect(auth.requires2fa).toBe(true); }); it('logout() очищает user даже если API упал', async () => { vi.mocked(authApi.logout).mockRejectedValue(new Error('Network error')); const auth = useAuthStore(); // Сначала задаём user. auth.user = { id: 1, email: 'x@example.ru', first_name: 'X', last_name: 'X', tenant_id: 1, totp_enabled: false, last_login_at: null, }; await auth.logout(); expect(auth.user).toBeNull(); expect(auth.requires2fa).toBe(false); }); });