Мокирование ключей и токенов в unit-тестах

Работа с JWT, JWS и JWE через библиотеку jose предполагает использование криптографических ключей и операций, которые в реальной среде требуют высокой энтропии, защищённого хранения и строгой валидации. В unit-тестах подобные требования избыточны и замедляют выполнение. Мокирование позволяет:

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

Типы объектов, подлежащих мокированию

В контексте jose чаще всего мокируются:

  • Ключи (симметричные и асимметричные)
  • JWT-токены (подписанные и зашифрованные)
  • Функции генерации ключей
  • Методы проверки подписи и расшифровки

Мокирование симметричных ключей

Для алгоритмов вроде HS256 используется общий секрет. В тестах допустимо использовать фиксированное значение:

const secret = new TextEncoder().encode('test-secret');

Это обеспечивает повторяемость и исключает случайность.

Пример создания тестового JWT:

import { SignJWT } from 'jose';

export async function createMockToken(payload) {
  return await new SignJWT(payload)
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt(0)
    .setExpirationTime('1h')
    .sign(secret);
}

Фиксированное время (setIssuedAt(0)) устраняет зависимость от системных часов.

Мокирование асимметричных ключей

Для алгоритмов RS256, ES256 и других используются пары ключей. Генерация таких ключей в тестах — дорогая операция. Вместо этого применяются:

  • заранее сгенерированные ключи
  • заглушки (stub-объекты)

Пример статического ключа:

const privateKey = await importPKCS8(`-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----`, 'RS256');

const publicKey = await importSPKI(`-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`, 'RS256');

Хранение ключей прямо в тестах допустимо, если они используются исключительно в тестовой среде.

Подмена генерации ключей

Функции вроде generateKeyPair можно замокировать через инструменты тестирования, например jest:

jest.mock('jose', () => {
  const original = jest.requireActual('jose');
  return {
    ...original,
    generateKeyPair: jest.fn(async () => ({
      publicKey: 'mocked-public',
      privateKey: 'mocked-private'
    }))
  };
});

Это исключает реальную генерацию ключей.

Мокирование проверки JWT

Проверка токена (jwtVerify) включает криптографическую валидацию и может быть заменена заглушкой:

jest.mock('jose', () => {
  return {
    jwtVerify: jest.fn(async (token, key) => ({
      payload: { userId: 1 },
      protectedHeader: { alg: 'HS256' }
    }))
  };
});

Такой подход позволяет тестировать бизнес-логику без зависимости от криптографии.

Контроль времени

JWT активно используют временные поля (iat, exp, nbf). Для стабильности тестов важно зафиксировать время:

jest.useFakeTimers().setSystemTime(new Date('2023-01-01'));

Это гарантирует, что токены не будут «протухать» во время тестирования.

Мокирование JWE (шифрование)

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

jest.mock('jose', () => ({
  CompactEncrypt: jest.fn().mockImplementation(() => ({
    encrypt: async () => 'mocked-encrypted-token'
  })),
  compactDecrypt: jest.fn(async () => ({
    plaintext: new TextEncoder().encode(JSON.stringify({ data: 'test' }))
  }))
}));

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

Создание фабрик упрощает повторное использование:

export function buildToken(overrides = {}) {
  return {
    sub: '123',
    role: 'user',
    ...overrides
  };
}

Комбинируется с функцией подписи:

const token = await createMockToken(buildToken({ role: 'admin' }));

Изоляция слоёв

Важно разделять:

  • слой работы с jose
  • бизнес-логику

Пример:

// crypto.js
export async function verifyToken(token) {
  return await jwtVerify(token, publicKey);
}

// service.js
export async function getUser(token) {
  const { payload } = await verifyToken(token);
  return findUser(payload.sub);
}

В тестах мокируется verifyToken, а не jose.

Частые ошибки

1. Использование реальных ключей в тестах Замедляет выполнение и усложняет поддержку.

2. Зависимость от текущего времени Приводит к нестабильным тестам.

3. Смешивание интеграционных и unit-тестов Unit-тесты не должны проверять криптографию библиотеки.

4. Избыточное мокирование Полная подмена всех функций может скрыть ошибки интеграции.

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

Практика показывает эффективность комбинированного подхода:

  • unit-тесты — с моками
  • интеграционные тесты — с реальными ключами

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

Организация тестовых ключей

Рекомендуется:

  • хранить ключи в отдельной директории (/test/keys)
  • использовать минимально допустимую длину ключей
  • явно помечать их как тестовые

Проверка ошибок

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

jwtVerify.mockImplementationOnce(async () => {
  throw new Error('Invalid token');
});

Это позволяет тестировать обработку ошибок без реальных токенов.

Вывод структуры тестов

Типичная структура:

/tests
  /mocks
    keys.js
    tokens.js
  /unit
    auth.test.js
  /integration
    jwt.test.js

Чёткое разделение повышает читаемость и поддержку.

Практика использования фикстур

Фикстуры — заранее подготовленные данные:

export const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
export const expiredToken = '...';

Используются для тестирования различных сценариев без генерации на лету.

Минимизация зависимости от jose

Обёртка над библиотекой снижает связанность:

// jwtService.js
export const jwtService = {
  sign: (payload) => new SignJWT(payload),
  verify: (token) => jwtVerify(token)
};

В тестах мокируется jwtService, а не сама библиотека.

Проверка структуры токена

Иногда достаточно проверить payload без валидации подписи:

const payload = JSON.parse(
  Buffer.from(token.split('.')[1], 'base64').toString()
);

Это ускоряет тесты, если криптография не является целью проверки.

Контроль заголовков

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

expect(header.alg).toBe('HS256');

Моки должны возвращать корректные значения.

Использование snapshot-тестов

JWT можно сравнивать через snapshot:

expect(token).toMatchSnapshot();

Однако это требует стабильности генерации (фиксированные даты и ключи).

Расширяемость моков

Хорошая практика — параметризуемые моки:

function mockJwtVerify(payload) {
  jwtVerify.mockResolvedValue({ payload });
}

Позволяет гибко управлять поведением в разных тестах.