diff options
author | Thomas <thomas.b.huang@gmail.com> | 2018-07-26 13:38:44 +0800 |
---|---|---|
committer | Thomas <thomas.b.huang@gmail.com> | 2018-07-26 13:38:44 +0800 |
commit | 138858647ed28a8aaba4b7e18e387ea0b6af0889 (patch) | |
tree | 0bc936ee7316112d01e617a246a645e3914da949 /test/unit | |
parent | fa02a6c7c65f6866998171881fd657570fe3fe7b (diff) | |
parent | a5d344a58223e029dc86bf33a8ca9357492765f3 (diff) | |
download | tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.gz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.bz2 tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.lz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.xz tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.tar.zst tangerine-wallet-browser-138858647ed28a8aaba4b7e18e387ea0b6af0889.zip |
Merge branch 'develop' into network-remove-provider-engine
Diffstat (limited to 'test/unit')
-rw-r--r-- | test/unit/actions/tx_test.js | 102 | ||||
-rw-r--r-- | test/unit/app/controllers/detect-tokens-test.js | 141 | ||||
-rw-r--r-- | test/unit/app/controllers/metamask-controller-test.js | 156 | ||||
-rw-r--r-- | test/unit/app/controllers/preferences-controller-test.js | 25 | ||||
-rw-r--r-- | test/unit/app/controllers/transactions/tx-controller-test.js | 13 | ||||
-rw-r--r-- | test/unit/app/controllers/transactions/tx-state-manager-test.js | 19 | ||||
-rw-r--r-- | test/unit/components/pending-tx-test.js | 67 | ||||
-rw-r--r-- | test/unit/migrations/027-test.js | 50 |
8 files changed, 439 insertions, 134 deletions
diff --git a/test/unit/actions/tx_test.js b/test/unit/actions/tx_test.js index c110f71fc..160cd4552 100644 --- a/test/unit/actions/tx_test.js +++ b/test/unit/actions/tx_test.js @@ -1,74 +1,54 @@ -// var jsdom = require('mocha-jsdom') var assert = require('assert') -var freeze = require('deep-freeze-strict') var path = require('path') -var sinon = require('sinon') -var actions = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'actions.js')) -var reducers = require(path.join(__dirname, '..', '..', '..', 'ui', 'app', 'reducers.js')) +import configureMockStore from 'redux-mock-store' +import thunk from 'redux-thunk' -describe('tx confirmation screen', function () { - beforeEach(function () { - this.sinon = sinon.createSandbox() - }) - - afterEach(function () { - this.sinon.restore() - }) +const actions = require(path.join(__dirname, '../../../ui/app/actions.js')) - var initialState, result +const middlewares = [thunk] +const mockStore = configureMockStore(middlewares) - describe('when there is only one tx', function () { - var firstTxId = 1457634084250832 - - beforeEach(function () { - initialState = { - appState: { - currentView: { - name: 'confTx', - }, - }, - metamask: { - unapprovedTxs: { - '1457634084250832': { - id: 1457634084250832, - status: 'unconfirmed', - time: 1457634084250, - }, - }, +describe('tx confirmation screen', function () { + const txId = 1457634084250832 + const initialState = { + appState: { + currentView: { + name: 'confTx', + }, + }, + metamask: { + unapprovedTxs: { + [txId]: { + id: txId, + status: 'unconfirmed', + time: 1457634084250, }, - } - freeze(initialState) + }, + }, + } + + const store = mockStore(initialState) + + describe('cancelTx', function () { + before(function (done) { + actions._setBackgroundConnection({ + approveTransaction (txId, cb) { cb('An error!') }, + cancelTransaction (txId, cb) { cb() }, + clearSeedWordCache (cb) { cb() }, + getState (cb) { cb() }, + }) + done() }) - describe('cancelTx', function () { - before(function (done) { - actions._setBackgroundConnection({ - approveTransaction (txId, cb) { cb('An error!') }, - cancelTransaction (txId, cb) { cb() }, - clearSeedWordCache (cb) { cb() }, + it('creates COMPLETED_TX with the cancelled transaction ID', function (done) { + store.dispatch(actions.cancelTx({ id: txId })) + .then(() => { + const storeActions = store.getActions() + const completedTxAction = storeActions.find(({ type }) => type === actions.COMPLETED_TX) + assert.equal(completedTxAction.value, txId) + done() }) - - actions.cancelTx({value: firstTxId})((action) => { - result = reducers(initialState, action) - }) - done() - }) - - it('should transition to the account detail view', function () { - assert.equal(result.appState.currentView.name, 'accountDetail') - }) - - it('should have no unconfirmed txs remaining', function () { - var count = getUnconfirmedTxCount(result) - assert.equal(count, 0) - }) }) }) }) - -function getUnconfirmedTxCount (state) { - var txs = state.metamask.unapprovedTxs - var count = Object.keys(txs).length - return count -} diff --git a/test/unit/app/controllers/detect-tokens-test.js b/test/unit/app/controllers/detect-tokens-test.js new file mode 100644 index 000000000..426ffe23a --- /dev/null +++ b/test/unit/app/controllers/detect-tokens-test.js @@ -0,0 +1,141 @@ +const assert = require('assert') +const sinon = require('sinon') +const ObservableStore = require('obs-store') +const DetectTokensController = require('../../../../app/scripts/controllers/detect-tokens') +const NetworkController = require('../../../../app/scripts/controllers/network/network') +const PreferencesController = require('../../../../app/scripts/controllers/preferences') + +describe('DetectTokensController', () => { + const sandbox = sinon.createSandbox() + let clock + let keyringMemStore + before(async () => { + keyringMemStore = new ObservableStore({ isUnlocked: false}) + }) + after(() => { + sandbox.restore() + }) + + it('should poll on correct interval', async () => { + const stub = sinon.stub(global, 'setInterval') + new DetectTokensController({ interval: 1337 }) // eslint-disable-line no-new + assert.strictEqual(stub.getCall(0).args[1], 1337) + stub.restore() + }) + + it('should be called on every polling period', async () => { + clock = sandbox.useFakeTimers() + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + var stub = sandbox.stub(controller, 'detectNewTokens') + + clock.tick(1) + sandbox.assert.notCalled(stub) + clock.tick(180000) + sandbox.assert.called(stub) + clock.tick(180000) + sandbox.assert.calledTwice(stub) + clock.tick(180000) + sandbox.assert.calledThrice(stub) + }) + + it('should not check tokens while in test network', async () => { + const network = new NetworkController() + network.setProviderType('rinkeby') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + var stub = sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4').returns(true) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388').returns(true) + + await controller.detectNewTokens() + sandbox.assert.notCalled(stub) + }) + + it('should only check and add tokens while in main network', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4') + .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388') + .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18)) + + await controller.detectNewTokens() + assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'}, + {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}]) + }) + + it('should not detect same token while in main network', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8) + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + + sandbox.stub(controller, 'detectTokenBalance') + .withArgs('0x0D262e5dC4A06a0F1c90cE79C7a60C09DfC884E4') + .returns(preferences.addToken('0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', 'J8T', 8)) + .withArgs('0xBC86727E770de68B1060C91f6BB6945c73e10388') + .returns(preferences.addToken('0xbc86727e770de68b1060c91f6bb6945c73e10388', 'XNK', 18)) + + await controller.detectNewTokens() + assert.deepEqual(preferences.store.getState().tokens, [{address: '0x0d262e5dc4a06a0f1c90ce79c7a60c09dfc884e4', decimals: 8, symbol: 'J8T'}, + {address: '0xbc86727e770de68b1060c91f6bb6945c73e10388', decimals: 18, symbol: 'XNK'}]) + }) + + it('should trigger detect new tokens when change address', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = true + var stub = sandbox.stub(controller, 'detectNewTokens') + await preferences.setSelectedAddress('0xbc86727e770de68b1060c91f6bb6945c73e10388') + sandbox.assert.called(stub) + }) + + it('should trigger detect new tokens when submit password', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.selectedAddress = '0x0' + var stub = sandbox.stub(controller, 'detectNewTokens') + await controller._keyringMemStore.updateState({ isUnlocked: true }) + sandbox.assert.called(stub) + }) + + it('should not trigger detect new tokens when not open or not unlocked', async () => { + const network = new NetworkController() + network.setProviderType('mainnet') + const preferences = new PreferencesController() + const controller = new DetectTokensController({ preferences: preferences, network: network, keyringMemStore: keyringMemStore }) + controller.isOpen = true + controller.isUnlocked = false + var stub = sandbox.stub(controller, 'detectTokenBalance') + clock.tick(180000) + sandbox.assert.notCalled(stub) + controller.isOpen = false + controller.isUnlocked = true + clock.tick(180000) + sandbox.assert.notCalled(stub) + }) +}) diff --git a/test/unit/app/controllers/metamask-controller-test.js b/test/unit/app/controllers/metamask-controller-test.js index cbd949e69..0f51ed6cb 100644 --- a/test/unit/app/controllers/metamask-controller-test.js +++ b/test/unit/app/controllers/metamask-controller-test.js @@ -224,6 +224,129 @@ describe('MetaMaskController', function () { }) }) + describe('connectHardware', function () { + + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.connectHardware('Some random device name', 0) + } catch (e) { + assert.equal(e, 'Error: MetamaskController:connectHardware - Unknown device') + } + }) + + it('should add the Trezor Hardware keyring', async function () { + sinon.spy(metamaskController.keyringController, 'addNewKeyring') + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + assert.equal(metamaskController.keyringController.addNewKeyring.getCall(0).args, 'Trezor Hardware') + assert.equal(keyrings.length, 1) + }) + + }) + + describe('checkHardwareStatus', function () { + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.checkHardwareStatus('Some random device name') + } catch (e) { + assert.equal(e, 'Error: MetamaskController:checkHardwareStatus - Unknown device') + } + }) + + it('should be locked by default', async function () { + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + const status = await metamaskController.checkHardwareStatus('trezor') + assert.equal(status, false) + }) + }) + + describe('forgetDevice', function () { + it('should throw if it receives an unknown device name', async function () { + try { + await metamaskController.forgetDevice('Some random device name') + } catch (e) { + assert.equal(e, 'Error: MetamaskController:forgetDevice - Unknown device') + } + }) + + it('should wipe all the keyring info', async function () { + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.forgetDevice('trezor') + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + + assert.deepEqual(keyrings[0].accounts, []) + assert.deepEqual(keyrings[0].page, 0) + assert.deepEqual(keyrings[0].isUnlocked(), false) + }) + }) + + describe('unlockTrezorAccount', function () { + let accountToUnlock + let windowOpenStub + let addNewAccountStub + let getAccountsStub + beforeEach(async function () { + accountToUnlock = 10 + windowOpenStub = sinon.stub(window, 'open') + windowOpenStub.returns(noop) + + addNewAccountStub = sinon.stub(metamaskController.keyringController, 'addNewAccount') + addNewAccountStub.returns({}) + + getAccountsStub = sinon.stub(metamaskController.keyringController, 'getAccounts') + // Need to return different address to mock the behavior of + // adding a new account from the keyring + getAccountsStub.onCall(0).returns(Promise.resolve(['0x1'])) + getAccountsStub.onCall(1).returns(Promise.resolve(['0x2'])) + getAccountsStub.onCall(2).returns(Promise.resolve(['0x3'])) + getAccountsStub.onCall(3).returns(Promise.resolve(['0x4'])) + sinon.spy(metamaskController.preferencesController, 'setAddresses') + sinon.spy(metamaskController.preferencesController, 'setSelectedAddress') + sinon.spy(metamaskController.preferencesController, 'setAccountLabel') + await metamaskController.connectHardware('trezor', 0).catch((e) => null) + await metamaskController.unlockTrezorAccount(accountToUnlock).catch((e) => null) + }) + + afterEach(function () { + metamaskController.keyringController.addNewAccount.restore() + window.open.restore() + }) + + it('should set accountToUnlock in the keyring', async function () { + const keyrings = await metamaskController.keyringController.getKeyringsByType( + 'Trezor Hardware' + ) + assert.equal(keyrings[0].unlockedAccount, accountToUnlock) + }) + + + it('should call keyringController.addNewAccount', async function () { + assert(metamaskController.keyringController.addNewAccount.calledOnce) + }) + + it('should call keyringController.getAccounts ', async function () { + assert(metamaskController.keyringController.getAccounts.called) + }) + + it('should call preferencesController.setAddresses', async function () { + assert(metamaskController.preferencesController.setAddresses.calledOnce) + }) + + it('should call preferencesController.setSelectedAddress', async function () { + assert(metamaskController.preferencesController.setSelectedAddress.calledOnce) + }) + + it('should call preferencesController.setAccountLabel', async function () { + assert(metamaskController.preferencesController.setAccountLabel.calledOnce) + }) + + + }) + describe('#setCustomRpc', function () { let rpcTarget @@ -355,6 +478,39 @@ describe('MetaMaskController', function () { }) }) + describe('#removeAccount', function () { + let ret + const addressToRemove = '0x1' + + beforeEach(async function () { + sinon.stub(metamaskController.preferencesController, 'removeAddress') + sinon.stub(metamaskController.accountTracker, 'removeAccount') + sinon.stub(metamaskController.keyringController, 'removeAccount') + + ret = await metamaskController.removeAccount(addressToRemove) + + }) + + afterEach(function () { + metamaskController.keyringController.removeAccount.restore() + metamaskController.accountTracker.removeAccount.restore() + metamaskController.preferencesController.removeAddress.restore() + }) + + it('should call preferencesController.removeAddress', async function () { + assert(metamaskController.preferencesController.removeAddress.calledWith(addressToRemove)) + }) + it('should call accountTracker.removeAccount', async function () { + assert(metamaskController.accountTracker.removeAccount.calledWith(addressToRemove)) + }) + it('should call keyringController.removeAccount', async function () { + assert(metamaskController.keyringController.removeAccount.calledWith(addressToRemove)) + }) + it('should return address', async function () { + assert.equal(ret, '0x1') + }) + }) + describe('#clearSeedWordCache', function () { it('should have set seed words', function () { diff --git a/test/unit/app/controllers/preferences-controller-test.js b/test/unit/app/controllers/preferences-controller-test.js index e5e751b57..e055500b1 100644 --- a/test/unit/app/controllers/preferences-controller-test.js +++ b/test/unit/app/controllers/preferences-controller-test.js @@ -52,6 +52,31 @@ describe('preferences controller', function () { }) }) + 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 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([ diff --git a/test/unit/app/controllers/transactions/tx-controller-test.js b/test/unit/app/controllers/transactions/tx-controller-test.js index 7eb111cfb..b62499a70 100644 --- a/test/unit/app/controllers/transactions/tx-controller-test.js +++ b/test/unit/app/controllers/transactions/tx-controller-test.js @@ -354,9 +354,16 @@ describe('Transaction Controller', function () { ]) }) - it('should set the transaction to rejected from unapproved', async function () { - await txController.cancelTransaction(0) - assert.equal(txController.txStateManager.getTx(0).status, 'rejected') + it('should emit a status change to rejected', function (done) { + txController.once('tx:status-update', (txId, status) => { + try { + assert.equal(status, 'rejected', 'status should e rejected') + assert.equal(txId, 0, 'id should e 0') + done() + } catch (e) { done(e) } + }) + + txController.cancelTransaction(0) }) }) diff --git a/test/unit/app/controllers/transactions/tx-state-manager-test.js b/test/unit/app/controllers/transactions/tx-state-manager-test.js index 2f91b1545..88bdaa60e 100644 --- a/test/unit/app/controllers/transactions/tx-state-manager-test.js +++ b/test/unit/app/controllers/transactions/tx-state-manager-test.js @@ -43,14 +43,13 @@ describe('TransactionStateManager', function () { }) describe('#setTxStatusRejected', function () { - it('sets the tx status to rejected', function () { + it('sets the tx status to rejected and removes it from history', function () { const tx = { id: 1, status: 'unapproved', metamaskNetworkId: currentNetworkId, txParams: {} } txStateManager.addTx(tx) txStateManager.setTxStatusRejected(1) const result = txStateManager.getTxList() assert.ok(Array.isArray(result)) - assert.equal(result.length, 1) - assert.equal(result[0].status, 'rejected') + assert.equal(result.length, 0) }) it('should emit a rejected event to signal the exciton of callback', (done) => { @@ -287,4 +286,18 @@ describe('TransactionStateManager', function () { }) }) + + describe('#_removeTx', function () { + it('should remove the transaction from the storage', () => { + txStateManager._saveTxList([ {id: 1} ]) + txStateManager._removeTx(1) + assert(!txStateManager.getFullTxList().length, 'txList should be empty') + }) + + it('should only remove the transaction with ID 1 from the storage', () => { + txStateManager._saveTxList([ {id: 1}, {id: 2} ]) + txStateManager._removeTx(1) + assert.equal(txStateManager.getFullTxList()[0].id, 2, 'txList should have a id of 2') + }) + }) }) diff --git a/test/unit/components/pending-tx-test.js b/test/unit/components/pending-tx-test.js deleted file mode 100644 index c68e013ac..000000000 --- a/test/unit/components/pending-tx-test.js +++ /dev/null @@ -1,67 +0,0 @@ -const assert = require('assert') -const h = require('react-hyperscript') -const PendingTx = require('../../../ui/app/components/pending-tx') -const ethUtil = require('ethereumjs-util') - -const { createMockStore } = require('redux-test-utils') -const { shallowWithStore } = require('../../lib/shallow-with-store') - -const identities = { abc: {}, def: {} } -const mockState = { - metamask: { - accounts: { abc: {} }, - identities, - conversionRate: 10, - selectedAddress: 'abc', - }, -} - -describe('PendingTx', function () { - const gasPrice = '0x4A817C800' // 20 Gwei - const txData = { - 'id': 5021615666270214, - 'time': 1494458763011, - 'status': 'unapproved', - 'metamaskNetworkId': '1494442339676', - 'txParams': { - 'from': '0xfdea65c8e26263f6d9a1b5de9555d2931a33b826', - 'to': '0xc5b8dbac4c1d3f152cdeb400e2313f309c410acb', - 'value': '0xde0b6b3a7640000', - gasPrice, - 'gas': '0x7b0c', - }, - 'gasLimitSpecified': false, - 'estimatedGas': '0x5208', - } - const newGasPrice = '0x77359400' - - const computedBalances = {} - computedBalances[Object.keys(identities)[0]] = { - ethBalance: '0x00000000000000056bc75e2d63100000', - } - const props = { - txData, - computedBalances, - sendTransaction: (txMeta, event) => { - // Assert changes: - const result = ethUtil.addHexPrefix(txMeta.txParams.gasPrice) - assert.notEqual(result, gasPrice, 'gas price should change') - assert.equal(result, newGasPrice, 'gas price assigned.') - }, - } - - let pendingTxComponent - let store - let component - beforeEach(function () { - store = createMockStore(mockState) - component = shallowWithStore(h(PendingTx, props), store) - pendingTxComponent = component - }) - - it('should render correctly', function (done) { - assert.equal(pendingTxComponent.props().identities, identities) - done() - }) -}) - diff --git a/test/unit/migrations/027-test.js b/test/unit/migrations/027-test.js new file mode 100644 index 000000000..3ec9f0c0e --- /dev/null +++ b/test/unit/migrations/027-test.js @@ -0,0 +1,50 @@ +const assert = require('assert') +const migration27 = require('../../../app/scripts/migrations/027') + +const oldStorage = { + 'meta': {}, + 'data': { + 'TransactionController': { + 'transactions': [ + ], + }, + }, +} + +const transactions = [] + + +while (transactions.length < 9) { + transactions.push({status: 'rejected'}) + transactions.push({status: 'unapproved'}) + transactions.push({status: 'approved'}) +} + + +oldStorage.data.TransactionController.transactions = transactions + +describe('migration #27', () => { + it('should remove rejected transactions', (done) => { + migration27.migrate(oldStorage) + .then((newStorage) => { + const newTransactions = newStorage.data.TransactionController.transactions + assert.equal(newTransactions.length, 6, 'transactions is expected to have the length of 6') + newTransactions.forEach((txMeta) => { + if (txMeta.status === 'rejected') done(new Error('transaction was found with a status of rejected')) + }) + done() + }) + .catch(done) + }) + + it('should successfully migrate first time state', (done) => { + migration27.migrate({ + meta: {}, + data: require('../../../app/scripts/first-time-state'), + }) + .then((migratedData) => { + assert.equal(migratedData.meta.version, migration27.version) + done() + }).catch(done) + }) +}) |