1
0
mirror of https://github.com/kremalicious/metamask-extension.git synced 2024-11-27 04:46:10 +01:00
metamask-extension/test/unit/app/controllers/preferences-controller-test.js
Mark Stacey 17f7ca2cfe
Update address book state upon custom RPC chainId edit (#9493)
When the `chainId` for a custom RPC endpoint is edited, we now migrate
the corresponding address book entries to ensure they are not orphaned.

The address book entries are grouped by the `metamask.network` state,
which unfortunately was sometimes the `chainId`, and sometimes the
`networkId`. It was always the `networkId` for built-in Infura
networks, but for custom RPC endpoints it would be set to the user-set
`chainId` field, with a fallback to the `networkId` of the network.

A recent change will force users to enter valid `chainId`s on all
custom networks, which will be normalized to be hex-prefixed. As a
result, address book contacts will now be keyed by a different string.
The contact entries are now migrated when this edit takes place.

There are some edge cases where two separate entries share the same set
of contacts. For example, if two entries have the same `chainId`, or if
they had the same `networkId` and had no `chainId` set. When the
`chainId` is edited in such cases, the contacts are duplicated on both
networks. This is the best we can do, as we don't have any way to know
which network the contacts _should_ be on.

The `typed-message-manager` unit tests have also been updated as part
of this commit because the addition of `sinon.restore()` to the
preferences controller tests ended up clearing a test object in-between
individual tests in that file. The test object is now re-constructed
before each individual test.
2020-10-07 15:02:17 -02:30

563 lines
23 KiB
JavaScript

import assert from 'assert'
import ObservableStore from 'obs-store'
import sinon from 'sinon'
import PreferencesController from '../../../../app/scripts/controllers/preferences'
import { addInternalMethodPrefix } from '../../../../app/scripts/controllers/permissions'
describe('preferences controller', function () {
let preferencesController
let network
const migrateAddressBookState = sinon.stub()
beforeEach(function () {
network = { providerStore: new ObservableStore({ type: 'mainnet' }) }
preferencesController = new PreferencesController({ migrateAddressBookState, network })
})
afterEach(function () {
sinon.restore()
})
describe('setAddresses', function () {
it('should keep a map of addresses to names and addresses in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const { identities } = preferencesController.store.getState()
assert.deepEqual(identities, {
'0xda22le': {
name: 'Account 1',
address: '0xda22le',
},
'0x7e57e2': {
name: 'Account 2',
address: '0x7e57e2',
},
})
})
it('should create account tokens for each account in the store', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
const { accountTokens } = preferencesController.store.getState()
assert.deepEqual(accountTokens, {
'0xda22le': {},
'0x7e57e2': {},
})
})
it('should replace its list of addresses', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.setAddresses([
'0xda22le77',
'0x7e57e277',
])
const { identities } = preferencesController.store.getState()
assert.deepEqual(identities, {
'0xda22le77': {
name: 'Account 1',
address: '0xda22le77',
},
'0x7e57e277': {
name: 'Account 2',
address: '0x7e57e277',
},
})
})
})
describe('removeAddress', function () {
it('should remove an address from state', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().identities['0xda22le'], undefined)
})
it('should remove an address from state and respective tokens', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.removeAddress('0xda22le')
assert.equal(preferencesController.store.getState().accountTokens['0xda22le'], undefined)
})
it('should switch accounts if the selected address is removed', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
preferencesController.setSelectedAddress('0x7e57e2')
preferencesController.removeAddress('0x7e57e2')
assert.equal(preferencesController.getSelectedAddress(), '0xda22le')
})
})
describe('setAccountLabel', function () {
it('should update a label for the given account', function () {
preferencesController.setAddresses([
'0xda22le',
'0x7e57e2',
])
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], {
name: 'Account 1',
address: '0xda22le',
})
preferencesController.setAccountLabel('0xda22le', 'Dazzle')
assert.deepEqual(preferencesController.store.getState().identities['0xda22le'], {
name: 'Dazzle',
address: '0xda22le',
})
})
})
describe('getTokens', function () {
it('should return an empty list initially', async function () {
preferencesController.setAddresses(['0x7e57e2'])
await preferencesController.setSelectedAddress('0x7e57e2')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 0, 'empty list of tokens')
})
})
describe('addToken', function () {
it('should add that token to its state', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
preferencesController.setAddresses(['0x7e57e2'])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
})
it('should allow updating a token value', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
preferencesController.setAddresses(['0x7e57e2'])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
const newDecimals = 6
await preferencesController.addToken(address, symbol, newDecimals)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token added')
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, newDecimals, 'updated decimals correctly')
})
it('should allow adding tokens to two separate addresses', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
preferencesController.setAddresses([
'0x7e57e2',
'0xda22le',
])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 1st address')
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(address, symbol, decimals)
assert.equal(preferencesController.getTokens().length, 1, 'one token added for 2nd address')
})
it('should add token per account', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
preferencesController.setAddresses([
'0x7e57e2',
'0xda22le',
])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0xda22le')
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two account and tokens are equal')
})
it('should add token per network', async function () {
const addressFirst = '0xabcdef1234567'
const addressSecond = '0xabcdef1234568'
const symbolFirst = 'ABBR'
const symbolSecond = 'ABBB'
const decimals = 5
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken(addressFirst, symbolFirst, decimals)
const tokensFirstAddress = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken(addressSecond, symbolSecond, decimals)
const tokensSeconAddress = preferencesController.getTokens()
assert.notEqual(tokensFirstAddress, tokensSeconAddress, 'add different tokens for two networks and tokens are equal')
})
})
describe('removeToken', function () {
it('should remove the only token from its state', async function () {
preferencesController.setAddresses(['0x7e57e2'])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 5)
await preferencesController.removeToken('0xa')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 0, 'one token removed')
})
it('should remove a token from its state', async function () {
preferencesController.setAddresses(['0x7e57e2'])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.removeToken('0xa')
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, 'one token removed')
const [token1] = tokens
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 })
})
it('should remove a token from its state on corresponding address', async function () {
preferencesController.setAddresses([
'0x7e57e2',
'0x7e57e3',
])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in account')
const [token1] = tokensFirst
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 })
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for account')
})
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensSecond = preferencesController.getTokens()
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.removeToken('0xa')
const tokensFirst = preferencesController.getTokens()
assert.equal(tokensFirst.length, 1, 'one token removed in network')
const [token1] = tokensFirst
assert.deepEqual(token1, { address: '0xb', symbol: 'B', decimals: 5 })
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensSecond, initialTokensSecond, 'token deleted for network')
})
})
describe('on setSelectedAddress', function () {
it('should update tokens from its state on corresponding address', async function () {
preferencesController.setAddresses([
'0x7e57e2',
'0x7e57e3',
])
await preferencesController.setSelectedAddress('0x7e57e2')
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
await preferencesController.setSelectedAddress('0x7e57e3')
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
await preferencesController.setSelectedAddress('0x7e57e2')
const initialTokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different accounts and tokens')
await preferencesController.setSelectedAddress('0x7e57e2')
const tokensFirst = preferencesController.getTokens()
await preferencesController.setSelectedAddress('0x7e57e3')
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same account')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same account')
})
})
describe('on updateStateNetworkType', function () {
it('should remove a token from its state on corresponding network', async function () {
network.providerStore.updateState({ type: 'mainnet' })
await preferencesController.addToken('0xa', 'A', 4)
await preferencesController.addToken('0xb', 'B', 5)
const initialTokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
await preferencesController.addToken('0xa', 'C', 4)
await preferencesController.addToken('0xb', 'D', 5)
const initialTokensSecond = preferencesController.getTokens()
assert.notDeepEqual(initialTokensFirst, initialTokensSecond, 'tokens not equal for different networks and tokens')
network.providerStore.updateState({ type: 'mainnet' })
const tokensFirst = preferencesController.getTokens()
network.providerStore.updateState({ type: 'rinkeby' })
const tokensSecond = preferencesController.getTokens()
assert.deepEqual(tokensFirst, initialTokensFirst, 'tokens equal for same network')
assert.deepEqual(tokensSecond, initialTokensSecond, 'tokens equal for same network')
})
})
describe('on watchAsset', function () {
let stubHandleWatchAssetERC20, asy, req, res
const sandbox = sinon.createSandbox()
beforeEach(function () {
req = { params: {} }
res = {}
asy = { next: sandbox.spy(), end: sandbox.spy() }
stubHandleWatchAssetERC20 = sandbox.stub(preferencesController, '_handleWatchAssetERC20')
})
after(function () {
sandbox.restore()
})
it('shouldn not do anything if method not corresponds', async function () {
req.method = 'metamask'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.notCalled(asy.end)
sandbox.assert.called(asy.next)
})
it('should do something if method is supported', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(asy.end)
sandbox.assert.notCalled(asy.next)
req.method = addInternalMethodPrefix('watchAsset')
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.calledTwice(asy.end)
sandbox.assert.notCalled(asy.next)
})
it('should through error if method is supported but asset type is not', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'someasset'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(asy.end)
sandbox.assert.notCalled(stubHandleWatchAssetERC20)
sandbox.assert.notCalled(asy.next)
assert.deepEqual(res, {})
})
it('should trigger handle add asset if type supported', async function () {
req.method = 'metamask_watchAsset'
req.params.type = 'ERC20'
await preferencesController.requestWatchAsset(req, res, asy.next, asy.end)
sandbox.assert.called(stubHandleWatchAssetERC20)
})
})
describe('on watchAsset of type ERC20', function () {
let req
const sandbox = sinon.createSandbox()
beforeEach(function () {
req = { params: { type: 'ERC20' } }
})
after(function () {
sandbox.restore()
})
it('should add suggested token', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.openPopup = async () => undefined
await preferencesController._handleWatchAssetERC20(req.params.options)
const suggested = preferencesController.getSuggestedTokens()
assert.equal(Object.keys(suggested).length, 1, `one token added ${Object.keys(suggested)}`)
assert.equal(suggested[address].address, address, 'set address correctly')
assert.equal(suggested[address].symbol, symbol, 'set symbol correctly')
assert.equal(suggested[address].decimals, decimals, 'set decimals correctly')
assert.equal(suggested[address].image, image, 'set image correctly')
})
it('should add token correctly if user confirms', async function () {
const address = '0xabcdef1234567'
const symbol = 'ABBR'
const decimals = 5
const image = 'someimage'
req.params.options = { address, symbol, decimals, image }
sandbox.stub(preferencesController, '_validateERC20AssetParams').returns(true)
preferencesController.openPopup = async () => {
await preferencesController.addToken(address, symbol, decimals, image)
}
await preferencesController._handleWatchAssetERC20(req.params.options)
const tokens = preferencesController.getTokens()
assert.equal(tokens.length, 1, `one token added`)
const added = tokens[0]
assert.equal(added.address, address, 'set address correctly')
assert.equal(added.symbol, symbol, 'set symbol correctly')
assert.equal(added.decimals, decimals, 'set decimals correctly')
const assetImages = preferencesController.getAssetImages()
assert.ok(assetImages[address], `set image correctly`)
})
it('should validate ERC20 asset correctly', async function () {
const validate = preferencesController._validateERC20AssetParams
assert.doesNotThrow(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: 0 }))
assert.throws(() => validate({ symbol: 'ABC', decimals: 0 }), 'missing address should fail')
assert.throws(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', decimals: 0 }), 'missing symbol should fail')
assert.throws(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC' }), 'missing decimals should fail')
assert.throws(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABCDEFGHI', decimals: 0 }), 'invalid symbol should fail')
assert.throws(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: -1 }), 'decimals < 0 should fail')
assert.throws(() => validate({ rawAddress: '0xd26114cd6EE289AccF82350c8d8487fedB8A0C07', symbol: 'ABC', decimals: 38 }), 'decimals > 36 should fail')
assert.throws(() => validate({ rawAddress: '0x123', symbol: 'ABC', decimals: 0 }), 'invalid address should fail')
})
})
describe('setPasswordForgotten', function () {
it('should default to false', function () {
const state = preferencesController.store.getState()
assert.equal(state.forgottenPassword, false)
})
it('should set the forgottenPassword property in state', function () {
assert.equal(preferencesController.store.getState().forgottenPassword, false)
preferencesController.setPasswordForgotten(true)
assert.equal(preferencesController.store.getState().forgottenPassword, true)
})
})
describe('#updateRpc', function () {
it('should update the rpcDetails properly', async function () {
preferencesController.store.updateState({ frequentRpcListDetail: [{}, { rpcUrl: 'test', chainId: '0x1' }, {}] })
await preferencesController.updateRpc({ rpcUrl: 'test', chainId: '0x1' })
await preferencesController.updateRpc({ rpcUrl: 'test/1', chainId: '0x1' })
await preferencesController.updateRpc({ rpcUrl: 'test/2', chainId: '0x1' })
await preferencesController.updateRpc({ rpcUrl: 'test/3', chainId: '0x1' })
const list = preferencesController.getFrequentRpcListDetail()
assert.deepEqual(list[1], { rpcUrl: 'test', chainId: '0x1' })
})
it('should migrate address book entries if chainId changes', async function () {
preferencesController.store.updateState({ frequentRpcListDetail: [{}, { rpcUrl: 'test', chainId: '1' }, {}] })
await preferencesController.updateRpc({ rpcUrl: 'test', chainId: '0x1' })
assert(migrateAddressBookState.calledWith('1', '0x1'))
})
})
describe('adding and removing from frequentRpcListDetail', function () {
it('should add custom RPC url to state', function () {
preferencesController.addToFrequentRpcList('rpc_url', '0x1')
preferencesController.addToFrequentRpcList('http://localhost:8545', '0x1')
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }])
preferencesController.addToFrequentRpcList('rpc_url', '0x1')
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }])
})
it('should throw if chainId is invalid', function () {
assert.throws(
() => {
preferencesController.addToFrequentRpcList('rpc_url', '1')
},
'should throw on invalid chainId',
)
})
it('should remove custom RPC url from state', function () {
preferencesController.addToFrequentRpcList('rpc_url', '0x1')
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [{ rpcUrl: 'rpc_url', chainId: '0x1', ticker: 'ETH', nickname: '', rpcPrefs: {} }])
preferencesController.removeFromFrequentRpcList('other_rpc_url')
preferencesController.removeFromFrequentRpcList('http://localhost:8545')
preferencesController.removeFromFrequentRpcList('rpc_url')
assert.deepEqual(preferencesController.store.getState().frequentRpcListDetail, [])
})
})
describe('setUsePhishDetect', function () {
it('should default to true', function () {
const state = preferencesController.store.getState()
assert.equal(state.usePhishDetect, true)
})
it('should set the usePhishDetect property in state', function () {
assert.equal(preferencesController.store.getState().usePhishDetect, true)
preferencesController.setUsePhishDetect(false)
assert.equal(preferencesController.store.getState().usePhishDetect, false)
})
})
})