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

Интеграционное тестирование аутентификации проверяет поведение системы как единого целого: HTTP-слой, бизнес-логику, работу с базой данных и криптографические операции с паролями. В случае использования bcrypt.js критически важно не изолировать хеширование паролей в моках, поскольку именно взаимодействие компонентов формирует реальную безопасность и корректность системы.

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


Роль bcrypt.js в цепочке аутентификации

bcrypt.js реализует алгоритм хеширования паролей на основе Blowfish cipher с добавлением соли. Основные свойства:

  • необратимость хеша
  • адаптивная сложность через cost factor (salt rounds)
  • устойчивость к радужным таблицам

Типичный сценарий использования:

  • при регистрации пароль пользователя преобразуется в хеш
  • при логине введённый пароль сравнивается с сохранённым хешем через bcrypt.compare

Архитектура тестируемого аутентификационного модуля

Интеграционные тесты обычно охватывают следующую цепочку:

HTTP запрос → контроллер → сервис аутентификации → bcrypt.js → база данных

Пример структуры:

  • AuthController — принимает запросы
  • AuthService — содержит бизнес-логику
  • UserRepository — взаимодействие с БД
  • bcrypt.js — хеширование и проверка паролей

Подготовка тестового окружения

Для интеграционных тестов используется отдельная тестовая база данных или in-memory решение (например, SQLite in-memory или MongoMemoryServer).

Пример конфигурации Jest:

module.exports = {
  testEnvironment: "node",
  setupFilesAfterEnv: ["./tests/setup.js"]
};

Подготовка базы:

beforeAll(async () => {
  await db.connect();
});

afterEach(async () => {
  await db.clear();
});

afterAll(async () => {
  await db.close();
});

Регистрация пользователя с bcrypt.js

Тест должен подтверждать, что пароль не сохраняется в открытом виде.

Пример теста:

const request = require("supertest");
const app = require("../app");
const User = require("../models/User");

test("регистрация пользователя сохраняет хеш пароля", async () => {
  await request(app)
    .post("/auth/register")
    .send({
      email: "user@test.com",
      password: "plainPassword123"
    })
    .expect(201);

  const user = await User.findOne({ email: "user@test.com" });

  expect(user.password).not.toBe("plainPassword123");
  expect(user.password.startsWith("$2")).toBe(true);
});

Проверяется не только факт создания пользователя, но и структура хеша bcrypt (префикс $2a$, $2b$ или $2y$).


Проверка корректности логина

Сценарий входа включает сравнение пароля через bcrypt.compare.

test("успешный вход с корректным паролем", async () => {
  await request(app)
    .post("/auth/register")
    .send({
      email: "login@test.com",
      password: "securePass"
    });

  const response = await request(app)
    .post("/auth/login")
    .send({
      email: "login@test.com",
      password: "securePass"
    })
    .expect(200);

  expect(response.body.token).toBeDefined();
});

Проверка отказа при неверном пароле

test("отказ при неверном пароле", async () => {
  await request(app)
    .post("/auth/register")
    .send({
      email: "fail@test.com",
      password: "correctPassword"
    });

  await request(app)
    .post("/auth/login")
    .send({
      email: "fail@test.com",
      password: "wrongPassword"
    })
    .expect(401);
});

Особенности тестирования bcrypt.js

bcrypt является CPU-интенсивной операцией, поэтому важно учитывать:

1. Salt rounds в тестовой среде

В production часто используется значение 10–12, но в тестах его снижают:

const saltRounds = process.env.NODE_ENV === "test" ? 4 : 10;

Это ускоряет выполнение тестов без изменения логики.


2. Недопустимость мокирования bcrypt в интеграционных тестах

Мокирование bcrypt.hash или bcrypt.compare:

  • искажает результат теста
  • не выявляет ошибки конфигурации
  • скрывает проблемы несовместимости версий

Интеграционные тесты должны использовать реальный bcrypt.js.


Тестирование повторного хеширования

Некоторые системы позволяют смену пароля или миграцию алгоритма.

test("обновление пароля изменяет bcrypt хеш", async () => {
  await request(app)
    .post("/auth/register")
    .send({
      email: "update@test.com",
      password: "oldPassword"
    });

  const oldUser = await User.findOne({ email: "update@test.com" });

  await request(app)
    .post("/auth/change-password")
    .send({
      email: "update@test.com",
      newPassword: "newPassword"
    });

  const updatedUser = await User.findOne({ email: "update@test.com" });

  expect(updatedUser.password).not.toBe(oldUser.password);
});

Проверка устойчивости к ошибочным данным

bcrypt должен корректно обрабатывать некорректные входы:

test("bcrypt не падает при пустом пароле", async () => {
  const response = await request(app)
    .post("/auth/login")
    .send({
      email: "test@test.com",
      password: ""
    });

  expect(response.status).toBe(400);
});

Работа с конкурентными запросами

Интеграционные тесты должны учитывать параллельные регистрации:

test("одновременная регистрация не создаёт дубликаты", async () => {
  const payload = {
    email: "race@test.com",
    password: "password123"
  };

  await Promise.all([
    request(app).post("/auth/register").send(payload),
    request(app).post("/auth/register").send(payload)
  ]);

  const users = await User.find({ email: "race@test.com" });

  expect(users.length).toBe(1);
});

Логирование и безопасность в тестах

Интеграционные тесты аутентификации часто требуют отключения логирования чувствительных данных:

  • пароли не должны попадать в логи
  • хеши bcrypt допустимы, но не должны раскрывать исходные значения
  • ошибки должны быть обобщёнными

Проверка структуры хеша bcrypt

Хеш bcrypt имеет предсказуемую структуру:

$2b$10$......................

Тестирование может включать:

expect(user.password).toMatch(/^\$2[aby]\$\d{2}\$/);

Это гарантирует корректный алгоритм и cost factor.


Проверка миграции salt rounds

При изменении конфигурации безопасности может потребоваться перехеширование:

test("обновление salt rounds приводит к перехешированию", async () => {
  process.env.BCRYPT_ROUNDS = "12";

  await request(app)
    .post("/auth/register")
    .send({
      email: "salt@test.com",
      password: "testpass"
    });

  const user = await User.findOne({ email: "salt@test.com" });

  const rounds = parseInt(user.password.split("$")[2], 10);

  expect(rounds).toBe(12);
});