BlueWallet/tests/setup.js
Overtorment 0181f0a849
Some checks are pending
Build Release and Upload to TestFlight (iOS) / build (push) Waiting to run
Build Release and Upload to TestFlight (iOS) / testflight-upload (push) Blocked by required conditions
BuildReleaseApk / buildReleaseApk (push) Waiting to run
BuildReleaseApk / browserstack (push) Blocked by required conditions
ADD: arkade ln pushes (#8634)
2026-06-10 17:35:17 +01:00

589 lines
19 KiB
JavaScript

/* global jest */
import mockClipboard from '@react-native-clipboard/clipboard/jest/clipboard-mock.js';
const consoleWarnOrig = console.warn;
console.warn = (...args) => {
if (
typeof args[0] === 'string' &&
(args[0].startsWith('WARNING: Sending to a future segwit version address can lead to loss of funds') ||
args[0].startsWith('only compressed public keys are good') ||
args[0].startsWith('Using standard fetch instead of expo/fetch'))
) {
return;
}
consoleWarnOrig.apply(consoleWarnOrig, args);
};
const consoleLogOrig = console.log;
console.debug = console.log = (...args) => {
if (
typeof args[0] === 'string' &&
(args[0].startsWith('updating exchange rate') ||
args[0].startsWith('Created new currency formatter for') ||
args[0].startsWith('fetch wrapper') ||
args[0].startsWith('Preferred currency') ||
args[0].startsWith('SelfTest - runSelfTest') ||
args[0].startsWith('Wallet.create() took') ||
args[0].startsWith('Cleared all cached currency formatters') ||
args[0].startsWith('[UnitSwitch/Fiat]') ||
args[0].startsWith('transactionDetail - useEffect') ||
args[0].startsWith('[electrum]'))
) {
return;
}
consoleLogOrig.apply(consoleLogOrig, args);
};
global.net = require('net'); // needed by Electrum client. For RN it is proviced in shim.js
global.tls = require('tls'); // needed by Electrum client. For RN it is proviced in shim.js
global.fetch = require('node-fetch');
jest.mock('@react-native-clipboard/clipboard', () => mockClipboard);
// Workaround for software-mansion/react-native-reanimated#8806.
// Fixed upstream in reanimated 4.3.0; remove once we upgrade.
// Path is held in a variable so tsc does not statically resolve into worklets'
// src/*.ts (which has its own type errors) under allowJs.
const workletsMockPath = 'react-native-worklets/src/mock';
jest.mock('react-native-worklets', () => require(workletsMockPath));
jest.mock('react-native-reanimated', () => require('react-native-reanimated/mock'));
jest.mock('react-native-capture-protection', () => ({
CaptureProtection: {
prevent: jest.fn(),
allow: jest.fn(),
isScreenRecording: jest.fn(() => Promise.resolve(false)),
},
}));
jest.mock('react-native-watch-connectivity', () => {
return {
getIsWatchAppInstalled: jest.fn(() => Promise.resolve(false)),
subscribeToMessages: jest.fn(),
updateApplicationContext: jest.fn(),
};
});
jest.mock('react-native-secure-key-store', () => {
return {};
});
jest.mock('react-native-notifications', () => {
return {};
});
jest.mock('react-native-background-fetch', () => {
// The real module instantiates `new NativeEventEmitter(...)` at module
// load, which throws under jest because the underlying native module is
// null. Test files that don't drive scheduler behavior (i.e. anything
// that transitively imports `blue_modules/arkade-background`) just need a
// safe default. Tests that exercise registration/run paths jest.mock this
// module locally with their own factory.
const noop = jest.fn();
const noopAsync = jest.fn().mockResolvedValue(undefined);
const stub = {
configure: noopAsync,
start: noopAsync,
stop: jest.fn().mockResolvedValue(true),
finish: noop,
scheduleTask: noopAsync,
registerHeadlessTask: noop,
STATUS_RESTRICTED: 0,
STATUS_DENIED: 1,
STATUS_AVAILABLE: 2,
NETWORK_TYPE_NONE: 0,
NETWORK_TYPE_ANY: 1,
NETWORK_TYPE_CELLULAR: 2,
NETWORK_TYPE_UNMETERED: 3,
NETWORK_TYPE_NOT_ROAMING: 4,
};
return { __esModule: true, default: stub, ...stub };
});
jest.mock('react-native-permissions', () => require('react-native-permissions/mock'));
jest.mock('react-native-device-info', () => {
return {
getUniqueId: jest.fn().mockReturnValue('uniqueId'),
getSystemName: jest.fn(),
getDeviceType: jest.fn().mockReturnValue(false),
hasGmsSync: jest.fn().mockReturnValue(true),
hasHmsSync: jest.fn().mockReturnValue(false),
isTablet: jest.fn().mockReturnValue(false),
};
});
jest.mock('react-native-quick-actions', () => {
return {
clearShortcutItems: jest.fn(),
setQuickActions: jest.fn(),
isSupported: jest.fn(),
};
});
jest.mock('react-native-default-preference', () => {
let mockPreferences = {};
let currentSuiteName = 'default';
const getSuite = name => {
if (!mockPreferences[name]) {
mockPreferences[name] = {};
}
return mockPreferences[name];
};
return {
setName: jest.fn(name => {
currentSuiteName = name;
if (!mockPreferences[name]) {
mockPreferences[name] = {};
}
return Promise.resolve();
}),
getName: jest.fn(() => {
return Promise.resolve(currentSuiteName);
}),
get: jest.fn(key => {
const suite = getSuite(currentSuiteName);
return Promise.resolve(Object.prototype.hasOwnProperty.call(suite, key) ? suite[key] : null);
}),
set: jest.fn((key, value) => {
const suite = getSuite(currentSuiteName);
suite[key] = value;
return Promise.resolve();
}),
clear: jest.fn(key => {
const suite = getSuite(currentSuiteName);
delete suite[key];
return Promise.resolve();
}),
getMultiple: jest.fn(keys => {
const suite = getSuite(currentSuiteName);
const values = keys.map(key => (Object.prototype.hasOwnProperty.call(suite, key) ? suite[key] : null));
return Promise.resolve(values);
}),
setMultiple: jest.fn(keyValuePairs => {
const suite = getSuite(currentSuiteName);
Object.entries(keyValuePairs).forEach(([key, value]) => {
suite[key] = value;
});
return Promise.resolve();
}),
clearMultiple: jest.fn(keys => {
const suite = getSuite(currentSuiteName);
keys.forEach(key => delete suite[key]);
return Promise.resolve();
}),
getAll: jest.fn(() => {
const suite = getSuite(currentSuiteName);
return Promise.resolve({ ...suite });
}),
clearAll: jest.fn(() => {
mockPreferences[currentSuiteName] = {};
return Promise.resolve();
}),
reset: jest.fn(() => {
mockPreferences = {};
currentSuiteName = 'default'; // Reset the current suite name
return Promise.resolve();
}),
};
});
jest.mock('react-native-fs', () => {
// Track existence per absolute path so the Arkade Realm adapter's
// ensureArkadeDir() / unlink() round trips behave coherently in tests.
const mockFsExisting = new Set();
const setExists = p => mockFsExisting.add(p);
const clearExists = p => mockFsExisting.delete(p);
return {
mkdir: jest.fn(async p => {
setExists(p);
}),
moveFile: jest.fn(),
copyFile: jest.fn(),
pathForBundle: jest.fn(),
pathForGroup: jest.fn(),
getFSInfo: jest.fn(),
getAllExternalFilesDirs: jest.fn(),
unlink: jest.fn(async p => {
clearExists(p);
}),
exists: jest.fn(async p => mockFsExisting.has(p)),
stopDownload: jest.fn(),
resumeDownload: jest.fn(),
isResumable: jest.fn(),
stopUpload: jest.fn(),
completeHandlerIOS: jest.fn(),
readDir: jest.fn(async () => []),
readDirAssets: jest.fn(),
existsAssets: jest.fn(),
readdir: jest.fn(),
setReadable: jest.fn(),
stat: jest.fn(),
readFile: jest.fn(),
read: jest.fn(),
readFileAssets: jest.fn(),
hash: jest.fn(),
copyFileAssets: jest.fn(),
copyFileAssetsIOS: jest.fn(),
copyAssetsVideoIOS: jest.fn(),
writeFile: jest.fn(),
appendFile: jest.fn(),
write: jest.fn(),
downloadFile: jest.fn(),
uploadFiles: jest.fn(),
touch: jest.fn(),
MainBundlePath: '/mock/MainBundle',
CachesDirectoryPath: '/mock/Caches',
DocumentDirectoryPath: '/mock/Documents',
ExternalDirectoryPath: '/mock/External',
ExternalStorageDirectoryPath: '/mock/ExternalStorage',
TemporaryDirectoryPath: '/mock/Temporary',
LibraryDirectoryPath: '/mock/Library',
PicturesDirectoryPath: '/mock/Pictures',
__mockFsHelpers: { setExists, clearExists, reset: () => mockFsExisting.clear() },
};
});
jest.mock('@react-native-documents/picker', () => ({}));
jest.mock('react-native-haptic-feedback', () => ({}));
// Per-path Realm mock so the Arkade Realm adapter (one encrypted file per Ark wallet)
// can be exercised in unit tests. Each `Realm.open({ path })` returns a stable
// instance for that path until it is closed or deleted; concurrent opens for the
// same path observe the same instance.
//
// The mock supports in-memory CRUD so SDK repository operations (saveContract,
// getContracts, saveVtxos, getVtxos, etc.) round-trip correctly. Without this,
// `annotateVtxos` cannot find contracts that were just saved and throws
// "no contract matched vtxo.script" when the test wallet has live VTXOs.
jest.mock('realm', () => {
const mockRealmStore = new Map();
// Persisted-on-disk view: paths that have been opened at least once and not
// yet deleted. Realm.exists / Realm.deleteFile read this rather than the
// live (memory-cached, possibly-closed) instance map so deleteArkadeRealm
// can realistically test the file-cleanup path.
const mockRealmFiles = new Set();
// Primary-key field per Realm object type. Used by create() to key the
// in-memory store and by delete() to remove individual objects.
const PK_FIELD = {
ArkContract: 'script',
ArkVtxo: 'pk',
ArkUtxo: 'pk',
ArkTransaction: 'pk',
ArkWalletState: 'key',
BoltzSwap: 'id',
ArkSwapNotificationSuppression: 'id',
};
// Split a query string at a top-level separator (i.e. not inside parens/braces).
const splitTop = (s, sep) => {
const parts = [];
let depth = 0;
let start = 0;
for (let i = 0; i <= s.length - sep.length; i++) {
const c = s[i];
if (c === '(' || c === '{') depth++;
else if (c === ')' || c === '}') depth--;
else if (depth === 0 && s.slice(i, i + sep.length) === sep) {
parts.push(s.slice(start, i).trim());
i += sep.length - 1;
start = i + 1;
}
}
parts.push(s.slice(start).trim());
return parts.length > 1 ? parts : [s.trim()];
};
// Evaluate a Realm query expression against a plain object.
// Handles: `field == $N`, `field IN {$0,$1,...}`, AND, OR, and parens.
const evalExpr = (obj, expr, args) => {
expr = expr.trim();
// Strip matching outer parens — e.g. "(a == $0 OR a == $1)" → "a == $0 OR a == $1"
while (expr.startsWith('(') && expr.endsWith(')')) {
let depth = 0;
let allWrapped = true;
for (let i = 0; i < expr.length - 1; i++) {
if (expr[i] === '(') depth++;
else if (expr[i] === ')') {
if (--depth === 0) {
allWrapped = false;
break;
}
}
}
if (allWrapped) expr = expr.slice(1, -1).trim();
else break;
}
// AND: all sub-expressions must match
const andParts = splitTop(expr, ' AND ');
if (andParts.length > 1) return andParts.every(p => evalExpr(obj, p, args));
// OR: any sub-expression must match
const orParts = splitTop(expr, ' OR ');
if (orParts.length > 1) return orParts.some(p => evalExpr(obj, p, args));
// IN {$0, $1, ...} — used by BoltzSwap repository
const inMatch = expr.match(/^(\w+)\s+IN\s+\{([^}]*)\}$/i);
if (inMatch) {
const field = inMatch[1];
const values = inMatch[2].split(',').map(p => {
const m = p.trim().match(/^\$(\d+)$/);
return m ? args[+m[1]] : undefined;
});
return values.includes(obj[field]);
}
// field == $N
const eqMatch = expr.match(/^(\w+)\s*==\s*\$(\d+)$/);
if (eqMatch) return obj[eqMatch[1]] === args[+eqMatch[2]];
return true; // unknown expression — pass through
};
// Build a chainable collection over an array of Realm objects.
const makeCollection = (type, items) => {
const arr = Array.isArray(items) ? items : [...items];
return {
filtered: (query, ...args) =>
makeCollection(
type,
arr.filter(o => evalExpr(o, query, args)),
),
sorted: (field, reverse) => {
const sorted = [...arr].sort((a, b) => {
if (a[field] < b[field]) return reverse ? 1 : -1;
if (a[field] > b[field]) return reverse ? -1 : 1;
return 0;
});
return makeCollection(type, sorted);
},
get length() {
return arr.length;
},
[Symbol.iterator]: function* () {
yield* arr;
},
// Internal: used by delete() to identify the backing type and items.
_type: type,
_items: arr,
};
};
const makeRealmInstance = path => {
let isClosed = false;
// type → Map<primaryKey, object>
const typeStore = new Map();
const getStore = type => {
if (!typeStore.has(type)) typeStore.set(type, new Map());
return typeStore.get(type);
};
return {
path,
get isClosed() {
return isClosed;
},
create(type, data) {
const store = getStore(type);
const pkField = PK_FIELD[type];
const pk = pkField !== undefined ? data[pkField] : JSON.stringify(data);
// Shallow-copy so later mutations to the caller's object don't affect
// what the store holds. Attach a non-enumerable tag for delete().
const stored = Object.defineProperty({ ...data }, '_realmMeta', {
value: { type, pk },
enumerable: false,
});
store.set(pk, stored);
},
delete(target) {
if (!target) return;
// Single object returned by objectForPrimaryKey (has _realmMeta)
if (target._realmMeta) {
const { type, pk } = target._realmMeta;
getStore(type).delete(pk);
return;
}
// Collection returned by objects() / filtered()
if (target._type !== undefined && target._items !== undefined) {
const store = getStore(target._type);
const pkField = PK_FIELD[target._type];
for (const item of target._items) {
const pk = pkField !== undefined ? item[pkField] : undefined;
if (pk !== undefined) store.delete(pk);
}
}
},
write(transactionFn) {
if (typeof transactionFn === 'function') transactionFn();
},
objectForPrimaryKey(type, pk) {
return getStore(type).get(pk) ?? null;
},
objects(type) {
return makeCollection(type, getStore(type).values());
},
close() {
isClosed = true;
},
addListener: jest.fn(),
removeAllListeners: jest.fn(),
// Exposed so __mockRealmHelpers.reset() can wipe data in open instances.
_clearData: () => typeStore.clear(),
};
};
return {
UpdateMode: { Modified: 1 },
open: jest.fn(async config => {
const path = (config && config.path) || '__default__';
const existing = mockRealmStore.get(path);
if (existing && !existing.isClosed) return existing;
const inst = makeRealmInstance(path);
mockRealmStore.set(path, inst);
mockRealmFiles.add(path);
return inst;
}),
// Real Realm.exists / Realm.deleteFile are synchronous in this version.
exists: jest.fn(arg => {
const path = typeof arg === 'string' ? arg : (arg && arg.path) || '__default__';
return mockRealmFiles.has(path);
}),
deleteFile: jest.fn(config => {
const path = (config && config.path) || '__default__';
mockRealmStore.delete(path);
mockRealmFiles.delete(path);
}),
__mockRealmHelpers: {
reset: () => {
// Clear data inside any open instances so tests don't leak state
// through instances cached in the app module's realmInstances map.
for (const inst of mockRealmStore.values()) {
if (typeof inst._clearData === 'function') inst._clearData();
}
mockRealmStore.clear();
mockRealmFiles.clear();
},
store: mockRealmStore,
files: mockRealmFiles,
},
};
});
jest.mock('react-native-camera-kit-no-google', () => ({
detectQRCodeInImage: jest.fn(base64 => {
if (base64 === 'invalid-image') {
return Promise.reject(new Error('Invalid image data'));
}
return Promise.resolve('mocked-qr-code');
}),
}));
jest.mock('react-native-haptic-feedback', () => {
return {
trigger: jest.fn(),
};
});
jest.mock('../blue_modules/analytics', () => {
const ret = jest.fn();
ret.ENUM = { CREATED_WALLET: '' };
return ret;
});
// addInvoice() registers a fire-and-forget payment-push callback; disable the
// URI in unit tests so node-fetch does not leave in-flight handles after the
// suite exits (which makes Jest fail with "did not exit one second after").
jest.mock('../blue_modules/constants', () => {
const actual = jest.requireActual('../blue_modules/constants');
return {
...actual,
arkadePaymentPushUri: '',
};
});
jest.mock('react-native-share', () => {
return {
open: jest.fn(),
};
});
// Service-keyed Keychain mock so Arkade adapter tests can exercise the per-wallet
// encryption-key lifecycle (load-or-create, then read on subsequent open). Defined
// inside the factory because Jest hoists `jest.mock` above module scope and refuses
// out-of-scope captures (only names matching /mock/i are allowed through).
jest.mock('react-native-keychain', () => {
const mockKeychainCreds = new Map();
return {
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
ACCESSIBLE: {
WHEN_UNLOCKED: 'AccessibleWhenUnlocked',
AFTER_FIRST_UNLOCK: 'AccessibleAfterFirstUnlock',
ALWAYS: 'AccessibleAlways',
WHEN_PASSCODE_SET_THIS_DEVICE_ONLY: 'AccessibleWhenPasscodeSetThisDeviceOnly',
WHEN_UNLOCKED_THIS_DEVICE_ONLY: 'AccessibleWhenUnlockedThisDeviceOnly',
AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY: 'AccessibleAfterFirstUnlockThisDeviceOnly',
},
SECURITY_LEVEL: {
SECURE_SOFTWARE: 'SECURE_SOFTWARE',
SECURE_HARDWARE: 'SECURE_HARDWARE',
ANY: 'ANY',
},
setGenericPassword: jest.fn(async (username, password, options) => {
const svc = (options && options.service) || '__default__';
mockKeychainCreds.set(svc, { username, password, service: svc });
return true;
}),
getGenericPassword: jest.fn(async options => {
const svc = (options && options.service) || '__default__';
return mockKeychainCreds.get(svc) || false;
}),
resetGenericPassword: jest.fn(async options => {
const svc = (options && options.service) || '__default__';
return mockKeychainCreds.delete(svc);
}),
// Default to the strongest level so the adapter's preflight selects
// SECURE_HARDWARE in the happy path. Tests override per-case via
// mockResolvedValueOnce when they need a downgrade scenario.
getSecurityLevel: jest.fn(async () => 'SECURE_HARDWARE'),
__mockKeychainHelpers: { reset: () => mockKeychainCreds.clear(), store: mockKeychainCreds },
};
});
// Historic copy-paste: react-native-tcp-socket pulled the Keychain mock. Keep the
// same surface so existing tests continue to mount, just with a fresh map.
jest.mock('react-native-tcp-socket', () => {
return {
SECURITY_LEVEL_ANY: 'MOCK_SECURITY_LEVEL_ANY',
SECURITY_LEVEL_SECURE_SOFTWARE: 'MOCK_SECURITY_LEVEL_SECURE_SOFTWARE',
SECURITY_LEVEL_SECURE_HARDWARE: 'MOCK_SECURITY_LEVEL_SECURE_HARDWARE',
setGenericPassword: jest.fn().mockResolvedValue(true),
getGenericPassword: jest.fn().mockResolvedValue(false),
resetGenericPassword: jest.fn().mockResolvedValue(true),
};
});
global.alert = () => {};