Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/content/docs/guides/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"large-files",
"secure-storage",
"crypto-wallet",
"testing-with-jest",
"migration",
"nitro-integration"
]
Expand Down
67 changes: 67 additions & 0 deletions docs/content/docs/guides/testing-with-jest.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
title: Testing with Jest
description: Run code that uses react-native-quick-crypto in Jest
---

import { Callout } from 'fumadocs-ui/components/callout';

Jest runs your tests in Node, where the native module behind `react-native-quick-crypto` doesn't exist, so importing it throws. The package ships a mock for this.

Node already implements the same `crypto` API that this library brings to React Native, so the mock forwards to `node:crypto` instead of stubbing each function. Hashes, ciphertexts and signatures in your tests are real, and your code runs unchanged.

## Setup

Register the mock once in a [Jest setup file](https://jestjs.io/docs/configuration#setupfilesafterenv-array):

```js title="jest.setup.js"
jest.mock('react-native-quick-crypto', () =>
require('react-native-quick-crypto/jest'),
);
```

```js title="jest.config.js"
module.exports = {
preset: 'react-native', // or 'jest-expo'
setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};
```

Or call `jest.mock(...)` at the top of an individual test file.

## Usage

Default and named imports both work:

```ts
import QuickCrypto, { createHash, randomBytes } from 'react-native-quick-crypto';

test('hashes a password', () => {
const salt = randomBytes(16);
const hash = createHash('sha256').update(salt).update('hunter2').digest('hex');
expect(hash).toHaveLength(64);
});

test('verifies an HMAC with subtle', async () => {
const key = await QuickCrypto.subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify'],
);
const data = new TextEncoder().encode('hello');
const signature = await QuickCrypto.subtle.sign('HMAC', key, data);
expect(await QuickCrypto.subtle.verify('HMAC', key, signature, data)).toBe(true);
});
```

`install()` is a no-op in the mock, because Node already provides `globalThis.crypto` and `Buffer`.

<Callout title="APIs Node doesn't have" type="warn">
The mock only covers what `node:crypto` provides. Library-specific APIs such as `blake3`, ML-KEM, `xsalsa20`, `randomUUIDv7`, and the `hkdfExtract`/`hkdfExpand` helpers are `undefined`, and `argon2` needs Node 24.7 or newer. If your code uses them, extend the mock:

```js
jest.mock('react-native-quick-crypto', () => ({
...require('react-native-quick-crypto/jest'),
blake3: jest.fn(),
}));
```
</Callout>
16 changes: 15 additions & 1 deletion packages/react-native-quick-crypto/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ export default [
languageOptions: {
parser: typescriptEslint.parser,
parserOptions: {
projectService: true,
projectService: {
// The Jest mock is plain CommonJS shipped as-is, outside tsconfig.
allowDefaultProject: ['jest/*.js'],
},
},
},
plugins: {
Expand Down Expand Up @@ -49,6 +52,17 @@ export default [
'react-native/no-inline-styles': 'warn',
},
},
// Jest mock: CommonJS that runs in Node
{
files: ['jest/*.js'],
languageOptions: {
sourceType: 'commonjs',
globals: { require: 'readonly', module: 'writable' },
},
rules: {
'@typescript-eslint/no-require-imports': 'off',
},
},
// Ignore patterns
{
ignores: [
Expand Down
16 changes: 16 additions & 0 deletions packages/react-native-quick-crypto/jest/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Jest mock: forwards to node:crypto. See docs/content/docs/guides/testing-with-jest.mdx.
const crypto = require('node:crypto');
const { Buffer } = require('node:buffer');

const QuickCrypto = {
...crypto,
Buffer,
CryptoKey: globalThis.CryptoKey,
install: () => {},
};

module.exports = {
__esModule: true,
default: QuickCrypto,
...QuickCrypto,
};
1 change: 1 addition & 0 deletions packages/react-native-quick-crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"files": [
"src",
"lib",
"jest",
"android/build.gradle",
"android/gradle.properties",
"android/CMakeLists.txt",
Expand Down
58 changes: 58 additions & 0 deletions packages/react-native-quick-crypto/test/jestMock.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// `virtual` because the package can't resolve its own name inside this repo;
// apps use the same call without it.
jest.mock('react-native-quick-crypto', () => require('../jest'), {
virtual: true,
});

import QuickCrypto, {
CryptoKey,
createCipheriv,
createDecipheriv,
createHash,
install,
randomBytes,
} from 'react-native-quick-crypto';

test('jest mock exposes the default export and named exports', () => {
expect(typeof QuickCrypto.createHash).toBe('function');
expect(QuickCrypto.createHash).toBe(createHash);
expect(typeof QuickCrypto.Buffer.from).toBe('function');
expect(() => install()).not.toThrow();
});

test('jest mock hashes with real results', () => {
expect(createHash('sha256').update('abc').digest('hex')).toBe(
'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
);
});

test('jest mock round-trips AES-256-GCM', () => {
const key = randomBytes(32);
const iv = randomBytes(12);
const cipher = createCipheriv('aes-256-gcm', key, iv);
const ciphertext = Buffer.concat([
cipher.update('secret', 'utf8'),
cipher.final(),
]);
const decipher = createDecipheriv('aes-256-gcm', key, iv);
decipher.setAuthTag(cipher.getAuthTag());
const plaintext = Buffer.concat([
decipher.update(ciphertext),
decipher.final(),
]);
expect(plaintext.toString('utf8')).toBe('secret');
});

test('jest mock subtle.verify resolves to a boolean', async () => {
const key = await QuickCrypto.subtle.generateKey(
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign', 'verify'],
);
expect(key).toBeInstanceOf(CryptoKey);
const data = new TextEncoder().encode('hello');
const signature = await QuickCrypto.subtle.sign('HMAC', key, data);
await expect(
QuickCrypto.subtle.verify('HMAC', key, signature, data),
).resolves.toBe(true);
});
Loading