Хеширование паролей с помощью bcrypt.js обладает ключевым свойством — недетерминированностью результата при одинаковом входном значении. Это напрямую влияет на подход к юнит-тестированию: сравнение хешей строка-в-строку невозможно без контроля параметров соли.
bcrypt.js использует встроенную генерацию соли, которая добавляет случайность в каждый результат хеширования. Поэтому базовая проверка вида «хеш равен ожидаемой строке» неприменима в большинстве сценариев.
В приложениях обычно выделяются две операции:
import bcrypt from 'bcryptjs';
export async function hashPassword(password) {
const saltRounds = 10;
return bcrypt.hash(password, saltRounds);
}
export async function comparePassword(password, hash) {
return bcrypt.compare(password, hash);
}
Юнит-тестирование таких функций требует разделения проверки на поведенческий уровень, а не на уровне конкретного значения строки.
Прямое сравнение результата хеширования невозможно, поэтому тестирование строится вокруг свойств результата.
Основные проверяемые свойства:
Пример теста:
import { hashPassword } from './auth';
test('hashPassword возвращает строку', async () => {
const hash = await hashPassword('secret123');
expect(typeof hash).toBe('string');
});
Проверка структуры хеша:
test('bcrypt hash имеет корректный формат', async () => {
const hash = await hashPassword('secret123');
expect(hash).toMatch(/^\$2[aby]\$\d{2}\$.+/);
});
Этот паттерн проверяет соответствие стандарту bcrypt: версия алгоритма, cost factor и соль.
Для получения детерминированного результата можно использовать фиксированную соль.
import bcrypt from 'bcryptjs';
test('детерминированное хеширование с фиксированной солью', () => {
const salt = '$2a$10$abcdefghijklmnopqrstuv';
const hash1 = bcrypt.hashSync('password', salt);
const hash2 = bcrypt.hashSync('password', salt);
expect(hash1).toBe(hash2);
});
Это позволяет проверять внутреннюю логику без влияния случайности, однако такой подход применим только в тестах.
Функция сравнения — основной объект юнит-тестирования, так как она детерминирована при корректном хеше.
test('comparePassword возвращает true для корректного пароля', async () => {
const password = 'myPassword';
const hash = await bcrypt.hash(password, 10);
const result = await bcrypt.compare(password, hash);
expect(result).toBe(true);
});
Негативный сценарий:
test('comparePassword возвращает false для неверного пароля', async () => {
const hash = await bcrypt.hash('correct', 10);
const result = await bcrypt.compare('wrong', hash);
expect(result).toBe(false);
});
bcrypt.js использует асинхронные операции, поэтому тесты должны корректно обрабатывать Promise.
Типичные подходы:
test('асинхронное сравнение пароля', async () => {
const hash = await bcrypt.hash('12345', 10);
const isValid = await bcrypt.compare('12345', hash);
expect(isValid).toBe(true);
});
В более сложных системах bcrypt часто заменяется моками для ускорения тестов и устранения зависимости от CPU-операций.
Пример с Jest:
jest.mock('bcryptjs', () => ({
hash: jest.fn(() => Promise.resolve('hashed_password')),
compare: jest.fn(() => Promise.resolve(true)),
}));
Это позволяет тестировать логику сервисов без реального хеширования.
Пример проверки вызова:
import bcrypt from 'bcryptjs';
import { hashPassword } from './auth';
test('bcrypt.hash вызывается с корректными параметрами', async () => {
await hashPassword('test');
expect(bcrypt.hash).toHaveBeenCalledWith('test', 10);
});
Несмотря на различие хешей, важно проверять, что bcrypt сохраняет корректность проверки:
test('одинаковый пароль проходит проверку с разными хешами', async () => {
const password = 'samePassword';
const hash1 = await bcrypt.hash(password, 10);
const hash2 = await bcrypt.hash(password, 10);
const result1 = await bcrypt.compare(password, hash1);
const result2 = await bcrypt.compare(password, hash2);
expect(result1).toBe(true);
expect(result2).toBe(true);
});
Особое внимание требуется к:
test('поддержка Unicode паролей', async () => {
const password = 'пароль?';
const hash = await bcrypt.hash(password, 10);
const result = await bcrypt.compare(password, hash);
expect(result).toBe(true);
});
test('пустой пароль корректно обрабатывается', async () => {
const hash = await bcrypt.hash('', 10);
const result = await bcrypt.compare('', hash);
expect(result).toBe(true);
});
bcrypt специально замедлен для повышения безопасности, поэтому в тестах часто уменьшают cost factor.
const TEST_SALT_ROUNDS = 4;
Использование низкого значения позволяет ускорить выполнение тестов без изменения логики проверки.
Важно тестировать некорректные входные данные:
test('compare возвращает false при невалидном хеше', async () => {
const result = await bcrypt.compare('password', 'invalid_hash');
expect(result).toBe(false);
});
Также возможна проверка выброса ошибок при критических сбоях (в зависимости от реализации обёртки над bcrypt).
В архитектуре тестирования bcrypt чаще рассматривается как внешняя зависимость. Это приводит к следующим практикам:
Такой подход снижает хрупкость тестов при изменении алгоритма или параметров соли.