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)
144 lines
6.0 KiB
TypeScript
144 lines
6.0 KiB
TypeScript
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: 'Не удалось создать пользователя. Проверьте соединение и попробуйте ещё раз' });
|
||
});
|
||
});
|