Files
iistwin/tests/password-flow.test.ts
Ильяс Султанов c8d7f9b336 fix(auth-ui): тихий фейл 400 при смене пароля, валидация, пароль при создании
PLAN-user-password-fixes.md (инцидент 2026-09-07, user 55/56):
- toServiceError в client auth.service: BadRequestError → {success:false, error}
- полная клиентская валидация пароля в ProfilePasswordRow
- опциональный пароль при создании пользователя (UserModal + POST /api/users)
- onError с toast в PollCard/ReactionBar (этап 4)
- 11 новых тестов (129/129)
2026-09-08 16:42:59 +03:00

144 lines
6.0 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 { vi, describe, it, expect, beforeEach } from 'vitest';
// Мокаем только apiRequest — классы ошибок (BadRequestError/ConflictError)
// оставляем настоящими, чтобы сервис распознавал их через instanceof.
const mockApiRequest = vi.hoisted(() => vi.fn());
vi.mock('@/lib/queryClient', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/lib/queryClient')>();
return {
...actual,
apiRequest: mockApiRequest,
};
});
import { authService } from '../client/src/services/auth.service';
import { BadRequestError, ConflictError } from '@/lib/queryClient';
const PASSWORD_RULE_ERROR = 'Пароль должен содержать заглавные и строчные буквы, цифры и специальные символы';
const NETWORK_FALLBACK = 'Не удалось изменить пароль. Проверьте соединение и попробуйте ещё раз';
function okResponse(body: unknown) {
return { json: async () => body } as Response;
}
describe('authService.changeUserPassword', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('при 400 с текстом правила возвращает { success: false, error } без throw', async () => {
mockApiRequest.mockRejectedValue(new BadRequestError({ success: false, error: PASSWORD_RULE_ERROR }));
const result = await authService.changeUserPassword(1, 'weak');
expect(result).toEqual({ success: false, error: PASSWORD_RULE_ERROR });
expect(mockApiRequest).toHaveBeenCalledWith('PUT', '/api/users/1/password', { password: 'weak' });
});
it('при 200 возвращает ответ сервера как есть', async () => {
mockApiRequest.mockResolvedValue(okResponse({ success: true, message: 'Пароль пользователя изменен' }));
const result = await authService.changeUserPassword(1, 'Strong1!');
expect(result).toEqual({ success: true, message: 'Пароль пользователя изменен' });
});
it('при сетевой ошибке возвращает общий текст', async () => {
mockApiRequest.mockRejectedValue(new TypeError('Failed to fetch'));
const result = await authService.changeUserPassword(1, 'Strong1!');
expect(result).toEqual({ success: false, error: NETWORK_FALLBACK });
});
it('при 400 без тела ответа возвращает общий текст, а не технический bad_request', async () => {
mockApiRequest.mockRejectedValue(new BadRequestError(undefined));
const result = await authService.changeUserPassword(1, 'Strong1!');
expect(result).toEqual({ success: false, error: NETWORK_FALLBACK });
});
});
describe('authService.changePassword', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('при 400 с текстом правила возвращает { success: false, error } без throw', async () => {
mockApiRequest.mockRejectedValue(new BadRequestError({ success: false, error: PASSWORD_RULE_ERROR }));
const result = await authService.changePassword('Old1!aaaa', 'weak');
expect(result).toEqual({ success: false, error: PASSWORD_RULE_ERROR });
expect(mockApiRequest).toHaveBeenCalledWith('POST', '/api/auth/change-password', {
currentPassword: 'Old1!aaaa',
newPassword: 'weak',
});
});
it('при 200 возвращает успех', async () => {
mockApiRequest.mockResolvedValue(okResponse({ success: true, message: 'Пароль успешно изменен' }));
const result = await authService.changePassword('Old1!aaaa', 'New1!bbbb');
expect(result).toEqual({ success: true, message: 'Пароль успешно изменен' });
});
it('при сетевой ошибке возвращает общий текст', async () => {
mockApiRequest.mockRejectedValue(new TypeError('Failed to fetch'));
const result = await authService.changePassword('Old1!aaaa', 'New1!bbbb');
expect(result).toEqual({ success: false, error: NETWORK_FALLBACK });
});
});
describe('authService.createUser', () => {
beforeEach(() => {
vi.clearAllMocks();
});
const userData = {
email: 'new@example.com',
firstName: 'Иван',
lastName: 'Петров',
appRole: 'user',
sendInvite: true,
};
it('при 400 (дубликат email / слабый пароль) возвращает текст сервера без throw', async () => {
mockApiRequest.mockRejectedValue(new BadRequestError({ success: false, error: 'Пользователь с таким email уже существует в организации' }));
const result = await authService.createUser(userData);
expect(result).toEqual({ success: false, error: 'Пользователь с таким email уже существует в организации' });
});
it('при 409 возвращает текст конфликта без throw', async () => {
mockApiRequest.mockRejectedValue(new ConflictError({ success: false, error: 'Конфликт данных' }));
const result = await authService.createUser(userData);
expect(result).toEqual({ success: false, error: 'Конфликт данных' });
});
it('при 201 возвращает успех с пользователем', async () => {
const body = { success: true, message: 'Пользователь создан', user: { id: 5, email: userData.email } };
mockApiRequest.mockResolvedValue(okResponse(body));
const result = await authService.createUser(userData);
expect(result).toEqual(body);
});
it('при сетевой ошибке возвращает общий текст', async () => {
mockApiRequest.mockRejectedValue(new TypeError('Failed to fetch'));
const result = await authService.createUser(userData);
expect(result).toEqual({ success: false, error: 'Не удалось создать пользователя. Проверьте соединение и попробуйте ещё раз' });
});
});