UNPKG

@aladas-org/cryptocalc

Version:
369 lines (307 loc) 12.8 kB
/** * ============================================================================ * Unit Tests - MainModel * ============================================================================ * Location: www/js/model/main_model.js * * MainModel est un singleton responsable de : * - saveWalletInfo() : sauvegarder le wallet complet (txt, json/wits, QR codes) * - saveWalletInfoAsJson(): sérialiser le wallet en JSON → wallet_info.wits * - loadWalletInfoFromJson(): charger un wallet depuis un .wits * - createQRCode() : générer des QR codes PNG/SVG via bwip-js * * Dépendances mockées : * - electron (app.getAppPath) * - fs (writeFileSync, mkdirSync, existsSync) * - bwip-js (toBuffer, toSVG) * - Modules internes : Skribi, log_utils, WalletInfoTemplate, Bip38Utils, FileUtils * * Périmètre testé : * - Singleton pattern (MainModel.This) * - saveWalletInfoAsJson() : contenu JSON, champs obligatoires, dérivation * du DERIVATION_PATH vers account/address_index * - Comportement is_not_null() (via les QR codes conditionnels) * ============================================================================ */ // --------------------------------------------------------------------------- // Mocks — doivent être déclarés avant les imports // --------------------------------------------------------------------------- // Mock electron jest.mock('electron', () => ({ app: { getAppPath: jest.fn(() => '/mock/app/path') } }), { virtual: true }); // Mock bwip-js jest.mock('bwip-js', () => ({ toBuffer: jest.fn((opts, cb) => cb(null, Buffer.from('PNG_DATA'))), toSVG: jest.fn(() => '<svg/>'), }), { virtual: true }); // Mock fs const mockFs = { writeFileSync: jest.fn(), mkdirSync: jest.fn(), existsSync: jest.fn(() => false), readFileSync: jest.fn(() => '{}'), }; jest.mock('fs', () => mockFs); // Mock des dépendances internes jest.mock('@util/log/skribi.js', () => ({ Skribi: { log: jest.fn() } })); jest.mock('@util/log/log_utils.js', () => ({ PrettyLog: { This: { logMode: 'test' }, logMode: 'test' }, pretty_func_header_log: jest.fn(), pretty_log: jest.fn(), UNIT_TESTS_LOG_MODE: 'test' })); jest.mock('@www/js/model/wallet_info_tmpl.js', () => ({ WalletInfoTemplate: { This: { clear: jest.fn(), getItems: jest.fn(() => []), getIndexInTemplate: jest.fn(() => -1), setItemValue: jest.fn(), getItem: jest.fn(), removeEmptyItems: jest.fn(), getItemKey: jest.fn(), getItemValue: jest.fn() } } })); jest.mock('@crypto/bip38_utils.js', () => ({ Bip38Utils: { This: { encrypt: jest.fn(() => Promise.resolve('encrypted_key')) } } })); jest.mock('@util/system/file_utils.js', () => ({ FileUtils: { CreateSubfolder: jest.fn() } })); jest.mock('@util/values/string_utils.js', () => ({ isString: jest.fn((val) => typeof val === 'string') })); jest.mock('@crypto/hex_utils.js', () => ({ isHexString: jest.fn(() => true) })); // --------------------------------------------------------------------------- // Imports (après les mocks) // --------------------------------------------------------------------------- const { PrettyLog, UNIT_TESTS_LOG_MODE } = require('@util/log/log_utils.js'); const { MainModel } = require('@www/js/model/main_model.js'); const { WALLET_MODE, BLOCKCHAIN, MNEMONICS, ENTROPY, ENTROPY_SIZE, WORD_INDEXES, BIP32_PASSPHRASE, BIP38_PASSPHRASE, DERIVATION_PATH, WIF, LANG, SIMPLE_WALLET_TYPE, HD_WALLET_TYPE, ACCOUNT, ADDRESS_INDEX } = require('@www/js/const_keywords.js'); const { ADDRESS, PRIVATE_KEY, BIP38_ENCRYPTED_PK } = require('@crypto/const_wallet.js'); const { COIN, BLOCKCHAIN_EXPLORER, BITCOIN, DOGECOIN } = require('@crypto/const_blockchains.js'); // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- const MOCK_CRYPTO_INFO_SIMPLE = { [WALLET_MODE]: SIMPLE_WALLET_TYPE, [BLOCKCHAIN]: BITCOIN, [ENTROPY]: 'deadbeef'.repeat(8), [ENTROPY_SIZE]: 256, [ADDRESS]: '1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf', [BLOCKCHAIN_EXPLORER]: 'https://blockchain.com/btc/address/1A1zP1eP5QGefi2DMPTfTL5SLmv7Divf', [PRIVATE_KEY]: 'a'.repeat(64), [WIF]: 'L1aW4aubDFB7yfras2S1mN3bqg9nwySY8nkoLmJebSLD5BWv3ENZ', [MNEMONICS]: 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about', [WORD_INDEXES]: '0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3', [LANG]: 'EN', }; const MOCK_CRYPTO_INFO_HD = { ...MOCK_CRYPTO_INFO_SIMPLE, [WALLET_MODE]: HD_WALLET_TYPE, [BIP32_PASSPHRASE]: 'my passphrase', [DERIVATION_PATH]: "m/44'/0'/2'/0/5'", [ADDRESS]: '1HDWalletAddress123456789', }; const MOCK_CRYPTO_INFO_DOGE = { ...MOCK_CRYPTO_INFO_SIMPLE, [BLOCKCHAIN]: DOGECOIN, [ADDRESS]: 'DQf4pDQoqaEuQGEpUv41Vx7TQWKzWhH2Ur', [LANG]: 'EN', }; const MOCK_TIMESTAMP = '2026_03_10_14h00m00s-0'; beforeAll(() => { PrettyLog.This.logMode = UNIT_TESTS_LOG_MODE; }); beforeEach(() => { jest.clearAllMocks(); mockFs.existsSync.mockReturnValue(false); // Mock de getAppVersion jest.spyOn(MainModel.This, 'getAppVersion').mockReturnValue('0.3.2'); }); // =========================================================================== describe('MainModel — Singleton', () => { test('MainModel.This retourne toujours la même instance', () => { const a = MainModel.This; const b = MainModel.This; expect(a).toBe(b); }); test('le constructeur privé rejette un appel direct', () => { expect(() => new MainModel()).toThrow(TypeError); }); test('l\'instance a la propriété app_version initialisée', () => { expect(MainModel.This).toHaveProperty('app_version'); }); }); // =========================================================================== describe('MainModel — saveWalletInfoAsJson() : Simple Wallet', () => { // Déplacer l'appel dans un beforeEach pour garantir l'ordre beforeEach(() => { mockFs.writeFileSync.mockClear(); MainModel.This.saveWalletInfoAsJson( '/mock/output/path', { ...MOCK_CRYPTO_INFO_SIMPLE }, MOCK_TIMESTAMP ); }); test('appelle fs.writeFileSync une fois', () => { expect(mockFs.writeFileSync).toHaveBeenCalledTimes(1); }); test('le JSON écrit contient WALLET_MODE', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[WALLET_MODE]).toBe(SIMPLE_WALLET_TYPE); }); test('le JSON écrit contient BLOCKCHAIN', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BLOCKCHAIN]).toBe(BITCOIN); }); test('le JSON écrit contient ADDRESS', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[ADDRESS]).toBe(MOCK_CRYPTO_INFO_SIMPLE[ADDRESS]); }); test('le JSON écrit contient ENTROPY', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[ENTROPY]).toBe(MOCK_CRYPTO_INFO_SIMPLE[ENTROPY]); }); test('le JSON écrit contient WIF', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[WIF]).toBe(MOCK_CRYPTO_INFO_SIMPLE[WIF]); }); test('le JSON écrit contient PRIVATE_KEY', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[PRIVATE_KEY]).toBe(MOCK_CRYPTO_INFO_SIMPLE[PRIVATE_KEY]); }); test('le JSON écrit contient MNEMONICS', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[MNEMONICS]).toBe(MOCK_CRYPTO_INFO_SIMPLE[MNEMONICS]); }); test('le JSON écrit contient LANG', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[LANG]).toBe('EN'); }); test('le JSON écrit contient le timestamp', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written['timestamp']).toBe(MOCK_TIMESTAMP); }); test('ENTROPY_SIZE est converti en entier', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(typeof written[ENTROPY_SIZE]).toBe('number'); expect(written[ENTROPY_SIZE]).toBe(256); }); test('ENTROPY_SIZE string est aussi converti en entier', () => { mockFs.writeFileSync.mockClear(); MainModel.This.saveWalletInfoAsJson( '/mock/output', { ...MOCK_CRYPTO_INFO_SIMPLE, [ENTROPY_SIZE]: '128' }, MOCK_TIMESTAMP ); const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(typeof written[ENTROPY_SIZE]).toBe('number'); expect(written[ENTROPY_SIZE]).toBe(128); }); test('Simple Wallet : BIP32_PASSPHRASE absent du JSON', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BIP32_PASSPHRASE]).toBeUndefined(); }); }); // =========================================================================== describe('MainModel — saveWalletInfoAsJson() : HD Wallet', () => { beforeEach(() => { mockFs.writeFileSync.mockClear(); MainModel.This.saveWalletInfoAsJson( '/mock/output/hd', { ...MOCK_CRYPTO_INFO_HD }, MOCK_TIMESTAMP ); }); test('WALLET_MODE est HD Wallet', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[WALLET_MODE]).toBe(HD_WALLET_TYPE); }); test('BIP32_PASSPHRASE est présent pour un HD Wallet', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BIP32_PASSPHRASE]).toBe('my passphrase'); }); test('DERIVATION_PATH est conservé', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[DERIVATION_PATH]).toBe("m/44'/0'/2'/0/5'"); }); test('ACCOUNT est extrait du DERIVATION_PATH (index 3 sans apostrophe)', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[ACCOUNT]).toBe('2'); }); test('ADDRESS_INDEX est extrait du DERIVATION_PATH (index 5 avec apostrophe)', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[ADDRESS_INDEX]).toMatch(/^5'?$/); }); }); // =========================================================================== describe('MainModel — saveWalletInfoAsJson() : Dogecoin', () => { beforeEach(() => { mockFs.writeFileSync.mockClear(); MainModel.This.saveWalletInfoAsJson( '/mock/output/doge', { ...MOCK_CRYPTO_INFO_DOGE }, MOCK_TIMESTAMP ); }); test('BLOCKCHAIN est Dogecoin', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BLOCKCHAIN]).toBe(DOGECOIN); }); test('COIN est l\'abréviation correcte pour Dogecoin', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[COIN]).toBe('DOGE'); }); test('ADDRESS contient une adresse Dogecoin valide', () => { const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[ADDRESS]).toMatch(/^D[1-9A-HJ-NP-Za-km-z]{32,34}$/); }); }); // =========================================================================== describe('MainModel — saveWalletInfoAsJson() : champs optionnels', () => { test('BIP38_ENCRYPTED_PK absent si non défini dans crypto_info', () => { mockFs.writeFileSync.mockClear(); const info = { ...MOCK_CRYPTO_INFO_SIMPLE }; delete info[BIP38_ENCRYPTED_PK]; MainModel.This.saveWalletInfoAsJson('/mock/out', info, MOCK_TIMESTAMP); const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BIP38_ENCRYPTED_PK]).toBeUndefined(); }); test('BIP38_PASSPHRASE absent si non défini', () => { mockFs.writeFileSync.mockClear(); const info = { ...MOCK_CRYPTO_INFO_SIMPLE }; delete info[BIP38_PASSPHRASE]; MainModel.This.saveWalletInfoAsJson('/mock/out', info, MOCK_TIMESTAMP); const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[BIP38_PASSPHRASE]).toBeUndefined(); }); test('WORD_INDEXES est inclus', () => { mockFs.writeFileSync.mockClear(); MainModel.This.saveWalletInfoAsJson('/mock/out', { ...MOCK_CRYPTO_INFO_SIMPLE }, MOCK_TIMESTAMP); const written = JSON.parse(mockFs.writeFileSync.mock.calls[0][1]); expect(written[WORD_INDEXES]).toBe(MOCK_CRYPTO_INFO_SIMPLE[WORD_INDEXES]); }); });