diff --git a/jest.config.js b/jest.config.js index 27a2b24..ddc6320 100644 --- a/jest.config.js +++ b/jest.config.js @@ -143,10 +143,12 @@ export default { // testLocationInResults: false, // The glob patterns Jest uses to detect test files - // testMatch: [ - // "**/__tests__/**/*.[jt]s?(x)", - // "**/?(*.)+(spec|test).[tj]s?(x)" - // ], + testMatch: [ + '**/test/command/dappServer/socket-sign.test.js', + '**/test/**/*.[jt]s?(x)' + // "**/?(*.)+(spec|test).[tj]s?(x)" + ], + testTimeout: 5000, // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ @@ -176,7 +178,7 @@ export default { // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation transformIgnorePatterns: [ // "/node_modules/(?!chalk|update-notifier|configstore|xdg-basedir|unique-string|crypto-random-string|semver-diff|latest-version|package-json|got|@sindresorhus|p-cancelable|@szmarczak/http-timer|cacheable-request|normalize-url|responselike|lowercase-keys|mimic-response|form-data-encoder)" - ], + ] // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, diff --git a/src/utils/utils.js b/src/utils/utils.js index c6eebdb..028c76d 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -289,7 +289,6 @@ async function getParams(method) { } catch (e) {} let paramValue; // todo: use recursion - if ( rule !== 'repeated' && innerType && @@ -366,7 +365,7 @@ async function deserializeLogs(aelf, logs = []) { let results = await Promise.all(logs.map(v => getProto(aelf, v.Address))); results = results.map((proto, index) => { const { Name, NonIndexed, Indexed = [] } = logs[index]; - const serializedData = [...(Indexed || [])]; + const serializedData = [...Indexed]; if (NonIndexed) { serializedData.push(NonIndexed); } diff --git a/test/dataDir/aelf/.aelfrc b/test/dataDir/aelf/.aelfrc new file mode 100644 index 0000000..0706a85 --- /dev/null +++ b/test/dataDir/aelf/.aelfrc @@ -0,0 +1,5 @@ +# THIS IS AN AUTOGENERATED FILE FOR AELF-COMMAND OPTIONS. DO NOT EDIT THIS FILE DIRECTLY. + + + +endpoint https://tdvw-test-node.aelf.io/ diff --git a/test/rc/index.test.js b/test/rc/index.test.js new file mode 100644 index 0000000..c851f14 --- /dev/null +++ b/test/rc/index.test.js @@ -0,0 +1,79 @@ +import fs from 'fs'; +import path from 'path'; +import Registry from '../../src/rc/index'; +import { userHomeDir } from '../../src/utils/userHomeDir'; +import { endpoint, account, password, dataDir } from '../constants.js'; + +jest.mock('../../src/utils/userHomeDir', () => { + const path = require('path'); + return { + userHomeDir: path.resolve(__dirname) + }; +}); + +describe('Registry', () => { + afterEach(() => { + jest.clearAllMocks(); + delete process.env.AELF_CLI_ENDPOINT; + }); + afterAll(() => { + fs.unlinkSync(path.resolve(userHomeDir, 'aelf/.aelfrc')); + fs.rmdirSync(path.resolve(userHomeDir, 'aelf')); + }); + test('should get file or not', () => { + const result = Registry.getFileOrNot(path.resolve(__dirname, '../datadir/aelf/.aelfrc')); + expect(result).toBe(`# THIS IS AN AUTOGENERATED FILE FOR AELF-COMMAND OPTIONS. DO NOT EDIT THIS FILE DIRECTLY. + + + +endpoint https://tdvw-test-node.aelf.io/ +`); + }); + test('should load config', () => { + const result = Registry.loadConfig(); + expect(result).toEqual({}); + }); + test('should get config from env', () => { + // mock + process.env.AELF_CLI_ENDPOINT = 'http://localhost:1234'; + const result = Registry.getConfigFromEnv(); + expect(result).toEqual({ endpoint: 'http://localhost:1234' }); + }); + test('should stringify', () => { + const result = Registry.stringify(); + expect(result).toEqual([ + '# THIS IS AN AUTOGENERATED FILE FOR AELF-COMMAND OPTIONS. DO NOT EDIT THIS FILE DIRECTLY.', + '', + '', + '' + ]); + }); + test('should get and set options correctly', () => { + const registry = new Registry(); + registry.setOption('endpoint', endpoint); + expect(registry.getOption('endpoint')).toBe(endpoint); + }); + test('should save options to file', () => { + const registry = new Registry(); + registry.saveOption('endpoint', endpoint); + expect(fs.readFileSync(registry.globalConfigLoc).toString()).toContain(`endpoint ${endpoint}`); + }); + test('should delete config key from file', () => { + const registry = new Registry(); + registry.saveOption('endpoint', endpoint); + registry.deleteConfig('endpoint'); + expect(fs.readFileSync(registry.globalConfigLoc).toString()).not.toContain(`endpoint ${endpoint}`); + }); + test('should get file configs correctly', () => { + const registry = new Registry(); + registry.saveOption('endpoint', endpoint); + const fileConfigs = registry.getFileConfigs(); + expect(fileConfigs.endpoint).toBe(endpoint); + }); + test('should get configs correctly', () => { + const registry = new Registry(); + registry.saveOption('endpoint', endpoint); + const configs = registry.getConfigs(); + expect(configs.endpoint).toBe(endpoint); + }); +}); diff --git a/test/utils/Logger.test.js b/test/utils/Logger.test.js new file mode 100644 index 0000000..dcba056 --- /dev/null +++ b/test/utils/Logger.test.js @@ -0,0 +1,52 @@ +import chalk from 'chalk'; +import Logger from '../../src/utils/Logger'; + +describe('Logger', () => { + let consoleLogSpy; + const fnName = 'trace', + level = 'Trace'; + beforeEach(() => { + consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {}); + }); + + afterEach(() => { + consoleLogSpy.mockRestore(); + }); + + test(`should log correctly formatted message`, () => { + const logger = new Logger({ log: true, onlyWords: false, name: 'TestLogger' }); + const message = 'Test message'; + logger[fnName](message); + const expectedPrefix = `[${level}]: `; + const expectedLog = chalk.gray(`TestLogger ${expectedPrefix}${message}`); + // second params: add spaces + expect(consoleLogSpy).toHaveBeenCalledWith(expectedLog, ''); + }); + test(`should return correctly formatted chalk message`, () => { + const logger = new Logger({ log: false, onlyWords: false, name: 'TestLogger' }); + const message = 'Test message'; + const result = logger[fnName](message); + const expectedPrefix = `TestLogger [${level}]: `; + const expectedChalk = chalk(chalk.gray(`${expectedPrefix}${message}`)); + expect(result.trim()).toEqual(expectedChalk); + }); + test(`should log correctly formatted object message`, () => { + const logger = new Logger({ log: true, onlyWords: false, name: 'TestLogger' }); + const message = { key: 'value' }; + logger[fnName](message); + const expectedPrefix = `TestLogger [${level}]: \n`; + const expectedLog = chalk.gray(`${expectedPrefix}`).trim(); + expect(consoleLogSpy).toHaveBeenCalledWith(expectedLog, message); + }); + test(`should log correctly formatted object message (onlyWords: true)`, () => { + const logger = new Logger({ onlyWords: true, name: 'TestLogger' }); + logger.symbol = '*'; + const message = { key: 'value' }; + logger[fnName](message); + + const expectedPrefix = `* [${level}]: \n`; + const expectedLog = chalk.gray(`${expectedPrefix}`).trim(); + + expect(consoleLogSpy).toHaveBeenCalledWith(expectedLog, message); + }); +}); diff --git a/test/utils/fs.test.js b/test/utils/fs.test.js new file mode 100644 index 0000000..0851867 --- /dev/null +++ b/test/utils/fs.test.js @@ -0,0 +1,51 @@ +import fs from 'fs'; +import os from 'os'; +import { writeFilePreservingEol } from '../../src/utils/fs.js'; +import { promisify } from '../../src/utils/utils'; + +jest.mock('fs'); +jest.mock('../../src/utils/utils', () => { + return { + promisify: fn => fn + }; +}); + +describe('File System Operators', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('writeFilePreservingEol', () => { + // cannot test /r/n because it depends on Windows + test('should write data with preserved EOL', async () => { + const path = '/path/to/existing/file.txt'; + const existingData = 'Line 1\nLine 2\nLine 3\n'; + const newData = 'New Line 1\nNew Line 2\nNew Line 3\n'; + const expectedData = 'New Line 1\nNew Line 2\nNew Line 3\n'; + + const mockBuffer = Buffer.from(existingData, 'utf-8'); + fs.existsSync.mockReturnValue(true); + fs.readFile.mockReturnValue(mockBuffer); + + let writtenData = ''; + fs.writeFile.mockImplementation((filePath, data) => { + writtenData = data.toString(); + }); + + await writeFilePreservingEol(path, newData); + expect(writtenData).toBe(expectedData); + }); + + test('should write data with default EOL if file does not exist', async () => { + const path = '/path/to/nonexistent/file.txt'; + const newData = 'Line 1\nLine 2\nLine 3\n'; + fs.existsSync.mockReturnValue(false); + let writtenData = ''; + fs.writeFile.mockImplementation((filePath, data) => { + writtenData = data.toString(); + }); + await writeFilePreservingEol(path, newData); + expect(writtenData).toBe(newData); + }); + }); +}); diff --git a/test/utils/userHomeDir.test.js b/test/utils/userHomeDir.test.js new file mode 100644 index 0000000..0fda4ac --- /dev/null +++ b/test/utils/userHomeDir.test.js @@ -0,0 +1,111 @@ +import path from 'path'; +import { homedir } from 'os'; +import { userHomeDir, home, isFakeRoot, isRootUser, ROOT_USER } from '../../src/utils/userHomeDir'; + +jest.mock('os', () => ({ + homedir: jest.fn(() => { + const mockHomeDir = '/mock/home'; + return mockHomeDir; + }) +})); +describe('userHomeDir', () => { + let originalPlatform; + let originalEnv; + const mockHomeDir = '/mock/home'; + beforeAll(() => { + originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform'); + originalEnv = { ...process.env }; + }); + + afterAll(() => { + Object.defineProperty(process, 'platform', originalPlatform); + process.env = originalEnv; + }); + + beforeEach(() => { + jest.clearAllMocks(); + jest.resetModules(); + }); + + test('home should be the user home directory', () => { + expect(home).toBe(mockHomeDir); + }); + + test('isFakeRoot should return true if FAKEROOTKEY is set', () => { + process.env.FAKEROOTKEY = 'true'; + expect(isFakeRoot()).toBe(true); + }); + + test('isFakeRoot should return false if FAKEROOTKEY is not set', () => { + delete process.env.FAKEROOTKEY; + expect(isFakeRoot()).toBe(false); + }); + + test('isRootUser should return true if uid is 0', () => { + expect(isRootUser(0)).toBe(true); + }); + + test('isRootUser should return false if uid is not 0', () => { + expect(isRootUser(1000)).toBe(false); + }); + + test('isWindows should return true if platform is win32', () => { + Object.defineProperty(process, 'platform', { + value: 'win32' + }); + expect(process.platform).toBe('win32'); + }); + + test('isWindows should return false if platform is not win32', () => { + Object.defineProperty(process, 'platform', { + value: 'linux' + }); + expect(process.platform).toBe('linux'); + }); + + test('userHomeDir should be correct for Windows platform', () => { + Object.defineProperty(process, 'platform', { + value: 'win32' + }); + + jest.resetModules(); + const { userHomeDir } = require('../../src/utils/userHomeDir'); + expect(userHomeDir).toBe(path.resolve(mockHomeDir, './AppData/Local')); + }); + + test('userHomeDir should be correct for non-Windows platform', () => { + Object.defineProperty(process, 'platform', { + value: 'linux' + }); + + jest.resetModules(); + const { userHomeDir } = require('../../src/utils/userHomeDir'); + expect(userHomeDir).toBe(path.resolve(mockHomeDir, './.local/share')); + }); + + test('ROOT_USER should be true if user is root and not fake root', () => { + jest.spyOn(process, 'getuid').mockReturnValue(0); + delete process.env.FAKEROOTKEY; + + jest.resetModules(); + const { ROOT_USER } = require('../../src/utils/userHomeDir'); + expect(ROOT_USER).toBe(true); + }); + + test('ROOT_USER should be false if user is not root', () => { + jest.spyOn(process, 'getuid').mockReturnValue(1000); + + jest.resetModules(); + const { ROOT_USER } = require('../../src/utils/userHomeDir'); + expect(ROOT_USER).toBe(false); + }); + + test('ROOT_USER should be false if user is root but is fake root', () => { + jest.spyOn(process, 'getuid').mockReturnValue(0); + process.env.FAKEROOTKEY = 'true'; + + jest.resetModules(); + const { ROOT_USER } = require('../../src/utils/userHomeDir'); + expect(ROOT_USER).toBe(false); + }); +}); diff --git a/test/utils/utils.test.js b/test/utils/utils.test.js new file mode 100644 index 0000000..b3e66d6 --- /dev/null +++ b/test/utils/utils.test.js @@ -0,0 +1,391 @@ +import path from 'path'; +import inquirer from 'inquirer'; +import moment from 'moment'; +import { v4 as uuid } from 'uuid'; +import AElf from 'aelf-sdk'; +import { + promisify, + camelCase, + getContractMethods, + getContractInstance, + getMethod, + promptTolerateSeveralTimes, + isAElfContract, + getTxResult, + parseJSON, + randomId, + getParams, + deserializeLogs +} from '../../src/utils/utils'; +import { plainLogger } from '../../src/utils/myLogger'; +import { endpoint, account, password, dataDir } from '../constants.js'; + +jest.mock('inquirer'); + +describe('utils', () => { + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + describe('promisify', () => { + test('should resolve with result when no error', async () => { + const mockFn = jest.fn((arg1, cb) => cb(null, arg1 + 1)); + const promiseFn = promisify(mockFn); + const result = await promiseFn(5); + expect(result).toBe(6); + }); + test('should reject with error when callback has error', async () => { + const mockFn = jest.fn((_, cb) => cb(new Error('Callback error'))); + const promiseFn = promisify(mockFn); + await expect(promiseFn(5)).rejects.toThrow('Callback error'); + }); + test('should handle firstData parameter correctly', async () => { + const mockFn = jest.fn((_, cb) => cb('result')); + const promiseFn = promisify(mockFn, true); + const result = await promiseFn(5); + expect(result).toBe('result'); + }); + }); + + describe('camelCase', () => { + test('should convert string to camelCase', () => { + expect(camelCase('hello_world')).toBe('helloWorld'); + }); + }); + + describe('isAElfContract', () => { + test('should return true for valid AElf contract name', () => { + expect(isAElfContract('aelf.contract')).toBe(true); + }); + + test('should return false for non-AElf contract name', () => { + expect(isAElfContract('not.aelf.contract')).toBe(false); + }); + }); + + describe('getContractMethods', () => { + test('should return methods starting with uppercase letters', () => { + const contract = { + Transfer: () => {}, + approve: () => {} + }; + const methods = getContractMethods(contract); + expect(methods).toEqual(['Transfer']); + }); + + test('should call plainLogger.fatal and exit process if no contract provided', () => { + const spyFatal = jest.spyOn(plainLogger, 'fatal').mockReturnValue(); + const spyExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + getContractMethods(''); + expect(spyFatal).toHaveBeenCalled(); + expect(spyExit).toHaveBeenCalledWith(1); + }); + }); + + describe('getContractInstance', () => { + let oraInstance; + const aelf = new AElf(new AElf.providers.HttpProvider(endpoint)); + const contractAddress = '2cVQrFiXNaedBYmUrovmUV2jcF9Hf6AXbh12gWsD4P49NaX99y'; + const wallet = AElf.wallet.getWalletByPrivateKey('9a2c6023e8b2221f4b02f4ccc5128392c1bd968ae45a42fa62848d793fff148f'); + + beforeEach(() => { + oraInstance = { + start: jest.fn(), + succeed: jest.fn(), + fail: jest.fn() + }; + }); + + test('should fetch contract by address if not AElf contract', async () => { + const contractInstance = await getContractInstance(contractAddress, aelf, wallet, oraInstance); + expect(oraInstance.start).toHaveBeenCalledWith('Fetching contract'); + expect(oraInstance.succeed).toHaveBeenCalledWith('Fetching contract successfully!'); + expect(contractInstance).toMatchObject({ address: contractAddress }); + }); + + test('should fetch AElf contract by name', async () => { + // contract Token + const contractInstance = await getContractInstance('AElf.ContractNames.Token', aelf, wallet, oraInstance); + expect(oraInstance.start).toHaveBeenCalledWith('Fetching contract'); + expect(oraInstance.succeed).toHaveBeenCalledWith('Fetching contract successfully!'); + expect(contractInstance).toMatchObject({ address: 'ASh2Wt7nSEmYqnGxPPzp4pnVDU4uhj1XW9Se5VeZcX2UDdyjx' }); + }); + + test('should fail and exit process if contract retrieval fails', async () => { + const spyFail = jest.spyOn(oraInstance, 'fail').mockReturnValue(); + const spyError = jest.spyOn(plainLogger, 'error').mockReturnValue('Error message'); + const spyExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + await expect(getContractInstance('invalidAddress', aelf, wallet, oraInstance)); + expect(spyFail).toHaveBeenCalled(); + expect(spyError).toHaveBeenCalled(); + expect(spyExit).toHaveBeenCalledWith(1); + }); + + test('should handle contract address which is not string', async () => { + // contract Token + const contractInstance = await getContractInstance({}, aelf, wallet, oraInstance); + expect(contractInstance).toEqual({}); + }); + }); + + describe('getMethod', () => { + test('should return method if exists in contract', () => { + const contract = { method: jest.fn() }; + const method = getMethod('method', contract); + expect(method).toBe(contract.method); + }); + + test('should throw error if method does not exist in contract', () => { + const contract = {}; + expect(() => getMethod('nonexistentMethod', contract)).toThrow('Not exist method nonexistentMethod'); + }); + + test('should return method directly if not a string', () => { + const contract = {}; + const method = getMethod(null, contract); + expect(method).toBeNull(); + }); + }); + + describe('promptTolerateSeveralTimes', () => { + let oraInstance; + + beforeEach(() => { + oraInstance = { + start: jest.fn(), + fail: jest.fn() + }; + }); + + test('should return valid input according to pattern', async () => { + inquirer.prompt.mockResolvedValueOnce({ input: 'validInput' }); + const processAfterPrompt = jest.fn().mockResolvedValue('processedInput'); + const input = await promptTolerateSeveralTimes( + { + processAfterPrompt, + pattern: /valid/, + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ); + expect(input).toBe('processedInput'); + expect(processAfterPrompt).toHaveBeenCalledWith({ input: 'validInput' }); + }); + + test('should retry prompt if input does not match pattern', async () => { + inquirer.prompt.mockResolvedValue({ input: 'invalidInput' }); + const processAfterPrompt = jest.fn().mockResolvedValue('processedInput'); + await promptTolerateSeveralTimes( + { + processAfterPrompt, + pattern: /valid/, + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ); + expect(inquirer.prompt).toHaveBeenCalledTimes(3); + }); + + test('should exit process if maximum attempts exceeded', async () => { + inquirer.prompt.mockResolvedValue({ input: null }); + const spyFail = jest.spyOn(oraInstance, 'fail').mockReturnValue(); + const spyFatal = jest.spyOn(plainLogger, 'fatal').mockReturnValue(); + const spyExit = jest.spyOn(process, 'exit').mockImplementation(() => {}); + const processAfterPrompt = jest.fn().mockResolvedValue(null); + await promptTolerateSeveralTimes( + { + processAfterPrompt, + pattern: /valid/, + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ); + expect(spyFail).toHaveBeenCalled(); + expect(spyFatal).toHaveBeenCalled(); + expect(spyExit).toHaveBeenCalledWith(1); + }); + test('should handle error pattern', async () => { + await expect( + promptTolerateSeveralTimes( + { + pattern: {}, + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ) + ).rejects.toThrow("param 'pattern' must be a regular expression!"); + }); + + test('should handle error processAfterPrompt', async () => { + await expect( + promptTolerateSeveralTimes( + { + processAfterPrompt: {}, + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ) + ).rejects.toThrow("Param 'processAfterPrompt' must be a function!"); + }); + + test('should handle no pattern', async () => { + await promptTolerateSeveralTimes( + { + times: 3, + prompt: [{ name: 'input', message: 'Enter input' }] + }, + oraInstance + ); + const spyFail = jest.spyOn(oraInstance, 'fail').mockReturnValue(); + expect(spyFail).toHaveBeenCalledWith('Failed'); + }); + }); + + describe('getTxResult', () => { + let aelf; + + beforeEach(() => { + aelf = { chain: { getTxResult: jest.fn() } }; + }); + + test('should return tx result when Status is MINED', async () => { + aelf.chain.getTxResult.mockResolvedValueOnce({ Status: 'MINED' }); + const tx = await getTxResult(aelf, 'txId'); + expect(tx).toEqual({ Status: 'MINED' }); + }); + + test('should throw error if Status is not MINED after retries', async () => { + aelf.chain.getTxResult.mockResolvedValue({ Status: 'PENDING' }); + const tx = await getTxResult(aelf, 'txId'); + expect(tx).toEqual({ Status: 'PENDING' }); + }); + + test('should retry if Status is PENDING', async () => { + aelf.chain.getTxResult.mockResolvedValueOnce({ Status: 'PENDING' }).mockResolvedValueOnce({ Status: 'MINED' }); + const tx = await getTxResult(aelf, 'txId'); + expect(tx).toEqual({ Status: 'MINED' }); + }); + + test('should retry if Status is FAILED', async () => { + aelf.chain.getTxResult.mockResolvedValueOnce({ Status: 'FAILED' }); + await expect(getTxResult(aelf, 'txId')).toThrow({ Status: 'FAILED' }); + }); + }); + + describe('parseJSON', () => { + test('should correctly parse JSON', function () { + expect(parseJSON('{"key": "value"}')).toEqual({ key: 'value' }); + expect(parseJSON('invalid')).toBe('invalid'); + expect(parseJSON('')).toBe(''); + }); + }); + + describe('randomId', () => { + it('should generate a random UUID without dashes', function () { + const uuid = randomId(); + expect(typeof uuid).toBe('string'); + expect(uuid.length).toBe(32); + }); + }); + + describe('getParams', () => { + let oraInstance; + const aelf = new AElf(new AElf.providers.HttpProvider(endpoint)); + const contractAddress = '2cVQrFiXNaedBYmUrovmUV2jcF9Hf6AXbh12gWsD4P49NaX99y'; + const wallet = AElf.wallet.getWalletByPrivateKey('9a2c6023e8b2221f4b02f4ccc5128392c1bd968ae45a42fa62848d793fff148f'); + beforeEach(() => { + oraInstance = { + start: jest.fn(), + succeed: jest.fn(), + fail: jest.fn() + }; + }); + test('should get parameters by method', async () => { + const contractInstance = await getContractInstance(contractAddress, aelf, wallet, oraInstance); + const method = getMethod('GetProposal', contractInstance); + inquirer.prompt.mockResolvedValue({ value: 'proposal' }); + const params = await getParams(method); + expect(params).toBe('proposal'); + }); + + test('should get parameters by method with not special params', async () => { + const contractInstance = await getContractInstance(contractAddress, aelf, wallet, oraInstance); + // console.log(Object.keys(contractInstance)); + const method = getMethod('GetMethodFee', contractInstance); + inquirer.prompt.mockResolvedValue({ value: 'method fee' }); + const params = await getParams(method); + expect(params).toEqual({ value: 'method fee' }); + }); + }); + + describe('deserializeLogs', () => { + let oraInstance; + const aelf = new AElf(new AElf.providers.HttpProvider(endpoint)); + beforeEach(() => { + oraInstance = { + start: jest.fn(), + succeed: jest.fn(), + fail: jest.fn() + }; + }); + test('test deserialize log', async () => { + const logs = [ + { + Address: 'ELF_2sGZFRtqQ57F55Z2KvhmoozKrf7ik2htNVQawEAo3Vyvcx9Qwr_tDVW', + Name: '.aelf.Hash', + NonIndexed: 'CgNFTEYQoI/hGQ==' + } + ]; + const result = await deserializeLogs(aelf, logs); + expect(result).toEqual(['454c46']); + }); + test('test deserialize log with VirtualTransactionCreated', async () => { + const logs = [ + { + Address: '238X6iw1j8YKcHvkDYVtYVbuYk2gJnK8UoNpVCtssynSpVC8hb', + Name: 'VirtualTransactionCreated', + Indexed: [ + 'CiIKIA8J04pLJGNHl4y2KWuBJipdXjtJ2ForrSRRuRx9w2LY', + 'EiIKIAR/b9iJa/+kT2+h9XAdQE0UX9wFZogfPtn9YvtlCnB2', + 'GiIKICeR6ZKlfyjnWhHxOvLArsiw6zXS8EjULrqJAckuA3jc', + 'IghUcmFuc2Zlcg==', + 'MiIKICWmXUMWhKDuXFdYz8/uF7ze4kC5r3i7boxM5Dj+RE4G' + ], + NonIndexed: 'KjAKIgogIKCTibOwFJNFp0zUNEXymkyazYKz8LLwLqOZxEqKRF0SA09NSRiA0NvD9AI=' + } + ]; + const result = await deserializeLogs(aelf, logs); + expect(result).toEqual([ + { + from: '2ytdtA2PDX7VLYWkqf36MQQ8wUtcXWRdpovX7Wxy8tJZXumaY', + methodName: 'Transfer', + params: 'CiIKICCgk4mzsBSTRadM1DRF8ppMms2Cs/Cy8C6jmcRKikRdEgNPTUkYgNDbw/QC', + signatory: 'HaiUnezHpBieiVZNuyQV4uLFspYDGxsEwt8wSFYqGSpXY3CzJ', + to: 'JRmBduh4nXWi1aXgdUsj5gJrzeZb2LxmrAbf7W99faZSvoAaE', + virtualHash: '0f09d38a4b246347978cb6296b81262a5d5e3b49d85a2bad2451b91c7dc362d8' + } + ]); + }); + test('test deserialize log with empty NonIndexed', async () => { + const logs = [ + { + Indexed: ['CiIKIPoq3y6L7T71F5BynCBXISeMFKrCt4QayljkLE4U8St4', 'EiIKIKt0P1P3+jKuU4Y5rSGOfzleHFw0YXn5eNM88jWfUWYR'], + Name: '.aelf.Hash', + Address: 'ELF_2sGZFRtqQ57F55Z2KvhmoozKrf7ik2htNVQawEAo3Vyvcx9Qwr_tDVW' + } + ]; + const result = await deserializeLogs(aelf, logs); + expect(result).toEqual(['0a20fa2adf2e8bed3ef51790729c205721278c14aac2b7841aca58e42c4e14f12b78']); + }); + test('test deserialize log with empty logs', async () => { + const result = await deserializeLogs(aelf); + expect(result).toEqual(null); + }); + }); +}); diff --git a/test/utils/wallet.test.js b/test/utils/wallet.test.js new file mode 100644 index 0000000..6abbb04 --- /dev/null +++ b/test/utils/wallet.test.js @@ -0,0 +1,81 @@ +import inquirer from 'inquirer'; +import fs from 'fs'; +import { mkdirpSync } from 'mkdirp'; +import { getWallet, saveKeyStore } from '../../src/utils/wallet'; +import { endpoint, account, password, dataDir } from '../constants.js'; +import keyJSON from '../datadir/aelf/keys/GyQX6t18kpwaD9XHXe1ToKxfov8mSeTLE9q9NwUAeTE8tULZk.json'; + +jest.mock('inquirer'); +jest.mock('mkdirp'); + +describe('wallet', () => { + describe('getWallet', () => { + test('should get wallet', () => { + const result = getWallet(dataDir, account, password); + expect(result.privateKey).toBe('9a2c6023e8b2221f4b02f4ccc5128392c1bd968ae45a42fa62848d793fff148f'); + expect(result.mnemonic).toBe('impact fork bulk museum swap design draw arctic load option ticket across'); + }); + test('should handle error', () => { + expect(() => getWallet(dataDir, 'test', password)).toThrow('Make sure you entered the correct account address'); + expect(() => getWallet(dataDir, account, 'test')).toThrow('Make sure you entered the correct password'); + }); + }); + describe('saveKeyStore', () => { + let existsSyncMock; + let writeFileSyncMock; + const wallet = getWallet(dataDir, account, password); + const keyStorePath = `${dataDir}/keys/${wallet.address}.json`; + + beforeEach(() => { + jest.clearAllMocks(); + inquirer.prompt.mockResolvedValue({ + password: '1234*Qwer', + confirmPassword: '1234*Qwer' + }); + }); + + beforeAll(() => { + // Mock fs methods + existsSyncMock = jest.spyOn(fs, 'existsSync').mockImplementation(() => true); + writeFileSyncMock = jest.spyOn(fs, 'writeFileSync').mockImplementation(() => {}); + }); + + afterAll(() => { + // Restore fs methods + existsSyncMock.mockRestore(); + writeFileSyncMock.mockRestore(); + }); + + test('should save keystore file and return its path', async () => { + existsSyncMock.mockReturnValueOnce(false); + const result = await saveKeyStore(wallet, dataDir); + expect(mkdirpSync).toHaveBeenCalled(); + expect(writeFileSyncMock).toHaveBeenCalled(); + expect(result).toBe(keyStorePath); + }); + + test('should throw error if passwords do not match', async () => { + inquirer.prompt.mockResolvedValueOnce({ + password: 'test-password', + confirmPassword: 'wrong-password' + }); + await expect(saveKeyStore(wallet, dataDir)).rejects.toThrow('Passwords are different'); + }); + + test('should throw error if password is too short', async () => { + inquirer.prompt.mockResolvedValueOnce({ + password: 'short', + confirmPassword: 'short' + }); + await expect(saveKeyStore(wallet, dataDir)).rejects.toThrow('password is too short'); + }); + + test('should not create directory if it already exists', async () => { + existsSyncMock.mockReturnValueOnce(true); + const result = await saveKeyStore(wallet, dataDir); + expect(mkdirpSync).not.toHaveBeenCalled(); + expect(writeFileSyncMock).toHaveBeenCalled(); + expect(result).toBe(keyStorePath); + }); + }); +});