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)
This commit is contained in:
143
tests/password-flow.test.ts
Normal file
143
tests/password-flow.test.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
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: 'Не удалось создать пользователя. Проверьте соединение и попробуйте ещё раз' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user