Тестирование с реальными вызовами bcrypt и управление rounds

В bcrypt.js параметр rounds (он же cost factor) определяет количество итераций алгоритма хеширования. Он влияет напрямую на вычислительную сложность генерации хеша и, соответственно, на время выполнения операции.

2^{cost}

Именно экспоненциальный характер роста делает увеличение rounds критически важным с точки зрения безопасности и производительности. Увеличение cost на 1 примерно удваивает время вычисления хеша.

В практических реализациях bcrypt.js используется следующая модель:

  • cost = 10 → базовый уровень для большинства приложений
  • cost = 12–14 → повышенная безопасность
  • cost ≥ 15 → высокая нагрузка на CPU, используется редко

Влияние rounds на тестирование

Тестирование кода, использующего bcrypt, сталкивается с фундаментальным противоречием:

  • безопасность требует высоких rounds
  • тесты требуют высокой скорости выполнения

Хеширование пароля — одна из самых дорогих операций в типичном backend-тестировании. При большом количестве тестов даже cost = 12 может привести к значительному замедлению CI-пайплайна.

Поэтому управление rounds становится частью архитектуры тестовой стратегии, а не просто конфигурацией библиотеки.

Разделение окружений: dev, test, production

Типичная практика заключается в разделении значений cost по окружениям:

const SALT_ROUNDS = {
  test: 4,
  development: 8,
  production: 12
};

В тестовой среде значение intentionally занижается. Это допустимо, потому что цель тестов — проверка корректности логики, а не криптостойкости.

Почему нельзя просто мокать bcrypt

Мокирование bcrypt кажется простым решением:

jest.mock('bcryptjs', () => ({
  hash: jest.fn(() => Promise.resolve('hashed_password')),
  compare: jest.fn(() => Promise.resolve(true))
}));

Однако такой подход создаёт ложное чувство корректности системы:

  • не проверяется реальная совместимость hash/compare
  • не выявляются ошибки конфигурации salt rounds
  • не тестируется асинхронное поведение
  • возможны расхождения между production и test реализацией

Моки уместны только для unit-тестов бизнес-логики, но не для криптографических операций.

Подход к реальным bcrypt тестам

Более надёжная стратегия — использование реального bcrypt.js с контролируемыми rounds.

Пример базового теста:

import bcrypt from 'bcryptjs';

test('password hashing and comparison works', async () => {
  const password = 'securePassword123';
  const saltRounds = 4;

  const hash = await bcrypt.hash(password, saltRounds);
  const result = await bcrypt.compare(password, hash);

  expect(result).toBe(true);
});

Ключевой момент: проверяется не значение хеша, а его поведенческая корректность.

Детеминированность и повторяемость тестов

bcrypt использует соль, поэтому хеш одного и того же пароля всегда различается:

password + salt → hash1
password + different salt → hash2

Поэтому сравнение строк хеша в тестах бессмысленно. Единственный стабильный критерий — результат compare().

Управление rounds через переменные окружения

Практическая схема конфигурации:

const getSaltRounds = () => {
  const env = process.env.NODE_ENV;

  if (env === 'test') return 4;
  if (env === 'development') return 8;
  return 12;
};

Использование в коде:

const saltRounds = getSaltRounds();
const hash = await bcrypt.hash(password, saltRounds);

Такой подход позволяет:

  • ускорить тесты
  • сохранить безопасность production
  • избежать дублирования логики

Интеграционные тесты против unit-тестов

Разделение тестов становится критическим:

Unit-тесты

  • bcrypt мокается или обходится
  • проверяется бизнес-логика
  • минимальное время выполнения

Интеграционные тесты

  • используется реальный bcrypt.js
  • проверяется полный цикл регистрации/авторизации
  • выполняются медленнее, но ближе к production

Пример интеграционного теста:

test('user registration stores valid bcrypt hash', async () => {
  const password = 'myPassword';
  const hash = await bcrypt.hash(password, 4);

  const isValid = await bcrypt.compare(password, hash);

  expect(isValid).toBe(true);
  expect(hash).not.toBe(password);
});

Баланс между скоростью CI и достоверностью

При большом наборе тестов даже cost = 6 может замедлить pipeline. Поэтому часто применяют гибридный подход:

  • unit-тесты: bcrypt отключён или упрощён
  • интеграционные тесты: bcrypt с cost 4–6
  • staging: cost приближен к production

Так достигается баланс между скоростью и реалистичностью.

Частые ошибки при тестировании bcrypt

1. Сравнение хешей напрямую

expect(hash).toBe(expectedHash); // нестабильно

Ошибка: bcrypt всегда генерирует разные значения из-за соли.

2. Использование production rounds в тестах

Это приводит к резкому замедлению CI без пользы.

3. Полное мокирование bcrypt везде

Ломает доверие к тестам как к механизму валидации безопасности.

Практика контроля производительности тестов

Иногда полезно измерять фактическое время хеширования:

const start = Date.now();
await bcrypt.hash('password', 4);
const duration = Date.now() - start;

Это позволяет отслеживать деградацию производительности при изменении зависимостей или окружения Node.js.

Параллелизм и bcrypt

bcrypt — CPU-bound операция. При массовом запуске тестов возможна перегрузка event loop.

Решения:

  • ограничение параллелизма тестов (Jest maxWorkers)
  • снижение rounds в CI
  • группировка криптографических тестов

Основная практическая модель тестирования bcrypt.js строится вокруг контроля cost factor, отказа от прямого сравнения хешей и разделения уровней тестирования по глубине проверки и стоимости выполнения.