Работа с JWT, JWS и JWE через библиотеку jose
предполагает использование криптографических ключей и операций, которые
в реальной среде требуют высокой энтропии, защищённого хранения и
строгой валидации. В unit-тестах подобные требования избыточны и
замедляют выполнение. Мокирование позволяет:
В контексте jose чаще всего мокируются:
Для алгоритмов вроде 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 и других
используются пары ключей. Генерация таких ключей в тестах — дорогая
операция. Вместо этого применяются:
Пример статического ключа:
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'
}))
};
});
Это исключает реальную генерацию ключей.
Проверка токена (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 можно заменить операции шифрования и расшифровки:
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. Избыточное мокирование Полная подмена всех функций может скрыть ошибки интеграции.
Практика показывает эффективность комбинированного подхода:
Это позволяет сохранить скорость разработки и уверенность в корректности системы.
Рекомендуется:
/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 = '...';
Используются для тестирования различных сценариев без генерации на лету.
Обёртка над библиотекой снижает связанность:
// 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');
Моки должны возвращать корректные значения.
JWT можно сравнивать через snapshot:
expect(token).toMatchSnapshot();
Однако это требует стабильности генерации (фиксированные даты и ключи).
Хорошая практика — параметризуемые моки:
function mockJwtVerify(payload) {
jwtVerify.mockResolvedValue({ payload });
}
Позволяет гибко управлять поведением в разных тестах.