From f20a03f7519950804b3b2c3989d062d09356da86 Mon Sep 17 00:00:00 2001 From: Attay Rasool Date: Mon, 5 Oct 2026 00:14:24 +0500 Subject: [PATCH 1/2] feat: ship a Jest mock backed by node:crypto Jest runs in Node, where the native module doesn't exist, so importing the library in a test throws. Add react-native-quick-crypto/jest, which forwards to node:crypto so tests get real results without hand-written stubs. Includes a unit test, a 'Testing with Jest' guide, and an ESLint override for the CommonJS mock file. --- docs/content/docs/guides/meta.json | 1 + .../content/docs/guides/testing-with-jest.mdx | 67 +++++++++++++++++++ .../eslint.config.mjs | 16 ++++- .../react-native-quick-crypto/jest/index.js | 33 +++++++++ .../react-native-quick-crypto/package.json | 1 + .../test/jestMock.test.ts | 56 ++++++++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 docs/content/docs/guides/testing-with-jest.mdx create mode 100644 packages/react-native-quick-crypto/jest/index.js create mode 100644 packages/react-native-quick-crypto/test/jestMock.test.ts diff --git a/docs/content/docs/guides/meta.json b/docs/content/docs/guides/meta.json index b1c4c7b1e..f7ec845f6 100644 --- a/docs/content/docs/guides/meta.json +++ b/docs/content/docs/guides/meta.json @@ -8,6 +8,7 @@ "large-files", "secure-storage", "crypto-wallet", + "testing-with-jest", "migration", "nitro-integration" ] diff --git a/docs/content/docs/guides/testing-with-jest.mdx b/docs/content/docs/guides/testing-with-jest.mdx new file mode 100644 index 000000000..016245414 --- /dev/null +++ b/docs/content/docs/guides/testing-with-jest.mdx @@ -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: ['/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`. + + + The mock only covers what `node:crypto` provides. Library-specific APIs such as BLAKE3 or ML-KEM 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(), + })); + ``` + diff --git a/packages/react-native-quick-crypto/eslint.config.mjs b/packages/react-native-quick-crypto/eslint.config.mjs index d4d51929a..8e8e7af31 100644 --- a/packages/react-native-quick-crypto/eslint.config.mjs +++ b/packages/react-native-quick-crypto/eslint.config.mjs @@ -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: { @@ -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: [ diff --git a/packages/react-native-quick-crypto/jest/index.js b/packages/react-native-quick-crypto/jest/index.js new file mode 100644 index 000000000..4ad19406a --- /dev/null +++ b/packages/react-native-quick-crypto/jest/index.js @@ -0,0 +1,33 @@ +/** + * Jest mock for react-native-quick-crypto. + * + * Jest runs on Node, which already implements the `crypto` API this library + * brings to React Native, so the mock forwards to `node:crypto` instead of + * stubbing each function. Results are real (hashes, ciphertexts, signatures), + * so tests exercise the same behavior as the app. + * + * Usage, in a Jest setup file or at the top of a test: + * + * jest.mock('react-native-quick-crypto', () => + * require('react-native-quick-crypto/jest') + * ); + * + * APIs that Node does not provide (for example blake3 or ML-KEM) are not + * included; mock those yourself if your code uses them. + */ +const crypto = require('node:crypto'); +const { Buffer } = require('node:buffer'); + +const QuickCrypto = { + ...crypto, + Buffer, + // The app may call install() to patch globals; Node already has + // globalThis.crypto and Buffer, so there is nothing to do here. + install: () => {}, +}; + +module.exports = { + __esModule: true, + default: QuickCrypto, + ...QuickCrypto, +}; diff --git a/packages/react-native-quick-crypto/package.json b/packages/react-native-quick-crypto/package.json index 240f1356e..c18be2033 100644 --- a/packages/react-native-quick-crypto/package.json +++ b/packages/react-native-quick-crypto/package.json @@ -25,6 +25,7 @@ "files": [ "src", "lib", + "jest", "android/build.gradle", "android/gradle.properties", "android/CMakeLists.txt", diff --git a/packages/react-native-quick-crypto/test/jestMock.test.ts b/packages/react-native-quick-crypto/test/jestMock.test.ts new file mode 100644 index 000000000..2164e205c --- /dev/null +++ b/packages/react-native-quick-crypto/test/jestMock.test.ts @@ -0,0 +1,56 @@ +// `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, { + 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'], + ); + 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); +}); From 94d5a453a032820756e1423da04f334fccb5d850 Mon Sep 17 00:00:00 2001 From: Brad Anderson Date: Mon, 5 Oct 2026 14:26:47 -0400 Subject: [PATCH 2/2] fix(jest): expose CryptoKey, trim mock header, list more unsupported APIs --- .../content/docs/guides/testing-with-jest.mdx | 2 +- .../react-native-quick-crypto/jest/index.js | 21 ++----------------- .../test/jestMock.test.ts | 2 ++ 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/docs/content/docs/guides/testing-with-jest.mdx b/docs/content/docs/guides/testing-with-jest.mdx index 016245414..6ea55959c 100644 --- a/docs/content/docs/guides/testing-with-jest.mdx +++ b/docs/content/docs/guides/testing-with-jest.mdx @@ -56,7 +56,7 @@ test('verifies an HMAC with subtle', async () => { `install()` is a no-op in the mock, because Node already provides `globalThis.crypto` and `Buffer`. - The mock only covers what `node:crypto` provides. Library-specific APIs such as BLAKE3 or ML-KEM are `undefined`, and `argon2` needs Node 24.7 or newer. If your code uses them, extend the mock: + 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', () => ({ diff --git a/packages/react-native-quick-crypto/jest/index.js b/packages/react-native-quick-crypto/jest/index.js index 4ad19406a..626892a81 100644 --- a/packages/react-native-quick-crypto/jest/index.js +++ b/packages/react-native-quick-crypto/jest/index.js @@ -1,28 +1,11 @@ -/** - * Jest mock for react-native-quick-crypto. - * - * Jest runs on Node, which already implements the `crypto` API this library - * brings to React Native, so the mock forwards to `node:crypto` instead of - * stubbing each function. Results are real (hashes, ciphertexts, signatures), - * so tests exercise the same behavior as the app. - * - * Usage, in a Jest setup file or at the top of a test: - * - * jest.mock('react-native-quick-crypto', () => - * require('react-native-quick-crypto/jest') - * ); - * - * APIs that Node does not provide (for example blake3 or ML-KEM) are not - * included; mock those yourself if your code uses them. - */ +// 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, - // The app may call install() to patch globals; Node already has - // globalThis.crypto and Buffer, so there is nothing to do here. + CryptoKey: globalThis.CryptoKey, install: () => {}, }; diff --git a/packages/react-native-quick-crypto/test/jestMock.test.ts b/packages/react-native-quick-crypto/test/jestMock.test.ts index 2164e205c..9261b1e6f 100644 --- a/packages/react-native-quick-crypto/test/jestMock.test.ts +++ b/packages/react-native-quick-crypto/test/jestMock.test.ts @@ -5,6 +5,7 @@ jest.mock('react-native-quick-crypto', () => require('../jest'), { }); import QuickCrypto, { + CryptoKey, createCipheriv, createDecipheriv, createHash, @@ -48,6 +49,7 @@ test('jest mock subtle.verify resolves to a boolean', async () => { false, ['sign', 'verify'], ); + expect(key).toBeInstanceOf(CryptoKey); const data = new TextEncoder().encode('hello'); const signature = await QuickCrypto.subtle.sign('HMAC', key, data); await expect(