Тестирование граничных случаев: пустой пароль, unicode, длинные строки

Пустой пароль

Обработка пустых строк — один из наиболее критичных сценариев при тестировании механизмов хеширования паролей. Библиотека Password-hash в JavaScript, как правило, принимает строку и возвращает её криптографический хеш с солью. Однако пустая строка ("") не должна рассматриваться как валидный пароль в большинстве систем аутентификации.

При тестировании важно учитывать два уровня поведения:

  1. Уровень библиотеки хеширования
  2. Уровень бизнес-логики приложения

С точки зрения библиотеки хеширования:

import passwordHash from 'password-hash';

const hash = passwordHash.generate('');
console.log(hash);

Большинство реализаций не выбрасывают ошибку, а спокойно вычисляют хеш от пустой строки. Это создаёт риск ложного ощущения допустимости такого пароля.

На уровне приложения необходимо проверять входные данные до вызова хеширования:

function safeHashPassword(password) {
  if (typeof password !== 'string' || password.length === 0) {
    throw new Error('Пароль не может быть пустым');
  }
  return passwordHash.generate(password);
}

Граничный случай: строка из пробелов

"   "

Она не является пустой с точки зрения длины, но по смыслу эквивалентна отсутствию пароля. Поэтому важно применять дополнительную нормализацию:

if (password.trim().length === 0) {
  throw new Error('Пароль не может состоять только из пробелов');
}

Unicode-символы и нормализация строк

Unicode вводит значительную сложность в тестирование паролей, поскольку визуально одинаковые строки могут иметь различное бинарное представление.

Примеры проблемных сценариев:

  • символы с диакритикой (é vs e + ́)
  • emoji (составные последовательности)
  • разные формы нормализации Unicode
Проблема эквивалентности строк
const a = 'café';      // NFC
const b = 'cafe\u0301'; // NFD

console.log(a === b); // false

Для системы аутентификации это критично: пользователь может вводить один и тот же пароль разными способами, но хеши будут различаться.

Решение: нормализация перед хешированием
function normalizePassword(password) {
  return password.normalize('NFC');
}

const hash = passwordHash.generate(normalizePassword(password));

Важно фиксировать выбранную форму нормализации (обычно NFC) и использовать её и при регистрации, и при входе.

Emoji и составные символы
const password = '?pass';

Emoji может занимать несколько UTF-16 кодовых единиц. При посимвольной обработке (password.length) возможны некорректные результаты.

Пример:

console.log('?'.length); // 2

Это влияет на:

  • проверку минимальной длины
  • лимиты на длину пароля
  • UI-индикаторы

Рекомендуется использовать Unicode-aware подсчёт:

const length = [...password].length;

Длинные строки и нагрузочное тестирование

Длинные пароли — потенциальный вектор атак на производительность (DoS через вычисление хеша).

Типичный сценарий:

const longPassword = 'a'.repeat(10_000_000);
const hash = passwordHash.generate(longPassword);

Риски:

  • рост времени хеширования
  • увеличение потребления памяти
  • блокировка event loop в Node.js
Ограничение длины пароля

Практическое решение — ввод жёсткого лимита:

const MAX_PASSWORD_LENGTH = 4096;

function validatePassword(password) {
  if (password.length > MAX_PASSWORD_LENGTH) {
    throw new Error('Пароль слишком длинный');
  }
}

Важно понимать, что многие криптографические библиотеки всё равно используют внутреннее усечение входа, но полагаться на это нельзя.

Тестирование устойчивости

Для проверки производительности применяются нагрузочные тесты:

import passwordHash from 'password-hash';

console.time('hash');
const hash = passwordHash.generate('a'.repeat(100000));
console.timeEnd('hash');

Цель — выявить экспоненциальные деградации или зависимость алгоритма от длины входа.


Граничные случаи кодировок и бинарных данных

Иногда в систему могут попасть строки, содержащие неожиданные управляющие символы:

  • \u0000 (null byte)
  • невидимые символы (\u200B zero-width space)
  • управляющие символы Unicode категории C

Пример:

const password = 'pass\u200Bword';

Визуально строка выглядит как password, но фактически это разные данные.

Рекомендация по очистке
function sanitize(password) {
  return password.replace(/[\u200B-\u200D\uFEFF]/g, '');
}

Особенности сравнения хешей при тестировании

Граничные случаи важно проверять не только на этапе генерации, но и на этапе сравнения:

const hash = passwordHash.generate('password');

console.log(passwordHash.verify('', hash)); // false

Для пустых и нормализованных строк важно убедиться, что:

  • одинаковые входы дают одинаковый хеш
  • разные представления Unicode не проходят проверку
  • пустые строки не валидируются случайно

Комбинированные граничные сценарии

Наиболее сложные случаи возникают при комбинации факторов:

  • пустая строка + пробелы
  • Unicode + длинные строки
  • emoji + нормализация + лимиты длины

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

describe('Password edge cases', () => {
  test('unicode normalization consistency', () => {
    const p1 = 'café';
    const p2 = 'cafe\u0301';

    const h1 = passwordHash.generate(p1.normalize('NFC'));
    const h2 = passwordHash.generate(p2.normalize('NFC'));

    expect(h1).toBe(h2);
  });

  test('empty password rejection', () => {
    expect(() => {
      safeHashPassword('');
    }).toThrow();
  });

  test('long password rejection', () => {
    expect(() => {
      safeHashPassword('a'.repeat(5000));
    }).toThrow();
  });
});

Поведение при нестандартных типах входа

JavaScript позволяет передавать нестроковые значения, что также требует тестирования:

passwordHash.generate(null);
passwordHash.generate(undefined);
passwordHash.generate(12345);

Некоторые реализации приводят их к строке, другие выбрасывают исключение. Поэтому обязательна явная валидация:

function assertString(password) {
  if (typeof password !== 'string') {
    throw new Error('Пароль должен быть строкой');
  }
}

Итоговые аспекты тестирования граничных случаев

Ключевые направления проверки в контексте Password-hash:

  • пустые строки и строки с пробелами
  • Unicode-эквивалентность и нормализация
  • emoji и составные символы
  • экстремально длинные строки
  • управляющие и невидимые символы
  • некорректные типы входных данных
  • согласованность генерации и проверки хеша