From 2a81083f14257bebcd9c9d5cb76910dd29e42d39 Mon Sep 17 00:00:00 2001 From: tmashuang Date: Thu, 26 Sep 2019 11:52:51 -0700 Subject: [PATCH] MM controller additions, balance controller, typedMessageManager, and addtional actions tests --- .../controllers/balance-controller.spec.js | 55 +++ .../controllers/metamask-controller-test.js | 46 ++ test/unit/app/typed-message-manager.spec.js | 116 +++++ test/unit/ui/app/actions.spec.js | 408 +++++++++++++++++- 4 files changed, 610 insertions(+), 15 deletions(-) create mode 100644 test/unit/app/controllers/balance-controller.spec.js create mode 100644 test/unit/app/typed-message-manager.spec.js diff --git a/test/unit/app/controllers/balance-controller.spec.js b/test/unit/app/controllers/balance-controller.spec.js new file mode 100644 index 000000000..9ef25b95f --- /dev/null +++ b/test/unit/app/controllers/balance-controller.spec.js @@ -0,0 +1,55 @@ +const assert = require('assert') +const ObservableStore = require('obs-store') +const PollingBlockTracker = require('eth-block-tracker') + +const BalanceController = require('../../../../app/scripts/controllers/balance') +const AccountTracker = require('../../../../app/scripts/lib/account-tracker') +const TransactionController = require('../../../../app/scripts/controllers/transactions') +const { createTestProviderTools } = require('../../../stub/provider') +const provider = createTestProviderTools({ scaffold: {}}).provider + +const TEST_ADDRESS = '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc' + +const accounts = { + '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc': { + balance: '0x5e942b06dc24c4d50', + address: TEST_ADDRESS, + }, +} + +describe('Balance Controller', () => { + + let balanceController + + it('errors when address, accountTracker, txController, or blockTracker', function () { + try { + balanceController = new BalanceController() + } catch (error) { + assert.equal(error.message, 'Cannot construct a balance checker without address, accountTracker, txController, and blockTracker.') + } + }) + + beforeEach(() => { + balanceController = new BalanceController({ + address: TEST_ADDRESS, + accountTracker: new AccountTracker({ + provider, + blockTracker: new PollingBlockTracker({ provider }), + }), + txController: new TransactionController({ + provider, + networkStore: new ObservableStore(), + blockTracker: new PollingBlockTracker({ provider }), + }), + blockTracker: new PollingBlockTracker({ provider }), + }) + + balanceController.accountTracker.store.updateState({ accounts }) + }) + + it('updates balance controller ethBalance from account tracker', async function () { + await balanceController.updateBalance() + const balanceControllerState = balanceController.store.getState() + assert.equal(balanceControllerState.ethBalance, '0x5e942b06dc24c4d50') + }) +}) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index 808ecc9a9..5e019b533 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -2,6 +2,7 @@ const assert = require('assert') const sinon = require('sinon') const clone = require('clone') const nock = require('nock') +const ethUtil = require('ethereumjs-util') const createThoughStream = require('through2').obj const blacklistJSON = require('eth-phishing-detect/src/config') const firstTimeState = require('../../../unit/localhostState') @@ -103,6 +104,51 @@ describe('MetaMaskController', function () { sandbox.restore() }) + describe('#getAccounts', function () { + + beforeEach(async function () { + const password = 'a-fake-password' + + await metamaskController.createNewVaultAndRestore(password, TEST_SEED) + }) + + it('returns first address when dapp calls web3.eth.getAccounts', function () { + metamaskController.networkController._baseProviderParams.getAccounts((err, res) => { + assert.ifError(err) + assert.equal(res.length, 1) + assert.equal(res[0], '0x0dcd5d886577d5081b0c52e242ef29e70be3e7bc') + }) + }) + }) + + describe('#importAccountWithStrategy', function () { + + const importPrivkey = '4cfd3e90fc78b0f86bf7524722150bb8da9c60cd532564d7ff43f5716514f553' + + beforeEach(async function () { + const password = 'a-fake-password' + + await metamaskController.createNewVaultAndRestore(password, TEST_SEED) + await metamaskController.importAccountWithStrategy('Private Key', [ importPrivkey ]) + }) + + it('adds private key to keyrings in KeyringController', async function () { + const simpleKeyrings = metamaskController.keyringController.getKeyringsByType('Simple Key Pair') + const privKeyBuffer = simpleKeyrings[0].wallets[0]._privKey + const pubKeyBuffer = simpleKeyrings[0].wallets[0]._pubKey + const addressBuffer = ethUtil.pubToAddress(pubKeyBuffer) + const privKey = ethUtil.bufferToHex(privKeyBuffer) + const pubKey = ethUtil.bufferToHex(addressBuffer) + assert.equal(privKey, ethUtil.addHexPrefix(importPrivkey)) + assert.equal(pubKey, '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc') + }) + + it('adds private key to keyrings in KeyringController', async function () { + const keyringAccounts = await metamaskController.keyringController.getAccounts() + assert.equal(keyringAccounts[keyringAccounts.length - 1], '0xe18035bf8712672935fdb4e5e431b1a0183d2dfc') + }) + }) + describe('submitPassword', function () { const password = 'password' diff --git a/test/unit/app/typed-message-manager.spec.js b/test/unit/app/typed-message-manager.spec.js new file mode 100644 index 000000000..3db92d4a3 --- /dev/null +++ b/test/unit/app/typed-message-manager.spec.js @@ -0,0 +1,116 @@ +import assert from 'assert' +import sinon from 'sinon' +import NetworkController from '../../../app/scripts/controllers/network/index' +import TypedMessageManager from '../../../app/scripts/lib/typed-message-manager' + +describe('Typed Message Manager', () => { + let typedMessageManager, msgParamsV1, msgParamsV3, typedMsgs, messages, msgId, numberMsgId + + const address = '0xc42edfcc21ed14dda456aa0756c153f7985d8813' + const networkController = new NetworkController() + sinon.stub(networkController, 'getNetworkState').returns('1') + + beforeEach(() => { + typedMessageManager = new TypedMessageManager({ + networkController, + }) + + msgParamsV1 = { + from: address, + data: [ + { type: 'string', name: 'unit test', value: 'hello there' }, + { type: 'uint32', name: 'A number, but not really a number', value: '$$$' }, + ], + } + + msgParamsV3 = { + from: address, + data: JSON.stringify({ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string' }, + {'name': 'version', 'type': 'string' }, + {'name': 'chainId', ' type': 'uint256' }, + {'name': 'verifyingContract', ' type': 'address' }, + ], + 'Person': [ + {'name': 'name', 'type': 'string' }, + {'name': 'wallet', ' type': 'address' }, + ], + 'Mail': [ + {'name': 'from', 'type': 'Person' }, + {'name': 'to', 'type': 'Person' }, + {'name': 'contents', 'type': 'string' }, + ], + }, + 'primaryType': 'Mail', + 'domain': { + 'name': 'Ether Mainl', + 'version': '1', + 'chainId': 1, + 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + 'message': { + 'from': { + 'name': 'Cow', + 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + 'to': { + 'name': 'Bob', + 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + 'contents': 'Hello, Bob!', + }, + }), + } + + typedMessageManager.addUnapprovedMessage(msgParamsV3, 'V3') + typedMsgs = typedMessageManager.getUnapprovedMsgs() + messages = typedMessageManager.messages + msgId = Object.keys(typedMsgs)[0] + messages[0].msgParams.metamaskId = parseInt(msgId) + numberMsgId = parseInt(msgId) + }) + + it('supports version 1 of signedTypedData', () => { + typedMessageManager.addUnapprovedMessage(msgParamsV1, 'V1') + assert.equal(messages[messages.length - 1].msgParams.data, msgParamsV1.data) + }) + + it('has params address', function () { + assert.equal(typedMsgs[msgId].msgParams.from, address) + }) + + it('adds to unapproved messages and sets status to unapproved', function () { + assert.equal(typedMsgs[msgId].status, 'unapproved') + }) + + it('validates params', function () { + assert.doesNotThrow(() => { + typedMessageManager.validateParams(messages[0]) + }, 'Does not throw with valid parameters') + }) + + it('gets unapproved by id', function () { + const getMsg = typedMessageManager.getMsg(numberMsgId) + assert.equal(getMsg.id, numberMsgId) + }) + + it('approves messages', async function () { + const messageMetaMaskId = messages[0].msgParams + typedMessageManager.approveMessage(messageMetaMaskId) + assert.equal(messages[0].status, 'approved') + }) + + it('sets msg status to signed and adds a raw sig to message details', function () { + typedMessageManager.setMsgStatusSigned(numberMsgId, 'raw sig') + assert.equal(messages[0].status, 'signed') + assert.equal(messages[0].rawSig, 'raw sig') + }) + + it('rejects message', function () { + typedMessageManager.rejectMsg(numberMsgId) + assert.equal(messages[0].status, 'rejected') + }) + +}) diff --git a/test/unit/ui/app/actions.spec.js b/test/unit/ui/app/actions.spec.js index 71df29a4e..81ed4122a 100644 --- a/test/unit/ui/app/actions.spec.js +++ b/test/unit/ui/app/actions.spec.js @@ -1,3 +1,4 @@ +/* eslint-disable */ // Used to inspect long objects // util.inspect({JSON}, false, null)) // const util = require('util') @@ -29,6 +30,8 @@ describe('Actions', () => { const noop = () => {} + const currentNetworkId = 42 + let background, metamaskController const TEST_SEED = 'debris dizzy just program just float decrease vacant alarm reduce speak stadium' @@ -37,7 +40,6 @@ describe('Actions', () => { beforeEach(async () => { - metamaskController = new MetaMaskController({ provider, keyringController: new KeyringController({}), @@ -407,22 +409,181 @@ describe('Actions', () => { describe('#addNewAccount', () => { - let addNewAccountSpy - - afterEach(() => { - addNewAccountSpy.restore() - }) - it('Adds a new account', () => { const store = mockStore({ metamask: devState }) - addNewAccountSpy = sinon.spy(background, 'addNewAccount') + const addNewAccountSpy = sinon.spy(background, 'addNewAccount') return store.dispatch(actions.addNewAccount()) .then(() => { assert(addNewAccountSpy.calledOnce) }) }) + + }) + + describe('#checkHardwareStatus', () => { + + let checkHardwareStatusSpy + + beforeEach(() => { + checkHardwareStatusSpy = sinon.stub(background, 'checkHardwareStatus') + }) + + afterEach(() => { + checkHardwareStatusSpy.restore() + }) + + it('calls checkHardwareStatus in background', (done) => { + + const store = mockStore() + + store.dispatch(actions.checkHardwareStatus('ledger', `m/44'/60'/0'/0`)) + assert.equal(checkHardwareStatusSpy.calledOnce, true) + done() + }) + + it('shows loading indicator and displays error', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + checkHardwareStatusSpy.callsFake((deviceName, hdPath, callback) => { + callback(new Error('error')) + }) + + return store.dispatch(actions.checkHardwareStatus()) + .catch(() => { + assert.deepEqual(store.getActions(), expectedActions) + }) + + }) + }) + + describe('#forgetDevice', () => { + + let forgetDeviceSpy + + beforeEach(() => { + forgetDeviceSpy = sinon.stub(background, 'forgetDevice') + }) + + afterEach(() => { + forgetDeviceSpy.restore() + }) + + it('calls forgetDevice in background', () => { + + const store = mockStore() + + store.dispatch(actions.forgetDevice('ledger')) + assert.equal(forgetDeviceSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + forgetDeviceSpy.callsFake((deviceName, callback) => { + callback(new Error('error')) + }) + + return store.dispatch(actions.forgetDevice()) + .catch(() => { + assert.deepEqual(store.getActions(), expectedActions) + }) + + }) + }) + + describe('#connectHardware', () => { + + let connectHardwareSpy + + beforeEach(() => { + connectHardwareSpy = sinon.stub(background, 'connectHardware') + }) + + afterEach(() => { + connectHardwareSpy.restore() + }) + + it('calls connectHardware in background', () => { + + const store = mockStore() + + store.dispatch(actions.connectHardware('ledger', 0, `m/44'/60'/0'/0`)) + assert.equal(connectHardwareSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + connectHardwareSpy.callsFake((deviceName, page, hdPath, callback) => { + callback(new Error('error')) + }) + + return store.dispatch(actions.connectHardware()) + .catch(() => { + assert.deepEqual(store.getActions(), expectedActions) + }) + + }) + }) + + describe('#unlockHardwareWalletAccount', () => { + + let unlockHardwareWalletAccountSpy + + beforeEach(() => { + unlockHardwareWalletAccountSpy = sinon.stub(background, 'unlockHardwareWalletAccount') + }) + + afterEach(() => { + unlockHardwareWalletAccountSpy.restore() + }) + + it('calls unlockHardwareWalletAccount in background', () => { + + const store = mockStore() + + store.dispatch(actions.unlockHardwareWalletAccount('ledger', 0, `m/44'/60'/0'/0`)) + assert.equal(unlockHardwareWalletAccountSpy.calledOnce, true) + + }) + + it('shows loading indicator and displays error', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + unlockHardwareWalletAccountSpy.callsFake((deviceName, page, hdPath, callback) => { + callback(new Error('error')) + }) + + return store.dispatch(actions.unlockHardwareWalletAccount()) + .catch(() => { + assert.deepEqual(store.getActions(), expectedActions) + }) + + }) }) describe('#setCurrentCurrency', () => { @@ -574,12 +735,100 @@ describe('Actions', () => { }) + describe('#signTypedMsg', () => { + let signTypedMsgSpy, messages, typedMessages, msgId + + const msgParamsV3 = { + from: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + data: JSON.stringify({ + 'types': { + 'EIP712Domain': [ + {'name': 'name', 'type': 'string'}, + {'name': 'version', 'type': 'string'}, + {'name': 'chainId', 'type': 'uint256'}, + {'name': 'verifyingContract', 'type': 'address'}, + ], + 'Person': [ + {'name': 'name', 'type': 'string'}, + {'name': 'wallet', 'type': 'address'}, + ], + 'Mail': [ + {'name': 'from', 'type': 'Person'}, + {'name': 'to', 'type': 'Person'}, + {'name': 'contents', 'type': 'string'}, + ], + }, + 'primaryType': 'Mail', + 'domain': { + 'name': 'Ether Mainl', + 'version': '1', + 'chainId': 1, + 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', + }, + 'message': { + 'from': { + 'name': 'Cow', + 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', + }, + 'to': { + 'name': 'Bob', + 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', + }, + 'contents': 'Hello, Bob!', + }, + }), + } + + beforeEach(() => { + metamaskController.newUnsignedTypedMessage(msgParamsV3, 'V3') + messages = metamaskController.typedMessageManager.getUnapprovedMsgs() + typedMessages = metamaskController.typedMessageManager.messages + msgId = Object.keys(messages)[0] + typedMessages[0].msgParams.metamaskId = parseInt(msgId) + }) + + afterEach(() => { + signTypedMsgSpy.restore() + }) + + it('calls signTypedMsg in background with no error', () => { + const store = mockStore() + signTypedMsgSpy = sinon.spy(background, 'signTypedMessage') + + return store.dispatch(actions.signTypedMsg(msgParamsV3)) + .then(() => { + assert(signTypedMsgSpy.calledOnce) + }) + }) + + it('returns expected actions with error', (done) => { + const store = mockStore() + const expectedActions = [ + { type: 'SHOW_LOADING_INDICATION', value: undefined }, + { type: 'UPDATE_METAMASK_STATE', value: undefined }, + { type: 'HIDE_LOADING_INDICATION' }, + { type: 'DISPLAY_WARNING', value: 'error' }, + ] + + signTypedMsgSpy = sinon.stub(background, 'signTypedMessage') + signTypedMsgSpy.callsFake((_, callback) => { + callback(new Error('error')) + }) + + store.dispatch(actions.signTypedMsg()) + .catch(() => { + assert.deepEqual(store.getActions(), expectedActions) + done() + }) + }) + + }) + describe('#signTx', () => { let sendTransactionSpy - beforeEach(() => { - global.ethQuery = new EthQuery(provider) + beforeEach(async () => { sendTransactionSpy = sinon.stub(global.ethQuery, 'sendTransaction') }) @@ -608,6 +857,71 @@ describe('Actions', () => { }) }) + describe('#updatedGasData', () => { + it('errors when get code does not return', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'GAS_LOADING_STARTED' }, + { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: 'gasLoadingError' } }, + { type: 'GAS_LOADING_FINISHED' }, + ] + + const mockData = { + gasPrice: '0x3b9aca00', // + blockGasLimit: '0x6ad79a', // 7002010 + selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', + value: '0xde0b6b3a7640000', // 1000000000000000000 + } + + store.dispatch(actions.updateGasData(mockData)) + .then(() => { + assert.equal(store.getActions(), expectedActions) + }) + }) + }) + + describe('#updatedGasData', () => { + + const stub = sinon.stub().returns('0x') + + const mockData = { + gasPrice: '0x3b9aca00', // + blockGasLimit: '0x6ad79a', // 7002010 + selectedAddress: '0x0DCD5D886577d5081B0c52e242Ef29E70Be3E7bc', + to: '0xEC1Adf982415D2Ef5ec55899b9Bfb8BC0f29251B', + value: '0xde0b6b3a7640000', // 1000000000000000000 + } + + beforeEach(() => { + global.eth = { + getCode: stub, + } + }) + + afterEach(() => { + stub.reset() + }) + + it('returns default gas limit for basic eth transaction', () => { + const store = mockStore() + + const expectedActions = [ + { type: 'GAS_LOADING_STARTED' }, + { type: 'UPDATE_GAS_LIMIT', value: '0x5208' }, + { type: 'metamask/gas/SET_CUSTOM_GAS_LIMIT', value: '0x5208' }, + { type: 'UPDATE_SEND_ERRORS', value: { gasLoadingError: null } }, + { type: 'GAS_LOADING_FINISHED' }, + ] + + store.dispatch(actions.updateGasData(mockData)) + .then(() => { + assert.deepEqual(store.getActions(), expectedActions) + }) + }) + }) + describe('#signTokenTx', () => { let tokenSpy @@ -628,6 +942,61 @@ describe('Actions', () => { }) }) + describe('#updateTransaction', () => { + + let updateTransactionSpy, updateTransactionParamsSpy + + const txParams = { + 'from': '0x1', + 'gas': '0x5208', + 'gasPrice': '0x3b9aca00', + 'to': '0x2', + 'value': '0x0', + } + + const txData = { id: '1', status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: txParams } + + beforeEach( async () => { + await metamaskController.txController.txStateManager.addTx(txData) + }) + + afterEach(() => { + updateTransactionSpy.restore() + updateTransactionParamsSpy.restore() + }) + + it('updates transaction', () => { + const store = mockStore() + + updateTransactionSpy = sinon.spy(background, 'updateTransaction') + updateTransactionParamsSpy = sinon.spy(actions, 'updateTransactionParams') + + const result = [ txData.id, txParams ] + + return store.dispatch(actions.updateTransaction(txData)) + .then(() => { + assert(updateTransactionSpy.calledOnce) + assert(updateTransactionParamsSpy.calledOnce) + + assert.deepEqual(updateTransactionParamsSpy.args[0], result) + }) + }) + + it('rejects with error message', () => { + const store = mockStore() + + updateTransactionSpy = sinon.stub(background, 'updateTransaction') + updateTransactionSpy.callsFake((res, callback) => { + callback(new Error('error')) + }) + + return store.dispatch(actions.updateTransaction(txData)) + .catch((error) => { + assert.equal(error.message, 'error') + }) + }) + }) + describe('#lockMetamask', () => { let backgroundSetLockedSpy @@ -1192,40 +1561,49 @@ describe('Actions', () => { }) describe('#markPasswordForgotten', () => { - let markPasswordForgottenSpy + let markPasswordForgottenSpy, forgotPasswordSpy beforeEach(() => { - markPasswordForgottenSpy = sinon.stub(background, 'markPasswordForgotten') + markPasswordForgottenSpy = sinon.spy(background, 'markPasswordForgotten') + forgotPasswordSpy = sinon.spy(actions, 'forgotPassword') }) afterEach(() => { markPasswordForgottenSpy.restore() + forgotPasswordSpy.restore() }) it('calls markPasswordForgotten', () => { const store = mockStore() store.dispatch(actions.markPasswordForgotten()) + assert(forgotPasswordSpy.calledOnce) assert(markPasswordForgottenSpy.calledOnce) }) }) describe('#unMarkPasswordForgotten', () => { - let unMarkPasswordForgottenSpy + let unMarkPasswordForgottenSpy, forgotPasswordSpy beforeEach(() => { unMarkPasswordForgottenSpy = sinon.stub(background, 'unMarkPasswordForgotten') + forgotPasswordSpy = sinon.spy(actions, 'forgotPassword') }) afterEach(() => { unMarkPasswordForgottenSpy.restore() + forgotPasswordSpy.restore() }) it('calls unMarkPasswordForgotten', () => { const store = mockStore() store.dispatch(actions.unMarkPasswordForgotten()) - assert(unMarkPasswordForgottenSpy.calledOnce) + .then((done) => { + assert(forgotPasswordSpy.calledOnce) + assert(forgotPasswordSpy.calledWith(false)) + assert(unMarkPasswordForgottenSpy.calledOnce) + done() + }) }) }) - })