diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..daedbf4b44 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,35 @@ +name: Format + +on: + pull_request: + paths: + - '**.css' + - '**.js' + - '**.ts' + - '**.vue' + +jobs: + eslint: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v1 + + - name: Update System + run: sudo apt-get update + + - name: Ledger + run: sudo apt-get install libudev-dev libusb-1.0-0-dev + + - name: Install + run: yarn global add node-gyp && yarn install --frozen-lockfile && npm rebuild + + - name: Run eslint + run: yarn lint:fix + + - uses: stefanzweifel/git-auto-commit-action@v2.1.0 + with: + commit_message: "style: resolve style guide violations" + branch: ${{ github.head_ref }} + env: + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dfeb33b2bb..7789a0793d 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -3,8 +3,8 @@ name: Test & Build on: push: branches: - - 'master' - - 'develop' + - "master" + - "develop" pull_request: types: [ready_for_review, synchronize, opened] @@ -44,14 +44,12 @@ jobs: run: yarn lint - name: Test - run: yarn test:unit --coverage + run: node --max-old-space-size=4096 ./node_modules/.bin/jest --config __tests__/unit.jest.conf.js __tests__/unit/ --logHeapUsage --coverage - name: Codecov run: ./node_modules/.bin/codecov --token=${{ secrets.CODECOV_TOKEN }} build-linux: - needs: lint-unit - runs-on: ubuntu-latest strategy: @@ -80,32 +78,33 @@ jobs: run: sudo apt-get install libudev-dev libusb-1.0-0-dev - name: Install - run: yarn global add node-gyp && yarn install --frozen-lockfile && npm rebuild + run: yarn global add node-gyp && yarn install --frozen-lockfile + + - name: Re-build bcrypto + run: cd node_modules/bcrypto && npm install && cd ../../ - name: Build - run: node .electron-vue/build.js && node ./node_modules/electron-builder/out/cli/cli.js --linux + run: yarn build:linux # - name: Upload .AppImage # uses: actions/upload-artifact@master # with: - # name: ark-desktop-wallet-linux-2.7.0.AppImage - # path: build/target/ark-desktop-wallet-linux-x86_64-2.7.0.AppImage + # name: ark-desktop-wallet-linux-2.8.0.AppImage + # path: build/target/ark-desktop-wallet-linux-x86_64-2.8.0.AppImage # - name: Upload .tar.gz # uses: actions/upload-artifact@master # with: - # name: ark-desktop-wallet-linux-2.7.0.tar.gz - # path: build/target/ark-desktop-wallet-linux-x64-2.7.0.tar.gz + # name: ark-desktop-wallet-linux-2.8.0.tar.gz + # path: build/target/ark-desktop-wallet-linux-x64-2.8.0.tar.gz - name: Upload .deb uses: actions/upload-artifact@master with: - name: ark-desktop-wallet-linux-2.7.0.deb - path: build/target/ark-desktop-wallet-linux-amd64-2.7.0.deb + name: ark-desktop-wallet-linux-2.8.0.deb + path: build/target/ark-desktop-wallet-linux-amd64-2.8.0.deb build-macOS: - needs: lint-unit - runs-on: macOS-latest strategy: @@ -128,26 +127,24 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install - run: yarn global add node-gyp && yarn install --frozen-lockfile && npm rebuild + run: yarn global add node-gyp && yarn install --frozen-lockfile - name: Build - run: node .electron-vue/build.js && node ./node_modules/electron-builder/out/cli/cli.js --mac + run: yarn build:mac # - name: Upload .zip # uses: actions/upload-artifact@master # with: - # name: ark-desktop-wallet-mac-2.7.0.zip - # path: build/target/ark-desktop-wallet-mac-2.7.0.zip + # name: ark-desktop-wallet-mac-2.8.0.zip + # path: build/target/ark-desktop-wallet-mac-2.8.0.zip - name: Upload .dmg - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v1 with: - name: ark-desktop-wallet-mac-2.7.0.dmg - path: build/target/ark-desktop-wallet-mac-2.7.0.dmg + name: ark-desktop-wallet-mac-2.8.0.dmg + path: build/target/ark-desktop-wallet-mac-2.8.0.dmg build-windows: - needs: lint-unit - runs-on: windows-latest strategy: @@ -163,15 +160,15 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install - run: yarn global add node-gyp && yarn install --frozen-lockfile && npm rebuild + run: yarn global add node-gyp && yarn install --frozen-lockfile shell: cmd - name: Build - run: node .electron-vue/build.js && node ./node_modules/electron-builder/out/cli/cli.js --win --x64 --ia32 + run: yarn build:win shell: cmd - name: Upload .exe - uses: actions/upload-artifact@master + uses: actions/upload-artifact@v1 with: - name: ark-desktop-wallet-win-2.7.0.exe - path: build/target/ark-desktop-wallet-win-2.7.0.exe + name: ark-desktop-wallet-win-2.8.0.exe + path: build/target/ark-desktop-wallet-win-2.8.0.exe diff --git a/.lintstagedrc b/.lintstagedrc index 23473633ed..14b3691d84 100644 --- a/.lintstagedrc +++ b/.lintstagedrc @@ -1,3 +1,3 @@ { - "*.{js,vue}": ["npm run lint:fix", "git add"] + "*.{js,vue}": ["npm run lint:fix"] } diff --git a/.travis.yml b/.travis.yml index 2d758bc952..3f0da248ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -27,11 +27,11 @@ before_script: script: - | if [ "$TRAVIS_OS_NAME" == "osx" ]; then - yarn build:mac + yarn build:mac:publish elif [ "$TRAVIS_OS_NAME" == "windows" ]; then - yarn build:win + yarn build:win:publish else - yarn build:linux + yarn build:linux:publish fi after_script: diff --git a/__tests__/unit.jest.conf.js b/__tests__/unit.jest.conf.js index c6c20e94e4..0c995c16c0 100644 --- a/__tests__/unit.jest.conf.js +++ b/__tests__/unit.jest.conf.js @@ -1,11 +1,9 @@ -const path = require('path') - module.exports = { verbose: false, globals: { __static: __dirname }, - rootDir: path.resolve(__dirname, '../'), + rootDir: require('path').resolve(__dirname, '../'), moduleFileExtensions: [ 'js', 'json', @@ -14,8 +12,8 @@ module.exports = { moduleNameMapper: { '^@tailwind': '/tailwind.js', '^@package.json$': '/package.json', - '^@config': '/config/index.js', '^@config/(.*)$': '/config/$1', + '^@config': '/config/index.js', '^@/(.*)$': '/src/renderer/$1', '^@tests/(.*)$': '/__tests__/$1', vue$: '/node_modules/vue/dist/vue.common.js' diff --git a/__tests__/unit/__fixtures__/models/transaction.js b/__tests__/unit/__fixtures__/models/transaction.js new file mode 100644 index 0000000000..977837d7fd --- /dev/null +++ b/__tests__/unit/__fixtures__/models/transaction.js @@ -0,0 +1,16 @@ +export default { + id: '022d918ed3b2174c4e288c675805fa2e85547cef8ba2020e54ca84c0c4b9b3ba', + blockId: '9014855312368837997', + version: 1, + type: 0, + typeGroup: 1, + amount: 1, + fee: 10000000, + sender: 'AJjv7WztjJNYHrLAeveG5NgHWp6699ZJwD', + senderPublicKey: '02275d8577a0ec2b75fc8683282d53c5db76ebc54514a80c2854e419b793ea259a', + recipientId: 'AdXbS4GKvV6TZVHrNzcYSQKfpenQnFGTxK', + signature: '304402206db4ffcdff1c6eb19ecdf0f83cf528a34673eac57defbf25298a6f25f9791fd602206f12834a752c11d749c7450c63550e2c09ff01092c23f67941d6695faaa9e0a9', + confirmations: 444027, + timestamp: 59960304, + nonce: '3' +} diff --git a/__tests__/unit/__fixtures__/services/client.js b/__tests__/unit/__fixtures__/services/client.js index d70fc700fe..50a61aa442 100644 --- a/__tests__/unit/__fixtures__/services/client.js +++ b/__tests__/unit/__fixtures__/services/client.js @@ -26,9 +26,10 @@ delegates.v2 = [ const transactions = { data: [ - { id: 1, amount: 100000, fee: 10000000, timestamp: { epoch: 47848091, human: '2018-09-26T08:08:11.000Z' }, sender: 'address1', recipient: 'address2' }, - { id: 2, amount: 200000, fee: 10000000, timestamp: { epoch: 47809625, human: '2018-09-25T21:27:05.000Z' }, sender: 'address2', recipient: 'address3' }, - { id: 3, amount: 300000, fee: 10000000, timestamp: { epoch: 47796863, human: '2018-09-25T17:54:23.000Z' }, sender: 'address3', recipient: 'address3' } + { id: 1, type: 0, amount: 100000, fee: 10000000, timestamp: { epoch: 47848091, human: '2018-09-26T08:08:11.000Z' }, sender: 'address1', recipient: 'address2' }, + { id: 2, type: 0, amount: 200000, fee: 10000000, timestamp: { epoch: 47809625, human: '2018-09-25T21:27:05.000Z' }, sender: 'address2', recipient: 'address3' }, + { id: 3, type: 0, amount: 300000, fee: 10000000, timestamp: { epoch: 47796863, human: '2018-09-25T17:54:23.000Z' }, sender: 'address3', recipient: 'address3' }, + { id: 3, type: 1, amount: 0, fee: 10000000, timestamp: { epoch: 47796863, human: '2018-09-25T17:54:23.000Z' }, sender: 'address3', recipient: 'address3' } ] } @@ -65,10 +66,12 @@ const staticFeeResponses = { delegateRegistration: 2500000000, vote: 100000000, multiSignature: 500000000, - ipfs: 0, - timelockTransfer: 0, - multiPayment: 0, - delegateResignation: 0 + ipfs: 500000000, + multiPayment: 100000000, + delegateResignation: 2500000000, + htlcLock: 100000000, + htlcClaim: 100000000, + htlcRefund: 100000000 } } } diff --git a/__tests__/unit/__mocks__/@/services/wallet.js b/__tests__/unit/__mocks__/@/services/wallet.js index 2be8f4db2f..12b0a73eb7 100644 --- a/__tests__/unit/__mocks__/@/services/wallet.js +++ b/__tests__/unit/__mocks__/@/services/wallet.js @@ -1,3 +1,5 @@ +import WalletServiceOriginal from '../../../../../src/renderer/services/wallet' + export default { generate: jest.fn(() => { return { @@ -5,9 +7,15 @@ export default { passphrase: 'passphrase' } }), + canResignBusiness: jest.fn(() => false), getAddressFromPublicKey: jest.fn(address => `public key of ${address}`), + getPublicKeyFromPassphrase: jest.fn(passphrase => `public key of ${passphrase}`), + getPublicKeyFromMultiSignatureAsset: jest.fn(multisignature => 'public key of multisignature'), + generateSecondPassphrase: jest.fn(() => 'second-passphrase'), validateAddress: jest.fn(() => true), validatePassphrase: jest.fn(() => true), + validateUsername: jest.fn(WalletServiceOriginal.validateUsername), verifyPassphrase: jest.fn(() => true), - isBip39Passphrase: jest.fn(() => true) + isBip39Passphrase: jest.fn(() => true), + isNeoAddress: jest.fn(() => false) } diff --git a/__tests__/unit/__mocks__/@arkecosystem/ledger-transport.js b/__tests__/unit/__mocks__/@arkecosystem/ledger-transport.js index c5acee04b2..10f59bc606 100644 --- a/__tests__/unit/__mocks__/@arkecosystem/ledger-transport.js +++ b/__tests__/unit/__mocks__/@arkecosystem/ledger-transport.js @@ -1,10 +1,8 @@ -export default class { +export class ARKTransport { constructor () { - this.getAddress = jest.fn(() => ({ - address: 'DLWeBuwSBFYtUFj8kFB8CFswfvN2ht3yKn', - publicKey: '0278a28d0eac9916ef46613d9dbac706acc218e64864d4b4c1fcb0c759b6205b2b' - })) - this.signTransaction = jest.fn(() => ({ signature: 'SIGNATURE' })) - this.getAppConfiguration = jest.fn() + this.getPublicKey = jest.fn(() => '0278a28d0eac9916ef46613d9dbac706acc218e64864d4b4c1fcb0c759b6205b2b') + this.signMessage = jest.fn(() => 'SIGNATURE') + this.signTransaction = jest.fn(() => 'SIGNATURE') + this.getVersion = jest.fn(() => '1.0.0') } } diff --git a/__tests__/unit/components/Button/ButtonDropdown.spec.js b/__tests__/unit/components/Button/ButtonDropdown.spec.js new file mode 100644 index 0000000000..2b6847db43 --- /dev/null +++ b/__tests__/unit/components/Button/ButtonDropdown.spec.js @@ -0,0 +1,145 @@ +import { mount } from '@vue/test-utils' +import { ButtonDropdown } from '@/components/Button' + +const stubs = { + Portal: '
' +} + +let wrapper +const createWrapper = (component, propsData) => { + component = component || ButtonDropdown + propsData = propsData || { + title: 'Test', + items: [ + 'Test 1', + 'Test 2' + ] + } + + wrapper = mount(component, { + stubs, + propsData + }) +} + +describe('ButtonDropdown', () => { + beforeEach(() => { + createWrapper() + + Element.prototype.__defineGetter__('clientHeight', () => 10) + Element.prototype.getBoundingClientRect = () => ({ + top: 10, + left: 15 + }) + }) + + it('should render with dropdown', () => { + expect(wrapper.contains('.ButtonDropdown')).toBeTruthy() + expect(wrapper.contains('.ButtonDropdown__button')).toBeTruthy() + expect(wrapper.contains('.ButtonDropdown__list')).toBeTruthy() + }) + + it('should have a primary button', () => { + createWrapper({ + template: `
+ +
+ test +
+
+
`, + + components: { + ButtonDropdown + } + }, {}) + + const buttonDropdown = wrapper.find({ ref: 'testButton' }) + + expect(buttonDropdown.contains('.ButtonDropdown__primary')).toBeTruthy() + expect(buttonDropdown.vm.hasPrimaryButton).toEqual(true) + }) + + it('should have a dropdown button per item', () => { + expect(wrapper.findAll('.ButtonDropdown__list__item').length).toBe(2) + }) + + it('should show the dropdown on click', () => { + wrapper.setMethods({ + ...wrapper.vm.methods, + toggleDropdown: jest.fn(wrapper.vm.toggleDropdown) + }) + wrapper.find('.ButtonDropdown__button').trigger('click') + + expect(wrapper.vm.showDropdown).toBe(true) + expect(wrapper.vm.toggleDropdown).toHaveBeenCalled() + }) + + it('should toggle dropdown programmatically', () => { + wrapper.vm.toggleDropdown() + + expect(wrapper.vm.showDropdown).toBe(true) + + wrapper.vm.toggleDropdown() + + expect(wrapper.vm.showDropdown).toBe(false) + }) + + it('should close dropdown on item click', () => { + wrapper.setMethods({ + ...wrapper.vm.methods, + triggerClose: jest.fn(wrapper.vm.triggerClose) + }) + wrapper.vm.showDropdown = true + + wrapper.find('.ButtonDropdown__list__item .ButtonGeneric').trigger('click') + + expect(wrapper.vm.showDropdown).toBe(false) + expect(wrapper.vm.triggerClose).toHaveBeenCalled() + }) + + it('should close dropdown programmatically', () => { + wrapper.vm.showDropdown = true + wrapper.vm.triggerClose() + + expect(wrapper.vm.showDropdown).toBe(false) + }) + + it('should popluate classes onto dropdown', () => { + createWrapper(ButtonDropdown, { + title: 'Test', + items: [ + 'Test 1', + 'Test 2' + ], + classes: 'test-class-1 test-class-2' + }) + + expect(wrapper.find('.ButtonDropdown__button').classes('test-class-1')).toBe(true) + expect(wrapper.find('.ButtonDropdown__button').classes('test-class-2')).toBe(true) + expect(wrapper.vm.dropdownButtonClasses).toEqual({ 'ButtonDropdown__button--nolabel': false, 'test-class-1': true, 'test-class-2': true }) + }) + + it('should change arrow viewbox if dropdown is open', () => { + expect(wrapper.vm.arrowViewbox).toBe('0 -2 12 16') + + wrapper.vm.showDropdown = true + + expect(wrapper.vm.arrowViewbox).toBe('0 2 12 16') + }) + + it('should generate dropdown style', () => { + // Create sub-component which correctly populates $refs for ButtonDropdown + createWrapper({ + template: `
+ +
`, + + components: { + ButtonDropdown + } + }, {}) + + expect(wrapper.find({ ref: 'testButton' }).vm.dropdownStyle).toBe('top: 20px;left: 15px;z-index: 10') + }) +}) diff --git a/__tests__/unit/components/Input/InputEditableList.spec.js b/__tests__/unit/components/Input/InputEditableList.spec.js new file mode 100644 index 0000000000..061dd5718b --- /dev/null +++ b/__tests__/unit/components/Input/InputEditableList.spec.js @@ -0,0 +1,178 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../__utils__/i18n' +import { InputEditableList } from '@/components/Input' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, propsData) => { + component = component || InputEditableList + propsData = propsData || { + value: [] + } + + wrapper = mount(component, { + i18n, + localVue, + propsData + }) +} + +describe('InputEditableList', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.InputEditableList')).toBe(true) + }) + + it('should show title', () => { + createWrapper(null, { + value: [], + title: 'Test List' + }) + + expect(wrapper.find('.InputField__label').text()).toBe('Test List') + }) + + it('should list each item', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ] + }) + + expect(wrapper.findAll('.InputEditableList__list__item').length).toBe(2) + }) + + it('should update items when property is updated', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ] + }) + + expect(wrapper.findAll('.InputEditableList__list__item').length).toBe(2) + + wrapper.setProps({ + value: [ + 'test item 1', + 'test item 2', + 'test item 3', + 'test item 4' + ] + }) + + expect(wrapper.findAll('.InputEditableList__list__item').length).toBe(4) + }) + + it('should show remove button if not readonly', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ], + readonly: false + }) + + expect(wrapper.contains('.InputEditableList__list__item__remove')).toBe(true) + }) + + it('should not show remove button if readonly', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ], + readonly: true + }) + + expect(wrapper.contains('.InputEditableList__list__item__remove')).toBe(false) + }) + + it('should trigger remove when clicking remove button', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ], + readonly: false + }) + + wrapper.setMethods({ + ...wrapper.vm.methods, + emitRemove: jest.fn(wrapper.vm.emitRemove) + }) + + wrapper.find('.InputEditableList__list__item__remove').trigger('click') + expect(wrapper.vm.emitRemove).toHaveBeenCalled() + expect(wrapper.emitted().remove).toBeTruthy() + }) + + it('should show as invalid if property is specified', () => { + createWrapper(null, { + value: [ + 'test item 1', + 'test item 2' + ], + isInvalid: true + }) + + expect(wrapper.classes('InputEditableList--invalid')).toBe(true) + }) + + it('should show as invalid if required and no items', () => { + createWrapper({ + template: `
+ +
+ {{ item }} +
+
+
`, + + components: { + InputEditableList + } + }, {}) + + const items = wrapper.findAll('.TestSlotItem') + + expect(items.length).toBe(2) + + for (let itemIndex = 0; itemIndex < items.length; itemIndex++) { + const item = items.at(itemIndex) + + expect(item.text()).toEqual(`test ${itemIndex + 1}`) + } + }) + + it('should output helper text', () => { + const helperText = 'This is a test helper message' + expect(wrapper.contains('.InputEditableList__helper-text')).toBe(false) + + createWrapper(null, { + value: [], + helperText + }) + + expect(wrapper.contains('.InputEditableList__helper-text')).toBe(true) + expect(wrapper.find('.InputEditableList__helper-text').text()).toBe(helperText) + }) + + it('should output helper text', () => { + const noItemsMessage = 'This is "no items" message' + expect(wrapper.find('.InputEditableList__no-items').text()).toBe('INPUT_EDITABLE_LIST.NO_ITEMS') + + createWrapper(null, { + value: [], + noItemsMessage + }) + + expect(wrapper.find('.InputEditableList__no-items').text()).toBe(noItemsMessage) + }) +}) diff --git a/__tests__/unit/components/Input/InputFee.spec.js b/__tests__/unit/components/Input/InputFee.spec.js index 9466c6ec6d..726c7c58e8 100644 --- a/__tests__/unit/components/Input/InputFee.spec.js +++ b/__tests__/unit/components/Input/InputFee.spec.js @@ -130,12 +130,12 @@ describe('InputFee', () => { let wrapper = mountComponent({ propsData: { transactionType: 0 } }) - expect(wrapper.vm.maxV1fee).toEqual(V1.fees[0]) + expect(wrapper.vm.maxV1fee).toEqual(V1.fees.GROUP_1[0]) wrapper = mountComponent({ propsData: { transactionType: 3 } }) - expect(wrapper.vm.maxV1fee).toEqual(V1.fees[3]) + expect(wrapper.vm.maxV1fee).toEqual(V1.fees.GROUP_1[3]) }) }) diff --git a/__tests__/unit/components/Input/InputPublicKey.spec.js b/__tests__/unit/components/Input/InputPublicKey.spec.js new file mode 100644 index 0000000000..3acf2bad77 --- /dev/null +++ b/__tests__/unit/components/Input/InputPublicKey.spec.js @@ -0,0 +1,103 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../__utils__/i18n' +import { InputPublicKey } from '@/components/Input' +import transaction from '../../__fixtures__/models/transaction' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) +localVue.use(Vuelidate) + +let wrapper +const createWrapper = (component, propsData) => { + component = component || InputPublicKey + propsData = propsData || { + value: '' + } + + wrapper = mount(component, { + i18n, + localVue, + propsData, + sync: false + }) +} + +describe('InputPublicKey', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.InputPublicKey')).toBe(true) + }) + + it('should update model if value property is updated', async () => { + expect(wrapper.vm.inputValue).toBe('') + + wrapper.setProps({ + value: 'test' + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$v.model.$model).toEqual('test') + expect(wrapper.vm.inputValue).toBe('test') + }) + + it('should return an error if invalid input', () => { + wrapper.vm.$v.model.$model = 'test' + + expect(wrapper.vm.error).toBeTruthy() + }) + + it('should reset the value', async () => { + wrapper.vm.$v.model.$model = 'test' + + expect(wrapper.vm.model).toBe('test') + + wrapper.vm.reset() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.model).toBe('') + expect(wrapper.vm.error).toBeFalsy() + }) + + describe('validation', () => { + it('should check if valid public key is entered', () => { + const spy = jest.spyOn(Identities.Address, 'fromPublicKey') + + wrapper.vm.model = 'test' + + expect(wrapper.vm.$v.model.isValid).toBe(false) + + wrapper.vm.model = transaction.senderPublicKey + + expect(wrapper.vm.$v.model.isValid).toBe(true) + expect(spy).toHaveBeenCalledTimes(2) + + spy.mockRestore() + }) + + it('should not check if not required and empty', () => { + wrapper.setProps({ + isRequired: false + }) + + const spy = jest.spyOn(Identities.Address, 'fromPublicKey') + + wrapper.vm.model = '' + + expect(wrapper.vm.$v.model.isValid).toBe(true) + expect(spy).not.toHaveBeenCalled() + + wrapper.vm.model = 'test' + + expect(wrapper.vm.$v.model.isValid).toBe(false) + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + }) +}) diff --git a/__tests__/unit/components/Network/NetworkCustomPeerModal.spec.js b/__tests__/unit/components/Network/NetworkCustomPeerModal.spec.js deleted file mode 100644 index 8d9aa94a86..0000000000 --- a/__tests__/unit/components/Network/NetworkCustomPeerModal.spec.js +++ /dev/null @@ -1,27 +0,0 @@ -import { shallowMount } from '@vue/test-utils' -import { useI18nGlobally } from '../../__utils__/i18n' -import Vue from 'vue' -import Vuelidate from 'vuelidate' -import { NetworkCustomPeerModal } from '@/components/Network' - -const i18n = useI18nGlobally() - -const mocks = { - $store: { - getters: { - 'peer/current': jest.fn() - } - } -} - -Vue.use(Vuelidate) - -describe('NetworkCustomPeerModal', () => { - it('should render modal', () => { - const wrapper = shallowMount(NetworkCustomPeerModal, { - i18n, - mocks - }) - expect(wrapper.isVueInstance()).toBeTrue() - }) -}) diff --git a/__tests__/unit/components/PluginManager/PluginManagerModals/PluginRemovalModal.spec.js b/__tests__/unit/components/PluginManager/PluginManagerModals/PluginRemovalModal.spec.js index 227b09dcc1..e15b2d3d49 100644 --- a/__tests__/unit/components/PluginManager/PluginManagerModals/PluginRemovalModal.spec.js +++ b/__tests__/unit/components/PluginManager/PluginManagerModals/PluginRemovalModal.spec.js @@ -14,6 +14,19 @@ beforeEach(() => { permissions: [] } }, + mocks: { + $store: { + getters: { + 'plugin/profileHasPluginOptions': (pluginId) => { + if (pluginId === 'hasOptions') { + return true + } + + return false + } + } + } + }, stubs: { ListDivided: '
' } @@ -30,10 +43,10 @@ describe('PluginRemovalModal', () => { expect(wrapper.find('.ListDivided').exists()).toBeFalse() }) - it('should render divided list if plugin has STORAGE permission', () => { + it('should render divided list if plugin has STORAGE permission and data stored', () => { wrapper.setProps({ plugin: { - id: 'test', + id: 'hasOptions', permissions: ['STORAGE'] } }) @@ -42,6 +55,18 @@ describe('PluginRemovalModal', () => { expect(wrapper.find('.ListDivided').exists()).toBeTrue() }) + it('should not render divided list if plugin has STORAGE permission but no data stored', () => { + wrapper.setProps({ + plugin: { + id: 'test', + permissions: ['STORAGE'] + } + }) + + expect(wrapper.vm.hasStorage).toBeFalse() + expect(wrapper.find('.ListDivided').exists()).toBeFalse() + }) + describe('Methods', () => { it('should emit cancel event', () => { wrapper.vm.emitCancel() diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirm.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirm.spec.js new file mode 100644 index 0000000000..ff15a7fe66 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirm.spec.js @@ -0,0 +1,595 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirm, * as TransactionConfirmComponents from '@/components/Transaction/TransactionConfirm' +import TransactionConfirmBusiness from '@/components/Transaction/TransactionConfirm/TransactionConfirmBusiness' +import TransactionConfirmBridgechain from '@/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain' +import CurrencyMixin from '@/mixins/currency' + +const transactions = { + delegateRegistration: { + type: 2, + asset: { + delegate: { + username: 'test_delegate' + } + } + }, + + delegateResignation: { + type: 7 + }, + + ipfs: { + type: 5 + }, + + multiPayment: { + type: 6, + asset: { + payments: [{ + address: 'address-1', + amount: (1 * 1e8).toString() + }, + { + address: 'address-2', + amount: (5 * 1e8).toString() + }] + } + }, + + multiSignature: { + type: 4, + multiSignature: {} + }, + + secondSignature: { + type: 1 + }, + + transfer: { + type: 0, + amount: (1 * 1e8).toString(), + fee: (0.1 * 1e8).toString(), + recipientId: 'recipient-address' + }, + + vote: { + type: 3 + }, + + businessRegistration: { + type: 0, + typeGroup: 2, + asset: { + businessRegistration: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + }, + + businessResignation: { + type: 1, + typeGroup: 2 + }, + + businessUpdate: { + type: 2, + typeGroup: 2, + asset: { + businessUpdate: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + }, + + bridgechainRegistration: { + type: 3, + typeGroup: 2, + asset: { + bridgechainRegistration: { + name: 'test bridgechain', + genesisHash: 'genesis_hash_1234', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + } + } + }, + + bridgechainResignation: { + type: 4, + typeGroup: 2 + }, + + bridgechainUpdate: { + type: 5, + typeGroup: 2, + asset: { + bridgechainUpdate: { + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + } + } + } + } +} + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirm + transaction = transaction || transactions.transfer + + if (!transaction.network) { + transaction.network = 23 + } + if (!transaction.fee) { + transaction.fee = (0.1 * 1e8).toString() + } + if (!transaction.version) { + transaction.version = 2 + } + if (!transaction.nonce) { + transaction.nonce = '1' + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`), + formatter_networkCurrency: jest.fn((amount) => amount.toString()), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + electron_writeFile: jest.fn((_, path) => `/home/test/${path}`), + wallet_fromRoute: { + address: 'address-1' + }, + wallet_name: jest.fn(wallet => wallet), + $success: jest.fn(), + $error: jest.fn() + }, + stubs: { + Identicon: true, + TransactionDetail: true, + ...TransactionConfirmComponents, + ...TransactionConfirmBusiness, + ...TransactionConfirmBridgechain + } + }) +} + +describe('TransactionConfirm', () => { + beforeEach(() => { + createWrapper() + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirm')).toBe(true) + }) + + it('should render transfer confirm component', async () => { + createWrapper(null, transactions.transfer) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmTransfer')).toBe(true) + }) + + it('should render second signature confirm component', async () => { + createWrapper(null, transactions.secondSignature) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmSecondSignature')).toBe(true) + }) + + it('should render delegate registration confirm component', async () => { + createWrapper(null, transactions.delegateRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmDelegateRegistration')).toBe(true) + }) + + it('should render vote confirm component', async () => { + createWrapper(null, transactions.vote) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmVote')).toBe(true) + }) + + it('should render multi-signature confirm component', async () => { + createWrapper(null, transactions.multiSignature) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmMultiSignature')).toBe(true) + }) + + it('should render ipfs confirm component', async () => { + createWrapper(null, transactions.ipfs) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmIpfs')).toBe(true) + }) + + it('should render multi-payment confirm component', async () => { + createWrapper(null, transactions.multiPayment) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmMultiPayment')).toBe(true) + }) + + it('should render delegate resignation confirm component', async () => { + createWrapper(null, transactions.delegateResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmDelegateResignation')).toBe(true) + }) + + it('should render business registration confirm component', async () => { + createWrapper(null, transactions.businessRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBusinessRegistration')).toBe(true) + }) + + it('should render business resignation confirm component', async () => { + createWrapper(null, transactions.businessResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBusinessResignation')).toBe(true) + }) + + it('should render business update confirm component', async () => { + createWrapper(null, transactions.businessUpdate) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBusinessUpdate')).toBe(true) + }) + + it('should render bridgechain registration confirm component', async () => { + createWrapper(null, transactions.bridgechainRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBridgechainRegistration')).toBe(true) + }) + + it('should render bridgechain resignation confirm component', async () => { + createWrapper(null, transactions.bridgechainResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBridgechainResignation')).toBe(true) + }) + + it('should render bridgechain update confirm component', async () => { + createWrapper(null, transactions.bridgechainUpdate) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionConfirmBridgechainUpdate')).toBe(true) + }) + }) + + describe('computed', () => { + describe('totalAmount', () => { + it('should calculate full amount of transaction', async () => { + createWrapper(null, transactions.transfer) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.totalAmount + '').toEqual('110000000') + }) + + it('should use only fee if no amount', async () => { + createWrapper(null, transactions.vote) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.totalAmount + '').toBe('10000000') + }) + + it('should calculate total including payments of multi-payment', async () => { + createWrapper(null, transactions.multiPayment) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.totalAmount + '').toBe('610000000') + }) + }) + + describe('currentWallet', () => { + it('should return wallet from prop if set', async () => { + const wallet = { + address: 'prop-address-1' + } + + wrapper.setProps({ + transaction: transactions.vote, + wallet + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.currentWallet).toBe(wallet) + }) + + it('should return wallet from route if no prop is set', async () => { + expect(wrapper.vm.currentWallet).toEqual({ + address: 'address-1' + }) + }) + + it('should return updated wallet from route if changed', async () => { + expect(wrapper.vm.currentWallet).toEqual({ + address: 'address-1' + }) + + const wallet = { + address: 'prop-address-1' + } + + wrapper.setProps({ + transaction: transactions.vote, + wallet + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.currentWallet).toEqual({ + address: 'prop-address-1' + }) + }) + }) + + describe('address', () => { + it('should get address of current wallet', () => { + expect(wrapper.vm.address).toEqual('address-1') + }) + + it('should update address when current wallet changes', async () => { + const wallet = { + address: 'prop-address-1' + } + + expect(wrapper.vm.address).toEqual('address-1') + + wrapper.setProps({ + transaction: transactions.vote, + wallet + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.address).toEqual('prop-address-1') + }) + }) + + describe('showSave', () => { + it('should return true if transaction is not multi-signature', async () => { + createWrapper(null, transactions.secondSignature) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.showSave).toBe(true) + }) + + it('should return false if transaction is multi-signature', () => { + createWrapper(null, transactions.multiSignature) + + expect(wrapper.vm.showSave).toBe(false) + }) + }) + }) + + describe('mounted hook', () => { + it('should render transfer confirm component', async () => { + createWrapper(null, transactions.transfer) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmTransfer') + }) + + it('should render second signature confirm component', async () => { + createWrapper(null, transactions.secondSignature) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmSecondSignature') + }) + + it('should render delegate registration confirm component', async () => { + createWrapper(null, transactions.delegateRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmDelegateRegistration') + }) + + it('should render vote confirm component', async () => { + createWrapper(null, transactions.vote) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmVote') + }) + + it('should render multi-signature confirm component', async () => { + createWrapper(null, transactions.multiSignature) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmMultiSignature') + }) + + it('should render ipfs confirm component', async () => { + createWrapper(null, transactions.ipfs) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmIpfs') + }) + + it('should render multi-payment confirm component', async () => { + createWrapper(null, transactions.multiPayment) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmMultiPayment') + }) + + it('should render delegate resignation confirm component', async () => { + createWrapper(null, transactions.delegateResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmDelegateResignation') + }) + + it('should render business registration confirm component', async () => { + createWrapper(null, transactions.businessRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBusinessRegistration') + }) + + it('should render business resignation confirm component', async () => { + createWrapper(null, transactions.businessResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBusinessResignation') + }) + + it('should render business update confirm component', async () => { + createWrapper(null, transactions.businessUpdate) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBusinessUpdate') + }) + + it('should render bridgechain registration confirm component', async () => { + createWrapper(null, transactions.bridgechainRegistration) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBridgechainRegistration') + }) + + it('should render bridgechain resignation confirm component', async () => { + createWrapper(null, transactions.bridgechainResignation) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.transaction.type).toBe(4) + expect(wrapper.vm.transaction.typeGroup).toBe(2) + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBridgechainResignation') + }) + + it('should render bridgechain update confirm component', async () => { + createWrapper(null, transactions.bridgechainUpdate) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.activeComponent).toBe('TransactionConfirmBridgechainUpdate') + }) + + it('should error if no component based on transaction type', () => { + expect(() => { createWrapper(null, { type: 1000 }) }).toThrow('[TransactionConfirm] - Confirm for type 1000 (group 1) not found.') + }) + }) + + describe('methods', () => { + describe('emitBack', () => { + it('should emit', () => { + wrapper.vm.emitBack() + + expect(wrapper.emitted().back).toBeTruthy() + }) + }) + + describe('emitConfirm', () => { + it('should emit if not already clicked', () => { + wrapper.vm.wasClicked = false + wrapper.vm.emitConfirm() + + expect(wrapper.emitted().confirm).toBeTruthy() + }) + + it('should not emit if already clicked', () => { + wrapper.vm.wasClicked = true + wrapper.vm.emitConfirm() + + expect(wrapper.emitted().confirm).toBeFalsy() + }) + }) + + describe('saveTransaction', () => { + it('should save to file & output success message', async () => { + const $tSpy = jest.spyOn(wrapper.vm, '$t') + await wrapper.vm.saveTransaction() + + const json = JSON.stringify(wrapper.vm.transaction) + const path = `${wrapper.vm.transaction.id}.json` + + expect(wrapper.vm.electron_writeFile).toHaveBeenCalledWith(json, path) + expect(wrapper.vm.$success).toHaveBeenCalledWith('TRANSACTION.SUCCESS.SAVE_OFFLINE') + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.SUCCESS.SAVE_OFFLINE', { path: `/home/test/${path}` }) + + $tSpy.mockRestore() + }) + + it('should error if saving fails', async () => { + const $tSpy = jest.spyOn(wrapper.vm, '$t') + wrapper.vm.electron_writeFile.mockImplementation(() => { + throw new Error('failed to save') + }) + + await wrapper.vm.saveTransaction() + + const json = JSON.stringify(wrapper.vm.transaction) + const path = `${wrapper.vm.transaction.id}.json` + + expect(wrapper.vm.electron_writeFile).toHaveBeenCalledWith(json, path) + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.SAVE_OFFLINE') + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.SAVE_OFFLINE', { error: 'failed to save' }) + + $tSpy.mockRestore() + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainRegistration.spec.js new file mode 100644 index 0000000000..b78b9b4c60 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainRegistration.spec.js @@ -0,0 +1,122 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBridgechainRegistration from '@/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainRegistration' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmBridgechainRegistration + transaction = transaction || { + asset: { + bridgechainRegistration: { + name: 'test bridgechain', + genesisHash: 'genesis_hash_1234', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + } + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBridgechainRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have bridgechain registration transaction type (3)', () => { + expect(wrapper.vm.$options.transactionType).toBe(3) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBridgechainRegistration')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBridgechainRegistration__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output name', () => { + expect(wrapper.find('.TransactionConfirmBridgechainRegistration__name .ListDividedItem__value').text()).toBe('test bridgechain') + }) + + it('should output genesis hash (truncated)', () => { + expect(wrapper.find('.TransactionConfirmBridgechainRegistration__genesis-hash .ListDividedItem__value').text()).toBe('genes…_1234') + }) + + it('should output seed nodes', () => { + const seedNodes = wrapper.findAll('.TransactionConfirmBridgechainRegistration__seed-nodes .ListDividedItem__value > div') + for (const seedNodeIndex in wrapper.vm.transaction.asset.bridgechainRegistration.seedNodes) { + expect(seedNodes.at(seedNodeIndex).text()).toBe(wrapper.vm.transaction.asset.bridgechainRegistration.seedNodes[seedNodeIndex]) + } + }) + + it('should output api port', () => { + expect(wrapper.find('.TransactionConfirmBridgechainRegistration__api-port .ListDividedItem__value').text()).toBe('4003') + }) + + it('should output bridgechain repo', () => { + expect(wrapper.find('.TransactionConfirmBridgechainRegistration__bridgechain-repo .ListDividedItem__value').text()).toBe('https://github.com/arkecosystem/core.git') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + + describe('apiPort', () => { + it('should return core-api port if provided', () => { + expect(wrapper.vm.apiPort).toBe(4003) + }) + + it('should return placeholder if no core-api port', () => { + createWrapper(null, { + asset: { + bridgechainRegistration: { + name: 'test', + genesisHash: '1234', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: {}, + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + } + } + }) + + expect(wrapper.vm.apiPort).toBe('-') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainResignation.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainResignation.spec.js new file mode 100644 index 0000000000..89cd446a34 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainResignation.spec.js @@ -0,0 +1,57 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBridgechainResignation from '@/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainResignation' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmBridgechainResignation + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBridgechainResignation', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have bridgechain resignation transaction type (4)', () => { + expect(wrapper.vm.$options.transactionType).toBe(4) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBridgechainResignation')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBridgechainResignation__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainUpdate.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainUpdate.spec.js new file mode 100644 index 0000000000..cf1caf60c1 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainUpdate.spec.js @@ -0,0 +1,107 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBridgechainUpdate from '@/components/Transaction/TransactionConfirm/TransactionConfirmBridgechain/TransactionConfirmBridgechainUpdate' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmBridgechainUpdate + transaction = transaction || { + asset: { + bridgechainUpdate: { + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + } + } + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBridgechainUpdate', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have bridgechain update transaction type (5)', () => { + expect(wrapper.vm.$options.transactionType).toBe(5) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBridgechainUpdate')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBridgechainUpdate__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output seed nodes', () => { + const seedNodes = wrapper.findAll('.TransactionConfirmBridgechainUpdate__seed-nodes .ListDividedItem__value > div') + for (const seedNodeIndex in wrapper.vm.transaction.asset.bridgechainUpdate.seedNodes) { + expect(seedNodes.at(seedNodeIndex).text()).toBe(wrapper.vm.transaction.asset.bridgechainUpdate.seedNodes[seedNodeIndex]) + } + }) + + it('should output api port', () => { + expect(wrapper.find('.TransactionConfirmBridgechainUpdate__api-port .ListDividedItem__value').text()).toBe('4003') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + + describe('apiPort', () => { + it('should return core-api port if provided', () => { + expect(wrapper.vm.apiPort).toBe(4003) + }) + + it('should return placeholder if no core-api port', () => { + createWrapper(null, { + asset: { + bridgechainUpdate: { + name: 'test', + genesisHash: '1234', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: {}, + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + } + } + }) + + expect(wrapper.vm.apiPort).toBe('-') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessRegistration.spec.js new file mode 100644 index 0000000000..dc1d6d3c83 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessRegistration.spec.js @@ -0,0 +1,112 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBusinessRegistration from '@/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessRegistration' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmBusinessRegistration + transaction = transaction || { + asset: { + businessRegistration: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBusinessRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business registration transaction type (0)', () => { + expect(wrapper.vm.$options.transactionType).toBe(0) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBusinessRegistration')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBusinessRegistration__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output name', () => { + expect(wrapper.find('.TransactionConfirmBusinessRegistration__name .ListDividedItem__value').text()).toBe('test business') + }) + + it('should output website', () => { + expect(wrapper.find('.TransactionConfirmBusinessRegistration__website .ListDividedItem__value').text()).toBe('https://ark.io') + }) + + it('should output vat when provided', () => { + expect(wrapper.find('.TransactionConfirmBusinessRegistration__vat .ListDividedItem__value').text()).toBe('GB12345678') + }) + + it('should not output vat when not provided', () => { + createWrapper(null, { + asset: { + businessRegistration: { + name: 'test business', + website: 'https://ark.io', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + }) + + expect(wrapper.contains('.TransactionConfirmBusinessRegistration__vat')).toBe(false) + }) + + it('should output repo when provided', () => { + expect(wrapper.find('.TransactionConfirmBusinessRegistration__repository .ListDividedItem__value').text()).toBe('https://github.com/arkecosystem/desktop-wallet.git') + }) + + it('should not output repo when not provided', () => { + createWrapper(null, { + asset: { + businessRegistration: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678' + } + } + }) + + expect(wrapper.contains('.TransactionConfirmBusinessRegistration__repository')).toBe(false) + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessResignation.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessResignation.spec.js new file mode 100644 index 0000000000..dd3bc742be --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessResignation.spec.js @@ -0,0 +1,57 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBusinessResignation from '@/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessResignation' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmBusinessResignation + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBusinessResignation', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business resignation transaction type (1)', () => { + expect(wrapper.vm.$options.transactionType).toBe(1) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBusinessResignation')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBusinessResignation__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessUpdate.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessUpdate.spec.js new file mode 100644 index 0000000000..38fc3d6c24 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessUpdate.spec.js @@ -0,0 +1,112 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../../__utils__/i18n' +import TransactionConfirmBusinessUpdate from '@/components/Transaction/TransactionConfirm/TransactionConfirmBusiness/TransactionConfirmBusinessUpdate' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmBusinessUpdate + transaction = transaction || { + asset: { + businessUpdate: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmBusinessUpdate', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrates transaction group (2)', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business update transaction type (2)', () => { + expect(wrapper.vm.$options.transactionType).toBe(2) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmBusinessUpdate')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmBusinessUpdate__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output name', () => { + expect(wrapper.find('.TransactionConfirmBusinessUpdate__name .ListDividedItem__value').text()).toBe('test business') + }) + + it('should output website', () => { + expect(wrapper.find('.TransactionConfirmBusinessUpdate__website .ListDividedItem__value').text()).toBe('https://ark.io') + }) + + it('should output vat when provided', () => { + expect(wrapper.find('.TransactionConfirmBusinessUpdate__vat .ListDividedItem__value').text()).toBe('GB12345678') + }) + + it('should not output vat when not provided', () => { + createWrapper(null, { + asset: { + businessUpdate: { + name: 'test business', + website: 'https://ark.io', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + }) + + expect(wrapper.contains('.TransactionConfirmBusinessUpdate__vat')).toBe(false) + }) + + it('should output repo when provided', () => { + expect(wrapper.find('.TransactionConfirmBusinessUpdate__repository .ListDividedItem__value').text()).toBe('https://github.com/arkecosystem/desktop-wallet.git') + }) + + it('should not output repo when not provided', () => { + createWrapper(null, { + asset: { + businessUpdate: { + name: 'test business', + website: 'https://ark.io', + vat: 'GB12345678' + } + } + }) + + expect(wrapper.contains('.TransactionConfirmBusinessUpdate__repository')).toBe(false) + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateRegistration.spec.js new file mode 100644 index 0000000000..c5db5b1c33 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateRegistration.spec.js @@ -0,0 +1,85 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmDelegateRegistration from '@/components/Transaction/TransactionConfirm/TransactionConfirmDelegateRegistration' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmDelegateRegistration + transaction = transaction || { + asset: { + delegate: { + username: 'test_delegate' + } + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmDelegateRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have delegate registration transaction type (2)', () => { + expect(wrapper.vm.$options.transactionType).toBe(2) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmDelegateRegistration')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmDelegateRegistration__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output username', () => { + expect(wrapper.find('.TransactionConfirmDelegateRegistration__username .ListDividedItem__value').text()).toBe('test_delegate') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + + describe('username', () => { + it('should return username if set', () => { + expect(wrapper.vm.username).toBe('test_delegate') + }) + + it('should return empty string if no asset', () => { + createWrapper(null, {}) + + expect(wrapper.vm.username).toBe('') + }) + + it('should return empty string if no delegate', () => { + createWrapper(null, { + asset: {} + }) + + expect(wrapper.vm.username).toBe('') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateResignation.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateResignation.spec.js new file mode 100644 index 0000000000..1ba684b9e8 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmDelegateResignation.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmDelegateResignation from '@/components/Transaction/TransactionConfirm/TransactionConfirmDelegateResignation' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmDelegateResignation + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmDelegateResignation', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have delegate resignation transaction type (7)', () => { + expect(wrapper.vm.$options.transactionType).toBe(7) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmDelegateResignation')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmDelegateResignation__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmIpfs.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmIpfs.spec.js new file mode 100644 index 0000000000..8399b7134b --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmIpfs.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmIpfs from '@/components/Transaction/TransactionConfirm/TransactionConfirmIpfs' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmIpfs + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmIpfs', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have ipfs transaction type (5)', () => { + expect(wrapper.vm.$options.transactionType).toBe(5) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmIpfs')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmIpfs__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiPayment.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiPayment.spec.js new file mode 100644 index 0000000000..c8b3f11df4 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiPayment.spec.js @@ -0,0 +1,102 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmMultiPayment from '@/components/Transaction/TransactionConfirm/TransactionConfirmMultiPayment' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmMultiPayment + transaction = transaction || { + asset: { + payments: [ + { + address: 'address-1', + amount: (1 * 1e8).toString() + }, + { + address: 'address-2', + amount: (2 * 1e8).toString() + } + ] + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + formatter_networkCurrency: jest.fn((amount) => amount), + wallet_formatAddress: jest.fn((address) => `formatted-${address}`), + wallet_name: jest.fn(wallet => wallet) + }, + stubs: { + Identicon: '
' + } + }) +} + +describe('TransactionConfirmMultiPayment', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have multi-payment transaction type (6)', () => { + expect(wrapper.vm.$options.transactionType).toBe(6) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmMultiPayment')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmMultiPayment__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output recipients', () => { + const recipients = wrapper.findAll('.TransactionConfirmMultiPayment__recipients .InputEditableList__list__item') + + for (const recipientIndex in wrapper.vm.transaction.asset.payments) { + const recipient = wrapper.vm.transaction.asset.payments[recipientIndex] + const recipientElement = recipients.at(recipientIndex) + const addressText = recipientElement.find('.TransactionMultiPaymentList__recipient').text().replace('TRANSACTION.RECIPIENT:', '') + const amountText = recipientElement.find('.TransactionMultiPaymentList__amount').text().replace('TRANSACTION.AMOUNT:', '') + + expect(addressText.trim()).toBe(recipient.address) + expect(amountText.trim()).toBe(recipient.amount) + } + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + + describe('payments', () => { + it('should return all payments', () => { + expect(wrapper.vm.payments).toEqual([ + { + address: 'address-1', + amount: '100000000' + }, + { + address: 'address-2', + amount: '200000000' + } + ]) + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiSignature.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiSignature.spec.js new file mode 100644 index 0000000000..ca17c0b11d --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmMultiSignature.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmMultiSignature from '@/components/Transaction/TransactionConfirm/TransactionConfirmMultiSignature' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmMultiSignature + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmMultiSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have multi-signature transaction type (4)', () => { + expect(wrapper.vm.$options.transactionType).toBe(4) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmMultiSignature')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmMultiSignature__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmSecondSignature.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmSecondSignature.spec.js new file mode 100644 index 0000000000..d462509f04 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmSecondSignature.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmSecondSignature from '@/components/Transaction/TransactionConfirm/TransactionConfirmSecondSignature' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmSecondSignature + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmSecondSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have second signature transaction type (1)', () => { + expect(wrapper.vm.$options.transactionType).toBe(1) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmSecondSignature')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmSecondSignature__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmTransfer.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmTransfer.spec.js new file mode 100644 index 0000000000..f93e40182c --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmTransfer.spec.js @@ -0,0 +1,93 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmTransfer from '@/components/Transaction/TransactionConfirm/TransactionConfirmTransfer' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, transaction) => { + component = component || TransactionConfirmTransfer + transaction = transaction || { + amount: (1 * 1e8).toString(), + fee: (0.1 * 1e8).toString(), + recipientId: 'recipient-address', + vendorField: 'test vendorField' + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + }, + transaction + }, + mocks: { + formatter_networkCurrency: jest.fn((amount) => amount), + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmTransfer', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have transfer transaction type (0)', () => { + expect(wrapper.vm.$options.transactionType).toBe(0) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmTransfer')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmTransfer__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + + it('should output amount', () => { + expect(wrapper.find('.TransactionConfirmTransfer__amount .ListDividedItem__value').text()).toBe('100000000') + }) + + it('should output recipientLabel', () => { + expect(wrapper.find('.TransactionConfirmTransfer__recipient .ListDividedItem__value span').text()).toBe('recipient-address') + }) + + it('should output vendorField', () => { + expect(wrapper.find('.TransactionConfirmTransfer__vendorfield .ListDividedItem__value').text()).toBe('test vendorField') + }) + + it('should not output vendorField if not provided', () => { + createWrapper(null, { + amount: (1 * 1e8).toString(), + fee: (0.1 * 1e8).toString(), + recipientId: 'recipient-address' + }) + + expect(wrapper.contains('.TransactionConfirmTransfer__vendorfield')).toBe(false) + }) + + it('should output fee', () => { + expect(wrapper.find('.TransactionConfirmTransfer__fee .ListDividedItem__value').text()).toBe('10000000') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + + describe('recipientLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.recipientLabel).toBe('formatted-recipient-address') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmVote.spec.js b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmVote.spec.js new file mode 100644 index 0000000000..076221c61e --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionConfirm/TransactionConfirmVote.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import TransactionConfirmVote from '@/components/Transaction/TransactionConfirm/TransactionConfirmVote' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || TransactionConfirmVote + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + provide: { + currentWallet: { + address: 'address-1' + } + }, + mocks: { + wallet_formatAddress: jest.fn((address) => `formatted-${address}`) + } + }) +} + +describe('TransactionConfirmVote', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have vote transaction type (3)', () => { + expect(wrapper.vm.$options.transactionType).toBe(3) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionConfirmVote')).toBe(true) + }) + + it('should output senderLabel', () => { + expect(wrapper.find('.TransactionConfirmVote__sender .ListDividedItem__value span').text()).toBe('address-1') + }) + }) + + describe('computed', () => { + describe('senderLabel', () => { + it('should return a formatted address', () => { + expect(wrapper.vm.senderLabel).toBe('formatted-address-1') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainRegistration.spec.js new file mode 100644 index 0000000000..8b20441b44 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainRegistration.spec.js @@ -0,0 +1,284 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBridgechainRegistration from '@/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainRegistration' +import CurrencyMixin from '@/mixins/currency' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +/* eslint-disable-next-line camelcase */ +const createWrapper = (component, wallet_fromRoute) => { + component = component || TransactionFormBridgechainRegistration + /* eslint-disable-next-line camelcase */ + wallet_fromRoute = wallet_fromRoute || { + passphrase: null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildBridgechainRegistration: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute + } + }) +} + +describe('TransactionFormBridgechainRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have bridgechain registration transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(3) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBridgechain')).toBe(true) + }) + + describe('step 1', () => { + it('should have seed node field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-node')).toBe(true) + }) + + it('should have add button', () => { + expect(wrapper.contains('.TransactionFormBridgechain__add')).toBe(true) + }) + + it('should have seed nodes list', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-nodes')).toBe(true) + }) + }) + + describe('step 2', () => { + beforeEach(() => { + wrapper.vm.step = 2 + }) + + it('should have name field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__name')).toBe(true) + }) + + it('should have genesis hash field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__genesis-hash')).toBe(true) + }) + + it('should have bridgechain repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-repository')).toBe(true) + }) + + it('should have bridgechain asset repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-asset-repository')).toBe(true) + }) + + it('should have api port field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__api-port')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', async () => { + createWrapper(null, { + isLedger: true + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', async () => { + createWrapper(null, { + isLedger: false + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(false) + }) + }) + }) + + describe('prev button', () => { + it('should be enabled if form is on step 2', async () => { + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is on step 1', async () => { + wrapper.vm.step = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBe('disabled') + }) + }) + + describe('next button', () => { + it('should be enabled if seed nodes is valid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [ + '1.1.1.1' + ] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be enabled if form is valid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.seedNodes.$model = [ + '1.1.1.1' + ] + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if seed nodes is invalid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + + it('should be disabled if form is invalid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('methods', () => { + describe('buildTransaction', () => { + it('should build bridgechain registration', async () => { + const transactionData = { + type: 3, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBridgechainRegistration).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build bridgechain registration with default arguments', async () => { + const transactionData = { + type: 3, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBridgechainRegistration).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BRIDGECHAIN_REGISTRATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainResignation.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainResignation.spec.js new file mode 100644 index 0000000000..742bbac403 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainResignation.spec.js @@ -0,0 +1,254 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBridgechainResignation from '@/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainResignation' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' +import WalletService from '@/services/wallet' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet, bridgechain) => { + component = component || TransactionFormBridgechainResignation + wallet = wallet || { + address: 'address-1', + passphrase: null + } + + if (bridgechain === undefined) { + bridgechain = { + isResigned: false, + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + bridgechain + }, + mocks: { + $client: { + buildBridgechainResignation: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +let spyCanResignBusiness +describe('TransactionFormBridgechainResignation', () => { + beforeEach(() => { + spyCanResignBusiness = jest.spyOn(WalletService, 'canResignBusiness').mockReturnValue(true) + + createWrapper() + }) + + afterEach(() => { + spyCanResignBusiness.mockRestore() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business resignation transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(4) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBridgechainResignation')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBridgechainResignation__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormBridgechainResignation__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormBridgechainResignation__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBridgechainResignation__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechainResignation__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechainResignation__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBridgechainResignation__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechainResignation__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechainResignation__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + bridgechainId: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + bridgechainId: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build business resignation', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBridgechainResignation).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build business resignation with default arguments', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBridgechainResignation).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BRIDGECHAIN_RESIGNATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainUpdate.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainUpdate.spec.js new file mode 100644 index 0000000000..e7a410356e --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainUpdate.spec.js @@ -0,0 +1,357 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBridgechainUpdate from '@/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainUpdate' +import CurrencyMixin from '@/mixins/currency' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet, bridgechain) => { + component = component || TransactionFormBridgechainUpdate + wallet = wallet || { + passphrase: null + } + + if (bridgechain === undefined) { + bridgechain = { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + bridgechain + }, + mocks: { + $client: { + buildBridgechainUpdate: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + } + }) +} + +describe('TransactionFormBridgechainUpdate', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have bridgechain update transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(5) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBridgechain')).toBe(true) + }) + + describe('step 1', () => { + it('should have seed node field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-node')).toBe(true) + }) + + it('should have add button', () => { + expect(wrapper.contains('.TransactionFormBridgechain__add')).toBe(true) + }) + + it('should have seed nodes list', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-nodes')).toBe(true) + }) + }) + + describe('step 2', () => { + beforeEach(() => { + wrapper.vm.step = 2 + }) + + it('should not have name field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__name')).toBe(false) + }) + + it('should not have genesis hash field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__genesis-hash')).toBe(false) + }) + + it('should have bridgechain repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-repository')).toBe(true) + }) + + it('should have bridgechain asset repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-asset-repository')).toBe(true) + }) + + it('should have api port field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__api-port')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', async () => { + createWrapper(null, { + isLedger: true + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', async () => { + createWrapper(null, { + isLedger: false + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(false) + }) + }) + }) + + describe('prev button', () => { + it('should be enabled if form is on step 2', async () => { + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is on step 1', async () => { + wrapper.vm.step = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBe('disabled') + }) + }) + + describe('next button', () => { + it('should be enabled if seed nodes is valid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [ + '1.1.1.1' + ] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be enabled if form is valid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.seedNodes.$model = [ + '1.1.1.1' + ] + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if seed nodes is invalid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + + it('should be disabled if form is invalid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('mounted hook', () => { + it('should load bridgechain into form', () => { + createWrapper(null, null, { + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + ports: { + '@arkecosystem/core-api': 8081 + }, + seedNodes: [ + '5.5.5.5', + '6.6.6.6' + ], + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + + expect(wrapper.vm.form.apiPort).toBe(8081) + expect(wrapper.vm.form.seedNodes).toEqual([ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false } + ]) + expect(wrapper.vm.form.asset).toEqual({ + name: '', + ports: {}, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + }) + + it('should use default api port if not provided', () => { + createWrapper(null, null, { + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + ports: {}, + seedNodes: [ + '5.5.5.5', + '6.6.6.6' + ], + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + + expect(wrapper.vm.form.apiPort).toBe(4003) + expect(wrapper.vm.form.seedNodes).toEqual([ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false } + ]) + expect(wrapper.vm.form.asset).toEqual({ + name: '', + ports: {}, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + }) + }) + + describe('methods', () => { + describe('buildTransaction', () => { + it('should build bridgechain update', async () => { + const transactionData = { + type: 5, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBridgechainUpdate).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build bridgechain update with default arguments', async () => { + const transactionData = { + type: 5, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBridgechainUpdate).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BRIDGECHAIN_UPDATE') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/mixin.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/mixin.spec.js new file mode 100644 index 0000000000..fccf5302c0 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBridgechain/mixin.spec.js @@ -0,0 +1,1132 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBridgechainRegistration from '@/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainRegistration' +import TransactionFormBridgechainUpdate from '@/components/Transaction/TransactionForm/TransactionFormBridgechain/TransactionFormBridgechainUpdate' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet, bridgechain) => { + wallet = wallet || { + address: 'address-1', + passphrase: null + } + + if (component.name === 'TransactionFormBridgechainUpdate' && !wallet.business) { + wallet.business = { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + + if (component.name === 'TransactionFormBridgechainUpdate' && bridgechain === undefined) { + bridgechain = { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + bridgechain + }, + mocks: { + $client: { + buildBridgechainRegistration: jest.fn((transactionData) => transactionData), + buildBridgechainUpdate: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + } + }) +} + +describe.each([ + ['TransactionFormBridgechainRegistration', TransactionFormBridgechainRegistration], + ['TransactionFormBridgechainUpdate', TransactionFormBridgechainUpdate] +])('%s', (componentName, component) => { + beforeEach(() => { + createWrapper(component) + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have correct transaction type', () => { + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$options.transactionType).toBe(3) + } else { + expect(wrapper.vm.$options.transactionType).toBe(5) + } + }) + + describe('data', () => { + it('should create form object', () => { + expect(Object.keys(wrapper.vm.form)).toEqual([ + 'fee', + 'passphrase', + 'walletPassword', + 'apiPort', + 'seedNodes', + 'asset' + ]) + + expect(Object.keys(wrapper.vm.form.asset)).toEqual([ + 'name', + 'ports', + 'genesisHash', + 'bridgechainRepository', + 'bridgechainAssetRepository' + ]) + }) + }) + + if (componentName === 'TransactionFormBridgechainUpdate') { + describe('mounted hook', () => { + it('should load bridgechain into form', () => { + createWrapper(component, null, { + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + ports: { + '@arkecosystem/core-api': 8081 + }, + seedNodes: [ + '5.5.5.5', + '6.6.6.6' + ], + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + + expect(wrapper.vm.form.apiPort).toBe(8081) + expect(wrapper.vm.form.seedNodes).toEqual([ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false } + ]) + expect(wrapper.vm.form.asset).toEqual({ + name: '', + ports: {}, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + }) + + it('should use default api port if not provided', () => { + createWrapper(component, null, { + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + ports: {}, + seedNodes: [ + '5.5.5.5', + '6.6.6.6' + ], + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + + expect(wrapper.vm.form.apiPort).toBe(4003) + expect(wrapper.vm.form.seedNodes).toEqual([ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false } + ]) + expect(wrapper.vm.form.asset).toEqual({ + name: '', + ports: {}, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: '', + bridgechainAssetRepository: '' + }) + }) + }) + } + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBridgechain')).toBe(true) + }) + + describe('step 1', () => { + it('should have seed node field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-node')).toBe(true) + }) + + it('should have add button', () => { + expect(wrapper.contains('.TransactionFormBridgechain__add')).toBe(true) + }) + + it('should have seed nodes list', () => { + expect(wrapper.contains('.TransactionFormBridgechain__seed-nodes')).toBe(true) + }) + }) + + describe('step 2', () => { + beforeEach(() => { + wrapper.vm.step = 2 + }) + + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('registration', () => { + it('should have name field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__name')).toBe(true) + }) + + it('should have genesis hash field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__genesis-hash')).toBe(true) + }) + }) + } else { + describe('update', () => { + it('should not have name field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__name')).toBe(false) + }) + + it('should not have genesis hash field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__genesis-hash')).toBe(false) + }) + }) + } + + it('should have bridgechain repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-repository')).toBe(true) + }) + + it('should have bridgechain asset repository field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__bridgechain-asset-repository')).toBe(true) + }) + + it('should have api port field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__api-port')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBridgechain__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', async () => { + createWrapper(component, { + isLedger: true + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', async () => { + createWrapper(component, { + isLedger: false + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', async () => { + createWrapper(component, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', async () => { + createWrapper(component, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormBridgechain__passphrase')).toBe(false) + }) + }) + }) + + describe('prev button', () => { + it('should be enabled if form is on step 2', async () => { + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is on step 1', async () => { + wrapper.vm.step = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__prev').attributes('disabled')).toBe('disabled') + }) + }) + + describe('next button', () => { + it('should be enabled if seed nodes is valid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false } + ] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be enabled if form is valid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false } + ] + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if seed nodes is invalid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + + it('should be disabled if form is invalid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBridgechain__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('isUpdate', () => { + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('TransactionFormBridgechainRegistration', () => { + it('should return false if bridgechain prop is set', () => { + wrapper.setProps({ + bridgechain: { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + }) + + expect(wrapper.vm.isUpdate).toBe(false) + }) + + it('should return false if bridgechain prop is not set', () => { + wrapper.setProps({ + bridgechain: null + }) + + expect(wrapper.vm.isUpdate).toBe(false) + }) + }) + } else { + describe('TransactionFormBridgechainUpdate', () => { + it('should return true if bridgechain prop is set', () => { + wrapper.setProps({ + bridgechain: { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + }) + + expect(wrapper.vm.isUpdate).toBe(true) + }) + + it('should return false if bridgechain prop is not set', () => { + wrapper.setProps({ + bridgechain: null + }) + + expect(wrapper.vm.isUpdate).toBe(false) + }) + }) + } + }) + + describe('isFormValid', () => { + it('should be true if seed nodes is valid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false } + ] + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isFormValid).toBe(true) + }) + + it('should be true if form is valid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false } + ] + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isFormValid).toBe(true) + }) + + it('should be false if seed nodes is invalid on step 1', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isFormValid).toBe(false) + }) + + it('should be false if form is invalid on step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.seedNodes.$model = [] + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isFormValid).toBe(false) + }) + }) + + describe('nameError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.name.$model = 'test' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.name.$model = '' + wrapper.vm.$v.form.asset.name.$reset() + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(false) + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + } else { + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + } + expect(wrapper.vm.nameError).toBe(null) + }) + + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('TransactionFormBridgechainRegistration', () => { + it('should return required if empty', () => { + wrapper.vm.$v.form.asset.name.$model = '' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.REQUIRED') + }) + + it('should not return error if shorter than max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(30, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if equal to max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(40, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should return error if longer than max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(50, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.name.$model = 'test' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.NAME_ERROR') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.name.$model = '$ARK' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.NAME_ERROR') + }) + }) + } + }) + + describe('seedNodeDisabled', () => { + it('should be false if seed node not empty and no error', () => { + wrapper.vm.$v.seedNode.$model = '5.5.5.5' + + expect(wrapper.vm.seedNodeDisabled).toBe(false) + }) + + it('should be true if seed node is empty', () => { + wrapper.vm.$v.seedNode.$model = '' + + expect(wrapper.vm.seedNodeDisabled).toBe(true) + }) + + it('should be true if seed node is invalid', () => { + wrapper.vm.$v.seedNode.$model = 'invalid seed node' + + expect(wrapper.vm.seedNodeDisabled).toBe(true) + }) + }) + + describe('seedNodeError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.seedNode.$model = '5.5.5.5' + + expect(wrapper.vm.$v.seedNode.$dirty).toBe(true) + expect(wrapper.vm.$v.seedNode.$invalid).toBe(false) + expect(wrapper.vm.seedNodeError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.seedNode.$model = '' + wrapper.vm.$v.seedNode.$reset() + + expect(wrapper.vm.$v.seedNode.$dirty).toBe(false) + expect(wrapper.vm.$v.seedNode.$invalid).toBe(false) + expect(wrapper.vm.seedNodeError).toBe(null) + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.seedNode.$model = 'invalid seed node' + + expect(wrapper.vm.$v.seedNode.$dirty).toBe(true) + expect(wrapper.vm.$v.seedNode.$invalid).toBe(true) + expect(wrapper.vm.seedNodeError).toBe('VALIDATION.INVALID_SEED') + }) + + it('should return error if duplicate', () => { + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '5.5.5.5', isInvalid: false } + ] + wrapper.vm.$v.seedNode.$model = '5.5.5.5' + + expect(wrapper.vm.$v.seedNode.$dirty).toBe(true) + expect(wrapper.vm.$v.seedNode.$invalid).toBe(true) + expect(wrapper.vm.seedNodeError).toBe('TRANSACTION.BRIDGECHAIN.ERROR_DUPLICATE') + }) + + it('should return error if too many', () => { + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.5.5.5', isInvalid: false }, + { ip: '2.5.5.5', isInvalid: false }, + { ip: '3.5.5.5', isInvalid: false }, + { ip: '4.5.5.5', isInvalid: false }, + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.5.5.5', isInvalid: false }, + { ip: '7.5.5.5', isInvalid: false }, + { ip: '8.5.5.5', isInvalid: false }, + { ip: '9.5.5.5', isInvalid: false }, + { ip: '10.5.5.5', isInvalid: false } + ] + wrapper.vm.$v.seedNode.$model = '6.6.6.6' + + expect(wrapper.vm.$v.seedNode.$dirty).toBe(true) + expect(wrapper.vm.$v.seedNode.$invalid).toBe(true) + expect(wrapper.vm.seedNodeError).toBe('VALIDATION.TOO_MANY') + }) + }) + + describe('seedNodesError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '5.5.5.5', isInvalid: false } + ] + + expect(wrapper.vm.$v.form.seedNodes.$dirty).toBe(true) + expect(wrapper.vm.$v.form.seedNodes.$invalid).toBe(false) + expect(wrapper.vm.seedNodesError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.seedNodes.$model = [] + wrapper.vm.$v.form.seedNodes.$reset() + + expect(wrapper.vm.$v.form.seedNodes.$dirty).toBe(false) + expect(wrapper.vm.$v.form.seedNodes.$invalid).toBe(true) + expect(wrapper.vm.seedNodesError).toBe(null) + }) + + it('should return error if empty', () => { + wrapper.vm.$v.form.seedNodes.$model = [] + + expect(wrapper.vm.$v.form.seedNodes.$dirty).toBe(true) + expect(wrapper.vm.$v.form.seedNodes.$invalid).toBe(true) + expect(wrapper.vm.seedNodesError).toBe('VALIDATION.REQUIRED') + }) + + it('should return error if too many', () => { + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.5.5.5', isInvalid: false }, + { ip: '2.5.5.5', isInvalid: false }, + { ip: '3.5.5.5', isInvalid: false }, + { ip: '4.5.5.5', isInvalid: false }, + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.5.5.5', isInvalid: false }, + { ip: '7.5.5.5', isInvalid: false }, + { ip: '8.5.5.5', isInvalid: false }, + { ip: '9.5.5.5', isInvalid: false }, + { ip: '10.5.5.5', isInvalid: false } + ] + + expect(wrapper.vm.$v.form.seedNodes.$dirty).toBe(true) + expect(wrapper.vm.$v.form.seedNodes.$invalid).toBe(true) + expect(wrapper.vm.seedNodesError).toBe('VALIDATION.TOO_MANY') + }) + }) + + describe('genesisHashError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + + expect(wrapper.vm.$v.form.asset.genesisHash.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.genesisHash.$invalid).toBe(false) + expect(wrapper.vm.genesisHashError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.genesisHash.$model = '' + wrapper.vm.$v.form.asset.genesisHash.$reset() + + expect(wrapper.vm.$v.form.asset.genesisHash.$dirty).toBe(false) + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$v.form.asset.genesisHash.$invalid).toBe(true) + } else { + expect(wrapper.vm.$v.form.asset.genesisHash.$invalid).toBe(false) + } + expect(wrapper.vm.genesisHashError).toBe(null) + }) + + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('TransactionFormBridgechainRegistration', () => { + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.genesisHash.$model = '1234' + + expect(wrapper.vm.$v.form.asset.genesisHash.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.genesisHash.$invalid).toBe(true) + expect(wrapper.vm.genesisHashError).toBe('VALIDATION.NOT_VALID') + }) + }) + } + }) + + describe('bridgechainRepositoryError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainRepositoryError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = '' + wrapper.vm.$v.form.asset.bridgechainRepository.$reset() + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(true) + expect(wrapper.vm.bridgechainRepositoryError).toBe(null) + }) + + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('TransactionFormBridgechainRegistration', () => { + it('should not return error if longer than min (12)', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'http://github.com' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainRepositoryError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if equal to min (12)', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'http://g.com' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainRepositoryError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should return error if shorter than min (12)', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'ftp://g.co' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(true) + expect(wrapper.vm.bridgechainRepositoryError).toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainRepositoryError).not.toBe('VALIDATION.INVALID_URL') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainRepository.$invalid).toBe(true) + expect(wrapper.vm.bridgechainRepositoryError).toBe('VALIDATION.INVALID_URL') + }) + }) + } + }) + + describe('bridgechainAssetRepositoryError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainAssetRepositoryError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = '' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$reset() + + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainAssetRepositoryError).toBe(null) + }) + + if (componentName === 'TransactionFormBridgechainRegistration') { + describe('TransactionFormBridgechainRegistration', () => { + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$invalid).toBe(false) + expect(wrapper.vm.bridgechainAssetRepositoryError).not.toBe('VALIDATION.INVALID_URL') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.bridgechainAssetRepository.$invalid).toBe(true) + expect(wrapper.vm.bridgechainAssetRepositoryError).toBe('VALIDATION.INVALID_URL') + }) + }) + } + }) + + describe('apiPortError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.apiPort.$model = 4003 + + expect(wrapper.vm.$v.form.apiPort.$dirty).toBe(true) + expect(wrapper.vm.$v.form.apiPort.$invalid).toBe(false) + expect(wrapper.vm.apiPortError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.apiPort.$model = '' + wrapper.vm.$v.form.apiPort.$reset() + + expect(wrapper.vm.$v.form.apiPort.$dirty).toBe(false) + expect(wrapper.vm.$v.form.apiPort.$invalid).toBe(true) + expect(wrapper.vm.apiPortError).toBe(null) + }) + + it('should return error if empty', () => { + wrapper.vm.$v.form.apiPort.$model = '' + + expect(wrapper.vm.$v.form.apiPort.$dirty).toBe(true) + expect(wrapper.vm.$v.form.apiPort.$invalid).toBe(true) + expect(wrapper.vm.apiPortError).toBe('VALIDATION.REQUIRED') + }) + + it('should return error if not numeric', () => { + wrapper.vm.$v.form.apiPort.$model = 'test' + + expect(wrapper.vm.$v.form.apiPort.$dirty).toBe(true) + expect(wrapper.vm.$v.form.apiPort.$invalid).toBe(true) + expect(wrapper.vm.apiPortError).toBe('VALIDATION.NOT_NUMERIC') + }) + + it('should return error if not valid port', () => { + wrapper.vm.$v.form.apiPort.$model = '9999999' + + expect(wrapper.vm.$v.form.apiPort.$dirty).toBe(true) + expect(wrapper.vm.$v.form.apiPort.$invalid).toBe(true) + expect(wrapper.vm.apiPortError).toBe('VALIDATION.INVALID_PORT') + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false }, + { ip: '2.2.2.2', isInvalid: false } + ] + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + const expectedAsset = { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + + if (componentName === 'TransactionFormBridgechainUpdate') { + wrapper.vm.form.asset.bridgechainId = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + expectedAsset.bridgechainId = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + } + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + asset: expectedAsset, + passphrase: 'passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(component, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + wrapper.vm.$v.form.asset.name.$model = 'bridgechain' + wrapper.vm.$v.form.asset.genesisHash.$model = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '1.1.1.1', isInvalid: false }, + { ip: '2.2.2.2', isInvalid: false } + ] + wrapper.vm.form.asset.ports = { + '@arkecosystem/core-api': 4003 + } + wrapper.vm.$v.form.asset.bridgechainRepository.$model = 'https://github.com/arkecosystem/core.git' + wrapper.vm.$v.form.asset.bridgechainAssetRepository.$model = 'https://github.com/arkecosystem/core-assets.git' + + const expectedAsset = { + name: 'bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git', + bridgechainAssetRepository: 'https://github.com/arkecosystem/core-assets.git' + } + + if (componentName === 'TransactionFormBridgechainUpdate') { + wrapper.vm.form.asset.bridgechainId = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + expectedAsset.bridgechainId = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' + } + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + asset: expectedAsset, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build bridgechain update', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$client.buildBridgechainRegistration).toHaveBeenCalledWith(transactionData, true, true) + } else { + expect(wrapper.vm.$client.buildBridgechainUpdate).toHaveBeenCalledWith(transactionData, true, true) + } + expect(response).toBe(transactionData) + }) + + it('should build bridgechain update with default arguments', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$client.buildBridgechainRegistration).toHaveBeenCalledWith(transactionData, false, false) + } else { + expect(wrapper.vm.$client.buildBridgechainUpdate).toHaveBeenCalledWith(transactionData, false, false) + } + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + if (componentName === 'TransactionFormBridgechainRegistration') { + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BRIDGECHAIN_REGISTRATION') + } else { + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BRIDGECHAIN_UPDATE') + } + }) + }) + + describe('previousStep', () => { + it('should go from step 2 to step 1', () => { + wrapper.vm.step = 2 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + + it('should do nothing on step 1', () => { + wrapper.vm.step = 1 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + }) + + describe('nextStep', () => { + it('should go from step 1 to step 2', () => { + wrapper.vm.step = 1 + + wrapper.vm.nextStep() + + expect(wrapper.vm.step).toBe(2) + }) + + it('should submit form data on step 2', async () => { + const spy = jest.spyOn(wrapper.vm, 'onSubmit').mockImplementation() + const validateSeedsSpy = jest.spyOn(wrapper.vm, 'validateSeeds').mockImplementation() + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + await wrapper.vm.nextStep() + + expect(validateSeedsSpy).toHaveBeenCalledTimes(1) + expect(wrapper.vm.invalidSeeds.length).toBe(0) + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('addSeedNode', () => { + it('should add current seed to list', async () => { + wrapper.vm.$v.form.seedNodes.$model = [] + wrapper.vm.$v.seedNode.$model = '5.5.5.5' + + await wrapper.vm.$nextTick() + + wrapper.vm.addSeedNode() + + expect(wrapper.vm.$v.form.seedNodes.$model).toEqual([{ ip: '5.5.5.5', isInvalid: false }]) + }) + + it('should reset current seed', async () => { + wrapper.vm.$v.seedNode.$model = '7.7.7.7' + + await wrapper.vm.$nextTick() + + wrapper.vm.addSeedNode() + + expect(wrapper.vm.$v.seedNode.$model).toBe('') + }) + + it('should do nothing if invalid seed', async () => { + wrapper.vm.$v.form.seedNodes.$model = [] + wrapper.vm.$v.seedNode.$model = 'invalid seed' + + await wrapper.vm.$nextTick() + + wrapper.vm.addSeedNode() + + expect(wrapper.vm.$v.form.seedNodes.$model).toEqual([]) + }) + }) + + describe('emitRemoveSeedNode', () => { + it('should remove seed at index', () => { + wrapper.vm.$v.form.seedNodes.$model = [ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false }, + { ip: '7.7.7.7', isInvalid: false } + ] + + wrapper.vm.emitRemoveSeedNode(1) + + expect(wrapper.vm.$v.form.seedNodes.$model).toEqual([ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '7.7.7.7', isInvalid: false } + ]) + }) + + it('should do nothing if index does not exist', () => { + const seeds = [ + { ip: '5.5.5.5', isInvalid: false }, + { ip: '6.6.6.6', isInvalid: false } + ] + + wrapper.vm.$v.form.seedNodes.$model = seeds + + wrapper.vm.emitRemoveSeedNode(3) + + expect(wrapper.vm.$v.form.seedNodes.$model).toBe(seeds) + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessRegistration.spec.js new file mode 100644 index 0000000000..e9a4174ac5 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessRegistration.spec.js @@ -0,0 +1,202 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBusinessRegistration from '@/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessRegistration' +import CurrencyMixin from '@/mixins/currency' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +/* eslint-disable-next-line camelcase */ +const createWrapper = (component, wallet_fromRoute) => { + component = component || TransactionFormBusinessRegistration + /* eslint-disable-next-line camelcase */ + wallet_fromRoute = wallet_fromRoute || { + passphrase: null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildBusinessRegistration: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute + } + }) +} + +describe('TransactionFormBusinessRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business registration transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(0) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBusiness')).toBe(true) + }) + + it('should have name field', () => { + expect(wrapper.contains('.TransactionFormBusiness__name')).toBe(true) + }) + + it('should have website field', () => { + expect(wrapper.contains('.TransactionFormBusiness__website')).toBe(true) + }) + + it('should have vat field', () => { + expect(wrapper.contains('.TransactionFormBusiness__vat')).toBe(true) + }) + + it('should have repository field', () => { + expect(wrapper.contains('.TransactionFormBusiness__repository')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBusiness__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'business' + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io' + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('methods', () => { + describe('buildTransaction', () => { + it('should build business registration', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBusinessRegistration).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build business registration with default arguments', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBusinessRegistration).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BUSINESS_REGISTRATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessResignation.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessResignation.spec.js new file mode 100644 index 0000000000..f4e0768e06 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessResignation.spec.js @@ -0,0 +1,264 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBusinessResignation from '@/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessResignation' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' +import WalletService from '@/services/wallet' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +/* eslint-disable-next-line camelcase */ +const createWrapper = (component, wallet_fromRoute) => { + component = component || TransactionFormBusinessResignation + /* eslint-disable-next-line camelcase */ + wallet_fromRoute = wallet_fromRoute || { + address: 'address-1', + passphrase: null + } + + if (!wallet_fromRoute.business) { + wallet_fromRoute.business = { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildBusinessResignation: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute + }, + stubs: { + Portal: true + } + }) +} + +let spyCanResignBusiness +describe('TransactionFormBusinessResignation', () => { + beforeEach(() => { + spyCanResignBusiness = jest.spyOn(WalletService, 'canResignBusiness').mockReturnValue(true) + + createWrapper() + }) + + afterEach(() => { + spyCanResignBusiness.mockRestore() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business resignation transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(1) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBusinessResignation')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBusinessResignation__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormBusinessResignation__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormBusinessResignation__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusinessResignation__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusinessResignation__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusinessResignation__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusinessResignation__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusinessResignation__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusinessResignation__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('canResignBusiness', () => { + it('should return true', () => { + spyCanResignBusiness.mockReturnValue(true) + + createWrapper() + + expect(wrapper.vm.canResignBusiness).toBe(true) + }) + + it('should return false', () => { + spyCanResignBusiness.mockReturnValue(false) + + createWrapper() + + expect(wrapper.vm.canResignBusiness).toBe(false) + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build business resignation', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBusinessResignation).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build business resignation with default arguments', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBusinessResignation).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BUSINESS_RESIGNATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessUpdate.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessUpdate.spec.js new file mode 100644 index 0000000000..1970a2dc31 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessUpdate.spec.js @@ -0,0 +1,222 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBusinessUpdate from '@/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessUpdate' +import CurrencyMixin from '@/mixins/currency' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +/* eslint-disable-next-line camelcase */ +const createWrapper = (component, wallet_fromRoute) => { + component = component || TransactionFormBusinessUpdate + /* eslint-disable-next-line camelcase */ + wallet_fromRoute = wallet_fromRoute || { + passphrase: null + } + + if (!wallet_fromRoute.business) { + wallet_fromRoute.business = { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildBusinessUpdate: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute + } + }) +} + +describe('TransactionFormBusinessUpdate', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have business update transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(2) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBusiness')).toBe(true) + }) + + it('should have name field', () => { + expect(wrapper.contains('.TransactionFormBusiness__name')).toBe(true) + }) + + it('should have website field', () => { + expect(wrapper.contains('.TransactionFormBusiness__website')).toBe(true) + }) + + it('should have vat field', () => { + expect(wrapper.contains('.TransactionFormBusiness__vat')).toBe(true) + }) + + it('should have repository field', () => { + expect(wrapper.contains('.TransactionFormBusiness__repository')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBusiness__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'business' + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io' + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('mounted hook', () => { + it('should load business into form', () => { + expect(wrapper.vm.form.asset).toEqual({ + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }) + }) + }) + + describe('methods', () => { + describe('buildTransaction', () => { + it('should build business update', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildBusinessUpdate).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build business update with default arguments', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildBusinessUpdate).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BUSINESS_UPDATE') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/mixin.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/mixin.spec.js new file mode 100644 index 0000000000..d0650db7e0 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormBusiness/mixin.spec.js @@ -0,0 +1,595 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../../__utils__/i18n' +import TransactionFormBusinessRegistration from '@/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessRegistration' +import TransactionFormBusinessUpdate from '@/components/Transaction/TransactionForm/TransactionFormBusiness/TransactionFormBusinessUpdate' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +/* eslint-disable-next-line camelcase */ +const createWrapper = (component, wallet_fromRoute) => { + /* eslint-disable-next-line camelcase */ + wallet_fromRoute = wallet_fromRoute || { + address: 'address-1', + passphrase: null + } + + if (component.name === 'TransactionFormBusinessUpdate' && !wallet_fromRoute.business) { + wallet_fromRoute.business = { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildBusinessRegistration: jest.fn((transactionData) => transactionData), + buildBusinessUpdate: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute + } + }) +} + +describe.each([ + ['TransactionFormBusinessRegistration', TransactionFormBusinessRegistration], + ['TransactionFormBusinessUpdate', TransactionFormBusinessUpdate] +])('%s', (componentName, component) => { + beforeEach(() => { + createWrapper(component) + }) + + it('should have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).toBe(2) + }) + + it('should have correct transaction type', () => { + if (componentName === 'TransactionFormBusinessRegistration') { + expect(wrapper.vm.$options.transactionType).toBe(0) + } else { + expect(wrapper.vm.$options.transactionType).toBe(2) + } + }) + + describe('data', () => { + it('should create form object', () => { + expect(Object.keys(wrapper.vm.form)).toEqual([ + 'fee', + 'passphrase', + 'walletPassword', + 'asset' + ]) + expect(Object.keys(wrapper.vm.form.asset)).toEqual([ + 'name', + 'website', + 'vat', + 'repository' + ]) + }) + }) + + if (componentName === 'TransactionFormBusinessUpdate') { + describe('mounted hook', () => { + it('should load business into form', () => { + expect(wrapper.vm.form.asset).toEqual({ + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }) + }) + }) + } + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormBusiness')).toBe(true) + }) + + it('should have name field', () => { + expect(wrapper.contains('.TransactionFormBusiness__name')).toBe(true) + }) + + it('should have website field', () => { + expect(wrapper.contains('.TransactionFormBusiness__website')).toBe(true) + }) + + it('should have vat field', () => { + expect(wrapper.contains('.TransactionFormBusiness__vat')).toBe(true) + }) + + it('should have repository field', () => { + expect(wrapper.contains('.TransactionFormBusiness__repository')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormBusiness__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(component, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(component, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormBusiness__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(component, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(component, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormBusiness__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'business' + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io' + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormBusiness__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('nameLabel', () => { + it('should be formatted', () => { + expect(wrapper.vm.nameLabel).toBe('WALLET_BUSINESS.NAME - VALIDATION.MAX_LENGTH') + }) + }) + + describe('nameError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.name.$model = 'test' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.name.$model = '' + wrapper.vm.$v.form.asset.name.$reset() + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe(null) + }) + + it('should return required if empty', () => { + wrapper.vm.$v.form.asset.name.$model = '' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.REQUIRED') + }) + + it('should not return error if shorter than max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(30, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if equal to max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(40, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should return error if longer than max (40)', () => { + wrapper.vm.$v.form.asset.name.$model = ''.padStart(50, '-') + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.name.$model = 'test' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(false) + expect(wrapper.vm.nameError).not.toBe('VALIDATION.NAME_ERROR') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.name.$model = '$ARK' + + expect(wrapper.vm.$v.form.asset.name.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.name.$invalid).toBe(true) + expect(wrapper.vm.nameError).toBe('VALIDATION.NAME_ERROR') + }) + }) + + describe('websiteError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.website.$model = 'http://ark.io' + + expect(wrapper.vm.$v.form.asset.website.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.website.$invalid).toBe(false) + expect(wrapper.vm.websiteError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.website.$model = '' + wrapper.vm.$v.form.asset.website.$reset() + + expect(wrapper.vm.$v.form.asset.website.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.website.$invalid).toBe(true) + expect(wrapper.vm.websiteError).toBe(null) + }) + + it('should return required if empty', () => { + wrapper.vm.$v.form.asset.website.$model = '' + + expect(wrapper.vm.$v.form.asset.website.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.website.$invalid).toBe(true) + expect(wrapper.vm.websiteError).toBe('VALIDATION.REQUIRED') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io:4003' + + expect(wrapper.vm.$v.form.asset.website.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.website.$invalid).toBe(false) + expect(wrapper.vm.websiteError).not.toBe('VALIDATION.INVALID_URL') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.website.$model = 'http://ark' + + expect(wrapper.vm.$v.form.asset.website.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.website.$invalid).toBe(true) + expect(wrapper.vm.websiteError).toBe('VALIDATION.INVALID_URL') + }) + }) + + describe('vatLabel', () => { + it('should be formatted', () => { + expect(wrapper.vm.vatLabel).toBe('WALLET_BUSINESS.VAT - VALIDATION.MIN_LENGTH VALIDATION.MAX_LENGTH') + }) + }) + + describe('vatError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.vat.$model = '' + wrapper.vm.$v.form.asset.vat.$reset() + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).toBe(null) + }) + + it('should return null if empty as not required', () => { + wrapper.vm.$v.form.asset.vat.$model = '' + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).toBe(null) + }) + + it('should not return error if shorter than max (15)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(10, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if equal to max (15)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(15, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).not.toBe('VALIDATION.TOO_LONG') + }) + + it('should not return error if longer than min (8)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(10, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if equal to min (8)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(15, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should return error if longer than max (40)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(50, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(true) + expect(wrapper.vm.vatError).toBe('VALIDATION.TOO_LONG') + }) + + it('should return error if shorter than min (8)', () => { + wrapper.vm.$v.form.asset.vat.$model = ''.padStart(5, '-') + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(true) + expect(wrapper.vm.vatError).toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + + expect(wrapper.vm.$v.form.asset.vat.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.vat.$invalid).toBe(false) + expect(wrapper.vm.vatError).not.toBe('VALIDATION.NAME_ERROR') + }) + }) + + describe('repositoryLabel', () => { + it('should be formatted', () => { + expect(wrapper.vm.repositoryLabel).toBe('WALLET_BUSINESS.REPOSITORY - VALIDATION.MIN_LENGTH') + }) + }) + + describe('repositoryError', () => { + it('should return null if valid', () => { + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(false) + expect(wrapper.vm.repositoryError).toBe(null) + }) + + it('should return null if not dirty', () => { + wrapper.vm.$v.form.asset.repository.$model = '' + wrapper.vm.$v.form.asset.repository.$reset() + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(false) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(false) + expect(wrapper.vm.repositoryError).toBe(null) + }) + + it('should not return error if longer than min (12)', () => { + wrapper.vm.$v.form.asset.repository.$model = 'http://github.com' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(false) + expect(wrapper.vm.repositoryError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if equal to min (12)', () => { + wrapper.vm.$v.form.asset.repository.$model = 'http://g.com' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(false) + expect(wrapper.vm.repositoryError).not.toBe('VALIDATION.TOO_SHORT') + }) + + it('should return error if shorter than min (12)', () => { + wrapper.vm.$v.form.asset.repository.$model = 'ftp://g.co' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(true) + expect(wrapper.vm.repositoryError).toBe('VALIDATION.TOO_SHORT') + }) + + it('should not return error if valid', () => { + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(false) + expect(wrapper.vm.repositoryError).not.toBe('VALIDATION.INVALID_URL') + }) + + it('should return error if invalid', () => { + wrapper.vm.$v.form.asset.repository.$model = 'https://github/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.$v.form.asset.repository.$dirty).toBe(true) + expect(wrapper.vm.$v.form.asset.repository.$invalid).toBe(true) + expect(wrapper.vm.repositoryError).toBe('VALIDATION.INVALID_URL') + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.asset.name.$model = 'business' + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io' + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + asset: { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }, + passphrase: 'passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(component, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + wrapper.vm.$v.form.asset.name.$model = 'business' + wrapper.vm.$v.form.asset.website.$model = 'https://ark.io' + wrapper.vm.$v.form.asset.vat.$model = 'GB12345678' + wrapper.vm.$v.form.asset.repository.$model = 'https://github.com/arkecosystem/desktop-wallet.git' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + asset: { + name: 'business', + website: 'https://ark.io', + vat: 'GB12345678', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build business update', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + if (componentName === 'TransactionFormBusinessRegistration') { + expect(wrapper.vm.$client.buildBusinessRegistration).toHaveBeenCalledWith(transactionData, true, true) + } else { + expect(wrapper.vm.$client.buildBusinessUpdate).toHaveBeenCalledWith(transactionData, true, true) + } + expect(response).toBe(transactionData) + }) + + it('should build business update with default arguments', async () => { + const transactionData = { + type: 2, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + if (componentName === 'TransactionFormBusinessRegistration') { + expect(wrapper.vm.$client.buildBusinessRegistration).toHaveBeenCalledWith(transactionData, false, false) + } else { + expect(wrapper.vm.$client.buildBusinessUpdate).toHaveBeenCalledWith(transactionData, false, false) + } + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + if (componentName === 'TransactionFormBusinessRegistration') { + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BUSINESS_REGISTRATION') + } else { + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.BUSINESS_UPDATE') + } + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateRegistration.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateRegistration.spec.js new file mode 100644 index 0000000000..394a88fe07 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateRegistration.spec.js @@ -0,0 +1,298 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormDelegateRegistration } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' +import store from '@/store' + +jest.mock('@/store', () => { + return { + getters: { + 'delegate/byUsername': jest.fn(() => false) + } + } +}) + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet) => { + component = component || TransactionFormDelegateRegistration + wallet = wallet || { + address: 'address-1', + passphrase: null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildDelegateRegistration: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormDelegateRegistration', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have delegate registration transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(2) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormDelegateRegistration')).toBe(true) + }) + + it('should only show message if already registered', () => { + createWrapper(null, { + isDelegate: true + }) + + expect(wrapper.contains('.TransactionFormDelegateRegistration__username')).toBe(false) + expect(wrapper.contains('.TransactionFormDelegateRegistration__fee')).toBe(false) + expect(wrapper.contains('.TransactionFormDelegateRegistration__ledger-notice')).toBe(false) + expect(wrapper.contains('.TransactionFormDelegateRegistration__password')).toBe(false) + expect(wrapper.contains('.TransactionFormDelegateRegistration__passphrase')).toBe(false) + expect(wrapper.contains('.TransactionFormDelegateRegistration__next')).toBe(false) + }) + + it('should have username field', () => { + expect(wrapper.contains('.TransactionFormDelegateRegistration__username')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormDelegateRegistration__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormDelegateRegistration__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormDelegateRegistration__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormDelegateRegistration__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormDelegateRegistration__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormDelegateRegistration__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormDelegateRegistration__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.passphrase.$model = 'this is a passphrase' + wrapper.vm.$v.form.username.$model = 'delegate_1' + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormDelegateRegistration__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormDelegateRegistration__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('usernameError', () => { + it('should return null if valid username', async () => { + wrapper.vm.$v.form.username.$model = 'delegate_1' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.usernameError).toBe(null) + }) + + it('should return error if empty username', async () => { + wrapper.vm.$v.form.username.$model = '' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.usernameError).toBe('WALLET_DELEGATES.USERNAME_EMPTY_ERROR') + }) + + it('should return error if username is too long', async () => { + wrapper.vm.$v.form.username.$model = ''.padStart(25, '_') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.usernameError).toBe('WALLET_DELEGATES.USERNAME_MAX_LENGTH_ERROR') + }) + + it('should return error if username exists', async () => { + const delegateByUsernameSpy = jest.spyOn(store.getters, 'delegate/byUsername').mockReturnValue(true) + wrapper.vm.$v.form.username.$model = 'delegate_2' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.usernameError).toBe('WALLET_DELEGATES.USERNAME_EXISTS') + delegateByUsernameSpy.mockRestore() + }) + + it('should return error if no valid username', async () => { + wrapper.vm.$v.form.username.$model = 'INVALID USERNAME' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.usernameError).toBe('WALLET_DELEGATES.USERNAME_ERROR') + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.username.$model = 'delegate_1' + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + username: 'delegate_1', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.username.$model = 'delegate_1' + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + username: 'delegate_1', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build bridgechain registration', async () => { + const transactionData = { + type: 2, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildDelegateRegistration).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build bridgechain registration with default arguments', async () => { + const transactionData = { + type: 2, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildDelegateRegistration).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.DELEGATE_REGISTRATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateResignation.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateResignation.spec.js new file mode 100644 index 0000000000..e316a0efed --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormDelegateResignation.spec.js @@ -0,0 +1,270 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormDelegateResignation } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet, delegate) => { + component = component || TransactionFormDelegateResignation + wallet = wallet || { + address: 'address-1' + } + + if (!Object.keys(wallet).includes('passphrase')) { + wallet.passphrase = null + } + + if (!Object.keys(wallet).includes('isDelegate')) { + wallet.isDelegate = true + } + + if (delegate === undefined) { + delegate = { + username: 'delegate-1' + } + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildDelegateResignation: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'delegate/byAddress': jest.fn(() => delegate), + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormDelegateResignation', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have delegate resignation transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(7) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormDelegateResignation')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormDelegateResignation__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormDelegateResignation__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormDelegateResignation__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormDelegateResignation__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormDelegateResignation__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormDelegateResignation__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormDelegateResignation__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormDelegateResignation__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormDelegateResignation__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('username', () => { + it('should return username', () => { + expect(wrapper.vm.username).toBe('delegate-1') + }) + + it('should return null if not a delegate', () => { + createWrapper(null, null, null) + expect(wrapper.vm.username).toBe(null) + }) + }) + + describe('canResign', () => { + it('should return true if delegate and has username', () => { + expect(wrapper.vm.canResign).toBe(true) + }) + + it('should return false if not a delegate', () => { + createWrapper(null, { + isDelegate: false + }) + + expect(wrapper.vm.canResign).toBe(false) + }) + + it('should return false if no username', () => { + createWrapper(null, { + isDelegate: true + }, {}) + + expect(wrapper.vm.canResign).toBe(false) + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build delegate resignation', async () => { + const transactionData = { + type: 7, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildDelegateResignation).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build delegate resignation with default arguments', async () => { + const transactionData = { + type: 7, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildDelegateResignation).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.DELEGATE_RESIGNATION') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormIpfs.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormIpfs.spec.js new file mode 100644 index 0000000000..858459d335 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormIpfs.spec.js @@ -0,0 +1,261 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormIpfs } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet) => { + component = component || TransactionFormIpfs + wallet = wallet || { + address: 'address-1' + } + + if (!Object.keys(wallet).includes('passphrase')) { + wallet.passphrase = null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildIpfs: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormIpfs', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have ipfs transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(5) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormIpfs')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormIpfs__fee')).toBe(true) + }) + + it('should have hash field', () => { + expect(wrapper.contains('.TransactionFormIpfs__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormIpfs__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormIpfs__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormIpfs__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormIpfs__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormIpfs__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormIpfs__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.hash.$model = 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6' + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormIpfs__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormIpfs__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('hashError', () => { + it('should return error if no valid hash', async () => { + wrapper.vm.$v.form.hash.$model = 'INVALID HASH' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.hashError).toBe('WALLET_IPFS.HASH_ERROR') + }) + + it('should return error if empty hash', async () => { + wrapper.vm.$v.form.hash.$model = '' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.hashError).toBe('WALLET_IPFS.HASH_ERROR') + }) + + it('should return null if valid hash', async () => { + wrapper.vm.$v.form.hash.$model = 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.hashError).toBe(null) + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.hash.$model = 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6' + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + hash: 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.hash.$model = 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6' + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + hash: 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build ipfs', async () => { + const transactionData = { + type: 5, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildIpfs).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build ipfs with default arguments', async () => { + const transactionData = { + type: 7, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildIpfs).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.IPFS') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiPayment.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiPayment.spec.js new file mode 100644 index 0000000000..132f7ed3b4 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiPayment.spec.js @@ -0,0 +1,950 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import cloneDeep from 'lodash/cloneDeep' +import installI18n from '../../../__utils__/i18n' +import { VENDOR_FIELD } from '@config' +import { TransactionFormMultiPayment } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' +import WalletService from '@/services/wallet' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const globalNetwork = Object.freeze({ + id: 'network-1', + fractionDigits: 8, + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + }, + nethash: 'nethash-1' +}) + +let wrapper +const createWrapper = (component, wallet, network, props = {}) => { + component = component || TransactionFormMultiPayment + + if (wallet === undefined) { + wallet = { + id: 'test-wallet' + } + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'passphrase')) { + wallet.passphrase = null + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'address')) { + wallet.address = 'address-1' + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'balance')) { + wallet.balance = (1000 * 1e8).toString() + } + + const mountNetwork = network || cloneDeep(globalNetwork) + + wrapper = mount(TransactionFormMultiPayment, { + i18n, + localVue, + sync: false, + propsData: props, + mocks: { + $client: { + buildMultiPayment: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $success: jest.fn(), + $store: { + getters: { + 'profile/byId': jest.fn(() => ({ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + })), + 'session/currency': 'EUR', + get 'ledger/isConnected' () { + return true + }, + get 'ledger/wallets' () { + return [{ + address: 'ledger-address-1' + }] + }, + 'wallet/byProfileId': jest.fn(() => [wallet]), + 'wallet/contactsByProfileId': jest.fn(() => []), + 'transaction/staticFee': jest.fn(() => null), + get 'session/profileId' () { + return 'profile-1' + }, + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'network/byToken': jest.fn(() => mountNetwork), + 'network/byId': jest.fn(() => mountNetwork), + 'profile/byCompatibleAddress': jest.fn(() => []), + get 'profile/all' () { + return [{ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + }, { + id: 'profile-2', + name: 'profile-2', + networkId: 'network-1' + }] + } + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_simpleFormatCrypto: jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + formatter_networkCurrency: jest.fn(), + session_network: mountNetwork, + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_truncate: jest.fn(address => address), + wallet_name: jest.fn(address => address), + wallet_fromRoute: wallet + }, + stubs: { + Identicon: true, + Portal: true + } + }) +} + +describe('TransactionFormMultiPayment', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have multi-payment transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(6) + }) + + describe('data', () => { + it('should has properties', () => { + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'step')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'amount')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'recipientId')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'form')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'recipients')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'fee')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'passphrase')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'walletPassword')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'vendorField')).toBe(true) + }) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormMultiPayment')).toBe(true) + }) + + it('should have recipient field', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__recipient')).toBe(true) + }) + + it('should have amount field', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__amount')).toBe(true) + }) + + it('should have add button', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__add')).toBe(true) + }) + + describe('step 2', () => { + beforeEach(() => { + wrapper.vm.step = 2 + }) + + it('should have vendorfield', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__vendorfield')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', async () => { + createWrapper(null, { + isLedger: true + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiPayment__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', async () => { + createWrapper(null, { + isLedger: false + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiPayment__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiPayment__password')).toBe(true) + }) + + it('should not show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiPayment__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiPayment__passphrase')).toBe(false) + }) + }) + }) + + describe('prev button', () => { + it('should be enabled if on second form', async () => { + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiPayment__prev').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if on step 1', async () => { + wrapper.vm.step = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiPayment__prev').attributes('disabled')).toBe('disabled') + }) + }) + + describe('next button', () => { + it('should be enabled if recipients form is valid', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-2', + amount: 10 + }, { + address: 'address-2', + amount: 10 + }] + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiPayment__next').attributes('disabled')).toBeFalsy() + }) + + it('should be enabled if both forms are valid', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-2', + amount: 10 + }, { + address: 'address-2', + amount: 10 + }] + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiPayment__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiPayment__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('alternativeCurrency', () => { + it('should return correct data', () => { + expect(wrapper.vm.alternativeCurrency).toEqual('EUR') + }) + + it('should update data', () => { + wrapper.vm.$store.getters['session/currency'] = 'GBP' + + expect(wrapper.vm.alternativeCurrency).toEqual('GBP') + }) + }) + + describe('amountTooLowError', () => { + it('should return formatted value', () => { + const $tSpy = jest.fn(translation => translation) + const simpleFormatCryptoSpy = jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto) + const response = TransactionFormMultiPayment.computed.amountTooLowError.call({ + walletNetwork: globalNetwork, + $t: $tSpy, + currency_simpleFormatCrypto: simpleFormatCryptoSpy, + session_network: globalNetwork + }) + + expect($tSpy).toHaveBeenCalledWith('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { + amount: '0.00000001 ARK' + }) + expect(response).toBe('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM') + expect(simpleFormatCryptoSpy).toHaveBeenCalledWith('0.00000001') + }) + + it('should return formatted value with different fraction digits', () => { + const $tSpy = jest.fn(translation => translation) + const simpleFormatCryptoSpy = jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto) + const response = TransactionFormMultiPayment.computed.amountTooLowError.call({ + walletNetwork: { + ...globalNetwork, + fractionDigits: 2 + }, + $t: $tSpy, + currency_simpleFormatCrypto: simpleFormatCryptoSpy, + session_network: globalNetwork + }) + + expect($tSpy).toHaveBeenCalledWith('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { + amount: '0.01 ARK' + }) + expect(response).toBe('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM') + expect(simpleFormatCryptoSpy).toHaveBeenCalledWith('0.01') + }) + }) + + describe('notEnoughBalanceError', () => { + it('should return a formatted value', () => { + const $tSpy = jest.fn(translation => translation) + const formatterNetworkCurrencySpy = jest.fn(value => value) + const response = TransactionFormMultiPayment.computed.notEnoughBalanceError.call({ + currentWallet: { + balance: (10 * 1e8).toString() + }, + $t: $tSpy, + formatter_networkCurrency: formatterNetworkCurrencySpy, + session_network: globalNetwork + }) + + expect(response).toBe('TRANSACTION_FORM.ERROR.NOT_ENOUGH_BALANCE') + expect($tSpy).toHaveBeenCalledWith('TRANSACTION_FORM.ERROR.NOT_ENOUGH_BALANCE', { + balance: '1000000000' + }) + expect(formatterNetworkCurrencySpy).toHaveBeenCalledWith('1000000000') + }) + }) + + describe('minimumAmount', () => { + it('should return the correct value', () => { + expect(wrapper.vm.minimumAmount + '').toBe('0.00000001') + }) + }) + + describe('maximumAvailableAmount', () => { + it('should return value', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + expect(wrapper.vm.maximumAvailableAmount).toEqual((new BigNumber(1000)).minus(0.1)) + }) + + it('should return value including all recipients', async () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('test') + wrapper.vm.$v.amount.$model = 10 + + await wrapper.vm.$nextTick() + wrapper.vm.addRecipient() + await wrapper.vm.$nextTick() + + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('test') + wrapper.vm.$v.amount.$model = 20 + + await wrapper.vm.$nextTick() + wrapper.vm.addRecipient() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.maximumAvailableAmount).toEqual((new BigNumber(1000)).minus(0.1).minus(30)) + }) + + it('should return value based on different fee', () => { + wrapper.vm.form.fee = 10 + + expect(wrapper.vm.maximumAvailableAmount).toEqual((new BigNumber(1000)).minus(10)) + }) + }) + + describe('vendorFieldLabel', () => { + it('should return value', () => { + expect(wrapper.vm.vendorFieldLabel).toBe('TRANSACTION.VENDOR_FIELD - VALIDATION.MAX_LENGTH') + }) + }) + + describe('vendorFieldHelperText', () => { + describe('default max length', () => { + let $tSpy + beforeEach(() => { + $tSpy = jest.spyOn(wrapper.vm, '$t') + }) + + afterEach(() => { + $tSpy.mockRestore() + }) + + it('should return null if vendorfield is empty', () => { + wrapper.vm.form.vendorField = '' + + expect(wrapper.vm.vendorFieldHelperText).toBe(null) + expect($tSpy).not.toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [VENDOR_FIELD.defaultMaxLength]) + }) + + it('should return length warning if equal to max', () => { + wrapper.vm.form.vendorField = ''.padStart(VENDOR_FIELD.defaultMaxLength, '-') + + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REACHED') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [VENDOR_FIELD.defaultMaxLength]) + }) + + it('should return length warning if less than max', () => { + wrapper.vm.form.vendorField = ''.padStart(VENDOR_FIELD.defaultMaxLength - 10, '-') + + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING', [ + 10, + VENDOR_FIELD.defaultMaxLength + ]) + }) + }) + + describe('network max length', () => { + let $tSpy + + beforeEach(() => { + const network = { + ...cloneDeep(globalNetwork), + vendorField: { + maxLength: 20 + } + } + createWrapper(null, undefined, network) + $tSpy = jest.spyOn(wrapper.vm, '$t') + }) + + afterEach(() => { + $tSpy.mockRestore() + }) + + it('should return null if vendorfield is empty', () => { + wrapper.vm.form.vendorField = '' + + expect(wrapper.vm.vendorFieldHelperText).toBe(null) + expect($tSpy).not.toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [20]) + }) + + it('should return length warning if equal to max', () => { + wrapper.vm.form.vendorField = ''.padStart(20, '-') + + expect(wrapper.vm.walletNetwork.vendorField.maxLength).toBe(20) + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REACHED') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [20]) + }) + + it('should return length warning if less than max', () => { + wrapper.vm.form.vendorField = ''.padStart(20 - 5, '-') + + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING', [ + 5, + 20 + ]) + }) + }) + }) + + describe('vendorFieldMaxLength', () => { + it('should return network max length', () => { + createWrapper(null, undefined, { + ...cloneDeep(globalNetwork), + vendorField: { + maxLength: 20 + } + }) + + expect(wrapper.vm.vendorFieldMaxLength).toBe(20) + }) + + it('should return default max length if network does not have vendorField max', () => { + expect(wrapper.vm.vendorFieldMaxLength).toBe(VENDOR_FIELD.defaultMaxLength) + }) + }) + + describe('recipientWarning', () => { + it('should return null if recipientId is not dirty', () => { + expect(wrapper.vm.recipientWarning).toBe(null) + }) + + it('should return message for duplicate recipients', () => { + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-2', + amount: 10 + }] + wrapper.vm.$v.recipientId.$model = 'address-2' + + expect(wrapper.vm.recipientWarning).toBe('TRANSACTION.MULTI_PAYMENT.WARNING_DUPLICATE') + }) + }) + + describe('maximumRecipients', () => { + it('should return default if no constants', () => { + expect(wrapper.vm.maximumRecipients).toBe(500) + }) + + it('should return default if no network value', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: {} + } + + createWrapper(null, undefined, network) + + expect(wrapper.vm.maximumRecipients).toBe(500) + }) + + it('should return network constant', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: { + multiPaymentLimit: 20 + } + } + + createWrapper(null, undefined, network) + + expect(wrapper.vm.maximumRecipients).toBe(20) + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-2', + amount: (1 * 1e8).toString() + }] + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + recipients: [{ + address: 'address-2', + amount: (1 * 1e8).toString() + }], + fee: new BigNumber(0.1 * 1e8), + vendorField: 'vendorfield test', + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-2', + amount: (1 * 1e8).toString() + }] + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + recipients: [{ + address: 'address-2', + amount: (1 * 1e8).toString() + }], + fee: new BigNumber(0.1 * 1e8), + vendorField: 'vendorfield test', + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build transfer', async () => { + const transactionData = { + type: 6, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildMultiPayment).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build transfer with default arguments', async () => { + const transactionData = { + type: 6, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildMultiPayment).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.MULTI_PAYMENT') + }) + }) + + describe('addRecipient', () => { + it('should add current recipient to list', async () => { + const address = Identities.Address.fromPassphrase('passphrase') + wrapper.vm.$v.recipientId.$model = address + wrapper.vm.$v.amount.$model = 100 + + await wrapper.vm.$nextTick() + + wrapper.vm.addRecipient() + + expect(wrapper.vm.$v.form.recipients.$model).toEqual([{ + address: address, + amount: new BigNumber(100 * 1e8) + }]) + }) + + it('should reset current recipient', async () => { + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('passphrase') + wrapper.vm.$v.amount.$model = 100 + + await wrapper.vm.$nextTick() + + wrapper.vm.addRecipient() + + expect(wrapper.vm.$v.recipientId.$model).toBe('') + expect(wrapper.vm.$v.amount.$model).toBe('') + }) + + it('should do nothing if invalid address', async () => { + WalletService.validateAddress.mockReturnValue(false) + + wrapper.vm.$v.recipientId.$model = 'invalid address' + wrapper.vm.$v.amount.$model = 100 + + await wrapper.vm.$nextTick() + + wrapper.vm.addRecipient() + + expect(wrapper.vm.$v.form.recipients.$model).toEqual([]) + + WalletService.validateAddress.mockReturnValue(true) + }) + + it('should do nothing if invalid fee', async () => { + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('passphrase') + wrapper.vm.$v.amount.$model = '' + + await wrapper.vm.$nextTick() + + wrapper.vm.addRecipient() + + expect(wrapper.vm.$v.form.recipients.$model).toEqual([]) + }) + }) + + describe('previousStep', () => { + it('should go from step 2 to step 1', () => { + wrapper.vm.step = 2 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + + it('should do nothing on step 1', () => { + wrapper.vm.step = 1 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + }) + + describe('nextStep', () => { + it('should go from step 1 to step 2', () => { + wrapper.vm.step = 1 + + wrapper.vm.nextStep() + + expect(wrapper.vm.step).toBe(2) + }) + + it('should submit form data on step 2', async () => { + const spy = jest.spyOn(wrapper.vm, 'onSubmit').mockImplementation() + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + wrapper.vm.nextStep() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('emitRemoveRecipient', () => { + it('should remove recipient at index', () => { + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-1', + amount: 10 + }, { + address: 'address-2', + amount: 10 + }, { + address: 'address-3', + amount: 10 + }] + + wrapper.vm.emitRemoveRecipient(1) + + expect(wrapper.vm.$v.form.recipients.$model).toEqual([{ + address: 'address-1', + amount: 10 + }, { + address: 'address-3', + amount: 10 + }]) + }) + + it('should do nothing if index does not exist', () => { + const recipients = [{ + address: 'address-1', + amount: 10 + }, { + address: 'address-2', + amount: 10 + }] + + wrapper.vm.$v.form.recipients.$model = recipients + + wrapper.vm.emitRemoveRecipient(3) + + expect(wrapper.vm.$v.form.recipients.$model).toBe(recipients) + }) + }) + }) + + describe('validations', () => { + describe('recipientId', () => { + it('should be required if not set', () => { + wrapper.vm.$v.recipientId.$model = '' + + expect(wrapper.vm.$v.recipientId.required).toBe(false) + }) + + it('should not be required if set', () => { + wrapper.vm.$v.recipientId.$model = 'test' + + expect(wrapper.vm.$v.recipientId.required).toBe(true) + }) + + it('should not be valid', async () => { + WalletService.validateAddress.mockReturnValue(false) + + wrapper.vm.$v.recipientId.$model = 'test' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.recipient.$v.$invalid).toBe(true) + expect(wrapper.vm.$v.recipientId.isValid).toBe(false) + + WalletService.validateAddress.mockReturnValue(true) + }) + + it('should not be valid if no recipient field', async () => { + wrapper.vm.$refs.recipient = null + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.recipient).toBe(null) + expect(wrapper.vm.$v.recipientId.isValid).toBe(false) + }) + + it('should be valid', async () => { + wrapper.vm.$v.recipientId.$model = Identities.Address.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.recipient.$v.$invalid).toBe(false) + expect(wrapper.vm.$v.recipientId.isValid).toBe(true) + }) + }) + + describe('amount', () => { + it('should be required if not set', () => { + wrapper.vm.$v.amount.$model = '' + + expect(wrapper.vm.$v.amount.required).toBe(false) + }) + + it('should not be required if set', () => { + wrapper.vm.$v.amount.$model = '10' + + expect(wrapper.vm.$v.amount.required).toBe(true) + }) + + it('should not be valid', async () => { + wrapper.vm.$v.amount.$model = 'test' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.amount.$v.$invalid).toBe(true) + expect(wrapper.vm.$v.amount.isValid).toBe(false) + }) + + it('should not be valid if no amount field', async () => { + wrapper.vm.$refs.amount = null + wrapper.vm.$v.amount.$model = 'test' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.amount).toBe(null) + expect(wrapper.vm.$v.amount.isValid).toBe(false) + }) + + it('should be valid', async () => { + wrapper.vm.$v.amount.$model = 10 + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.amount.$v.$invalid).toBe(false) + expect(wrapper.vm.$v.amount.isValid).toBe(true) + }) + }) + + describe('form', () => { + describe('recipients', () => { + it('should not be above minimum if not set', () => { + wrapper.vm.$v.form.recipients.$model = [] + + expect(wrapper.vm.$v.form.recipients.aboveMinimum).toBe(false) + }) + + it('should not be above minimum if not enough', () => { + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-1', + amount: 10 + }] + + expect(wrapper.vm.$v.form.recipients.aboveMinimum).toBe(false) + }) + + it('should be above minimum if set', () => { + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-1', + amount: 10 + }, { + address: 'address-1', + amount: 10 + }] + + expect(wrapper.vm.$v.form.recipients.aboveMinimum).toBe(true) + }) + + it('should not be below maximum if too many', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: { + multiPaymentLimit: 2 + } + } + + createWrapper(null, undefined, network) + + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-1', + amount: 10 + }, { + address: 'address-1', + amount: 10 + }, { + address: 'address-1', + amount: 10 + }] + + expect(wrapper.vm.$v.form.recipients.belowMaximum).toBe(false) + }) + + it('should be above minimum if set', () => { + wrapper.vm.$v.form.recipients.$model = [{ + address: 'address-1', + amount: 10 + }] + + expect(wrapper.vm.$v.form.recipients.belowMaximum).toBe(true) + }) + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSign.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSign.spec.js new file mode 100644 index 0000000000..a3e776720a --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSign.spec.js @@ -0,0 +1,221 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormMultiSign } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + } +} + +let wrapper +const createWrapper = (component, wallet, transaction) => { + component = component || TransactionFormMultiSign + wallet = wallet || { + address: 'address-1', + publicKey: 'public-key-1' + } + transaction = transaction || { + amount: (1 * 1e8).toString(), + fee: (0.1 * 1e8).toString(), + recipientId: 'recipient-address', + vendorField: 'test vendorField' + } + + if (!Object.keys(wallet).includes('passphrase')) { + wallet.passphrase = null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + transaction + }, + mocks: { + $client: { + multiSign: jest.fn((transaction) => transaction) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => (network)) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormMultiSign', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have multisign transaction type - placeholder', () => { + expect(wrapper.vm.$options.transactionType).toBe(-1) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormMultiSign')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormMultiSign__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormMultiSign__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormMultiSign__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiSign__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiSign__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormMultiSign__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSign__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSign__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + publicKey: 'public-key-1', + passphrase: 'passphrase', + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + publicKey: 'public-key-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + publicKey: 'public-key-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build signed transaction', async () => { + const transaction = { + type: 0, + amount: 10, + fee: 0.1 + } + const transactionData = { + type: 0, + typeGroup: 1 + } + + wrapper.setProps({ + transaction + }) + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.multiSign).toHaveBeenCalledWith(transaction, transactionData) + expect(response).toBe(transaction) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.MULTI_SIGN') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSignature.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSignature.spec.js new file mode 100644 index 0000000000..046347f8b5 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormMultiSignature.spec.js @@ -0,0 +1,1277 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import cloneDeep from 'lodash/cloneDeep' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormMultiSignature } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import BigNumber from '@/plugins/bignumber' +import WalletService from '@/services/wallet' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const globalNetwork = Object.freeze({ + id: 'network-1', + fractionDigits: 8, + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + }, + nethash: 'nethash-1' +}) + +let wrapper +const createWrapper = (component, wallet, network, props = {}) => { + component = component || TransactionFormMultiSignature + + if (wallet === undefined) { + wallet = { + id: 'test-wallet' + } + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'passphrase')) { + wallet.passphrase = null + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'address')) { + wallet.address = 'address-1' + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'publicKey')) { + wallet.publicKey = 'public-key-1' + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'balance')) { + wallet.balance = (1000 * 1e8).toString() + } + + const mountNetwork = network || cloneDeep(globalNetwork) + + wrapper = mount(TransactionFormMultiSignature, { + i18n, + localVue, + sync: false, + propsData: props, + mocks: { + $client: { + buildMultiSignature: jest.fn((transactionData) => transactionData), + fetchWallet: jest.fn((address) => ({ + address, + publicKey: address.replace('address', 'public-key') + })) + }, + $error: jest.fn(), + $success: jest.fn(), + $store: { + getters: { + 'profile/byId': jest.fn(() => ({ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + })), + 'session/currency': 'EUR', + get 'ledger/isConnected' () { + return true + }, + get 'ledger/wallets' () { + return [{ + address: 'ledger-address-1' + }] + }, + 'wallet/byAddress': jest.fn((address) => ({ + address, + publicKey: address.replace('address', 'public-key') + })), + 'wallet/byProfileId': jest.fn(() => [wallet]), + 'wallet/contactsByProfileId': jest.fn(() => []), + 'transaction/staticFee': jest.fn(() => null), + get 'session/profileId' () { + return 'profile-1' + }, + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'network/byToken': jest.fn(() => mountNetwork), + 'network/byId': jest.fn(() => mountNetwork), + 'profile/byCompatibleAddress': jest.fn(() => []), + get 'profile/all' () { + return [{ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + }, { + id: 'profile-2', + name: 'profile-2', + networkId: 'network-1' + }] + } + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_simpleFormatCrypto: jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + formatter_networkCurrency: jest.fn(), + session_network: mountNetwork, + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_truncate: jest.fn(address => address), + wallet_name: jest.fn(address => address), + wallet_fromRoute: wallet + }, + stubs: { + Identicon: true, + Portal: true + } + }) +} + +describe('TransactionFormMultiSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have multi-payment transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(4) + }) + + describe('data', () => { + it('should has properties', () => { + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'step')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'currentTab')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'address')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'publicKey')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'form')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'publicKeys')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'minKeys')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'fee')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'passphrase')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'walletPassword')).toBe(true) + }) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormMultiSignature')).toBe(true) + }) + + describe('step 1', () => { + it('should have add button', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__add')).toBe(true) + }) + + describe('address tab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 0 + }) + + it('should have address field', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__address')).toBe(true) + }) + }) + + describe('public key tab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 1 + }) + + it('should have address field', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__public-key')).toBe(true) + }) + }) + }) + + describe('step 2', () => { + beforeEach(() => { + wrapper.vm.step = 2 + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', async () => { + createWrapper(null, { + isLedger: true + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiSignature__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', async () => { + createWrapper(null, { + isLedger: false + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiSignature__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiSignature__password')).toBe(true) + }) + + it('should not show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormMultiSignature__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormMultiSignature__passphrase')).toBe(false) + }) + }) + }) + + describe('prev button', () => { + it('should be enabled if on second form', async () => { + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSignature__prev').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if on step 1', async () => { + wrapper.vm.step = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSignature__prev').attributes('disabled')).toBe('disabled') + }) + }) + + describe('next button', () => { + it('should be enabled if recipients form is valid', async () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSignature__next').attributes('disabled')).toBeFalsy() + }) + + it('should be enabled if both forms are valid', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSignature__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormMultiSignature__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + + describe('computed', () => { + describe('addressTab', () => { + it('should return true if 0', () => { + wrapper.vm.currentTab = 0 + + expect(wrapper.vm.addressTab).toBe(true) + }) + + it('should return false if 1', () => { + wrapper.vm.currentTab = 1 + + expect(wrapper.vm.addressTab).toBe(false) + }) + }) + + describe('publicKeyTab', () => { + it('should return true if 1', () => { + wrapper.vm.currentTab = 1 + + expect(wrapper.vm.publicKeyTab).toBe(true) + }) + + it('should return false if 0', () => { + wrapper.vm.currentTab = 0 + + expect(wrapper.vm.publicKeyTab).toBe(false) + }) + }) + + describe('tabs', () => { + it('should return tabs', () => { + expect(wrapper.vm.tabs).toEqual([ + { + text: 'TRANSACTION.MULTI_SIGNATURE.TAB.ADDRESS' + }, + { + text: 'TRANSACTION.MULTI_SIGNATURE.TAB.PUBLIC_KEY' + } + ]) + }) + }) + + describe('validStep1', () => { + describe('addressTab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 0 + }) + + it('should return false if address not dirty', () => { + wrapper.vm.$v.address.$model = Identities.Address.fromPassphrase('passphrase') + wrapper.vm.$v.address.$reset() + + expect(wrapper.vm.$v.address.$dirty).toBe(false) + expect(wrapper.vm.$v.address.$invalid).toBe(false) + expect(wrapper.vm.addressWarning).toBeFalsy() + expect(wrapper.vm.address.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + }) + + it('should return false if address is invalid', async () => { + WalletService.validateAddress.mockReturnValue(false) + + wrapper.vm.$v.address.$model = 'wut' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$v.address.$dirty).toBe(true) + expect(wrapper.vm.$v.address.$invalid).toBe(true) + expect(wrapper.vm.addressWarning).toBeFalsy() + expect(wrapper.vm.address.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + + WalletService.validateAddress.mockReturnValue(true) + }) + + it('should return false if address warning', () => { + const address = Identities.Address.fromPassphrase('passphrase') + wrapper.vm.$v.address.$model = address + wrapper.vm.form.publicKeys = [{ + address: address, + publicKey: 'public-key-2' + }] + + expect(wrapper.vm.$v.address.$dirty).toBe(true) + expect(wrapper.vm.$v.address.$invalid).toBe(false) + expect(wrapper.vm.addressWarning).toBeTruthy() + expect(wrapper.vm.address.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + }) + + it('should return false if address is empty', () => { + wrapper.vm.$v.address.$model = '' + + expect(wrapper.vm.$v.address.$dirty).toBe(true) + expect(wrapper.vm.$v.address.$invalid).toBe(false) + expect(wrapper.vm.addressWarning).toBeFalsy() + expect(wrapper.vm.address.replace(/\s+/, '') === '').toBe(true) + expect(wrapper.vm.validStep1).toBe(false) + }) + }) + + describe('publicKeyTab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 1 + }) + + it('should return false if publicKey not dirty', () => { + wrapper.vm.$v.publicKey.$model = Identities.PublicKey.fromPassphrase('passphrase') + wrapper.vm.$v.publicKey.$reset() + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(false) + expect(wrapper.vm.$v.publicKey.$invalid).toBe(false) + expect(wrapper.vm.publicKeyWarning).toBeFalsy() + expect(wrapper.vm.publicKey.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + }) + + it('should return false if publicKey is invalid', async () => { + wrapper.vm.$v.publicKey.$model = 'wut' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(true) + expect(wrapper.vm.$v.publicKey.$invalid).toBe(true) + expect(wrapper.vm.publicKeyWarning).toBeFalsy() + expect(wrapper.vm.publicKey.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + }) + + it('should return false if publicKey warning', () => { + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + wrapper.vm.$v.publicKey.$model = publicKey + wrapper.vm.form.publicKeys = [{ + address: 'address-2', + publicKey: publicKey + }] + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(true) + expect(wrapper.vm.$v.publicKey.$invalid).toBe(false) + expect(wrapper.vm.publicKeyWarning).toBeTruthy() + expect(wrapper.vm.publicKey.replace(/\s+/, '') === '').toBe(false) + expect(wrapper.vm.validStep1).toBe(false) + }) + + it('should return false if publicKey is empty', () => { + wrapper.vm.$v.publicKey.$model = '' + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(true) + expect(wrapper.vm.$v.publicKey.$invalid).toBe(false) + expect(wrapper.vm.publicKeyWarning).toBeFalsy() + expect(wrapper.vm.publicKey.replace(/\s+/, '') === '').toBe(true) + expect(wrapper.vm.validStep1).toBe(false) + }) + }) + }) + + describe('isFormValid', () => { + it('should return true if valid step 1', () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + + expect(wrapper.vm.isFormValid).toBe(true) + }) + + it('should return false if valid step 1 and no keys', () => { + wrapper.vm.step = 1 + wrapper.vm.$v.form.publicKeys.$model = [] + + expect(wrapper.vm.isFormValid).toBe(false) + }) + + it('should return true if valid step 2', async () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.form.minKeys.$model = 2 + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isFormValid).toBe(true) + }) + + it('should return false if valid step 1 and no keys', () => { + wrapper.vm.step = 2 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + + expect(wrapper.vm.isFormValid).toBe(false) + }) + }) + + describe('addressWarning', () => { + it('should return null if not dirty', () => { + wrapper.vm.$v.address.$reset() + + expect(wrapper.vm.$v.address.$dirty).toBe(false) + expect(wrapper.vm.addressWarning).toBe(null) + }) + + it('should return null if not duplicate', () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.address.$model = 'address-1' + + expect(wrapper.vm.$v.address.$dirty).toBe(true) + expect(wrapper.vm.addressWarning).toBe(null) + }) + + it('should return error if duplicate', () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.address.$model = 'address-2' + + expect(wrapper.vm.$v.address.$dirty).toBe(true) + expect(wrapper.vm.addressWarning).toBe('TRANSACTION.MULTI_SIGNATURE.ERROR_DUPLICATE') + }) + }) + + describe('publicKeyWarning', () => { + it('should return null if not dirty', () => { + wrapper.vm.$v.publicKey.$reset() + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(false) + expect(wrapper.vm.publicKeyWarning).toBe(null) + }) + + it('should return null if not duplicate', () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.publicKey.$model = 'public-key-1' + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(true) + expect(wrapper.vm.publicKeyWarning).toBe(null) + }) + + it('should return error if duplicate', () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.publicKey.$model = 'public-key-2' + + expect(wrapper.vm.$v.publicKey.$dirty).toBe(true) + expect(wrapper.vm.publicKeyWarning).toBe('TRANSACTION.MULTI_SIGNATURE.ERROR_DUPLICATE') + }) + }) + + describe('maximumPublicKeys', () => { + it('should return default if no constants', () => { + expect(wrapper.vm.maximumPublicKeys).toBe(16) + }) + + it('should return default if no network value', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: {} + } + + createWrapper(null, undefined, network) + + expect(wrapper.vm.maximumPublicKeys).toBe(16) + }) + + it('should return network constant', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: { + maxMultiSignatureParticipants: 20 + } + } + + createWrapper(null, undefined, network) + + expect(wrapper.vm.maximumPublicKeys).toBe(20) + }) + }) + + describe('minKeysError', () => { + beforeEach(() => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + }) + + it('should return null if valid value', async () => { + wrapper.vm.$v.form.minKeys.$model = 1 + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.minKeysError).toBe(null) + }) + + it('should return error if empty value', async () => { + wrapper.vm.$v.form.minKeys.$model = '' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.minKeysError).toBe('VALIDATION.REQUIRED') + }) + + it('should return error if above maximum', async () => { + wrapper.vm.$v.form.minKeys.$model = 4 + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.minKeysError).toBe('TRANSACTION.MULTI_SIGNATURE.ERROR_MIN_KEYS_TOO_HIGH') + }) + + it('should return error if below minimum', async () => { + wrapper.vm.$v.form.minKeys.$model = 0 + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.minKeysError).toBe('TRANSACTION.MULTI_SIGNATURE.ERROR_MIN_KEYS_TOO_LOW') + }) + }) + }) + + describe('mounted hook', () => { + it('should add current wallet to public keys', () => { + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-1', + publicKey: 'public-key-1' + }]) + }) + + it('should update min keys', () => { + const updateMinKeysOriginal = TransactionFormMultiSignature.methods.updateMinKeys + TransactionFormMultiSignature.methods.updateMinKeys = jest.fn() + + createWrapper() + + expect(TransactionFormMultiSignature.methods.updateMinKeys).toHaveBeenCalledTimes(1) + + TransactionFormMultiSignature.methods.updateMinKeys = updateMinKeysOriginal + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.minKeys.$model = 3 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }] + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + publicKeys: [ + 'public-key-2', + 'public-key-3', + 'public-key-4' + ], + minKeys: 3, + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170 + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + wrapper.vm.$v.form.minKeys.$model = 3 + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }] + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + publicKeys: [ + 'public-key-2', + 'public-key-3', + 'public-key-4' + ], + minKeys: 3, + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170 + }) + }) + }) + + describe('buildTransaction', () => { + it('should build multi-signature', async () => { + const transactionData = { + type: 6, + typeGroup: 1, + asset: { + multiSignature: {} + } + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildMultiSignature).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build multi-signature with default arguments', async () => { + const transactionData = { + type: 6, + typeGroup: 1, + asset: { + multiSignature: {} + } + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildMultiSignature).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.MULTI_SIGNATURE') + }) + }) + + describe('addPublicKey', () => { + beforeEach(() => { + wrapper.vm.form.publicKeys = [] + }) + + describe('addressTab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 0 + }) + + it('should get wallet from store', async () => { + const spy = jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress') + + wrapper.vm.address = 'address-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('address-4') + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should check api for wallet', async () => { + jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress').mockReturnValue(null) + const spy = jest.spyOn(wrapper.vm.$client, 'fetchWallet') + + wrapper.vm.address = 'address-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('address-4') + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should error if no wallet', async () => { + jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress').mockReturnValue(null) + jest.spyOn(wrapper.vm.$client, 'fetchWallet').mockReturnValue(null) + const spy = jest.spyOn(wrapper.vm, '$error') + + wrapper.vm.address = 'address-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('TRANSACTION.MULTI_SIGNATURE.ERROR_PUBLIC_KEY_NOT_FOUND') + expect(wrapper.vm.form.publicKeys).toEqual([]) + }) + + it('should error if duplicate entry', async () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-4', + publicKey: 'public-key-4' + }] + + jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress') + const spy = jest.spyOn(wrapper.vm, '$error') + + wrapper.vm.address = 'address-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('TRANSACTION.MULTI_SIGNATURE.ERROR_PUBLIC_KEY_EXISTS') + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should update duplicate entry if only has public key', async () => { + wrapper.vm.form.publicKeys = [{ + address: null, + publicKey: 'public-key-4' + }] + + jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress') + const spy = jest.spyOn(wrapper.vm, '$error') + + wrapper.vm.address = 'address-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('TRANSACTION.MULTI_SIGNATURE.ERROR_PUBLIC_KEY_EXISTS') + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should reset field', async () => { + wrapper.vm.$v.address.$model = 'address-4' + + await wrapper.vm.addPublicKey() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.address.$v.$dirty).toBe(false) + expect(wrapper.vm.$v.address.$model).toBe('') + }) + }) + + describe('publicKeyTab', () => { + beforeEach(() => { + wrapper.vm.currentTab = 1 + }) + + it('should store public key', async () => { + wrapper.vm.publicKey = 'public-key-4' + + await wrapper.vm.addPublicKey() + + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: null, + publicKey: 'public-key-4' + }]) + }) + + it('should error if duplicate entry', async () => { + wrapper.vm.form.publicKeys = [{ + address: 'address-4', + publicKey: 'public-key-4' + }] + + jest.spyOn(wrapper.vm.$store.getters, 'wallet/byAddress') + const spy = jest.spyOn(wrapper.vm, '$error') + + wrapper.vm.publicKey = 'public-key-4' + + await wrapper.vm.addPublicKey() + + expect(spy).toHaveBeenCalledWith('TRANSACTION.MULTI_SIGNATURE.ERROR_PUBLIC_KEY_EXISTS') + expect(wrapper.vm.form.publicKeys).toEqual([{ + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should reset field', async () => { + wrapper.vm.$v.publicKey.$model = 'public-key-4' + + await wrapper.vm.addPublicKey() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.publicKey.$v.$dirty).toBe(false) + expect(wrapper.vm.$v.publicKey.$model).toBe('') + }) + }) + }) + + describe('updateMinKeys', () => { + it('should update min keys to be length of public keys', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }] + wrapper.vm.$v.form.minKeys.$model = 1 + + wrapper.vm.updateMinKeys() + + expect(wrapper.vm.$v.form.minKeys.$model).toBe(3) + }) + }) + + describe('previousStep', () => { + it('should go from step 2 to step 1', () => { + wrapper.vm.step = 2 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + + it('should do nothing on step 1', () => { + wrapper.vm.step = 1 + + wrapper.vm.previousStep() + + expect(wrapper.vm.step).toBe(1) + }) + }) + + describe('nextStep', () => { + it('should go from step 1 to step 2', () => { + wrapper.vm.step = 1 + + wrapper.vm.nextStep() + + expect(wrapper.vm.step).toBe(2) + }) + + it('should submit form data on step 2', async () => { + const spy = jest.spyOn(wrapper.vm, 'onSubmit').mockImplementation() + + wrapper.vm.step = 2 + + await wrapper.vm.$nextTick() + + wrapper.vm.nextStep() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('emitRemovePublicKey', () => { + it('should remove recipient at index', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }] + + wrapper.vm.emitRemovePublicKey(1) + + expect(wrapper.vm.$v.form.publicKeys.$model).toEqual([{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }]) + }) + + it('should do nothing if index does not exist', () => { + const publicKeys = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + + wrapper.vm.$v.form.publicKeys.$model = publicKeys + + wrapper.vm.emitRemovePublicKey(3) + + expect(wrapper.vm.$v.form.publicKeys.$model).toEqual(publicKeys) + }) + }) + }) + + describe('validations', () => { + describe('publicKey', () => { + beforeEach(() => { + wrapper.vm.currentTab = 1 + }) + + it('should not be valid', async () => { + wrapper.vm.$v.publicKey.$model = 'test' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.publicKey.$v.$invalid).toBe(true) + expect(wrapper.vm.$v.publicKey.isValid).toBe(false) + }) + + it('should not be valid if no publicKey field', async () => { + wrapper.vm.$refs.publicKey = null + wrapper.vm.$v.publicKey.$model = Identities.Address.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.publicKey).toBe(null) + expect(wrapper.vm.$v.publicKey.isValid).toBe(false) + }) + + it('should be valid', async () => { + wrapper.vm.$v.publicKey.$model = Identities.PublicKey.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.publicKey.$v.$invalid).toBe(false) + expect(wrapper.vm.$v.publicKey.isValid).toBe(true) + }) + }) + + describe('address', () => { + beforeEach(() => { + wrapper.vm.currentTab = 0 + }) + + it('should not be valid', async () => { + WalletService.validateAddress.mockReturnValue(false) + + wrapper.vm.$v.address.$model = 'test' + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.address.$v.$invalid).toBe(true) + expect(wrapper.vm.$v.address.isValid).toBe(false) + + WalletService.validateAddress.mockReturnValue(true) + }) + + it('should not be valid if no address field', async () => { + wrapper.vm.$refs.address = null + wrapper.vm.$v.address.$model = Identities.Address.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.address).toBe(null) + expect(wrapper.vm.$v.address.isValid).toBe(false) + }) + + it('should be valid', async () => { + wrapper.vm.$v.address.$model = Identities.Address.fromPassphrase('passphrase') + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.$refs.address.$v.$invalid).toBe(false) + expect(wrapper.vm.$v.address.isValid).toBe(true) + }) + }) + + describe('form', () => { + describe('publicKeys', () => { + it('should not be notEmpty', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-1', + publicKey: 'public-key-1' + }] + + expect(wrapper.vm.$v.form.publicKeys.notEmpty).toBe(true) + }) + + it('should be notEmpty', () => { + wrapper.vm.$v.form.publicKeys.$model = [] + + expect(wrapper.vm.$v.form.publicKeys.notEmpty).toBe(false) + }) + + it('should not be above minimum if not set', () => { + wrapper.vm.$v.form.publicKeys.$model = [] + + expect(wrapper.vm.$v.form.publicKeys.aboveMinimum).toBe(false) + }) + + it('should not be above minimum if not enough', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }] + + expect(wrapper.vm.$v.form.publicKeys.aboveMinimum).toBe(false) + }) + + it('should be above minimum if set', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + + expect(wrapper.vm.$v.form.publicKeys.aboveMinimum).toBe(true) + }) + + it('should not be below maximum if too many', () => { + const network = { + ...cloneDeep(globalNetwork), + constants: { + maxMultiSignatureParticipants: 2 + } + } + + createWrapper(null, undefined, network) + + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }, { + address: 'address-4', + publicKey: 'public-key-4' + }] + + expect(wrapper.vm.$v.form.publicKeys.belowMaximum).toBe(false) + }) + + it('should be below maximum if set', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }] + + expect(wrapper.vm.$v.form.publicKeys.belowMaximum).toBe(true) + }) + }) + + describe('minKeys', () => { + it('should be required if not set', () => { + wrapper.vm.$v.form.minKeys.$model = '' + + expect(wrapper.vm.$v.form.minKeys.required).toBe(false) + }) + + it('should not be required if set', () => { + wrapper.vm.$v.form.minKeys.$model = 1 + + expect(wrapper.vm.$v.form.minKeys.required).toBe(true) + }) + + it('should not be above minimum if not set', () => { + wrapper.vm.$v.form.minKeys.$model = '' + + expect(wrapper.vm.$v.form.minKeys.aboveMinimum).toBe(false) + }) + + it('should not be above minimum if not enough', () => { + wrapper.vm.$v.form.minKeys.$model = 0 + + expect(wrapper.vm.$v.form.minKeys.aboveMinimum).toBe(false) + }) + + it('should be above minimum if set', () => { + wrapper.vm.$v.form.minKeys.$model = 1 + + expect(wrapper.vm.$v.form.minKeys.aboveMinimum).toBe(true) + }) + + it('should not be below maximum if too many', () => { + wrapper.vm.$v.form.minKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.form.minKeys.$model = 3 + + expect(wrapper.vm.$v.form.minKeys.belowMaximum).toBe(false) + }) + + it('should be below maximum if set', () => { + wrapper.vm.$v.form.publicKeys.$model = [{ + address: 'address-2', + publicKey: 'public-key-2' + }, { + address: 'address-3', + publicKey: 'public-key-3' + }] + wrapper.vm.$v.form.minKeys.$model = 2 + + expect(wrapper.vm.$v.form.minKeys.belowMaximum).toBe(true) + }) + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormSecondSignature.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormSecondSignature.spec.js new file mode 100644 index 0000000000..6188c171d4 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormSecondSignature.spec.js @@ -0,0 +1,473 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormSecondSignature } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import FormatterMixin from '@/mixins/formatter' +import BigNumber from '@/plugins/bignumber' +import WalletService from '@/services/wallet' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + symbol: 'ARK', + fractionDigits: 8, + version: 23, + wif: 170, + market: { + enabled: false + }, + knownWallets: {} +} + +let wrapper +const createWrapper = (component, wallet) => { + component = component || TransactionFormSecondSignature + wallet = wallet || { + address: 'address-1', + secondPublicKey: false + } + + if (!Object.keys(wallet).includes('passphrase')) { + wallet.passphrase = null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + mocks: { + $client: { + buildSecondSignatureRegistration: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => network) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_profile: { + bip39Language: 'EN' + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + formatter_percentage: jest.fn(FormatterMixin.methods.formatter_percentage), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormSecondSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have second signature transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(1) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormSecondSignature')).toBe(true) + }) + + describe('step 1', () => { + it('should have step 1', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + expect(wrapper.find('.TransactionFormSecondSignature__step-1').props('isOpen')).toBe(true) + }) + + it('should have passphrase words', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + expect(wrapper.contains('.TransactionFormSecondSignature__passphrase-words')).toBe(true) + }) + + it('should have next button', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + expect(wrapper.contains('.TransactionFormSecondSignature__step-1__next')).toBe(true) + }) + }) + + describe('passphrase step', () => { + beforeEach(() => { + wrapper.vm.isPassphraseStep = true + }) + + it('should hide passphrase words when on passphrase step', () => { + expect(wrapper.find('.TransactionFormSecondSignature__step-1').props('isOpen')).toBe(false) + }) + + it('should have passphrase verification field', () => { + expect(wrapper.contains('.TransactionFormSecondSignature__passphrase-verification')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormSecondSignature__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormSecondSignature__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormSecondSignature__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormSecondSignature__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormSecondSignature__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormSecondSignature__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormSecondSignature__passphrase')).toBe(false) + }) + }) + + it('should have back button', () => { + expect(wrapper.contains('.TransactionFormSecondSignature__back')).toBe(true) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.isPassphraseVerified = true + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormSecondSignature__step-2__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormSecondSignature__step-2__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + }) + + describe('computed', () => { + describe('wordPositions', () => { + it('should return required word permissions', () => { + expect(wrapper.vm.wordPositions).toEqual([3, 6, 9]) + }) + }) + + describe('passphraseWords', () => { + it('should split passphrase by space', () => { + wrapper.vm.secondPassphrase = 'this is a passphrase' + + expect(wrapper.vm.passphraseWords).toEqual([ + 'this', + 'is', + 'a', + 'passphrase' + ]) + }) + + it('should split japanese passphrase by japanese space', () => { + wrapper.vm.secondPassphrase = 'this\u3000is\u3000a\u3000passphrase' + + expect(wrapper.vm.passphraseWords).toEqual([ + 'this', + 'is', + 'a', + 'passphrase' + ]) + }) + }) + }) + + describe('watch', () => { + describe('isPassphraseStep', () => { + it('should focus on passphrase verification if passphrase step', async () => { + const spy = jest.spyOn(wrapper.vm.$refs.passphraseVerification, 'focusFirst') + + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalled() + }) + + it('should not focus on passphrase verification if not passphrase step', async () => { + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + const spy = jest.spyOn(wrapper.vm.$refs.passphraseVerification, 'focusFirst') + wrapper.vm.isPassphraseStep = false + + await wrapper.vm.$nextTick() + + expect(spy).not.toHaveBeenCalled() + }) + }) + }) + + describe('created hook', () => { + it('should generate second passphrase', () => { + WalletService.generateSecondPassphrase.mockClear() + + createWrapper() + + expect(WalletService.generateSecondPassphrase).toHaveBeenNthCalledWith(1, 'EN') + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.secondPassphrase = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build second signature', async () => { + const transactionData = { + type: 5, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildSecondSignatureRegistration).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build second signature with default arguments', async () => { + const transactionData = { + type: 7, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildSecondSignatureRegistration).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.SECOND_SIGNATURE') + }) + }) + + describe('postSubmit', () => { + it('should call reset method', () => { + const spy = jest.spyOn(wrapper.vm, 'reset') + + wrapper.vm.postSubmit() + + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should set password verified to true', () => { + wrapper.vm.isPassphraseVerified = false + + wrapper.vm.postSubmit() + + expect(wrapper.vm.isPassphraseVerified).toBe(true) + }) + }) + + describe('toggleStep', () => { + it('should toggle passphrase step', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + + wrapper.vm.toggleStep() + + expect(wrapper.vm.isPassphraseStep).toBe(true) + + wrapper.vm.toggleStep() + + expect(wrapper.vm.isPassphraseStep).toBe(false) + }) + }) + + describe('displayPassphraseWords', () => { + it('should toggle generating and show passphrase', (done) => { + wrapper.vm.displayPassphraseWords() + + expect(wrapper.vm.isGenerating).toBe(true) + + setTimeout(() => { + expect(wrapper.vm.isGenerating).toBe(false) + expect(wrapper.vm.showPassphraseWords).toBe(true) + + done() + }, 301) + }) + }) + + describe('generateNewPassphrase', () => { + it('should call reset method', () => { + const spy = jest.spyOn(wrapper.vm, 'reset') + + wrapper.vm.generateNewPassphrase() + + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should toggle generating and show passphrase', (done) => { + WalletService.generateSecondPassphrase.mockClear() + + wrapper.vm.generateNewPassphrase() + + expect(wrapper.vm.isGenerating).toBe(true) + + setTimeout(() => { + expect(wrapper.vm.isGenerating).toBe(false) + expect(WalletService.generateSecondPassphrase).toHaveBeenNthCalledWith(1, 'EN') + + done() + }, 301) + }) + }) + + describe('onVerification', () => { + it('should set password verified', () => { + wrapper.vm.isPassphraseVerified = false + + wrapper.vm.onVerification() + + expect(wrapper.vm.isPassphraseVerified).toBe(true) + }) + }) + + describe('reset', () => { + it('should reset to delegate detail view', () => { + wrapper.vm.isPassphraseStep = true + + expect(wrapper.vm.isPassphraseStep).toBe(true) + + wrapper.vm.reset() + + expect(wrapper.vm.isPassphraseStep).toBe(false) + }) + + it('should reset passphrase field if not encrypted or ledger', () => { + const spy = jest.spyOn(wrapper.vm, '$set') + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.$v.form.passphrase.$dirty).toBe(true) + expect(wrapper.vm.form.passphrase).toBe('passphrase') + + wrapper.vm.reset() + + expect(wrapper.vm.$v.form.passphrase.$dirty).toBe(false) + expect(wrapper.vm.form.passphrase).toBe('') + expect(spy).toHaveBeenCalledWith(wrapper.vm.form, 'passphrase', '') + }) + + it('should reset password field if encrypted and not ledger', () => { + createWrapper(null, { + passphrase: 'password' + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + wrapper.vm.$v.form.walletPassword.$model = 'password' + + expect(wrapper.vm.$v.form.walletPassword.$dirty).toBe(true) + expect(wrapper.vm.form.walletPassword).toBe('password') + + wrapper.vm.reset() + + expect(wrapper.vm.$v.form.walletPassword.$dirty).toBe(false) + expect(wrapper.vm.form.walletPassword).toBe('') + expect(spy).toHaveBeenCalledWith(wrapper.vm.form, 'walletPassword', '') + }) + + it('should do nothing if ledger', () => { + createWrapper(null, { + isLedger: true + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.reset() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should do nothing if multi-signature wallet', () => { + createWrapper(null, { + multiSignature: true + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.reset() + + expect(spy).not.toHaveBeenCalled() + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormTransfer.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormTransfer.spec.js index 3071218ac8..0003cdb9e2 100644 --- a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormTransfer.spec.js +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormTransfer.spec.js @@ -1,107 +1,1222 @@ -import merge from 'lodash/merge' -import Vue from 'vue' +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' import Vuelidate from 'vuelidate' -import { shallowMount } from '@vue/test-utils' -import useI18nGlobally from '../../../__utils__/i18n' -import TransactionFormTransfer from '@/components/Transaction/TransactionForm/TransactionFormTransfer' +import cloneDeep from 'lodash/cloneDeep' +import installI18n from '../../../__utils__/i18n' +import { VENDOR_FIELD } from '@config' +import { TransactionFormTransfer } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' import BigNumber from '@/plugins/bignumber' import WalletService from '@/services/wallet' -const i18n = useI18nGlobally() +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) -Vue.use(Vuelidate) +const globalNetwork = Object.freeze({ + id: 'network-1', + fractionDigits: 8, + token: 'ARK', + version: 23, + wif: 170, + market: { + enabled: false + }, + nethash: 'nethash-1' +}) + +let wrapper +const createWrapper = (component, wallet, network, props = {}) => { + component = component || TransactionFormTransfer + + if (wallet === undefined) { + wallet = { + id: 'test-wallet' + } + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'passphrase')) { + wallet.passphrase = null + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'address')) { + wallet.address = 'address-1' + } + + if (wallet && !Object.prototype.hasOwnProperty.call(wallet, 'balance')) { + wallet.balance = (1000 * 1e8).toString() + } + + const mountNetwork = network || cloneDeep(globalNetwork) + + wrapper = mount(TransactionFormTransfer, { + i18n, + localVue, + sync: false, + propsData: props, + mocks: { + $client: { + buildTransfer: jest.fn((transactionData) => transactionData) + }, + $error: jest.fn(), + $success: jest.fn(), + $store: { + getters: { + 'profile/byId': jest.fn(() => ({ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + })), + 'session/currency': 'EUR', + get 'ledger/isConnected' () { + return true + }, + get 'ledger/wallets' () { + return [{ + address: 'ledger-address-1' + }] + }, + 'wallet/byProfileId': jest.fn(() => [wallet]), + 'wallet/contactsByProfileId': jest.fn(() => []), + 'transaction/staticFee': jest.fn(() => null), + get 'session/profileId' () { + return 'profile-1' + }, + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'network/byToken': jest.fn(() => mountNetwork), + 'network/byId': jest.fn(() => mountNetwork), + 'profile/byCompatibleAddress': jest.fn(() => []), + get 'profile/all' () { + return [{ + id: 'profile-1', + name: 'profile-1', + networkId: 'network-1' + }, { + id: 'profile-2', + name: 'profile-2', + networkId: 'network-1' + }] + } + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: mountNetwork, + currency_simpleFormatCrypto: jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + formatter_networkCurrency: jest.fn(), + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_truncate: jest.fn(address => address), + wallet_name: jest.fn(address => address), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} describe('TransactionFormTransfer', () => { - const mountComponent = config => { - return shallowMount(TransactionFormTransfer, merge({ - i18n, - mocks: { - $error: jest.fn(), - $store: { - getters: { - 'profile/byId': () => {}, - 'session/currency': 'EUR' + beforeEach(() => { + createWrapper() + }) + + it('should not have magistrate transaction group', () => { + expect(wrapper.vm.$options.transactionGroup).not.toBe(2) + }) + + it('should have transfer transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(0) + }) + + describe('props', () => { + it('should allow schema', () => { + wrapper.setProps({ + schema: { + test: true + } + }) + + expect(wrapper.vm.schema).toEqual({ test: true }) + }) + + it('should default schema', () => { + expect(wrapper.vm.schema).toEqual(undefined) + }) + }) + + describe('data', () => { + it('should has properties', () => { + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'form')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'amount')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'fee')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'passphrase')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'walletPassword')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'recipientId')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data.form, 'vendorField')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'isSendAllActive')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'previousAmount')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'wallet')).toBe(true) + expect(Object.prototype.hasOwnProperty.call(wrapper.vm._data, 'showConfirmSendAll')).toBe(true) + }) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormTransfer')).toBe(true) + }) + + it('should have recipient field', () => { + expect(wrapper.contains('.TransactionFormTransfer__recipient')).toBe(true) + }) + + it('should have amount field', () => { + expect(wrapper.contains('.TransactionFormTransfer__amount')).toBe(true) + }) + + it('should have send all switch', () => { + expect(wrapper.contains('.TransactionFormTransfer__send-all')).toBe(true) + }) + + it('should have vendorfield', () => { + expect(wrapper.contains('.TransactionFormTransfer__vendorfield')).toBe(true) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormTransfer__fee')).toBe(true) + }) + + describe('wallet selection', () => { + it('should show if schema prop is provided with address', async () => { + wrapper.setProps({ + schema: { + address: 'address-1' } - }, - $success: jest.fn(), - currency_simpleFormatCrypto: amount => amount, - currency_subToUnit: amount => new BigNumber(amount), - formatter_networkCurrency: jest.fn(), - session_network: { - token: 'NET', - version: 11 - }, - wallet_formatAddress: address => address, - wallet_truncate: address => address - } - }, config)) - } + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.contains('.TransactionFormTransfer__wallet')).toBe(true) + }) + + it('should not show if no schema prop is provided', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormTransfer__wallet')).toBe(false) + }) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormTransfer__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormTransfer__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormTransfer__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormTransfer__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormTransfer__passphrase')).toBe(true) + }) - it('should be instantiated', () => { - const wrapper = mountComponent() - expect(wrapper.isVueInstance()).toBeTrue() + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormTransfer__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.amount.$model = 1 + wrapper.vm.$v.form.recipientId.$model = 'address-2' + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormTransfer__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormTransfer__next').attributes('disabled')).toBe('disabled') + }) + }) }) - describe('loadTransaction', () => { - describe('when a valid JSON file is opened', () => { - it('should display an error alert if the transaction has the wrong type', async () => { - const wrapper = mountComponent({ - mocks: { - electron_readFile: jest.fn(async () => { - return '{ "type": "1" }' - }) + describe('computed', () => { + describe('alternativeCurrency', () => { + it('should return correct data', () => { + expect(wrapper.vm.alternativeCurrency).toEqual('EUR') + }) + + it('should update data', () => { + wrapper.vm.$store.getters['session/currency'] = 'GBP' + + expect(wrapper.vm.alternativeCurrency).toEqual('GBP') + }) + }) + + describe('amountTooLowError', () => { + it('should return formatted value', () => { + const $tSpy = jest.fn(translation => translation) + const simpleFormatCryptoSpy = jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto) + const response = TransactionFormTransfer.computed.amountTooLowError.call({ + walletNetwork: globalNetwork, + $t: $tSpy, + currency_simpleFormatCrypto: simpleFormatCryptoSpy, + session_network: globalNetwork + }) + + expect($tSpy).toHaveBeenCalledWith('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { + amount: '0.00000001 ARK' + }) + expect(response).toBe('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM') + expect(simpleFormatCryptoSpy).toHaveBeenCalledWith('0.00000001') + }) + + it('should return formatted value with different fraction digits', () => { + const $tSpy = jest.fn(translation => translation) + const simpleFormatCryptoSpy = jest.fn(CurrencyMixin.methods.currency_simpleFormatCrypto) + const response = TransactionFormTransfer.computed.amountTooLowError.call({ + walletNetwork: { + ...globalNetwork, + fractionDigits: 2 + }, + $t: $tSpy, + currency_simpleFormatCrypto: simpleFormatCryptoSpy, + session_network: globalNetwork + }) + + expect($tSpy).toHaveBeenCalledWith('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM', { + amount: '0.01 ARK' + }) + expect(response).toBe('INPUT_CURRENCY.ERROR.LESS_THAN_MINIMUM') + expect(simpleFormatCryptoSpy).toHaveBeenCalledWith('0.01') + }) + }) + + describe('notEnoughBalanceError', () => { + it('should return empty if no wallet', () => { + createWrapper(null, null) + + expect(wrapper.vm.notEnoughBalanceError).toBe('') + }) + + it('should return a formatted value', () => { + const $tSpy = jest.fn(translation => translation) + const formatterNetworkCurrencySpy = jest.fn(value => value) + const response = TransactionFormTransfer.computed.notEnoughBalanceError.call({ + currentWallet: { + balance: (10 * 1e8).toString() + }, + $t: $tSpy, + formatter_networkCurrency: formatterNetworkCurrencySpy, + session_network: globalNetwork + }) + + expect(response).toBe('TRANSACTION_FORM.ERROR.NOT_ENOUGH_BALANCE') + expect($tSpy).toHaveBeenCalledWith('TRANSACTION_FORM.ERROR.NOT_ENOUGH_BALANCE', { + balance: '1000000000' + }) + expect(formatterNetworkCurrencySpy).toHaveBeenCalledWith('1000000000') + }) + }) + + describe('minimumAmount', () => { + it('should return the correct value', () => { + expect(wrapper.vm.minimumAmount + '').toBe('0.00000001') + }) + }) + + describe('maximumAvailableAmount', () => { + it('should return zero if no wallet', () => { + createWrapper(null, null) + + expect(wrapper.vm.maximumAvailableAmount).toEqual(new BigNumber('0')) + }) + + it('should return value', () => { + expect(wrapper.vm.maximumAvailableAmount).toEqual((new BigNumber(1000)).minus(0.1)) + }) + + it('should return value based on different fee', () => { + wrapper.vm.form.fee = 10 + + expect(wrapper.vm.maximumAvailableAmount).toEqual((new BigNumber(1000)).minus(10)) + }) + }) + + describe('canSendAll', () => { + it('should return true if amount is greater than 0', () => { + expect(wrapper.vm.currentWallet.balance).toBe((1000 * 1e8).toString()) + expect(wrapper.vm.form.fee).toBe('0.1') + expect(wrapper.vm.canSendAll).toBe(true) + }) + + it('should return false if maximumAvailableAmount is 0', () => { + createWrapper(null, { + balance: (0.1 * 1e8).toString() + }) + + expect(wrapper.vm.currentWallet.balance).toBe((0.1 * 1e8).toString()) + expect(wrapper.vm.form.fee).toBe('0.1') + expect(wrapper.vm.canSendAll).toBe(false) + }) + }) + + describe('senderLabel', () => { + it('should return formatted address if currentWallet', () => { + expect(wrapper.vm.senderLabel).toEqual('formatted-address-1') + }) + + it('should return null if no current wallet', async () => { + createWrapper(null, null) + + expect(wrapper.vm.senderLabel).toEqual(null) + }) + }) + + describe('senderWallet', () => { + it('should return wallet if set', () => { + wrapper.vm.wallet = { + address: 'address-1' + } + + expect(wrapper.vm.senderWallet).toEqual({ + address: 'address-1' + }) + }) + }) + + describe('walletNetwork', () => { + it('should return current network if no wallet selected', () => { + const profileByIdSpy = jest.fn() + const networkByIdSpy = jest.fn() + const response = TransactionFormTransfer.computed.walletNetwork.call({ + $store: { + getters: { + 'network/byId': networkByIdSpy, + 'profile/byId': profileByIdSpy + } + }, + session_network: globalNetwork, + currentWallet: null + }) + + expect(response).toBe(globalNetwork) + expect(profileByIdSpy).not.toHaveBeenCalled() + expect(networkByIdSpy).not.toHaveBeenCalled() + }) + + it('should return current network if wallet does not have id', () => { + const profileByIdSpy = jest.fn() + const networkByIdSpy = jest.fn() + const response = TransactionFormTransfer.computed.walletNetwork.call({ + $store: { + getters: { + 'network/byId': networkByIdSpy, + 'profile/byId': profileByIdSpy + } + }, + session_network: globalNetwork, + currentWallet: {} + }) + + expect(response).toBe(globalNetwork) + expect(profileByIdSpy).not.toHaveBeenCalled() + expect(networkByIdSpy).not.toHaveBeenCalled() + }) + + it('should return current network if no profile selected', () => { + const profileByIdSpy = jest.fn() + const networkByIdSpy = jest.fn() + const response = TransactionFormTransfer.computed.walletNetwork.call({ + $store: { + getters: { + 'network/byId': networkByIdSpy, + 'profile/byId': profileByIdSpy + } + }, + session_network: globalNetwork, + currentWallet: { + id: 'test', + profileId: 'profile-id' + } + }) + + expect(response).toBe(globalNetwork) + expect(profileByIdSpy).toHaveBeenCalledWith('profile-id') + expect(networkByIdSpy).not.toHaveBeenCalled() + }) + + it('should return current network if no network for profile selected', () => { + const profileByIdSpy = jest.fn(() => ({ + id: 'profile-id', + networkId: 'network-id' + })) + const networkByIdSpy = jest.fn() + const response = TransactionFormTransfer.computed.walletNetwork.call({ + $store: { + getters: { + 'network/byId': networkByIdSpy, + 'profile/byId': profileByIdSpy + } + }, + session_network: globalNetwork, + currentWallet: { + id: 'test', + profileId: 'profile-id' } }) - await wrapper.vm.loadTransaction() + expect(response).toBe(globalNetwork) + expect(profileByIdSpy).toHaveBeenCalledWith('profile-id') + expect(networkByIdSpy).toHaveBeenCalledWith('network-id') + }) + + it('should return profile network if no network for profile selected', () => { + const profileNetwork = { + fractionDigits: 2, + token: 'DARK', + version: 30, + wif: 170, + market: { + enabled: false + } + } + const profileByIdSpy = jest.fn(() => ({ + id: 'profile-id', + networkId: 'network-id' + })) + const networkByIdSpy = jest.fn(() => profileNetwork) + const response = TransactionFormTransfer.computed.walletNetwork.call({ + $store: { + getters: { + 'network/byId': networkByIdSpy, + 'profile/byId': profileByIdSpy + } + }, + session_network: globalNetwork, + currentWallet: { + id: 'test', + profileId: 'profile-id' + } + }) + + expect(response).toBe(profileNetwork) + expect(profileByIdSpy).toHaveBeenCalledWith('profile-id') + expect(networkByIdSpy).toHaveBeenCalledWith('network-id') + }) + }) + + describe('currentWallet', () => { + it('should get sender wallet', () => { + wrapper.vm.wallet = { + id: 'test', + balance: 0, + address: 'address-3' + } + + expect(wrapper.vm.currentWallet).toBe(wrapper.vm.wallet) + }) + + it('should get wallet from route', () => { + const newWallet = { + id: 'test', + balance: 20, + address: 'address-2' + } + createWrapper(null, newWallet) - expect(wrapper.vm.$error).toHaveBeenCalled() + wrapper.vm.wallet = null + + expect(wrapper.vm.senderWallet).toBe(null) + expect(wrapper.vm.currentWallet).toBe(newWallet) + }) + + it('should set wallet', () => { + const newWallet = { + id: 'test', + balance: 20, + address: 'address-2' + } + + wrapper.vm.wallet = null + wrapper.vm.currentWallet = newWallet + + expect(wrapper.vm.wallet).toBe(newWallet) + }) + }) + + describe('vendorFieldLabel', () => { + it('should return value', () => { + expect(wrapper.vm.vendorFieldLabel).toBe('TRANSACTION.VENDOR_FIELD - VALIDATION.MAX_LENGTH') + }) + }) + + describe('vendorFieldHelperText', () => { + describe('default max length', () => { + let $tSpy + beforeEach(() => { + $tSpy = jest.spyOn(wrapper.vm, '$t') + }) + + afterEach(() => { + $tSpy.mockRestore() + }) + + it('should return null if vendorfield is empty', () => { + wrapper.vm.form.vendorField = '' + + expect(wrapper.vm.vendorFieldHelperText).toBe(null) + expect($tSpy).not.toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [VENDOR_FIELD.defaultMaxLength]) + }) + + it('should return length warning if equal to max', () => { + wrapper.vm.form.vendorField = ''.padStart(VENDOR_FIELD.defaultMaxLength, '-') + + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REACHED') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [VENDOR_FIELD.defaultMaxLength]) + }) + + it('should return length warning if less than max', () => { + wrapper.vm.form.vendorField = ''.padStart(VENDOR_FIELD.defaultMaxLength - 10, '-') + + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING', [ + 10, + VENDOR_FIELD.defaultMaxLength + ]) + }) }) - it('should display an error alert if the recipient is on a different network', async () => { - WalletService.validateAddress = jest.fn(() => false) + describe('network max length', () => { + let $tSpy - const wrapper = mountComponent({ - mocks: { - electron_readFile: jest.fn(async () => { - return '{ "recipientId": "AJAAfMJj1w6U5A3t6BGA7NYZsaVve6isMm" }' - }) + beforeEach(() => { + const network = { + ...cloneDeep(globalNetwork), + vendorField: { + maxLength: 20 + } } + createWrapper(null, undefined, network) + $tSpy = jest.spyOn(wrapper.vm, '$t') }) - await wrapper.vm.loadTransaction() + afterEach(() => { + $tSpy.mockRestore() + }) + + it('should return null if vendorfield is empty', () => { + wrapper.vm.form.vendorField = '' + + expect(wrapper.vm.vendorFieldHelperText).toBe(null) + expect($tSpy).not.toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [20]) + }) + + it('should return length warning if equal to max', () => { + wrapper.vm.form.vendorField = ''.padStart(20, '-') + + expect(wrapper.vm.walletNetwork.vendorField.maxLength).toBe(20) + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REACHED') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REACHED', [20]) + }) + + it('should return length warning if less than max', () => { + wrapper.vm.form.vendorField = ''.padStart(20 - 5, '-') - expect(wrapper.vm.$error).toHaveBeenCalled() + expect(wrapper.vm.vendorFieldHelperText).toBe('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING') + expect($tSpy).toHaveBeenCalledWith('VALIDATION.VENDOR_FIELD.LIMIT_REMAINING', [ + 5, + 20 + ]) + }) }) + }) - it('should display a success alert', async () => { - const wrapper = mountComponent({ - mocks: { - electron_readFile: jest.fn(async () => { - return '{ "type": "0" }' - }) + describe('vendorFieldMaxLength', () => { + it('should return network max length', () => { + createWrapper(null, undefined, { + ...cloneDeep(globalNetwork), + vendorField: { + maxLength: 20 } }) - await wrapper.vm.loadTransaction() + expect(wrapper.vm.vendorFieldMaxLength).toBe(20) + }) + + it('should return default max length if network does not have vendorField max', () => { + expect(wrapper.vm.vendorFieldMaxLength).toBe(VENDOR_FIELD.defaultMaxLength) + }) + }) + }) + + describe('watch', () => { + it('should ensure available amount', async () => { + const spy = jest.spyOn(wrapper.vm, 'ensureAvailableAmount') + + wrapper.vm.wallet = { + balance: 0, + address: 'address-4', + passphrase: null + } + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should check trigger recipient validation', async () => { + const spy = jest.spyOn(wrapper.vm.$v.form.recipientId, '$touch', 'get') + + wrapper.vm.wallet = { + balance: 0, + address: 'address-4', + passphrase: null + } + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('mounted hook', () => { + it('should set wallet object', () => { + expect(wrapper.vm.currentWallet).toBe(wrapper.vm.currentWallet) + expect(wrapper.vm.wallet).toBe(wrapper.vm.currentWallet) + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.amount.$model = 1 + wrapper.vm.$v.form.recipientId.$model = 'address-2' + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + recipientId: 'address-2', + fee: new BigNumber(0.1 * 1e8), + amount: new BigNumber(1 * 1e8), + vendorField: 'vendorfield test', + wif: undefined, + networkWif: 170, + networkId: 'network-1', + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.amount.$model = 1 + wrapper.vm.$v.form.recipientId.$model = 'address-2' + wrapper.vm.$v.form.vendorField.$model = 'vendorfield test' + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + recipientId: 'address-2', + secondPassphrase: 'second passphrase', + fee: new BigNumber(0.1 * 1e8), + amount: new BigNumber(1 * 1e8), + vendorField: 'vendorfield test', + wif: undefined, + networkWif: 170, + networkId: 'network-1', + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build transfer', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildTransfer).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build transfer with default arguments', async () => { + const transactionData = { + type: 0, + typeGroup: 2 + } - expect(wrapper.vm.$success).toHaveBeenCalled() + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildTransfer).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) }) }) - describe('when an invalid JSON file is opened', () => { - it('should display an error alert', async () => { - const wrapper = mountComponent({ - mocks: { - electron_readFile: jest.fn(async () => { - return 'invalid json' - }) + describe('populateSchema', () => { + it('should do nothing if no schema data', () => { + const spy = jest.spyOn(wrapper.vm, '$set') + + wrapper.setProps({ + schema: null + }) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should load in schema form data', () => { + wrapper.setProps({ + schema: { + amount: (10 * 1e8).toString(), + address: 'address-5', + vendorField: 'test vendorfield' + } + }) + + wrapper.vm.populateSchema() + + expect(wrapper.vm.form.amount).toBe((10 * 1e8).toString()) + expect(wrapper.vm.form.recipientId).toBe('address-5') + expect(wrapper.vm.form.vendorField).toBe('test vendorfield') + }) + + it('should load in schema wallet data', () => { + const sessionProfileIdSpy = jest.spyOn(wrapper.vm.$store.getters, 'session/profileId', 'get') + const ledgerConnectedSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/isConnected', 'get') + const ledgerWalletsSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/wallets', 'get') + const profileAllSpy = jest.spyOn(wrapper.vm.$store.getters, 'profile/all', 'get') + + wrapper.setProps({ + schema: { + wallet: 'address-1' + } + }) + + wrapper.vm.$store.getters['profile/byId'].mockClear() + wrapper.vm.$store.getters['network/byId'].mockClear() + + wrapper.vm.populateSchema() + + expect(sessionProfileIdSpy).toHaveBeenCalled() + expect(ledgerConnectedSpy).toHaveBeenCalled() + expect(ledgerWalletsSpy).toHaveBeenCalled() + expect(profileAllSpy).toHaveBeenCalled() + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-1') + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-2') + expect(wrapper.vm.$store.getters['profile/byId']).not.toHaveBeenCalled() + expect(wrapper.vm.$store.getters['network/byId']).not.toHaveBeenCalled() + expect(wrapper.vm.currentWallet).toBe(wrapper.vm.wallet_fromRoute) + }) + + it('should load data for network with nethash', () => { + const sessionProfileIdSpy = jest.spyOn(wrapper.vm.$store.getters, 'session/profileId', 'get') + const ledgerConnectedSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/isConnected', 'get') + const ledgerWalletsSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/wallets', 'get') + const profileAllSpy = jest.spyOn(wrapper.vm.$store.getters, 'profile/all', 'get') + + wrapper.setProps({ + schema: { + wallet: 'address-1', + nethash: 'nethash-1' } }) - await wrapper.vm.loadTransaction() + wrapper.vm.$store.getters['profile/byId'].mockClear() + wrapper.vm.$store.getters['network/byId'].mockClear() + + wrapper.vm.populateSchema() + + expect(sessionProfileIdSpy).toHaveBeenCalled() + expect(ledgerConnectedSpy).toHaveBeenCalled() + expect(ledgerWalletsSpy).toHaveBeenCalled() + expect(profileAllSpy).toHaveBeenCalled() + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-1') + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-2') + expect(wrapper.vm.$store.getters['profile/byId']).toHaveBeenCalledWith('profile-1') + expect(wrapper.vm.$store.getters['network/byId']).toHaveBeenCalledWith('network-1') + expect(wrapper.vm.currentWallet).toBe(wrapper.vm.wallet_fromRoute) + }) + + it('should check other profiles if no current profile', () => { + const sessionProfileIdSpy = jest.spyOn(wrapper.vm.$store.getters, 'session/profileId', 'get').mockReturnValue(null) + const ledgerConnectedSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/isConnected', 'get') + const ledgerWalletsSpy = jest.spyOn(wrapper.vm.$store.getters, 'ledger/wallets', 'get') + const profileAllSpy = jest.spyOn(wrapper.vm.$store.getters, 'profile/all', 'get') + + wrapper.setProps({ + schema: { + wallet: 'address-1', + nethash: 'nethash-1' + } + }) + + wrapper.vm.$store.getters['profile/byId'].mockClear() + wrapper.vm.$store.getters['network/byId'].mockClear() + + wrapper.vm.populateSchema() + + expect(sessionProfileIdSpy).toHaveBeenCalled() + expect(ledgerConnectedSpy).toHaveBeenCalled() + expect(ledgerWalletsSpy).toHaveBeenCalled() + expect(profileAllSpy).toHaveBeenCalled() + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-1') + expect(wrapper.vm.$store.getters['wallet/byProfileId']).toHaveBeenCalledWith('profile-2') + expect(wrapper.vm.$store.getters['profile/byId']).not.toHaveBeenCalled() + expect(wrapper.vm.$store.getters['network/byId']).toHaveBeenCalledWith('network-1') + expect(wrapper.vm.currentWallet).toBe(wrapper.vm.wallet_fromRoute) + }) + + it('should error when no network', () => { + const $tSpy = jest.spyOn(wrapper.vm, '$t') + + wrapper.setProps({ + schema: { + wallet: 'address-1', + nethash: 'wrong nethash' + } + }) + + wrapper.vm.populateSchema() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.NETWORK_NOT_CONFIGURED') + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.NETWORK_NOT_CONFIGURED: wrong nethash') + + $tSpy.mockRestore() + }) + + it('should error when no wallets', () => { + const $tSpy = jest.spyOn(wrapper.vm, '$t') + + wrapper.setProps({ + schema: { + wallet: 'wrong address' + } + }) + + wrapper.vm.populateSchema() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.WALLET_NOT_IMPORTED') + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.WALLET_NOT_IMPORTED: wrong address') + + $tSpy.mockRestore() + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.TRANSFER') + }) + }) + + describe('emitNext', () => { + it('should emit', () => { + wrapper.vm.emitNext({ + recipientId: 'address-2' + }) + + expect(wrapper.emitted('next')).toEqual([ + [{ + transaction: { + recipientId: 'address-2' + }, + wallet: wrapper.vm.senderWallet + }] + ]) + }) + + it('should emit with current wallet', () => { + wrapper.vm.wallet = { + address: 'address-1' + } + + wrapper.vm.emitNext({ + recipientId: 'address-2' + }) + + expect(wrapper.emitted('next')).toEqual([ + [{ + transaction: { + recipientId: 'address-2' + }, + wallet: { + address: 'address-1' + } + }] + ]) + }) + }) + + describe('onFee', () => { + it('should set fee in form', () => { + wrapper.vm.onFee(20) + + expect(wrapper.vm.form.fee).toEqual(20) + }) + + it('should ensure amount is available', () => { + const spy = jest.spyOn(wrapper.vm, 'ensureAvailableAmount').mockImplementation() + + wrapper.vm.onFee(20) + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('setSendAll', () => { + it('should trigger send all', () => { + const spy = jest.spyOn(wrapper.vm, 'confirmSendAll').mockImplementation() + + wrapper.vm.form.amount = 50 + wrapper.vm.setSendAll(true) + + expect(spy).toHaveBeenCalledTimes(1) + expect(wrapper.vm.previousAmount).toEqual(50) + }) + + it('should trigger when disabled', () => { + const spy = jest.spyOn(wrapper.vm, 'ensureAvailableAmount').mockImplementation() + const spySet = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.form.amount = 10 + wrapper.vm.previousAmount = 50 + wrapper.vm.setSendAll(false) + + expect(spy).toHaveBeenCalledTimes(1) + expect(spySet).toHaveBeenNthCalledWith(1, wrapper.vm.form, 'amount', 50) + expect(wrapper.vm.form.amount).toEqual(50) + expect(wrapper.vm.previousAmount).toEqual('') + expect(wrapper.vm.isSendAllActive).toEqual(false) + }) + + it('should not update amount when disabled', () => { + const spy = jest.spyOn(wrapper.vm, 'ensureAvailableAmount').mockImplementation() + const spySet = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.form.amount = 10 + wrapper.vm.previousAmount = 50 + wrapper.vm.setSendAll(false, false) + + expect(spy).toHaveBeenCalled() + expect(spySet).not.toHaveBeenCalled() + expect(wrapper.vm.form.amount).toEqual(10) + expect(wrapper.vm.previousAmount).toEqual('') + expect(wrapper.vm.isSendAllActive).toEqual(false) + }) + }) + + describe('ensureAvailableAmount', () => { + it('should set amount to max if send all is enabled', async () => { + const spySet = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.form.amount = 0 + wrapper.vm.isSendAllActive = true + + await wrapper.vm.$nextTick() + + wrapper.vm.ensureAvailableAmount() + + expect(wrapper.vm.isSendAllActive).toBe(true) + expect(wrapper.vm.canSendAll).toBe(true) + expect(spySet).toHaveBeenNthCalledWith(1, wrapper.vm.form, 'amount', new BigNumber('999.9')) + expect(wrapper.vm.form.amount).toEqual(new BigNumber('999.9')) + }) + + it('should not set amount to max if send all is disabled', async () => { + const spySet = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.form.amount = 10 + + await wrapper.vm.$nextTick() + + wrapper.vm.ensureAvailableAmount() + + expect(spySet).not.toHaveBeenCalled() + expect(wrapper.vm.form.amount).toEqual(10) + }) + }) + + describe('enableSendAll', () => { + it('should force send all (for when modal is confirmed)', () => { + const spy = jest.spyOn(wrapper.vm, 'ensureAvailableAmount') + + wrapper.vm.enableSendAll() + + expect(spy).toHaveBeenCalledTimes(1) + expect(wrapper.vm.isSendAllActive).toBe(true) + expect(wrapper.vm.showConfirmSendAll).toBe(false) + }) + }) + + describe('confirmSendAll', () => { + it('should set to true (to show modal)', () => { + wrapper.vm.confirmSendAll() + + expect(wrapper.vm.showConfirmSendAll).toBe(true) + }) + }) + + describe('cancelSendAll', () => { + it('should set to false (to hide modal)', () => { + wrapper.vm.cancelSendAll() + + expect(wrapper.vm.isSendAllActive).toBe(false) + expect(wrapper.vm.showConfirmSendAll).toBe(false) + }) + }) + + describe('loadTransaction', () => { + let $tSpy + beforeEach(() => { + $tSpy = jest.spyOn(wrapper.vm, '$t') + }) + + afterEach(() => { + $tSpy.mockRestore() + }) + + describe('when a valid JSON file is opened', () => { + it('should display an error alert if the transaction has the wrong type', async () => { + wrapper.vm.electron_readFile = jest.fn(async () => { + return '{ "type": "1" }' + }) + + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('VALIDATION.INVALID_TYPE') + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE') + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE: VALIDATION.INVALID_TYPE') + }) + + it('should display an error alert if the recipient is on a different network', async () => { + WalletService.validateAddress = jest.fn(() => false) + + wrapper.vm.electron_readFile = jest.fn(async () => { + return '{ "type": "0", "recipientId": "AJAAfMJj1w6U5A3t6BGA7NYZsaVve6isMm" }' + }) + + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('VALIDATION.RECIPIENT_DIFFERENT_NETWORK', [ + 'AJAAfMJj1w6U5A3t6BGA7NYZsaVve6isMm' + ]) + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE: VALIDATION.RECIPIENT_DIFFERENT_NETWORK') + }) + + it('should set data from json', async () => { + WalletService.validateAddress = jest.fn(() => true) + + const json = JSON.stringify({ + type: 0, + recipientId: 'AJAAfMJj1w6U5A3t6BGA7NYZsaVve6isMm', + fee: (0.1 * 1e8).toString(), + amount: (20 * 1e8).toString(), + vendorField: 'vendorfield test' + }) + + wrapper.vm.electron_readFile = jest.fn(async () => { + return json + }) + + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.SUCCESS.LOAD_FROM_FILE') + expect(wrapper.vm.$success).toHaveBeenCalledWith('TRANSACTION.SUCCESS.LOAD_FROM_FILE') + expect(wrapper.vm.form.recipientId).toEqual('AJAAfMJj1w6U5A3t6BGA7NYZsaVve6isMm') + expect(wrapper.vm.form.fee).toEqual('0.1') + expect(wrapper.vm.form.amount).toEqual('20') + expect(wrapper.vm.form.vendorField).toEqual('vendorfield test') + }) + + it('should display a success alert', async () => { + wrapper.vm.electron_readFile = jest.fn(async () => { + return '{ "type": "0" }' + }) - expect(wrapper.vm.$error).toHaveBeenCalled() + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.SUCCESS.LOAD_FROM_FILE') + expect(wrapper.vm.$success).toHaveBeenCalledWith('TRANSACTION.SUCCESS.LOAD_FROM_FILE') + }) + }) + + describe('when an invalid JSON file is opened', () => { + it('should display an error alert', async () => { + wrapper.vm.electron_readFile = jest.fn(async () => { + return 'invalid json' + }) + + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE') + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE: VALIDATION.INVALID_FORMAT') + }) + + it('should display an error alert when error thrown', async () => { + wrapper.vm.electron_readFile = jest.fn(async () => { + throw new Error('invalid json') + }) + + await wrapper.vm.loadTransaction() + + expect($tSpy).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE') + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.LOAD_FROM_FILE: invalid json') + }) }) }) }) diff --git a/__tests__/unit/components/Transaction/TransactionForm/TransactionFormVote.spec.js b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormVote.spec.js new file mode 100644 index 0000000000..718c50aed4 --- /dev/null +++ b/__tests__/unit/components/Transaction/TransactionForm/TransactionFormVote.spec.js @@ -0,0 +1,617 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import { Identities } from '@arkecosystem/crypto' +import Vuelidate from 'vuelidate' +import installI18n from '../../../__utils__/i18n' +import { TransactionFormVote } from '@/components/Transaction/TransactionForm' +import CurrencyMixin from '@/mixins/currency' +import FormatterMixin from '@/mixins/formatter' +import BigNumber from '@/plugins/bignumber' + +const localVue = createLocalVue() +localVue.use(Vuelidate) +const i18n = installI18n(localVue) + +const network = { + token: 'ARK', + symbol: 'ARK', + fractionDigits: 8, + version: 23, + wif: 170, + market: { + enabled: false + }, + knownWallets: {} +} + +let wrapper +const createWrapper = (component, wallet, delegate) => { + component = component || TransactionFormVote + wallet = wallet || { + address: 'address-1' + } + delegate = delegate || { + username: 'delegate-1', + publicKey: 'public-key-1', + address: 'delegate-address-1', + blocks: { + produced: 10 + }, + production: { + approval: 1.0 + }, + forged: { + total: (10 * 1e8).toString() + }, + voters: 10 + } + + if (!Object.keys(wallet).includes('passphrase')) { + wallet.passphrase = null + } + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + propsData: { + delegate + }, + mocks: { + $client: { + buildVote: jest.fn((transactionData) => transactionData), + fetchDelegateForged: jest.fn((delegate) => delegate.forged.total), + fetchDelegateVoters: jest.fn((delegate) => delegate.voters) + }, + $error: jest.fn(), + $store: { + getters: { + 'transaction/staticFee': jest.fn(() => null), + 'session/lastFeeByType': jest.fn(() => (1 * 1e8).toString()), + 'session/network': network, + 'network/byToken': jest.fn(() => network) + } + }, + $synchronizer: { + appendFocus: jest.fn() + }, + session_network: network, + currency_format: jest.fn(CurrencyMixin.methods.currency_format), + currency_subToUnit: jest.fn(CurrencyMixin.methods.currency_subToUnit), + currency_toBuilder: jest.fn(CurrencyMixin.methods.currency_toBuilder), + currency_unitToSub: jest.fn(CurrencyMixin.methods.currency_unitToSub), + formatter_percentage: jest.fn(FormatterMixin.methods.formatter_percentage), + wallet_formatAddress: jest.fn(address => `formatted-${address}`), + wallet_fromRoute: wallet + }, + stubs: { + Portal: true + } + }) +} + +describe('TransactionFormVote', () => { + beforeEach(() => { + createWrapper() + }) + + it('should have vote transaction type', () => { + expect(wrapper.vm.$options.transactionType).toBe(3) + }) + + describe('template', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionFormVote')).toBe(true) + }) + + it('should have delegate details', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + expect(wrapper.find('.TransactionFormVote__delegate-details').props('isOpen')).toBe(true) + }) + + describe('passphrase step', () => { + beforeEach(() => { + wrapper.vm.isPassphraseStep = true + }) + + it('should hide delegate details when choosing to vote', () => { + expect(wrapper.find('.TransactionFormVote__delegate-details').props('isOpen')).toBe(false) + }) + + it('should have fee field', () => { + expect(wrapper.contains('.TransactionFormVote__fee')).toBe(true) + }) + + it('should have hash field', () => { + expect(wrapper.contains('.TransactionFormVote__fee')).toBe(true) + }) + + describe('ledger notice', () => { + it('should show if wallet is a ledger', () => { + createWrapper(null, { + isLedger: true + }) + + expect(wrapper.contains('.TransactionFormVote__ledger-notice')).toBe(true) + }) + + it('should show if wallet is not a ledger', () => { + createWrapper(null, { + isLedger: false + }) + + expect(wrapper.contains('.TransactionFormVote__ledger-notice')).toBe(false) + }) + }) + + describe('password field', () => { + it('should show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormVote__password')).toBe(true) + }) + + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormVote__password')).toBe(false) + }) + }) + + describe('passphrase field', () => { + it('should show if wallet does not have a password', () => { + expect(wrapper.contains('.TransactionFormVote__passphrase')).toBe(true) + }) + + it('should not show if wallet does have a password', () => { + createWrapper(null, { + passphrase: 'password' + }) + + expect(wrapper.contains('.TransactionFormVote__passphrase')).toBe(false) + }) + }) + + describe('next button', () => { + it('should be enabled if form is valid', async () => { + wrapper.vm.$v.form.fee.$model = (0.1 * 1e8).toString() + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormVote__next').attributes('disabled')).toBeFalsy() + }) + + it('should be disabled if form is invalid', async () => { + wrapper.vm.$v.form.$touch() + + await wrapper.vm.$nextTick() + + expect(wrapper.find('.TransactionFormVote__next').attributes('disabled')).toBe('disabled') + }) + }) + }) + }) + + describe('computed', () => { + describe('blocksProduced', () => { + it('should return blocks produced for delegate', () => { + expect(wrapper.vm.blocksProduced).toBe(10) + }) + + it('should return 0 if no delegate', () => { + wrapper.setProps({ + delegate: { + blocks: null + } + }) + + expect(wrapper.vm.blocksProduced).toBe(0) + + wrapper.setProps({ + delegate: { + blocks: { + produced: null + }, + production: { + approval: 1.0 + } + } + }) + + expect(wrapper.vm.blocksProduced).toBe(0) + }) + }) + + describe('showVoteUnvoteButton', () => { + it('should return false if wallet is a contact', () => { + createWrapper(null, { + isContact: true + }) + + expect(wrapper.vm.showVoteUnvoteButton).toBe(false) + }) + + it('should return false if wallet is voting but not for delegate', () => { + wrapper.setProps({ + isVoter: false, + votedDelegate: { + username: 'delegate-2', + publicKey: 'public-key-2', + address: 'delegate-address-2', + blocks: { + produced: 10 + }, + production: { + approval: 1.0 + }, + forged: { + total: '0' + }, + voters: 10 + } + }) + + expect(wrapper.vm.showVoteUnvoteButton).toBe(false) + }) + + it('should return true if not voting', () => { + wrapper.setProps({ + votedDelegate: null + }) + + expect(wrapper.vm.showVoteUnvoteButton).toBe(true) + }) + + it('should return true if voting for this delegate', () => { + wrapper.setProps({ + isVoter: true, + votedDelegate: { + username: 'delegate-1', + publicKey: 'public-key-1', + address: 'delegate-address-1', + blocks: { + produced: 10 + }, + production: { + approval: 1.0 + }, + forged: { + total: '0' + }, + voters: 10 + } + }) + + expect(wrapper.vm.showVoteUnvoteButton).toBe(true) + }) + }) + + describe('showCurrentlyVoting', () => { + it('should return true if voting for a different delegate', () => { + wrapper.setProps({ + isVoter: false, + votedDelegate: { + username: 'delegate-2', + publicKey: 'public-key-2', + address: 'delegate-address-2', + blocks: { + produced: 10 + }, + production: { + approval: 1.0 + }, + forged: { + total: '0' + }, + voters: 10 + } + }) + + expect(wrapper.vm.showCurrentlyVoting).toBe(true) + }) + }) + }) + + describe('watch', () => { + describe('isPassphraseStep', () => { + it('should do nothing if ledger wallet', async () => { + createWrapper(null, { + isLedger: true, + get passphrase () { + return null + } + }) + + const spy = jest.spyOn(wrapper.vm.currentWallet, 'passphrase', 'get') + + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should do nothing if multi-signature', async () => { + createWrapper(null, { + multiSignature: true, + get passphrase () { + return null + } + }) + + const spy = jest.spyOn(wrapper.vm.currentWallet, 'passphrase', 'get') + + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should focus on password field', async () => { + createWrapper(null, { + passphrase: 'password' + }) + + const spy = jest.spyOn(wrapper.vm.$refs.password, 'focus') + + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should focus on passphrase field', async () => { + const spy = jest.spyOn(wrapper.vm.$refs.passphrase, 'focus') + + wrapper.vm.isPassphraseStep = true + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + }) + + describe('mounted hook', () => { + it('should fetch delegate data', () => { + const fetchForgedOriginal = TransactionFormVote.methods.fetchForged + const fetchVotersOriginal = TransactionFormVote.methods.fetchVoters + TransactionFormVote.methods.fetchForged = jest.fn() + TransactionFormVote.methods.fetchVoters = jest.fn() + + createWrapper() + + expect(TransactionFormVote.methods.fetchForged).toHaveBeenCalledTimes(1) + expect(TransactionFormVote.methods.fetchVoters).toHaveBeenCalledTimes(1) + + TransactionFormVote.methods.fetchForged = fetchForgedOriginal + TransactionFormVote.methods.fetchVoters = fetchVotersOriginal + }) + }) + + describe('methods', () => { + describe('getTransactionData', () => { + it('should return correct data with passphrase', () => { + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + votes: [ + '+public-key-1' + ], + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data when unvoting with passphrase', () => { + wrapper.setProps({ + isVoter: true + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + votes: [ + '-public-key-1' + ], + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + + it('should return correct data with passphrase and second passphrase', () => { + createWrapper(null, { + address: 'address-1', + passphrase: null, + secondPublicKey: Identities.PublicKey.fromPassphrase('second passphrase') + }) + + wrapper.vm.$v.form.fee.$model = 0.1 + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + wrapper.vm.$v.form.secondPassphrase.$model = 'second passphrase' + + expect(wrapper.vm.getTransactionData()).toEqual({ + address: 'address-1', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + votes: [ + '+public-key-1' + ], + fee: new BigNumber(0.1 * 1e8), + wif: undefined, + networkWif: 170, + multiSignature: undefined + }) + }) + }) + + describe('buildTransaction', () => { + it('should build vote', async () => { + const transactionData = { + type: 5, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData, true, true) + + expect(wrapper.vm.$client.buildVote).toHaveBeenCalledWith(transactionData, true, true) + expect(response).toBe(transactionData) + }) + + it('should build vote with default arguments', async () => { + const transactionData = { + type: 7, + typeGroup: 1 + } + + const response = await wrapper.vm.buildTransaction(transactionData) + + expect(wrapper.vm.$client.buildVote).toHaveBeenCalledWith(transactionData, false, false) + expect(response).toBe(transactionData) + }) + }) + + describe('transactionError', () => { + it('should generate transaction error', () => { + wrapper.vm.transactionError() + + expect(wrapper.vm.$error).toHaveBeenCalledWith('TRANSACTION.ERROR.VALIDATION.VOTE') + }) + }) + + describe('postSubmit', () => { + it('should call reset method', () => { + const spy = jest.spyOn(wrapper.vm, 'reset') + + wrapper.vm.postSubmit() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('toggleStep', () => { + it('should toggle passphrase step', () => { + expect(wrapper.vm.isPassphraseStep).toBe(false) + + wrapper.vm.toggleStep() + + expect(wrapper.vm.isPassphraseStep).toBe(true) + + wrapper.vm.toggleStep() + + expect(wrapper.vm.isPassphraseStep).toBe(false) + }) + }) + + describe('fetchForged', () => { + it('should update forged value', () => { + wrapper.vm.fetchForged() + + expect(wrapper.vm.forged).toEqual('ARK 10.00') + }) + }) + + describe('fetchVoters', () => { + it('should update voters value', async () => { + await wrapper.vm.fetchVoters() + + expect(wrapper.vm.voters).toEqual(10) + }) + + it('should update voters value to default if no response', async () => { + jest.spyOn(wrapper.vm.$client, 'fetchDelegateVoters').mockReturnValue(null) + await wrapper.vm.fetchVoters() + + expect(wrapper.vm.voters).toEqual('0') + }) + }) + + describe('reset', () => { + it('should reset to delegate detail view', () => { + wrapper.vm.isPassphraseStep = true + + expect(wrapper.vm.isPassphraseStep).toBe(true) + + wrapper.vm.reset() + + expect(wrapper.vm.isPassphraseStep).toBe(false) + }) + + it('should reset passphrase field if not encrypted or ledger', () => { + const spy = jest.spyOn(wrapper.vm, '$set') + wrapper.vm.$v.form.passphrase.$model = 'passphrase' + + expect(wrapper.vm.$v.form.passphrase.$dirty).toBe(true) + expect(wrapper.vm.form.passphrase).toBe('passphrase') + + wrapper.vm.reset() + + expect(wrapper.vm.$v.form.passphrase.$dirty).toBe(false) + expect(wrapper.vm.form.passphrase).toBe('') + expect(spy).toHaveBeenCalledWith(wrapper.vm.form, 'passphrase', '') + }) + + it('should reset password field if encrypted and not ledger', () => { + createWrapper(null, { + passphrase: 'password' + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + wrapper.vm.$v.form.walletPassword.$model = 'password' + + expect(wrapper.vm.$v.form.walletPassword.$dirty).toBe(true) + expect(wrapper.vm.form.walletPassword).toBe('password') + + wrapper.vm.reset() + + expect(wrapper.vm.$v.form.walletPassword.$dirty).toBe(false) + expect(wrapper.vm.form.walletPassword).toBe('') + expect(spy).toHaveBeenCalledWith(wrapper.vm.form, 'walletPassword', '') + }) + + it('should do nothing if ledger', () => { + createWrapper(null, { + isLedger: true + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.reset() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should do nothing if multi-signature wallet', () => { + createWrapper(null, { + multiSignature: true + }) + + const spy = jest.spyOn(wrapper.vm, '$set') + + wrapper.vm.reset() + + expect(spy).not.toHaveBeenCalled() + }) + }) + + describe('emitCancel', () => { + it('should emit cancel', () => { + wrapper.vm.emitCancel() + + expect(wrapper.emitted('cancel')).toEqual([['navigateToTransactions']]) + }) + }) + }) +}) diff --git a/__tests__/unit/components/Transaction/TransactionModal.spec.js b/__tests__/unit/components/Transaction/TransactionModal.spec.js index 95adf86282..b9fe9370ed 100644 --- a/__tests__/unit/components/Transaction/TransactionModal.spec.js +++ b/__tests__/unit/components/Transaction/TransactionModal.spec.js @@ -1,5 +1,6 @@ import { shallowMount } from '@vue/test-utils' import { useI18nGlobally } from '../../__utils__/i18n' +import transaction from '../../__fixtures__/models/transaction' import TransactionModal from '@/components/Transaction/TransactionModal' const i18n = useI18nGlobally() @@ -118,34 +119,26 @@ describe('TransactionModal', () => { describe('storeTransaction', () => { it('should dispatch `transaction/create` using the transaction, but replacing some attributes, and the current profile', () => { - const transaction = { - id: 'tx1', - type: 0, - amount: 10000, - fee: 1000, - timestamp: 120, - recipientId: 'Arecipient', - senderPublicKey: 'Asender', - vendorField: 'smartbridge' - } wrapper.vm.storeTransaction(transaction) - const { id, type, amount, fee, vendorField } = transaction + const { id, type, typeGroup, amount, fee, vendorField } = transaction const timestamp = (new Date(wrapper.vm.session_network.constants.epoch)).getTime() + transaction.timestamp * 1000 const expected = { profileId, id, type, + typeGroup, amount, fee, vendorField, confirmations: 0, timestamp, - sender: 'public key of Asender', + sender: `public key of ${transaction.senderPublicKey}`, recipient: transaction.recipientId, raw: transaction } + expect($store.dispatch).toHaveBeenCalledWith('transaction/create', expected) }) }) diff --git a/__tests__/unit/components/Transaction/TransactionShow.spec.js b/__tests__/unit/components/Transaction/TransactionShow.spec.js index 3e44aabbfb..9368240159 100644 --- a/__tests__/unit/components/Transaction/TransactionShow.spec.js +++ b/__tests__/unit/components/Transaction/TransactionShow.spec.js @@ -1,6 +1,6 @@ import { mount } from '@vue/test-utils' import { useI18nGlobally } from '../../__utils__/i18n' -import TransactionShow from '@/components/Transaction/TransactionShow' +import { TransactionShow } from '@/components/Transaction' import FormatterMixin from '@/mixins/formatter' const i18n = useI18nGlobally() diff --git a/__tests__/unit/components/Wallet/WalletAddress.spec.js b/__tests__/unit/components/Wallet/WalletAddress.spec.js index fb697ed77a..049f4ff6cb 100644 --- a/__tests__/unit/components/Wallet/WalletAddress.spec.js +++ b/__tests__/unit/components/Wallet/WalletAddress.spec.js @@ -131,21 +131,33 @@ describe('WalletAddress', () => { expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.IPFS')) }) - it('should display Timelock Transfer for type 6', () => { + it('should display Multi Payment for type 6', () => { const wrapper = mount({ address: 'dummyAddress', type: 6 }) - expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.TIMELOCK_TRANSFER')) + expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.MULTI_PAYMENT')) }) - it('should display Multi Payment for type 7', () => { + it('should display Delegate Resignation for type 7', () => { const wrapper = mount({ address: 'dummyAddress', type: 7 }) - expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.MULTI_PAYMENT')) + expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.DELEGATE_RESIGNATION')) }) - it('should display Delegate Resignation for type 8', () => { + it('should display HTLC Lock for type 8', () => { const wrapper = mount({ address: 'dummyAddress', type: 8 }) - expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.DELEGATE_RESIGNATION')) + expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.HTLC_LOCK')) + }) + + it('should display HTLC Claim for type 9', () => { + const wrapper = mount({ address: 'dummyAddress', type: 9 }) + + expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.HTLC_CLAIM')) + }) + + it('should display HTLC Refund for type 10', () => { + const wrapper = mount({ address: 'dummyAddress', type: 10 }) + + expect(wrapper.text()).toEqual(expect.stringContaining('TRANSACTION.TYPE.HTLC_REFUND')) }) }) diff --git a/__tests__/unit/components/Wallet/WalletBusiness/WalletBusiness.spec.js b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusiness.spec.js new file mode 100644 index 0000000000..2595d9c3c8 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusiness.spec.js @@ -0,0 +1,60 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletBusiness } from '@/components/Wallet/WalletBusiness' +import { TRANSACTION_GROUPS, TRANSACTION_TYPES } from '@config' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || WalletBusiness + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + stubs: { + WalletBusinessBridgechains: `
+
+
` + } + }) +} + +describe('WalletBusiness', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletBusiness')).toBe(true) + }) + + it('should include WalletBusinessBridgechains component', () => { + expect(wrapper.contains('.WalletBusinessBridgechains')).toBe(true) + }) + + it('should initiate with bridgechain lookup', () => { + expect(wrapper.vm.bridgechainRegistration).toEqual({ + type: TRANSACTION_TYPES.GROUP_2.BRIDGECHAIN_REGISTRATION, + group: TRANSACTION_GROUPS.MAGISTRATE + }) + }) + + describe('closeTransactionModal', () => { + it('should toggle modal if open', () => { + const toggleMethod = jest.fn() + wrapper.vm.closeTransactionModal(toggleMethod, true) + + expect(toggleMethod).toHaveBeenCalled() + }) + + it('should not toggle modal if closed', () => { + const toggleMethod = jest.fn() + wrapper.vm.closeTransactionModal(toggleMethod, false) + + expect(toggleMethod).not.toHaveBeenCalled() + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechains.spec.js b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechains.spec.js new file mode 100644 index 0000000000..67c8cdacd0 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechains.spec.js @@ -0,0 +1,380 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletBusinessBridgechains } from '@/components/Wallet/WalletBusiness' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +let clientFetchBridgechainsMock +let eventOnMock +let eventOffMock +let storeDispatchMock +const createWrapper = (component) => { + component = component || WalletBusinessBridgechains + + eventOnMock = jest.fn() + eventOffMock = jest.fn() + storeDispatchMock = jest.fn() + clientFetchBridgechainsMock = jest.fn(() => ({ + data: [], + meta: { + count: 0, + pageCount: 1, + totalCount: 0, + next: null, + previous: null, + self: '/api/businesses/public-key-1/bridgechains?page=1&limit=100', + first: '/api/businesses/public-key-1/bridgechains?page=1&limit=100', + last: null + } + })) + + wrapper = mount(component, { + i18n, + localVue, + mocks: { + $client: { + fetchBusinessBridgechains: clientFetchBridgechainsMock + }, + $eventBus: { + on: eventOnMock, + off: eventOffMock + }, + $store: { + dispatch: storeDispatchMock, + getters: { + get 'session/transactionTableRowCount' () { + return 10 + } + } + }, + session_profile: { + id: 1 + } + }, + mixins: [ + { + data: () => ({ + mockWalletRoute: 1 + }), + computed: { + wallet_fromRoute () { + return { + address: `address-${this.mockWalletRoute}`, + business: { + name: 'business-name', + website: 'http://business.website', + publicKey: `public-key-${this.mockWalletRoute}`, + resigned: false + }, + publicKey: `public-key-${this.mockWalletRoute}` + } + } + } + } + ], + stubs: { + WalletBusinessBridgechainsTable: `
+
+
` + } + }) +} + +describe('WalletBusinessBridgechains', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletBusinessBridgechains')).toBe(true) + }) + + it('should include WalletBusinessBridgechainsTable component', () => { + expect(wrapper.contains('.WalletBusinessBridgechainsTable')).toBe(true) + }) + + it('should reset when wallet route changes', async () => { + const spyLoadBridgechains = jest.spyOn(wrapper.vm, 'loadBridgechains').mockImplementation(jest.fn()) + const spyReset = jest.spyOn(wrapper.vm, 'reset').mockImplementation(jest.fn()) + + wrapper.setData({ + mockWalletRoute: 2 + }) + + expect(wrapper.vm.wallet_fromRoute.address).toEqual('address-2') + + expect(spyLoadBridgechains).toHaveBeenCalledTimes(1) + expect(spyReset).toHaveBeenCalledTimes(1) + }) + + describe('created', () => { + const spy = jest.spyOn(WalletBusinessBridgechains.methods, 'loadBridgechains').mockImplementation(jest.fn()) + createWrapper() + + expect(spy).toHaveBeenCalledTimes(1) + expect(eventOnMock).toHaveBeenCalledTimes(1) + expect(eventOnMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadBridgechains) + + spy.mockRestore() + }) + + describe('beforeDestroy', () => { + wrapper.destroy() + expect(eventOffMock).toHaveBeenCalledTimes(1) + expect(eventOffMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadBridgechains) + }) + + describe('fetchBridgechains', () => { + it('should not run if already fetching', () => { + clientFetchBridgechainsMock.mockClear() + + wrapper.vm.isFetching = true + wrapper.vm.fetchBridgechains() + + expect(clientFetchBridgechainsMock).not.toHaveBeenCalled() + }) + + it('should fetch bridgechains via client', async () => { + const clientFetchBridgechains = clientFetchBridgechainsMock() + clientFetchBridgechainsMock.mockClear().mockImplementation(() => { + clientFetchBridgechains.data = [ + 'test' + ] + clientFetchBridgechains.meta.totalCount = 100 + + return clientFetchBridgechains + }) + + const params = { + page: 10, + limit: 100, + sort: { + field: 'amount', + type: 'asc' + } + } + wrapper.vm.__updateParams(params) + wrapper.vm.fetchBridgechains() + + expect(clientFetchBridgechainsMock).toHaveBeenCalledTimes(1) + expect(clientFetchBridgechainsMock).toHaveBeenCalledWith('public-key-1', { + page: params.page, + limit: params.limit, + orderBy: `${params.sort.field}:${params.sort.type}` + }) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.fetchedBridgechains).toEqual([ + 'test' + ]) + expect(wrapper.vm.totalCount).toBe(100) + }) + + it('should clear bridgechains on error', async () => { + clientFetchBridgechainsMock.mockClear().mockImplementation(() => { + throw new Error('Failed') + }) + + wrapper.vm.fetchedBridgechains = [ + 'test' + ] + wrapper.vm.totalCount = 100 + wrapper.vm.fetchBridgechains() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.fetchedBridgechains).toEqual([]) + expect(wrapper.vm.totalCount).toBe(0) + }) + }) + + describe('loadBridgechains', () => { + it('should not load if not viewing a wallet', () => { + const spy = jest.spyOn(wrapper.vm, 'fetchBridgechains') + jest.spyOn(wrapper.vm, 'wallet_fromRoute', 'get').mockReturnValue(null) + + wrapper.vm.loadBridgechains() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should not load if already fetching', () => { + const spy = jest.spyOn(wrapper.vm, 'fetchBridgechains') + + wrapper.vm.isFetching = true + wrapper.vm.loadBridgechains() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should load bridgechains', () => { + const spy = jest.spyOn(wrapper.vm, 'fetchBridgechains').mockImplementation() + + wrapper.vm.loadBridgechains() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + describe('onPageChange', () => { + it('should change page', () => { + const currentPage = 2 + const spyUpdateParams = jest.spyOn(wrapper.vm, '__updateParams') + const spyLoadBridgechains = jest.spyOn(wrapper.vm, 'loadBridgechains') + + wrapper.vm.onPageChange({ currentPage }) + + expect(wrapper.vm.currentPage).toBe(currentPage) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + expect(spyUpdateParams).toHaveBeenCalledWith({ page: currentPage }) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(1) + }) + }) + + describe('onPerPageChange', () => { + it('should update page row count', () => { + const currentPerPage = 1 + const spyUpdateParams = jest.spyOn(wrapper.vm, '__updateParams') + const spyLoadBridgechains = jest.spyOn(wrapper.vm, 'loadBridgechains') + + wrapper.vm.onPerPageChange({ currentPerPage }) + + expect(storeDispatchMock).toHaveBeenCalledWith('session/setTransactionTableRowCount', currentPerPage) + expect(storeDispatchMock).toHaveBeenCalledWith('profile/update', { + id: 1, + transactionTableRowCount: currentPerPage + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + expect(spyUpdateParams).toHaveBeenCalledWith({ limit: currentPerPage, page: 1 }) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(1) + }) + }) + + describe('onSortChange', () => { + let spyUpdateParams, spyLoadBridgechains + + beforeEach(() => { + spyUpdateParams = jest.spyOn(wrapper.vm, '__updateParams') + spyLoadBridgechains = jest.spyOn(wrapper.vm, 'loadBridgechains') + }) + + it('should update sort column', () => { + wrapper.vm.onSortChange({ + source: 'from-third-party-component', + field: 'amount', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + expect(spyUpdateParams).toHaveBeenCalledWith({ + sort: { + field: 'amount', + type: 'desc' + }, + page: 1 + }) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(1) + }) + + it('should update sort column direction', () => { + wrapper.vm.onSortChange({ + source: 'from-third-party-component', + field: 'timestamp', + type: 'asc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + expect(spyUpdateParams).toHaveBeenCalledWith({ + sort: { + field: 'timestamp', + type: 'asc' + }, + page: 1 + }) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(1) + }) + + it('should not do anything if source is falsy', () => { + wrapper.vm.onSortChange({ + source: null, + field: 'amount', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(0) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(0) + }) + + it('should not do anything if column and direction do not change', () => { + wrapper.vm.onSortChange({ + source: 'from-third-party-component', + field: 'timestamp', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(0) + expect(spyLoadBridgechains).toHaveBeenCalledTimes(0) + }) + }) + + describe('reset', () => { + it('should reset values', () => { + wrapper.vm.currentPage = 10 + wrapper.vm.queryParams.page = 10 + wrapper.vm.totalCount = 10 + wrapper.vm.fetchedBridgechains = [ + 'fake entry' + ] + + wrapper.vm.reset() + + expect(wrapper.vm.currentPage).toBe(1) + expect(wrapper.vm.queryParams.page).toBe(1) + expect(wrapper.vm.totalCount).toBe(0) + expect(wrapper.vm.fetchedBridgechains).toEqual([]) + }) + }) + + describe('__updateParams', () => { + const expected = { + page: 1, + limit: 10, + sort: { + field: 'timestamp', + type: 'desc' + } + } + + it('should update query parameters', () => { + const params = { + page: 10, + limit: 100, + sort: { + field: 'amount', + type: 'asc' + } + } + wrapper.vm.__updateParams(params) + + expect(wrapper.vm.queryParams).toEqual(params) + }) + + it('should not update if invalid value', () => { + wrapper.vm.__updateParams(null) + + expect(wrapper.vm.queryParams).toEqual(expected) + + wrapper.vm.__updateParams([]) + + expect(wrapper.vm.queryParams).toEqual(expected) + + wrapper.vm.__updateParams('test') + + expect(wrapper.vm.queryParams).toEqual(expected) + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechainsTable.spec.js b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechainsTable.spec.js new file mode 100644 index 0000000000..d33312433d --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessBridgechainsTable.spec.js @@ -0,0 +1,117 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletBusinessBridgechainsTable } from '@/components/Wallet/WalletBusiness' +import truncateMiddle from '@/filters/truncate-middle' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component) => { + component = component || WalletBusinessBridgechainsTable + + wrapper = mount(component, { + i18n, + localVue, + sync: false, + stubs: { + Portal: '
', + TableWrapper: '
', + WalletBusinessShowBridgechain: '
' + } + }) +} + +describe('WalletBusinessBridgechainsTable', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletBusinessBridgechainsTable')).toBe(true) + }) + + it('should include TableWrapper component', () => { + expect(wrapper.contains('.TableWrapper')).toBe(true) + }) + + describe('computed columns', () => { + it('should return correct columns', () => { + expect(wrapper.vm.columns[0].label).toEqual('WALLET_BUSINESS.COLUMN.NAME') + expect(wrapper.vm.columns[0].field).toEqual('name') + + expect(wrapper.vm.columns[1].label).toEqual('WALLET_BUSINESS.COLUMN.SEEDS') + expect(wrapper.vm.columns[1].field).toEqual('seedNodes') + expect(typeof wrapper.vm.columns[1].formatFn).toEqual('function') + + expect(wrapper.vm.columns[2].label).toEqual('WALLET_BUSINESS.COLUMN.GENESIS_HASH') + expect(wrapper.vm.columns[2].field).toEqual('genesisHash') + expect(typeof wrapper.vm.columns[2].formatFn).toEqual('function') + + expect(wrapper.vm.columns[3].label).toEqual('WALLET_BUSINESS.COLUMN.REPOSITORY') + expect(wrapper.vm.columns[3].field).toEqual('bridgechainRepository') + }) + + it('should format seed nodes as quantity', () => { + const seeds = [ + 1, + 2, + 3, + 4 + ] + + expect(wrapper.vm.columns[1].formatFn(seeds)).toBe(seeds.length) + }) + + it('should format genesis hash as truncated', () => { + const genesisHash = '123456789012345678901234567890' + + expect(wrapper.vm.columns[2].formatFn(genesisHash)).toBe(truncateMiddle(genesisHash, 14)) + }) + }) + + it('should show WalletBusinessShowBridgechain if selected row', async () => { + wrapper.vm.selected = { type: 1 } + await wrapper.vm.$nextTick() + + expect(wrapper.find('.WalletBusinessShowBridgechain').isVisible()).toBe(true) + }) + + describe('onSortChange', () => { + it('should emit on-sort-change', () => { + wrapper.vm.onSortChange([{ field: 'amount' }]) + + expect(wrapper.emitted('on-sort-change')[0][0]).toEqual({ + source: 'bridgechainsTab', + field: 'amount' + }) + }) + }) + + describe('onRowClick', () => { + it('should update selected row', () => { + const fakeTransaction = { type: 1 } + + expect(wrapper.vm.selected).toBe(null) + + wrapper.vm.onRowClick({ row: fakeTransaction }) + + expect(wrapper.vm.selected).toBe(fakeTransaction) + }) + }) + + describe('onCloseModal', () => { + it('should close reset selected row & close modal', async () => { + wrapper.vm.selected = { type: 1 } + await wrapper.vm.$nextTick() + + expect(wrapper.find('.WalletBusinessShowBridgechain').isVisible()).toBe(true) + + wrapper.vm.onCloseModal() + await wrapper.vm.$nextTick() + + expect(wrapper.vm.selected).toBe(null) + expect(wrapper.contains('.WalletBusinessShowBridgechain')).toBe(false) + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessShowBridgechain.spec.js b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessShowBridgechain.spec.js new file mode 100644 index 0000000000..09b26afa62 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletBusiness/WalletBusinessShowBridgechain.spec.js @@ -0,0 +1,144 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletBusinessShowBridgechain } from '@/components/Wallet/WalletBusiness' +import truncateMiddle from '@/filters/truncate-middle' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, propsData) => { + component = component || WalletBusinessShowBridgechain + propsData = propsData || { + bridgechain: { + name: 'test bridgechain', + seedNodes: ['1.1.1.1', '2.2.2.2'], + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'http://ark.io', + ports: { + '@arkecosystem/core-api': 4003 + }, + isResigned: false + } + } + + wrapper = mount(component, { + i18n, + localVue, + propsData, + sync: false, + stubs: { + Portal: '
' + } + }) +} + +describe('WalletBusinessShowBridgechain', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletBusinessShowBridgechain')).toBe(true) + }) + + describe('template', () => { + it('should output bridgechain name', () => { + const props = wrapper.find('.WalletBusinessShowBridgechain__name').props() + + expect(props.label).toBe('WALLET_BUSINESS.BRIDGECHAIN.NAME') + expect(props.value).toBe('test bridgechain') + }) + + it('should output seed nodes', () => { + const seedNodes = wrapper.find('.WalletBusinessShowBridgechain__seed-nodes') + const seedNodeItems = seedNodes.findAll('.WalletBusinessShowBridgechain__seed-nodes__item') + + expect(seedNodes.props('label')).toBe('WALLET_BUSINESS.BRIDGECHAIN.SEED_NODES') + + for (let itemIndex = 0; itemIndex < seedNodeItems.length; itemIndex++) { + const seedNode = seedNodeItems.at(itemIndex) + expect(seedNode.text()).toBe(wrapper.vm.bridgechain.seedNodes[itemIndex]) + } + }) + + it('should output genesis hash', () => { + const genesisHash = wrapper.find('.WalletBusinessShowBridgechain__genesis-hash') + const genesisHashItem = genesisHash.find('.WalletBusinessShowBridgechain__genesis-hash__item') + + expect(genesisHash.props('label')).toBe('WALLET_BUSINESS.BRIDGECHAIN.GENESIS_HASH') + expect(genesisHashItem.text()).toBe(truncateMiddle(wrapper.vm.bridgechain.genesisHash, 10)) + }) + + it('should output genesis hash', () => { + const bridgechainRepo = wrapper.find('.WalletBusinessShowBridgechain__bridgechain-repo') + const bridgechainRepoItem = bridgechainRepo.find('.WalletBusinessShowBridgechain__bridgechain-repo__item') + + expect(bridgechainRepo.props('label')).toBe('WALLET_BUSINESS.BRIDGECHAIN.BRIDGECHAIN_REPOSITORY') + expect(bridgechainRepoItem.text()).toBe(wrapper.vm.bridgechain.bridgechainRepository) + }) + }) + + describe('computed isResigned', () => { + it('should return false if not present in bridgechain', () => { + const props = { + bridgechain: { + ...wrapper.vm.bridgechain, + isResigned: undefined + } + } + createWrapper(null, props) + + expect(wrapper.vm.isResigned).toBe(false) + }) + + it('should return false if bridgechain is false', () => { + expect(wrapper.vm.isResigned).toBe(false) + }) + + it('should return true if bridgechain is true', async () => { + const props = { + bridgechain: { + ...wrapper.vm.bridgechain, + isResigned: true + } + } + createWrapper(null, props) + await wrapper.vm.$nextTick() + + expect(wrapper.vm.isResigned).toBe(true) + }) + }) + + describe('closeTransactionModal', () => { + it('should toggle method & call "emitClose" if open', () => { + const spy = jest.spyOn(wrapper.vm, 'emitClose').mockImplementation() + const toggleClose = jest.fn() + wrapper.vm.closeTransactionModal(toggleClose, true) + + expect(toggleClose).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should only call "emitClose" if already closed', () => { + const spy = jest.spyOn(wrapper.vm, 'emitClose').mockImplementation() + const toggleClose = jest.fn() + wrapper.vm.closeTransactionModal(toggleClose, false) + + expect(toggleClose).not.toHaveBeenCalled() + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should not toggle method if not a function', async () => { + expect(() => { wrapper.vm.closeTransactionModal(null, true) }).not.toThrowError() + }) + }) + + describe('emitClose', () => { + it('should emit "close"', () => { + wrapper.vm.emitClose() + + expect(wrapper.emitted('close')[0][0]).toBe('navigateToTransactions') + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletDelegates.spec.js b/__tests__/unit/components/Wallet/WalletDelegates.spec.js index 2eeccf939c..e0ae8a9f31 100644 --- a/__tests__/unit/components/Wallet/WalletDelegates.spec.js +++ b/__tests__/unit/components/Wallet/WalletDelegates.spec.js @@ -1,3 +1,4 @@ +import { merge } from 'lodash' import { mount } from '@vue/test-utils' import { useI18nGlobally } from '../../__utils__/i18n' import { WalletDelegates } from '@/components/Wallet' @@ -8,8 +9,20 @@ describe('WalletDelegates', () => { let showExplanation let walletVote = {} - const mountWrapper = () => { - return mount(WalletDelegates, { + const activeDelegatesMock = count => { + return { + mocks: { + session_network: { + constants: { + activeDelegates: count + } + } + } + } + } + + const mountWrapper = config => { + return mount(WalletDelegates, merge({ i18n, provide: { walletVote @@ -33,7 +46,7 @@ describe('WalletDelegates', () => { stubs: { TableWrapper: true } - }) + }, config)) } it('should render', () => { @@ -41,6 +54,22 @@ describe('WalletDelegates', () => { expect(wrapper.isVueInstance()).toBeTrue() }) + it('should dynamically calculate the per page options', () => { + let wrapper = mountWrapper(activeDelegatesMock(25)) + expect(wrapper.vm.perPageOptions).toEqual([25]) + + wrapper = mountWrapper(activeDelegatesMock(53)) + expect(wrapper.vm.perPageOptions).toEqual([25, 53]) + + wrapper = mountWrapper(activeDelegatesMock(101)) + expect(wrapper.vm.perPageOptions).toEqual([25, 50, 75, 100]) + }) + + it('should cap the query limit at 100', () => { + const wrapper = mountWrapper(activeDelegatesMock(101)) + expect(wrapper.vm.queryParams.limit).toBe(100) + }) + describe('when the wallet is voting', () => { beforeEach(() => { walletVote = { username: 'key' } diff --git a/__tests__/unit/components/Wallet/WalletHeading.spec.js b/__tests__/unit/components/Wallet/WalletHeading.spec.js index 98b2d45685..c055aaef3b 100644 --- a/__tests__/unit/components/Wallet/WalletHeading.spec.js +++ b/__tests__/unit/components/Wallet/WalletHeading.spec.js @@ -37,6 +37,15 @@ const sampleWalletData = { } const mocks = { + $store: { + getters: { + 'session/network': { + milestone: { + aip11: false + } + } + } + }, wallet_fromRoute: sampleWalletData, wallet_truncate: value => value, walletVote: { diff --git a/__tests__/unit/components/Wallet/WalletIpfs.spec.js b/__tests__/unit/components/Wallet/WalletIpfs.spec.js new file mode 100644 index 0000000000..c1a03168fb --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletIpfs.spec.js @@ -0,0 +1,53 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../__utils__/i18n' +import { WalletIpfs } from '@/components/Wallet/WalletIpfs' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) + +let wrapper +const createWrapper = (component, propsData) => { + component = component || WalletIpfs + + wrapper = mount(component, { + i18n, + localVue, + stubs: { + WalletTransactions: '
' + } + }) +} + +describe('WalletIpfs', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletIpfs')).toBe(true) + }) + + it('should include WalletTransactions component', () => { + expect(wrapper.contains('.WalletTransactions')).toBe(true) + }) + + describe('closeTransactionModal', () => { + it('should toggle method if open', () => { + const toggleClose = jest.fn() + wrapper.vm.closeTransactionModal(toggleClose, true) + + expect(toggleClose).toHaveBeenCalledTimes(1) + }) + + it('should do nothing if already closed', () => { + const toggleClose = jest.fn() + wrapper.vm.closeTransactionModal(toggleClose, false) + + expect(toggleClose).not.toHaveBeenCalled() + }) + + it('should not toggle method if not a function', async () => { + expect(() => { wrapper.vm.closeTransactionModal(null, true) }).not.toThrowError() + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletMultiSignature.spec.js b/__tests__/unit/components/Wallet/WalletMultiSignature.spec.js new file mode 100644 index 0000000000..d89020d5ae --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletMultiSignature.spec.js @@ -0,0 +1,140 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../__utils__/i18n' +import { WalletMultiSignature } from '@/components/Wallet/WalletMultiSignature' +import MultiSignatureClient from '@/services/client-multisig' + +const localVue = createLocalVue() +const i18n = installI18n(localVue) +const samplePeer = Object.freeze({ + host: 'http://1.1.1.1', + port: '1234' +}) + +let wrapper +let errorMock +let successMock +let dispatchMock +const createWrapper = (component, gettersPeer) => { + component = component || WalletMultiSignature + gettersPeer = gettersPeer === undefined ? samplePeer : gettersPeer + + errorMock = jest.fn() + successMock = jest.fn() + dispatchMock = jest.fn() + + wrapper = mount(component, { + i18n, + localVue, + mocks: { + $error: errorMock, + $success: successMock, + $store: { + dispatch: dispatchMock, + getters: { + get 'session/multiSignaturePeer' () { + return gettersPeer + } + } + } + }, + stubs: { + Portal: '
', + WalletTransactionsMultiSignature: '
' + } + }) +} + +describe('WalletMultiSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletMultiSignature')).toBe(true) + }) + + it('should include WalletTransactionsMultiSignature component', () => { + expect(wrapper.contains('.WalletTransactionsMultiSignature')).toBe(true) + }) + + describe('computed peer', () => { + it('should get value from store', () => { + expect(wrapper.vm.peer).toBe(samplePeer) + }) + }) + + describe('computed peerOutput', () => { + it('should output peer if connected to one', () => { + expect(wrapper.vm.peerOutput).toBe(`${samplePeer.host}:${samplePeer.port}`) + }) + + it('should output message if not connected to one', async () => { + createWrapper(null, null) + + await wrapper.vm.$nextTick() + + expect(wrapper.vm.peerOutput).toBe('PEER.NONE') + }) + }) + + describe('showLoadingModal', () => { + it('should update when connecting to peer', async () => { + const spyHandshake = jest.spyOn(MultiSignatureClient, 'performHandshake').mockImplementation(async () => false) + const spyShowLoading = jest.spyOn(wrapper.vm, 'showLoadingModal', 'set').mockImplementation() + + await wrapper.vm.connectPeer({ peer: samplePeer, closeTrigger: null }) + + expect(spyShowLoading).toHaveBeenCalledWith(true) + expect(spyShowLoading).toHaveBeenCalledWith(false) + expect(spyShowLoading).toHaveBeenCalledTimes(2) + + spyHandshake.mockRestore() + }) + + it('should show loader when true', async () => { + expect(wrapper.contains('.ModalLoader')).toBe(false) + + wrapper.vm.showLoadingModal = true + + expect(wrapper.find('.ModalLoader').isVisible()).toBe(true) + }) + }) + + describe('connectPeer', () => { + it('should save peer if handshake is successful', async () => { + const spyHandshake = jest.spyOn(MultiSignatureClient, 'performHandshake').mockImplementation(async () => true) + + await wrapper.vm.connectPeer({ peer: samplePeer, closeTrigger: null }) + + expect(dispatchMock).toHaveBeenCalledWith('session/setMultiSignaturePeer', samplePeer) + expect(dispatchMock).toHaveBeenCalledWith('profile/setMultiSignaturePeer', samplePeer) + expect(dispatchMock).toHaveBeenCalledTimes(2) + expect(successMock).toHaveBeenCalledWith(`PEER.CONNECTED: ${samplePeer.host}:${samplePeer.port}`) + expect(successMock).toHaveBeenCalledTimes(1) + + spyHandshake.mockRestore() + }) + + it('should trigger close method if handshake is successful', async () => { + const spyHandshake = jest.spyOn(MultiSignatureClient, 'performHandshake').mockImplementation(async () => true) + const closeTrigger = jest.fn() + + await wrapper.vm.connectPeer({ peer: samplePeer, closeTrigger }) + + expect(closeTrigger).toHaveBeenCalledTimes(1) + + spyHandshake.mockRestore() + }) + + it('should throw error if handshake is not successful', async () => { + const spyHandshake = jest.spyOn(MultiSignatureClient, 'performHandshake').mockImplementation(async () => false) + + await wrapper.vm.connectPeer({ peer: samplePeer, closeTrigger: null }) + + expect(errorMock).toHaveBeenCalledWith('PEER.CONNECT_FAILED') + expect(errorMock).toHaveBeenCalledTimes(1) + + spyHandshake.mockRestore() + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletTransactions.spec.js b/__tests__/unit/components/Wallet/WalletTransactions.spec.js deleted file mode 100644 index 6814a5142f..0000000000 --- a/__tests__/unit/components/Wallet/WalletTransactions.spec.js +++ /dev/null @@ -1,20 +0,0 @@ -import { shallowMount } from '@vue/test-utils' -import { WalletTransactions } from '@/components/Wallet' - -describe('WalletTransactions', () => { - it('should render', () => { - const wrapper = shallowMount(WalletTransactions, { - stubs: { - TransactionTable: true - }, - mocks: { - $store: { - getters: { - 'transaction/byAddress': jest.fn(() => []) - } - } - } - }) - expect(wrapper.isVueInstance()).toBeTrue() - }) -}) diff --git a/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactions.spec.js b/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactions.spec.js new file mode 100644 index 0000000000..284d9e3b17 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactions.spec.js @@ -0,0 +1,536 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import nock from 'nock' +import installI18n from '../../../__utils__/i18n' +import { WalletTransactions } from '@/components/Wallet/WalletTransactions' +import WalletTransactionsMixin from '@/components/Wallet/WalletTransactions/mixin' +import * as mergeTableTransactions from '@/components/utils/merge-table-transactions' +import ClientService from '@/services/client' + +const clientService = new ClientService(false) + +const localVue = createLocalVue() +const i18n = installI18n(localVue) +const sampleTransactions = Object.freeze([]) + +let wrapper + +let errorMock +let successMock +let eventOnMock +let eventOffMock +let clientFetchTransactionsMock +let loggerErrorMock +let dispatchMock +const createWrapper = (component, gettersTransactions) => { + component = component || WalletTransactions + gettersTransactions = gettersTransactions === undefined ? sampleTransactions : gettersTransactions + + errorMock = jest.fn() + successMock = jest.fn() + eventOnMock = jest.fn() + eventOffMock = jest.fn() + clientFetchTransactionsMock = jest.fn() + loggerErrorMock = jest.fn() + dispatchMock = jest.fn() + + wrapper = mount(component, { + i18n, + localVue, + mocks: { + $error: errorMock, + $success: successMock, + $client: { + fetchWalletTransactions: clientFetchTransactionsMock + }, + $eventBus: { + on: eventOnMock, + off: eventOffMock + }, + $logger: { + error: loggerErrorMock + }, + $store: { + dispatch: dispatchMock, + getters: { + get 'transaction/byAddress' () { + return gettersTransactions + } + } + }, + session_profile: { + id: 'profile-1' + } + }, + mixins: [{ + data: () => ({ + mockWalletRoute: 1 + }), + computed: { + wallet_fromRoute () { + return !this.mockWalletRoute ? null : { + address: `address-${this.mockWalletRoute}`, + business: { + name: 'business-name', + website: 'http://business.website', + publicKey: `public-key-${this.mockWalletRoute}`, + resigned: false + }, + publicKey: `public-key-${this.mockWalletRoute}` + } + } + } + }], + stubs: { + TransactionTable: '
' + } + }) +} + +describe('WalletTransactions', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletTransactions')).toBe(true) + }) + + describe('props transactionType', () => { + beforeEach(() => { + wrapper.setProps({ + transactionType: 7 + }) + }) + + it('should be set', () => { + expect(wrapper.vm.transactionType).toBe(7) + }) + + it('should be passed to TransactionTable component', () => { + expect(wrapper.find('.TransactionTable').props('transactionType')).toBe(7) + }) + }) + + describe('created hook', () => { + it('should load transactions', () => { + const spy = jest.spyOn(WalletTransactionsMixin.methods, 'loadTransactions').mockImplementation() + createWrapper() + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should initiate event', () => { + const spy = jest.spyOn(WalletTransactions.methods, 'enableNewTransactionEvent').mockImplementation() + createWrapper() + + expect(eventOnMock).toHaveBeenCalledTimes(1) + expect(eventOnMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadTransactions) + + spy.mockRestore() + }) + + it('should setup "new transaction" event', () => { + const spy = jest.spyOn(WalletTransactions.methods, 'enableNewTransactionEvent').mockImplementation() + createWrapper() + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith('address-1') + + spy.mockRestore() + }) + }) + + describe('beforeDestroy hook', () => { + it('should disable event', () => { + jest.spyOn(wrapper.vm, 'disableNewTransactionEvent').mockImplementation() + + eventOffMock.mockReset() + wrapper.destroy() + + expect(eventOffMock).toHaveBeenCalledTimes(1) + expect(eventOffMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadTransactions) + }) + + it('should disable "new transaction" event', () => { + const spy = jest.spyOn(wrapper.vm, 'disableNewTransactionEvent').mockImplementation() + + wrapper.destroy() + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith('address-1') + }) + + it('should not disable "new transaction" event if no wallet from route', () => { + const spy = jest.spyOn(wrapper.vm, 'disableNewTransactionEvent').mockImplementation() + + wrapper.vm.mockWalletRoute = null + spy.mockReset() + wrapper.destroy() + + expect(spy).not.toHaveBeenCalled() + }) + }) + + describe('methods', () => { + describe('enableNewTransactionEvent', () => { + let spyDisableNew + beforeEach(() => { + spyDisableNew = jest.spyOn(wrapper.vm, 'disableNewTransactionEvent') + }) + + it('should not do anything if no address', () => { + wrapper.vm.enableNewTransactionEvent(null) + + expect(spyDisableNew).not.toHaveBeenCalled() + }) + + it('should disable "new transaction" event if address value is given', () => { + eventOnMock.mockReset() + wrapper.vm.enableNewTransactionEvent('address') + + expect(spyDisableNew).toHaveBeenCalledTimes(1) + expect(spyDisableNew).toHaveBeenCalledWith('address') + expect(eventOnMock).toHaveBeenCalledTimes(1) + expect(eventOnMock).toHaveBeenCalledWith('wallet:address:transaction:new', wrapper.vm.refreshStatusEvent) + }) + }) + + describe('disableNewTransactionEvent', () => { + beforeEach(() => { + eventOffMock.mockReset() + }) + + it('should not do anything if no address', () => { + wrapper.vm.disableNewTransactionEvent(null) + + expect(eventOffMock).not.toHaveBeenCalled() + }) + + it('should disable "new transaction" event if address value is given', () => { + wrapper.vm.enableNewTransactionEvent('address') + + expect(eventOffMock).toHaveBeenCalledTimes(1) + expect(eventOffMock).toHaveBeenCalledWith('wallet:address:transaction:new', wrapper.vm.refreshStatusEvent) + }) + }) + + describe('getStoredTransactions', () => { + const transactions = [ + { type: 10 }, + { type: 1 }, + { type: 7 }, + { type: 0 } + ] + + it('should not do anything if no address', () => { + const spy = jest.fn(() => []) + createWrapper(null, spy) + wrapper.vm.getStoredTransactions(null) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should get stored transactions if address is provided', () => { + const spy = jest.fn(() => []) + createWrapper(null, spy) + wrapper.vm.getStoredTransactions('address') + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith('address', { includeExpired: true }) + }) + + it('should return all transactions if no transactionType prop is specified', () => { + createWrapper(null, jest.fn(() => transactions)) + wrapper.setProps({ + transactionType: 0 + }) + + expect(wrapper.vm.getStoredTransactions('address')).toEqual([{ type: 0 }]) + + wrapper.setProps({ + transactionType: null + }) + + expect(wrapper.vm.getStoredTransactions('address')).toEqual(transactions) + }) + + it('should filter transactions if transactionType prop is specified', () => { + createWrapper(null, jest.fn(() => transactions)) + wrapper.setProps({ + transactionType: 7 + }) + + expect(wrapper.vm.getStoredTransactions('address')).toEqual([{ type: 7 }]) + }) + + it('should return empty array if no results', () => { + createWrapper(null, jest.fn(() => transactions)) + wrapper.setProps({ + transactionType: 2 + }) + + expect(wrapper.vm.getStoredTransactions('address')).toEqual([]) + }) + }) + + describe('getTransactions', () => { + it('should not do anything if no address', async () => { + const spy = jest.spyOn(wrapper.vm, 'queryParams', 'get') + await wrapper.vm.getTransactions(null) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should get stored transactions if address is provided', async () => { + clientFetchTransactionsMock.mockReset() + await wrapper.vm.getTransactions('address') + + expect(clientFetchTransactionsMock).toHaveBeenCalledTimes(1) + expect(clientFetchTransactionsMock).toHaveBeenCalledWith('address', { + transactionType: null, + page: 1, + limit: 10, + orderBy: 'timestamp:desc' + }) + }) + + it('should use updated props when changed', async () => { + const params = { + page: 2, + limit: 5, + sort: { + field: 'amount', + type: 'asc' + } + } + wrapper.setProps({ + transactionType: 7 + }) + wrapper.vm.__updateParams(params) + + clientFetchTransactionsMock.mockReset() + await wrapper.vm.getTransactions('address') + + expect(clientFetchTransactionsMock).toHaveBeenCalledTimes(1) + expect(clientFetchTransactionsMock).toHaveBeenCalledWith('address', { + transactionType: 7, + page: params.page, + limit: params.limit, + orderBy: `${params.sort.field}:${params.sort.type}` + }) + }) + }) + + describe('fetchTransactions', () => { + const remoteTransactions = [ + 'remote-1' + ] + const localTransactions = [ + 'local-1' + ] + + let mergeTableMock + beforeEach(() => { + mergeTableMock = jest.spyOn(mergeTableTransactions, 'default') + }) + + afterEach(() => { + mergeTableMock.mockRestore() + }) + + it('should not fetch if already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + wrapper.vm.isFetching = true + await wrapper.vm.fetchTransactions() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should fetch if not already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + await wrapper.vm.fetchTransactions() + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith('address-1') + }) + + it('should delete transactions received from api', async () => { + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions: remoteTransactions }) + + await wrapper.vm.fetchTransactions() + + expect(dispatchMock).toHaveBeenCalledTimes(1) + expect(dispatchMock).toHaveBeenCalledWith('transaction/deleteBulk', { + transactions: remoteTransactions, + profileId: 'profile-1' + }) + }) + + it('should attempt to merge stored and remote transaction arrays', async () => { + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions: remoteTransactions }) + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue(localTransactions) + + await wrapper.vm.fetchTransactions() + + expect(mergeTableMock).toHaveBeenCalledTimes(1) + expect(mergeTableMock).toHaveBeenCalledWith(remoteTransactions, localTransactions, wrapper.vm.queryParams.sort) + }) + + it('should store updated transactions if route address has not changed', async () => { + mergeTableMock.mockReturnValue([...remoteTransactions, ...localTransactions]) + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue(localTransactions) + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ + transactions: remoteTransactions, + totalCount: 1 + }) + + await wrapper.vm.fetchTransactions() + + expect(wrapper.vm.fetchedTransactions).toEqual([...remoteTransactions, ...localTransactions]) + expect(wrapper.vm.totalCount).toEqual(1) + }) + + it('should not store transactions if route address has changed', async () => { + wrapper.vm.fetchedTransactions = ['placeholder'] + wrapper.vm.totalCount = 31 + + await wrapper.vm.$nextTick() + + mergeTableMock.mockReturnValue([...remoteTransactions, ...localTransactions]) + jest.spyOn(wrapper.vm, 'reset').mockImplementation() + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue(localTransactions) + jest.spyOn(wrapper.vm, 'getTransactions').mockImplementation(async () => { + wrapper.vm.mockWalletRoute = 2 + + await wrapper.vm.$nextTick() + + return { + transactions: remoteTransactions, + totalCount: 1 + } + }) + + await wrapper.vm.fetchTransactions() + + expect(wrapper.vm.fetchedTransactions).toEqual(['placeholder']) + expect(wrapper.vm.totalCount).toEqual(31) + }) + + it('should output error if not "wallet not found"', async () => { + clientFetchTransactionsMock.mockImplementation((address, options = {}) => { + return clientService.fetchWalletTransactions(address, options) + }) + nock('http://localhost') + .get('/wallets/address-1/transactions') + .query(true) + .reply(500, { + message: 'oops' + }) + + wrapper.vm.fetchedTransactions = ['placeholder'] + + loggerErrorMock.mockReset() + errorMock.mockReset() + await wrapper.vm.fetchTransactions() + + expect(loggerErrorMock).toHaveBeenCalled() + expect(errorMock).toHaveBeenCalledWith('COMMON.FAILED_FETCH') + expect(wrapper.vm.fetchedTransactions).toEqual([]) + }) + + it('should not output error if "wallet not found"', async () => { + clientFetchTransactionsMock.mockImplementation((address, options = {}) => { + return clientService.fetchWalletTransactions(address, options) + }) + nock('http://localhost') + .get('/wallets/address-1/transactions') + .query(true) + .reply(404, { + message: 'Wallet not found' + }) + + wrapper.vm.fetchedTransactions = ['placeholder'] + + loggerErrorMock.mockReset() + errorMock.mockReset() + await wrapper.vm.fetchTransactions() + + expect(loggerErrorMock).not.toHaveBeenCalled() + expect(errorMock).not.toHaveBeenCalled() + expect(wrapper.vm.fetchedTransactions).toEqual([]) + }) + }) + + describe('refreshStatusEvent', () => { + it('should call refreshStatus', () => { + const spy = jest.spyOn(WalletTransactions.methods, 'refreshStatus').mockImplementation() + createWrapper() + + wrapper.vm.refreshStatusEvent() + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + }) + + describe('refreshStatus', () => { + it('should do nothing if no wallet', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + wrapper.vm.mockWalletRoute = null + await wrapper.vm.refreshStatus() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should update transaction notice if new transactions', async () => { + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions: [{ id: 'test' }] }) + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue([]) + + await wrapper.vm.refreshStatus() + + expect(wrapper.vm.newTransactionsNotice).toBe('WALLET_TRANSACTIONS.NEW_TRANSACTIONS') + }) + + it('should not update transaction notice if no new transactions', async () => { + const transactions = [{ id: 'test' }] + wrapper.vm.fetchedTransactions = transactions + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions }) + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue(transactions) + + await wrapper.vm.refreshStatus() + + expect(wrapper.vm.newTransactionsNotice).not.toBe('WALLET_TRANSACTIONS.NEW_TRANSACTIONS') + }) + + it('should update transaction confirmations', async () => { + const newTransactions = [{ id: 'test', confirmations: 2 }] + const oldTransactions = [{ id: 'test', confirmations: 1 }] + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions: newTransactions }) + jest.spyOn(wrapper.vm, 'getStoredTransactions').mockReturnValue([]) + wrapper.vm.fetchedTransactions = oldTransactions + + await wrapper.vm.refreshStatus() + + expect(oldTransactions[0].confirmations).toBe(2) + }) + + it('should do nothing on error', async () => { + wrapper.vm.newTransactionsNotice = 'test' + const error = new Error('failed web request') + jest.spyOn(wrapper.vm, 'getTransactions').mockImplementation(() => { + throw error + }) + + await wrapper.vm.refreshStatus() + + expect(loggerErrorMock).toHaveBeenCalledWith('Failed to update confirmations: ', error) + expect(wrapper.vm.newTransactionsNotice).toBe('test') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactionsMultiSignature.spec.js b/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactionsMultiSignature.spec.js new file mode 100644 index 0000000000..86c2236562 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletTransactions/WalletTransactionsMultiSignature.spec.js @@ -0,0 +1,286 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletTransactionsMultiSignature } from '@/components/Wallet/WalletTransactions' +import WalletTransactionsMixin from '@/components/Wallet/WalletTransactions/mixin' +import MultiSignatureClient from '@/services/client-multisig' +import WalletService from '@/services/wallet' + +jest.mock('@/services/wallet') + +const localVue = createLocalVue() +const i18n = installI18n(localVue) +const sampleTransactions = Object.freeze([]) + +let wrapper + +let errorMock +let successMock +let eventOnMock +let eventOffMock +let multiSignatureClientMock +let publicKeyFromWalletMock +let loggerErrorMock +let dispatchMock +const createWrapper = (component, gettersTransactions) => { + component = component || WalletTransactionsMultiSignature + gettersTransactions = gettersTransactions === undefined ? sampleTransactions : gettersTransactions + + errorMock = jest.fn() + successMock = jest.fn() + eventOnMock = jest.fn() + eventOffMock = jest.fn() + multiSignatureClientMock = jest.spyOn(MultiSignatureClient, 'getTransactions').mockImplementation() + loggerErrorMock = jest.fn() + dispatchMock = jest.fn() + publicKeyFromWalletMock = jest.fn() // jest.spyOn(WalletService.default, 'getPublicKeyFromWallet').mockImplementation() + WalletService.getPublicKeyFromWallet = publicKeyFromWalletMock.bind(WalletService) + + wrapper = mount(component, { + i18n, + localVue, + mocks: { + $error: errorMock, + $success: successMock, + $eventBus: { + on: eventOnMock, + off: eventOffMock + }, + $logger: { + error: loggerErrorMock + }, + $store: { + dispatch: dispatchMock, + getters: { + get 'session/multiSignaturePeer' () { + return { + ip: 'http://1.2.3.4', + port: 1234 + } + } + // get 'transaction/byAddress' () { + // return gettersTransactions + // } + } + }, + session_profile: { + id: 'profile-1' + } + }, + mixins: [{ + data: () => ({ + mockWalletRoute: 1 + }), + computed: { + wallet_fromRoute () { + return !this.mockWalletRoute ? null : { + address: `address-${this.mockWalletRoute}`, + business: { + name: 'business-name', + website: 'http://business.website', + publicKey: `public-key-${this.mockWalletRoute}`, + resigned: false + }, + publicKey: `public-key-${this.mockWalletRoute}` + } + } + } + }], + stubs: { + TransactionTable: '
' + } + }) +} + +describe('WalletTransactionsMultiSignature', () => { + beforeEach(() => { + createWrapper() + }) + + it('should render', () => { + expect(wrapper.contains('.WalletTransactions')).toBe(true) + }) + + describe('created hook', () => { + it('should load transactions', () => { + const spy = jest.spyOn(WalletTransactionsMixin.methods, 'loadTransactions').mockImplementation() + createWrapper() + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should initiate event', () => { + const spy = jest.spyOn(WalletTransactionsMixin.methods, 'loadTransactions').mockImplementation() + createWrapper() + + expect(eventOnMock).toHaveBeenCalledTimes(2) + expect(eventOnMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadTransactions) + expect(eventOnMock).toHaveBeenCalledWith('wallet:reload:multi-signature', wrapper.vm.loadTransactions) + + spy.mockRestore() + }) + }) + + describe('beforeDestroy hook', () => { + it('should disable event', () => { + eventOffMock.mockReset() + wrapper.destroy() + + expect(eventOffMock).toHaveBeenCalledTimes(2) + expect(eventOffMock).toHaveBeenCalledWith('wallet:reload', wrapper.vm.loadTransactions) + expect(eventOffMock).toHaveBeenCalledWith('wallet:reload:multi-signature', wrapper.vm.loadTransactions) + }) + }) + + describe('methods', () => { + describe('getTransactions', () => { + it('should not do anything if no address', async () => { + const spy = jest.spyOn(wrapper.vm, 'queryParams', 'get') + await wrapper.vm.getTransactions(null) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should get transactions from peer', async () => { + multiSignatureClientMock.mockReset() + await wrapper.vm.getTransactions('publicKey') + + expect(multiSignatureClientMock).toHaveBeenCalledTimes(1) + expect(multiSignatureClientMock).toHaveBeenCalledWith({ + ip: 'http://1.2.3.4', + port: 1234 + }, 'publicKey') + }) + }) + + describe('fetchTransactions', () => { + const remoteTransactions = [ + 'remote-1' + ] + + beforeEach(() => { + publicKeyFromWalletMock.mockReset() + publicKeyFromWalletMock.mockReturnValue('publicKey') + }) + + it('should not fetch if already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + wrapper.vm.isFetching = true + await wrapper.vm.fetchTransactions() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should fetch if not already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + await wrapper.vm.fetchTransactions() + + expect(spy).toHaveBeenCalledTimes(1) + expect(spy).toHaveBeenCalledWith('publicKey') + }) + + it('should not fetch if no wallet', async () => { + wrapper.vm.mockWalletRoute = null + + await wrapper.vm.$nextTick() + await wrapper.vm.fetchTransactions() + + expect(publicKeyFromWalletMock).not.toHaveBeenCalled() + }) + + it('should store updated transactions if route address has not changed', async () => { + wrapper.vm.fetchedTransactions = ['placeholder'] + wrapper.vm.totalCount = 31 + + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ + transactions: remoteTransactions, + totalCount: 1 + }) + + await wrapper.vm.fetchTransactions() + + expect(wrapper.vm.fetchedTransactions).toEqual(remoteTransactions) + expect(wrapper.vm.totalCount).toEqual(1) + }) + + it('should not store transactions if route address has changed', async () => { + wrapper.vm.fetchedTransactions = ['placeholder'] + wrapper.vm.totalCount = 31 + + await wrapper.vm.$nextTick() + jest.spyOn(wrapper.vm, 'getTransactions').mockImplementation(async () => { + publicKeyFromWalletMock.mockReturnValue('differentPublicKey') + + return { + transactions: remoteTransactions, + totalCount: 1 + } + }) + + await wrapper.vm.fetchTransactions() + + expect(wrapper.vm.fetchedTransactions).toEqual(['placeholder']) + expect(wrapper.vm.totalCount).toEqual(31) + }) + }) + + describe('refreshStatus', () => { + beforeEach(() => { + publicKeyFromWalletMock.mockReset() + publicKeyFromWalletMock.mockReturnValue('publicKey') + }) + + it('should do nothing if no wallet', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + wrapper.vm.mockWalletRoute = null + await wrapper.vm.refreshStatus() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should update transaction notice if new transactions', async () => { + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions: [{ id: 'test' }] }) + + await wrapper.vm.refreshStatus() + + expect(wrapper.vm.newTransactionsNotice).toBe('WALLET_TRANSACTIONS.NEW_TRANSACTIONS') + }) + + it('should not update transaction notice if no new transactions', async () => { + const transactions = [{ id: 'test' }] + wrapper.vm.fetchedTransactions = transactions + jest.spyOn(wrapper.vm, 'getTransactions').mockReturnValue({ transactions }) + + await wrapper.vm.refreshStatus() + + expect(wrapper.vm.newTransactionsNotice).not.toBe('WALLET_TRANSACTIONS.NEW_TRANSACTIONS') + }) + + it('should do nothing if no publicKey', async () => { + const spy = jest.spyOn(wrapper.vm, 'getTransactions') + + publicKeyFromWalletMock.mockReturnValue(null) + await wrapper.vm.refreshStatus() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should do nothing on error', async () => { + wrapper.vm.newTransactionsNotice = 'test' + const error = new Error('failed web request') + jest.spyOn(wrapper.vm, 'getTransactions').mockImplementation(() => { + throw error + }) + + await wrapper.vm.refreshStatus() + + expect(loggerErrorMock).toHaveBeenCalledWith('Failed to update confirmations: ', error) + expect(wrapper.vm.newTransactionsNotice).toBe('test') + }) + }) + }) +}) diff --git a/__tests__/unit/components/Wallet/WalletTransactions/mixin.spec.js b/__tests__/unit/components/Wallet/WalletTransactions/mixin.spec.js new file mode 100644 index 0000000000..a2034265e8 --- /dev/null +++ b/__tests__/unit/components/Wallet/WalletTransactions/mixin.spec.js @@ -0,0 +1,539 @@ +import { createLocalVue, mount } from '@vue/test-utils' +import installI18n from '../../../__utils__/i18n' +import { WalletTransactions, WalletTransactionsMultiSignature } from '@/components/Wallet/WalletTransactions' + +// Do not mock WalletService +jest.mock('@/services/wallet', () => jest.requireActual('@/services/wallet')) + +const localVue = createLocalVue() +const i18n = installI18n(localVue) +const sampleTransactions = Object.freeze([]) + +let wrapper +let errorMock +let successMock +let loggerErrorMock +let dispatchMock +const createWrapper = (component, gettersTransactions, gettersTableRowCount) => { + component = component || WalletTransactions + gettersTransactions = gettersTransactions === undefined ? sampleTransactions : gettersTransactions + gettersTableRowCount = gettersTableRowCount === undefined ? 10 : gettersTableRowCount + + errorMock = jest.fn() + successMock = jest.fn() + loggerErrorMock = jest.fn() + dispatchMock = jest.fn() + + wrapper = mount(component, { + i18n, + localVue, + mocks: { + $error: errorMock, + $success: successMock, + $logger: { + error: loggerErrorMock + }, + $store: { + dispatch: dispatchMock, + getters: { + get 'transaction/byAddress' () { + return gettersTransactions + }, + get 'session/transactionTableRowCount' () { + return gettersTableRowCount + } + } + }, + session_profile: { + id: 'profile-1' + } + }, + mixins: [{ + data: () => ({ + mockWalletRoute: 1, + mockWalletRouteBusinessName: 'business-name' + }), + computed: { + wallet_fromRoute () { + return !this.mockWalletRoute ? null : { + address: `address-${this.mockWalletRoute}`, + business: { + name: this.mockWalletRouteBusinessName, + website: 'http://business.website', + publicKey: `public-key-${this.mockWalletRoute}`, + resigned: false + }, + publicKey: `public-key-${this.mockWalletRoute}` + } + } + } + }], + stubs: { + TransactionTable: '
' + } + }) +} + +describe.each([ + ['WalletTransactions', WalletTransactions], + ['WalletTransactionsMultiSignature', WalletTransactionsMultiSignature] +])('%s', (componentName, component) => { + beforeEach(() => { + createWrapper(component) + }) + + describe('template', () => { + describe('TransactionTable', () => { + it('should render', () => { + expect(wrapper.contains('.TransactionTable')).toBe(true) + }) + + it('should pass correct props', async () => { + wrapper.vm.isLoading = false + + const props = wrapper.find('.TransactionTable').vm.$attrs + + await wrapper.vm.$nextTick() + + const expectedProps = { + 'current-page': 1, + rows: [], + 'total-rows': 0, + 'is-loading': false, + 'is-remote': true, + 'has-pagination': false, + 'sort-query': { + field: 'timestamp', + type: 'desc' + }, + 'per-page': 10 + } + + if (componentName === 'WalletTransactionsMultiSignature') { + expectedProps['is-remote'] = false + } + + expect(props).toEqual(expectedProps) + + if (componentName === 'WalletTransactions') { + expect(props['transaction-type']).toBe(undefined) + } + }) + + it('should pass updated props', async () => { + createWrapper(component, undefined, 20) + wrapper.setProps({ + transactionType: 7 + }) + + wrapper.vm.currentPage = 3 + wrapper.vm.fetchedTransactions = ['test'] + wrapper.vm.totalCount = 12 + wrapper.vm.isLoading = true + wrapper.vm.queryParams.sort = { + field: 'amount', + type: 'asc' + } + + const props = wrapper.find('.TransactionTable').vm.$attrs + + await wrapper.vm.$nextTick() + + const expectedProps = { + 'current-page': 3, + rows: ['test'], + 'total-rows': 12, + 'is-loading': true, + 'is-remote': true, + 'has-pagination': true, + 'sort-query': { + field: 'amount', + type: 'asc' + }, + 'per-page': 20 + } + + if (componentName === 'WalletTransactionsMultiSignature') { + expectedProps['is-remote'] = false + expectedProps['has-pagination'] = false + } + + expect(props).toEqual(expectedProps) + + if (componentName === 'WalletTransactions') { + expect(wrapper.find('.TransactionTable').props('transactionType')).toBe(7) + } + }) + }) + }) + + describe('newTransactionsNotice', () => { + it('should not show notice bar if not set', () => { + wrapper.vm.newTransactionsNotice = 'TEST NOTICE' + + expect(wrapper.contains('.WalletTransactions__notice')).toBe(true) + + wrapper.vm.newTransactionsNotice = null + + expect(wrapper.contains('.WalletTransactions__notice')).toBe(false) + }) + + it('should output new transactions notice when set', () => { + expect(wrapper.contains('.WalletTransactions__notice')).toBe(false) + + wrapper.vm.newTransactionsNotice = 'TEST NOTICE' + + expect(wrapper.contains('.WalletTransactions__notice')).toBe(true) + expect(wrapper.find('.WalletTransactions__notice').text()).toBe('TEST NOTICE') + }) + }) + + describe('watch wallet_fromRoute', () => { + it('should reset when wallet route changes', async () => { + const spyLoadTransactions = jest.spyOn(wrapper.vm, 'loadTransactions').mockImplementation(jest.fn()) + const spyReset = jest.spyOn(wrapper.vm, 'reset').mockImplementation() + + wrapper.setData({ + mockWalletRoute: 2 + }) + + expect(wrapper.vm.wallet_fromRoute.address).toEqual('address-2') + + expect(spyLoadTransactions).toHaveBeenCalledTimes(1) + expect(spyReset).toHaveBeenCalledTimes(1) + }) + }) + + describe('computed', () => { + describe('sortQuery', () => { + it('should get data from queryParams', () => { + expect(wrapper.vm.sortQuery).toEqual({ + field: wrapper.vm.queryParams.sort.field, + type: wrapper.vm.queryParams.sort.type + }) + + wrapper.vm.queryParams.sort.field = 'test' + wrapper.vm.queryParams.sort.type = 'test' + + expect(wrapper.vm.sortQuery).toEqual({ + field: wrapper.vm.queryParams.sort.field, + type: wrapper.vm.queryParams.sort.type + }) + }) + }) + + describe('transactionTableRowCount', () => { + it('should get data from session', () => { + createWrapper(null, undefined, 50) + + expect(wrapper.vm.transactionTableRowCount).toBe(50) + + createWrapper(null, undefined, 20) + + expect(wrapper.vm.transactionTableRowCount).toBe(20) + }) + + it('should get data from session', () => { + expect(wrapper.vm.transactionTableRowCount).toBe(10) + + wrapper.vm.transactionTableRowCount = 50 + + expect(dispatchMock).toHaveBeenCalledWith('session/setTransactionTableRowCount', 50) + expect(dispatchMock).toHaveBeenCalledWith('profile/update', { + id: 'profile-1', + transactionTableRowCount: 50 + }) + }) + }) + }) + + describe('watch', () => { + describe('wallet_fromRoute', () => { + it('should reset when wallet changes', async () => { + const spy = jest.spyOn(wrapper.vm, 'reset') + + spy.mockReset() + wrapper.vm.mockWalletRoute++ + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + + it('should refresh status if wallet has not changed', async () => { + const spy = jest.spyOn(wrapper.vm, 'refreshStatus') + + spy.mockReset() + wrapper.vm.mockWalletRouteBusinessName = 'new business' + + await wrapper.vm.$nextTick() + + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + }) + + describe('methods', () => { + describe('loadTransactions', () => { + it('should not run if no wallet', async () => { + const spy = jest.spyOn(wrapper.vm, 'fetchTransactions') + + wrapper.vm.mockWalletRoute = 0 + await wrapper.vm.loadTransactions() + await wrapper.vm.$nextTick() + + expect(spy).not.toHaveBeenCalled() + }) + + it('should not run if already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'fetchTransactions') + + wrapper.vm.isFetching = true + await wrapper.vm.loadTransactions() + + expect(spy).toHaveBeenCalledTimes(0) + }) + + it('should run if wallet and not already fetching', async () => { + const spy = jest.spyOn(wrapper.vm, 'fetchTransactions') + + await wrapper.vm.loadTransactions() + + expect(wrapper.vm.isFetching).toBe(false) + expect(wrapper.vm.wallet_fromRoute).toBeTruthy() + expect(spy).toHaveBeenCalledTimes(1) + }) + }) + + if (componentName === 'WalletTransactions') { + describe('onPageChange', () => { + it('should update page data', () => { + const updateParamsSpy = jest.spyOn(wrapper.vm, '__updateParams') + const loadTransactionsSpy = jest.spyOn(wrapper.vm, 'loadTransactions') + + expect(wrapper.vm.currentPage).toBe(1) + + wrapper.vm.onPageChange({ currentPage: 10 }) + + expect(wrapper.vm.currentPage).toBe(10) + expect(wrapper.vm.queryParams.page).toBe(10) + expect(updateParamsSpy).toHaveBeenCalledTimes(1) + expect(updateParamsSpy).toHaveBeenCalledWith({ page: 10 }) + expect(loadTransactionsSpy).toHaveBeenCalledTimes(1) + }) + + it('should do nothing if invalid page', () => { + const updateParamsSpy = jest.spyOn(wrapper.vm, '__updateParams') + const loadTransactionsSpy = jest.spyOn(wrapper.vm, 'loadTransactions') + + expect(wrapper.vm.currentPage).toBe(1) + + wrapper.vm.onPageChange({ currentPage: null }) + + expect(wrapper.vm.currentPage).toBe(1) + expect(wrapper.vm.queryParams.page).toBe(1) + expect(updateParamsSpy).not.toHaveBeenCalled() + expect(loadTransactionsSpy).not.toHaveBeenCalled() + }) + }) + + describe('onPerPageChange', () => { + it('should update page data', () => { + const updateParamsSpy = jest.spyOn(wrapper.vm, '__updateParams') + const loadTransactionsSpy = jest.spyOn(wrapper.vm, 'loadTransactions') + + wrapper.vm.queryParams.page = 11 + expect(wrapper.vm.queryParams.limit).toBe(10) + + wrapper.vm.onPerPageChange({ currentPerPage: 20 }) + + expect(wrapper.vm.queryParams.limit).toBe(20) + expect(wrapper.vm.queryParams.page).toBe(1) + expect(updateParamsSpy).toHaveBeenCalledTimes(1) + expect(updateParamsSpy).toHaveBeenCalledWith({ limit: 20, page: 1 }) + expect(loadTransactionsSpy).toHaveBeenCalledTimes(1) + expect(dispatchMock).toHaveBeenCalledWith('session/setTransactionTableRowCount', 20) + }) + + it('should do nothing if invalid page', () => { + const updateParamsSpy = jest.spyOn(wrapper.vm, '__updateParams') + const loadTransactionsSpy = jest.spyOn(wrapper.vm, 'loadTransactions') + + wrapper.vm.queryParams.page = 11 + expect(wrapper.vm.queryParams.limit).toBe(10) + + wrapper.vm.onPerPageChange({ currentPerPage: null }) + + expect(wrapper.vm.queryParams.limit).toBe(10) + expect(wrapper.vm.queryParams.page).toBe(11) + expect(updateParamsSpy).not.toHaveBeenCalled() + expect(loadTransactionsSpy).not.toHaveBeenCalled() + expect(dispatchMock).not.toHaveBeenCalledWith('session/setTransactionTableRowCount', 20) + }) + }) + } + + describe('onSortChange', () => { + let spyUpdateParams, spyLoadTransactions + + beforeEach(() => { + spyUpdateParams = jest.spyOn(wrapper.vm, '__updateParams') + spyLoadTransactions = jest.spyOn(wrapper.vm, 'loadTransactions') + }) + + it('should update sort column', () => { + wrapper.vm.onSortChange({ + source: 'transactionsTab', + field: 'amount', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + + const expectedUpdateParams = { + sort: { + field: 'amount', + type: 'desc' + } + } + + if (componentName === 'WalletTransactions') { + expectedUpdateParams.page = 1 + expect(spyLoadTransactions).toHaveBeenCalledTimes(1) + } + + expect(spyUpdateParams).toHaveBeenCalledWith(expectedUpdateParams) + }) + + it('should update sort column direction', () => { + wrapper.vm.onSortChange({ + source: 'transactionsTab', + field: 'timestamp', + type: 'asc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(1) + + const expectedUpdateParams = { + sort: { + field: 'timestamp', + type: 'asc' + } + } + + if (componentName === 'WalletTransactions') { + expectedUpdateParams.page = 1 + expect(spyLoadTransactions).toHaveBeenCalledTimes(1) + } + + expect(spyUpdateParams).toHaveBeenCalledWith(expectedUpdateParams) + }) + + it('should not do anything if source is falsy', () => { + wrapper.vm.onSortChange({ + source: null, + field: 'amount', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(0) + expect(spyLoadTransactions).toHaveBeenCalledTimes(0) + }) + + it('should not do anything if source is not transactionsTab', () => { + wrapper.vm.onSortChange({ + source: 'notTransactionsTab', + field: 'amount', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(0) + expect(spyLoadTransactions).toHaveBeenCalledTimes(0) + }) + + it('should not do anything if column and direction do not change', () => { + wrapper.vm.onSortChange({ + source: 'transactionsTab', + field: 'timestamp', + type: 'desc' + }) + + expect(spyUpdateParams).toHaveBeenCalledTimes(0) + expect(spyLoadTransactions).toHaveBeenCalledTimes(0) + }) + }) + + describe('reset', () => { + if (componentName === 'WalletTransactions') { + it('should reset values', () => { + wrapper.vm.currentPage = 10 + wrapper.vm.queryParams.page = 10 + wrapper.vm.totalCount = 10 + wrapper.vm.fetchedTransactions = [ + 'fake entry' + ] + + wrapper.vm.reset() + + expect(wrapper.vm.currentPage).toBe(1) + expect(wrapper.vm.queryParams.page).toBe(1) + expect(wrapper.vm.totalCount).toBe(0) + expect(wrapper.vm.fetchedTransactions).toEqual([]) + }) + } else { + it('should reset values', () => { + wrapper.vm.newTransactionsNotice = 'TEST NOTICE' + wrapper.vm.totalCount = 10 + wrapper.vm.fetchedTransactions = [ + 'fake entry' + ] + + wrapper.vm.reset() + + expect(wrapper.vm.newTransactionsNotice).toBe(null) + expect(wrapper.vm.totalCount).toBe(0) + expect(wrapper.vm.fetchedTransactions).toEqual([]) + }) + } + }) + + describe('__updateParams', () => { + const expected = { + page: 1, + limit: 10, + sort: { + field: 'timestamp', + type: 'desc' + } + } + + it('should update query parameters', () => { + const params = { + page: 10, + limit: 100, + sort: { + field: 'amount', + type: 'asc' + } + } + wrapper.vm.__updateParams(params) + + expect(wrapper.vm.queryParams).toEqual(params) + }) + + it('should not update if invalid value', () => { + wrapper.vm.__updateParams(null) + + expect(wrapper.vm.queryParams).toEqual(expected) + + wrapper.vm.__updateParams([]) + + expect(wrapper.vm.queryParams).toEqual(expected) + + wrapper.vm.__updateParams('test') + + expect(wrapper.vm.queryParams).toEqual(expected) + }) + }) + }) +}) diff --git a/__tests__/unit/services/client.spec.js b/__tests__/unit/services/client.spec.js index fb13e7864d..f0e65ef3e5 100644 --- a/__tests__/unit/services/client.spec.js +++ b/__tests__/unit/services/client.spec.js @@ -1,22 +1,41 @@ import { cloneDeep } from 'lodash' import nock from 'nock' +import { Identities, Managers, Transactions } from '@arkecosystem/crypto' import errorCapturer from '../__utils__/error-capturer' import fixtures from '../__fixtures__/services/client' import ClientService from '@/services/client' import BigNumber from '@/plugins/bignumber' +import store from '@/store' +import logger from 'electron-log' +import TransactionService from '@/services/transaction' +import WalletService from '@/services/wallet' + +const sessionNetwork = Object.freeze({ + nethash: 'test-nethash', + constants: { + epoch: '2017-03-21T13:00:00.000Z', + aip11: false + }, + vendorField: { + maxLength: 64 + } +}) jest.mock('@/store', () => ({ + // __mock__: { + + // }, getters: { - 'session/network': { - constants: { - epoch: '2017-03-21T13:00:00.000Z' - } + 'session/profile': { + id: 'test-profile' }, + 'session/network': {}, 'network/byId': (id) => { let version = 23 if (id === 'ark.devnet') { version = 30 } + return { constants: { epoch: '2017-03-21T13:00:00.000Z' @@ -33,22 +52,82 @@ jest.mock('@/store', () => ({ } } }, - 'transaction/staticFee': (type) => { - const fees = [ - 0.1 * 1e8, - 5 * 1e8, - 25 * 1e8, - 1 * 1e8 - ] - - return fees[type] - } + 'transaction/staticFee': (type, group = 1) => { + const fees = { + 1: [ + 0.1 * 1e8, // Transfer + 5 * 1e8, // Second signautre + 25 * 1e8, // Delegate registration + 1 * 1e8, // Vote + 5 * 1e8, // Multisignature + 5 * 1e8, // IPFS + 1 * 1e8, // Multi-payment + 25 * 1e8 // Delegate resignation + ], + 2: [ + 50 * 1e8, // Business Registration + 50 * 1e8, // Business Resignation + 50 * 1e8, // Business Update + 50 * 1e8, // Bridgechain Registration + 50 * 1e8, // Bridgechain Resignation + 50 * 1e8 // Bridgechain Update + ] + } + + return fees[group][type] + }, + 'peer/current': () => ({ + ip: '1.1.1.1', + port: '8080', + isHttps: false + }), + 'peer/broadcastPeers': () => [ + { + ip: '1.1.1.1', + port: '8080', + isHttps: false + }, + { + ip: '2.2.2.2', + port: '8080', + isHttps: false + } + ] }, dispatch: jest.fn(), - watch: jest.fn() + watch: jest.fn((getter, callback, options) => { + // getter() + // require('@/store').__mock__.watch = { + // getter: getter(), + // getter: callback(), + // options + // } + }) })) +const setAip11AndSpy = (enabled = true, spy = true) => { + const network = { + ...sessionNetwork, + constants: { + ...sessionNetwork.constants, + aip11: enabled + } + } + + Managers.configManager.getMilestone().aip11 = enabled + store.getters['session/network'] = network + + if (!spy) { + return + } + + return jest.spyOn(store.getters, 'network/byId').mockReturnValue(network) +} + beforeEach(() => { + Managers.configManager.setFromPreset('testnet') + store.getters['session/network'] = cloneDeep(sessionNetwork) + nock.cleanAll() }) @@ -83,167 +162,271 @@ describe('Services > Client', () => { const getWalletEndpoint = jest.fn(generateWalletResponse) - const fees = [ - 0.1 * 1e8, - 5 * 1e8, - 25 * 1e8, - 1 * 1e8 - ] + const fees = { + 1: [ + 0.1 * 1e8, // Transfer + 5 * 1e8, // Second signautre + 25 * 1e8, // Delegate registration + 1 * 1e8, // Vote + 5 * 1e8, // Multisignature + 5 * 1e8, // IPFS + 1 * 1e8, // Multi-payment + 25 * 1e8 // Delegate resignation + ], + 2: [ + 50 * 1e8, // Business Registration + 50 * 1e8, // Business Resignation + 50 * 1e8, // Business Update + 50 * 1e8, // Bridgechain Registration + 50 * 1e8, // Bridgechain Resignation + 50 * 1e8 // Bridgechain Update + ] + } beforeEach(() => { client = new ClientService() client.host = 'http://127.0.0.1:4003' }) - describe('fetchWallet', () => { - const data = { - address: 'address', - balance: '1202', - publicKey: 'public key' - } - const wallet = { - body: { - data: { - ...data, - isDelegate: true, - username: 'test' - } - } + describe('constructor', () => { + it('should not watch profile if false', () => { + const watchProfileOriginal = ClientService.__watchProfile + ClientService.__watchProfile = jest.fn() + + client = new ClientService(false) + + expect(ClientService.__watchProfile).not.toHaveBeenCalled() + ClientService.__watchProfile = watchProfileOriginal + }) + }) + + describe('normalizePassphrase', () => { + it('should normalize if provided', () => { + const spy = jest.spyOn(String.prototype, 'normalize') + + const passphrase = client.normalizePassphrase('test') + + expect(spy).toHaveBeenNthCalledWith(1, 'NFD') + expect(passphrase).toBe('test') + + spy.mockRestore() + }) + + it('should not normalize if no passphrase', () => { + const spy = jest.spyOn(String.prototype, 'normalize') + + client.normalizePassphrase(null) + + expect(spy).not.toHaveBeenCalled() + + spy.mockRestore() + }) + }) + + describe('newConnection', () => { + it('should create a new connection', () => { + const connection = ClientService.newConnection('http://localhost') + + expect(connection.host).toBe('http://localhost/api/v2') + }) + + it('should set timeout if provided', () => { + const connection = ClientService.newConnection('http://localhost', 100) + + expect(connection.opts.timeout).toBe(100) + }) + + it('should use default 5000ms timeout if not provided', () => { + const connection = ClientService.newConnection('http://localhost') + + expect(connection.opts.timeout).toBe(5000) + }) + + it('should throw error if no server', () => { + expect(() => ClientService.newConnection(null)).toThrow() + }) + }) + + describe('fetchNetworkConfig', () => { + const mockEndpoint = (config) => { + nock('http://127.0.0.1') + .get('/api/v2/node/configuration') + .reply(200, { + data: config + }) } - const account = { - data: { - success: true, - account: { - ...data, - unconfirmedBalance: 'NO', - unconfirmedSignature: 'NO', - secondSignature: 'NO', - multisignatures: 'NO', - u_multisignatures: 'NO' - } + + it('should fetch network config', async () => { + const networkConfig = { + nethash: 'nethash' } - } - beforeEach(() => { - const resource = resource => { - if (resource === 'accounts') { - return { - get: () => account - } - } else if (resource === 'wallets') { - return { - get: () => wallet - } + mockEndpoint(networkConfig) + + expect(await ClientService.fetchNetworkConfig('http://127.0.0.1')).toEqual(networkConfig) + }) + + it('should update network store', async () => { + const networkConfig = { + nethash: 'test-nethash', + constants: { + vendorFieldLength: 10 } } - client.client.api = resource - }) + mockEndpoint(networkConfig) - describe('when version is 2', () => { - it('should return almost all properties from the wallet endpoint', async () => { - const wallet = await client.fetchWallet('address') - expect(wallet).toHaveProperty('address', data.address) - expect(wallet).toHaveProperty('balance', data.balance) - expect(wallet).toHaveProperty('publicKey', data.publicKey) - expect(wallet).toHaveProperty('isDelegate', true) + await ClientService.fetchNetworkConfig('http://127.0.0.1') + const spy = jest.spyOn(store, 'dispatch') + + expect(spy).toHaveBeenCalledWith('network/update', { + ...sessionNetwork, + vendorField: { + ...sessionNetwork.vendorField, + maxLength: 10 + } }) + + spy.mockRestore() }) - }) - describe('fetchWallets', () => { - const walletAddresses = ['address1', 'address2'] - const walletsResponse = { - body: { - data: [ - { - ...wallets[0], - isDelegate: true, - username: 'test' - }, - { - ...wallets[1], - isDelegate: false, - username: null - } - ] - } - } - const searchWalletEndpoint = jest.fn(() => walletsResponse) - beforeEach(() => { - const resource = resource => { - if (resource === 'wallets') { - return { - get: getWalletEndpoint, - search: searchWalletEndpoint - } + it('should not update network store if vendorfield is the same', async () => { + const networkConfig = { + nethash: 'test-nethash', + constants: { + vendorFieldLength: 64 } } - client.client.api = jest.fn(resource) + mockEndpoint(networkConfig) + + await ClientService.fetchNetworkConfig('http://127.0.0.1') + const spy = jest.spyOn(store, 'dispatch') + + expect(spy).not.toHaveBeenCalled() + + spy.mockRestore() }) + }) - it('should call the wallet search endpoint', async () => { - const fetchedWallets = await client.fetchWallets(walletAddresses) + describe('fetchNetworkCrypto', () => { + it('should fetch data from crypto endpoint', async () => { + const cryptoConfig = { + exceptions: {}, + genesisBlock: {}, + milestones: {}, + network: {} + } - expect(client.client.api).toHaveBeenNthCalledWith(1, 'wallets') - expect(searchWalletEndpoint).toHaveBeenNthCalledWith(1, { addresses: walletAddresses }) - expect(fetchedWallets).toEqual(walletsResponse.body.data) + nock('http://127.0.0.1') + .get('/api/v2/node/configuration/crypto') + .reply(200, { + data: cryptoConfig + }) + + expect(await ClientService.fetchNetworkCrypto('http://127.0.0.1')).toEqual(cryptoConfig) }) }) - describe('fetchWalletVote', () => { - const voteDelegate = 'voted delegate' + describe('fetchFeeStatistics', () => { + const mockEndpoint = (config) => { + nock('http://127.0.0.1') + .get('/api/v2/node/fees') + .query({ days: 7 }) + .reply(200, { + data: config + }) + } - beforeEach(() => { - const resource = resource => { - if (resource === 'wallets') { - return { - get: generateWalletResponse + it('should fetch fees for updated endpoint schema', async () => { + mockEndpoint({ + 1: { + transfer: { + avg: '9434054', + max: '10000000', + min: '500000', + sum: '1962283291' } } - } + }) - client.client.api = resource + expect(await ClientService.fetchFeeStatistics('http://127.0.0.1')).toEqual({ + 1: [ + { + type: 0, + fees: { + minFee: 500000, + maxFee: 10000000, + avgFee: 9434054 + } + } + ] + }) }) - it('should return delegate public key if wallet is voting', async () => { - const response = await client.fetchWalletVote('address1') - expect(response).toBe(voteDelegate) + it('should fetch fees for old endpoint schema', async () => { + mockEndpoint([ + { + type: 0, + avg: '9434054', + max: '10000000', + min: '500000', + sum: '1962283291' + } + ]) + + expect(await ClientService.fetchFeeStatistics('http://127.0.0.1')).toEqual([ + { + type: 0, + fees: { + minFee: 500000, + maxFee: 10000000, + avgFee: 9434054 + } + } + ]) }) - it('should return null if wallet is not voting', async () => { - const response = await client.fetchWalletVote('address2') - expect(response).toBeNull() + it('should return empty array on error', async () => { + nock('http://127.0.0.1') + .get('/api/v2/node/fees') + .query({ days: 7 }) + .reply(500) + + expect(await ClientService.fetchFeeStatistics('http://127.0.0.1')).toEqual([]) }) }) - describe('fetchWalletVotes', () => { - const transactions = [{ - asset: { - votes: ['+test'] - } - }, { - asset: { - votes: ['+test2'] - } - }] + describe('host getter/setter', () => { + it('should return formatted host', () => { + client.host = 'http://6.6.6.6' - beforeEach(() => { - const resource = resource => { - if (resource === 'wallets') { - return { - votes: () => ({ body: { data: transactions } }) - } - } - } + expect(client.host).toEqual('http://6.6.6.6/api/v2') + }) - client.client.api = resource + it('should update current client', () => { + client.host = 'http://7.7.7.7' + + expect(client.client.host).toEqual('http://7.7.7.7/api/v2') }) + }) - it('should return vote transactions', async () => { - const response = await client.fetchWalletVotes() - expect(response).toBe(transactions) + describe('fetchPeerStatus', () => { + it('should fetch data from syncing endpoint', async () => { + const response = { + syncing: false, + blocks: -7, + height: 10969757, + id: 'e1315d04c74bb7edc0a0c902e399eafb2e6cf12c0d36e8ecd692408a14d9dda3' + } + + nock('http://127.0.0.1:4003') + .get('/api/v2/node/syncing') + .reply(200, { + data: response + }) + + expect(await client.fetchPeerStatus()).toEqual(response) }) }) @@ -279,49 +462,52 @@ describe('Services > Client', () => { }) }) - describe('fetchStaticFees', () => { - const data = fixtures.staticFeeResponses.v2.data + describe('fetchDelegateVoters', () => { + it('should fetch data from syncing endpoint', async () => { + const meta = { + totalCount: 531 + } - beforeEach(() => { - const resource = resource => { - if (resource === 'transactions') { - return { - fees: () => ({ body: fixtures.staticFeeResponses.v2 }) - } + nock('http://127.0.0.1:4003') + .get('/api/v2/delegates/USERNAME/voters') + .reply(200, { + meta, + data: {} + }) + + expect(await client.fetchDelegateVoters({ username: 'USERNAME' })).toEqual(meta.totalCount) + }) + }) + + describe('fetchDelegateForged', () => { + it('should return the forged property from the given delegate', async () => { + const forged = await client.fetchDelegateForged({ + publicKey: 'dummyKey', + forged: { + total: 100 } - } + }) - client.client.api = resource + expect(forged).toEqual(100) }) - it('should return and match fees to types', async () => { - const response = await client.fetchStaticFees() + it('should return 0 if no forged data', async () => { + const forged = await client.fetchDelegateForged({ + publicKey: 'dummyKey' + }) - expect(response[0]).toEqual(data.transfer) - expect(response[1]).toEqual(data.secondSignature) - expect(response[2]).toEqual(data.delegateRegistration) - expect(response[3]).toEqual(data.vote) - expect(response[4]).toEqual(data.multiSignature) - expect(response[5]).toEqual(data.ipfs) - expect(response[6]).toEqual(data.timelockTransfer) - expect(response[7]).toEqual(data.multiPayment) - expect(response[8]).toEqual(data.delegateResignation) + expect(forged).toEqual('0') }) }) - describe('fetchDelegateForged', () => { - const delegateV2 = { - publicKey: 'dummyKey', - forged: { - total: 100 - } - } + describe('fetchStaticFees', () => { + const data = fixtures.staticFeeResponses.v2.data beforeEach(() => { const resource = resource => { - if (resource === 'delegates') { + if (resource === 'transactions') { return { - forged: () => ({ body: { forged: 200, success: true } }) + fees: () => ({ body: fixtures.staticFeeResponses.v2 }) } } } @@ -329,9 +515,20 @@ describe('Services > Client', () => { client.client.api = resource }) - it('should return the forged property from the given delegate', async () => { - const forged = await client.fetchDelegateForged(delegateV2) - expect(forged).toEqual(100) + it('should return and match fees to types', async () => { + const response = await client.fetchStaticFees() + + expect(response.transfer).toBe(data.transfer) + expect(response.secondSignature).toBe(data.secondSignature) + expect(response.delegateRegistration).toBe(data.delegateRegistration) + expect(response.vote).toBe(data.vote) + expect(response.multiSignature).toBe(data.multiSignature) + expect(response.ipfs).toBe(data.ipfs) + expect(response.multiPayment).toBe(data.multiPayment) + expect(response.delegateResignation).toBe(data.delegateResignation) + expect(response.htlcLock).toBe(data.htlcLock) + expect(response.htlcClaim).toBe(data.htlcClaim) + expect(response.htlcRefund).toBe(data.htlcRefund) }) }) @@ -372,6 +569,31 @@ describe('Services > Client', () => { }) }) + describe('fetchBusinessBridgechains', () => { + it('should fetch data from bridgechains endpoint', async () => { + const response = { + meta: { + totalCount: 137 + }, + data: [ + { + address: 'address-1', + publicKey: 'publicKey-1', + name: 'Business Name', + website: 'http://t-explorer.ark.io', + isResigned: false + } + ] + } + + nock('http://127.0.0.1:4003') + .get('/api/v2/businesses/BUSINESS_ID/bridgechains') + .reply(200, response) + + expect(await client.fetchBusinessBridgechains('BUSINESS_ID')).toEqual(response) + }) + }) + describe('fetchWalletTransactions', () => { const { data, meta } = fixtures.transactions @@ -380,7 +602,18 @@ describe('Services > Client', () => { const resource = resource => { if (resource === 'wallets') { return { - transactions: () => ({ body: { data: transactions, meta: { totalCount: meta.count } } }) + transactions: (_, queryOptions) => { + const transactionsResponse = transactions.filter(t => !queryOptions.type || (queryOptions.type && t.type === queryOptions.type)) + + return { + body: { + data: transactionsResponse, + meta: { + totalCount: meta.count + } + } + } + } } } } @@ -409,6 +642,13 @@ describe('Services > Client', () => { expect(transaction).not.toHaveProperty('recipientId') }) }) + + it('should filter transactions by transaction type', async () => { + const response = await client.fetchWalletTransactions('address', { transactionType: 1 }) + + expect(response.transactions.length).toBe(1) + expect(response.transactions[0].type).toBe(1) + }) }) describe('fetchTransactionsForWallets', () => { @@ -444,77 +684,1886 @@ describe('Services > Client', () => { expect(searchTransactionsEndpoint).toHaveBeenNthCalledWith(1, { addresses: walletAddresses }) expect(fetchedWallets).toEqual(walletTransactions) }) - }) - describe('buildDelegateRegistration', () => { - describe('when the fee is bigger than the static fee', () => { - it('should throw an Error', async () => { - const fee = new BigNumber(fees[2] + 1) - expect(await errorCapturer(client.buildDelegateRegistration({ fee }))).toThrow(/fee/) + it('should log error if api fails', async () => { + const spy = jest.spyOn(logger, 'error').mockImplementation() + jest.spyOn(client, 'fetchWalletTransactions').mockReturnValue({}) + + await client.fetchTransactionsForWallets(['address-1'], null) + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should fall back to fetchWalletTransactions if search endpoint fails', async () => { + const spy = jest.spyOn(client, 'fetchWalletTransactions').mockReturnValue({ + transactions: ['test'] }) + + const response = await client.fetchTransactionsForWallets(['address-1']) + + expect(spy).toHaveBeenCalledWith('address-1', {}) + expect(response['address-1']).toEqual(['test']) }) - describe('when the fee is smaller or equal to the static fee (25)', () => { + it('should log error for each fetchWalletTransactions failure', async () => { + const spy = jest.spyOn(logger, 'error').mockImplementation() + const error = new Error('Wallet not found') + jest.spyOn(client, 'fetchWalletTransactions').mockImplementation(() => { + throw error + }) + + const response = await client.fetchTransactionsForWallets(['address-1', 'address-2']) + + expect(spy).toHaveBeenNthCalledWith(2, error) + expect(response).toEqual({}) + + spy.mockRestore() + }) + + it('should throw error on fetchWalletTransactions failure (not "Wallet not found")', async () => { + const spy = jest.spyOn(logger, 'error').mockImplementation() + const error = new Error('oops') + jest.spyOn(client, 'fetchWalletTransactions').mockImplementation(() => { + throw error + }) + + expect(await errorCapturer(client.fetchTransactionsForWallets(['address-1']))).toThrow('oops') + + spy.mockRestore() + }) + }) + + describe('fetchWallet', () => { + const data = { + address: 'address', + balance: '1202', + publicKey: 'public key' + } + const wallet = { + body: { + data: { + ...data, + isDelegate: true, + username: 'test' + } + } + } + const account = { + data: { + success: true, + account: { + ...data, + unconfirmedBalance: 'NO', + unconfirmedSignature: 'NO', + secondSignature: 'NO', + multisignatures: 'NO', + u_multisignatures: 'NO' + } + } + } + + beforeEach(() => { + const resource = resource => { + if (resource === 'accounts') { + return { + get: () => account + } + } else if (resource === 'wallets') { + return { + get: () => wallet + } + } + } + + client.client.api = resource + }) + + describe('when version is 2', () => { + it('should return almost all properties from the wallet endpoint', async () => { + const wallet = await client.fetchWallet('address') + expect(wallet).toHaveProperty('address', data.address) + expect(wallet).toHaveProperty('balance', data.balance) + expect(wallet).toHaveProperty('publicKey', data.publicKey) + expect(wallet).toHaveProperty('isDelegate', true) + }) + }) + }) + + describe('fetchWallets', () => { + const walletAddresses = ['address1', 'address2'] + const walletsResponse = { + body: { + data: [ + { + ...wallets[0], + isDelegate: true, + username: 'test' + }, + { + ...wallets[1], + isDelegate: false, + username: null + } + ] + } + } + const searchWalletEndpoint = jest.fn(() => walletsResponse) + beforeEach(() => { + const resource = resource => { + if (resource === 'wallets') { + return { + get: getWalletEndpoint, + search: searchWalletEndpoint + } + } + } + + client.client.api = jest.fn(resource) + }) + + it('should call the wallet search endpoint', async () => { + const fetchedWallets = await client.fetchWallets(walletAddresses) + + expect(client.client.api).toHaveBeenNthCalledWith(1, 'wallets') + expect(searchWalletEndpoint).toHaveBeenNthCalledWith(1, { addresses: walletAddresses }) + expect(fetchedWallets).toEqual(walletsResponse.body.data) + }) + }) + + describe('fetchWalletVote', () => { + const voteDelegate = 'voted delegate' + + beforeEach(() => { + const resource = resource => { + if (resource === 'wallets') { + return { + get: generateWalletResponse + } + } + } + + client.client.api = resource + }) + + it('should return delegate public key if wallet is voting', async () => { + const response = await client.fetchWalletVote('address1') + expect(response).toBe(voteDelegate) + }) + + it('should return null if wallet is not voting', async () => { + const response = await client.fetchWalletVote('address2') + expect(response).toBeNull() + }) + + it('should log error', async () => { + const spy = jest.spyOn(logger, 'error').mockImplementation() + const error = new Error('Wallet not found') + jest.spyOn(client, 'fetchWallet').mockImplementation(() => { + throw error + }) + + const response = await client.fetchWalletVote('address2') + + expect(spy).toHaveBeenCalledWith(error) + expect(response).toBe(null) + + spy.mockRestore() + }) + + it('should throw if error is not "Wallet not found"', async () => { + const spy = jest.spyOn(logger, 'error').mockImplementation() + const error = new Error('oops') + jest.spyOn(client, 'fetchWallet').mockImplementation(() => { + throw error + }) + + expect(await errorCapturer(client.fetchWalletVote('address2'))).toThrow('oops') + spy.mockRestore() + }) + }) + + describe('fetchWalletVotes', () => { + const transactions = [{ + asset: { + votes: ['+test'] + } + }, { + asset: { + votes: ['+test2'] + } + }] + + beforeEach(() => { + const resource = resource => { + if (resource === 'wallets') { + return { + votes: () => ({ body: { data: transactions } }) + } + } + } + + client.client.api = resource + }) + + it('should return vote transactions', async () => { + const response = await client.fetchWalletVotes() + expect(response).toBe(transactions) + }) + }) + + describe('__parseCurrentPeer', () => { + it('should parse peer from current host', () => { + client.host = 'http://1.1.1.1:8080' + + expect(client.__parseCurrentPeer()).toEqual({ + ip: '1.1.1.1', + port: '8080', + isHttps: false + }) + }) + + it('should parse https', () => { + client.host = 'https://1.1.1.1:8080' + + expect(client.__parseCurrentPeer()).toEqual({ + ip: '1.1.1.1', + port: '8080', + isHttps: true + }) + }) + + it('should default port if not found', () => { + client.host = 'http://1.1.1.1' + + expect(client.__parseCurrentPeer()).toEqual({ + ip: '1.1.1.1', + port: '80', + isHttps: false + }) + + client.host = 'https://1.1.1.1' + + expect(client.__parseCurrentPeer()).toEqual({ + ip: '1.1.1.1', + port: '443', + isHttps: true + }) + }) + }) + + describe('buildVote', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + votes: [ + `+${publicKey}` + ], + fee: new BigNumber(fees[1][3]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should build a valid v1 transaction', async () => { + Managers.configManager.getMilestone().aip11 = false + + const transaction = await client.buildVote(rawTransaction, true) + + expect(transaction.asset.votes).toEqual(rawTransaction.votes) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.recipientId).toEqual(address) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(3) + expect(transaction.version).toEqual(1) + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + Managers.configManager.getMilestone().aip11 = true + + const transaction = await client.buildVote(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.votes).toEqual(rawTransaction.votes) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(3) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const fee = new BigNumber(fees[1][3] + 1) + expect(await errorCapturer(client.buildVote({ fee }))).toThrow(/fee/) + }) + }) + + describe('when the fee is smaller or equal to the static fee fee (0.1)', () => { it('should not throw an Error', async () => { - expect(await errorCapturer(client.buildDelegateRegistration({ fee: new BigNumber(fees[2]) }))).not.toThrow(/fee/) - expect(await errorCapturer(client.buildDelegateRegistration({ fee: new BigNumber(fees[2] - 1) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildVote({ fee: new BigNumber(fees[1][3]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildVote({ fee: new BigNumber(fees[1][3] - 1) }))).not.toThrow(/fee/) }) }) }) - describe('buildSecondSignatureRegistration', () => { + describe('buildDelegateRegistration', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + username: 'bob', + fee: new BigNumber(fees[1][2]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should build a valid v1 transaction', async () => { + setAip11AndSpy(false, false) + + const transaction = await client.buildDelegateRegistration(rawTransaction, true) + + expect(transaction.asset.delegate.username).toEqual(rawTransaction.username) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(2) + expect(transaction.version).toEqual(1) + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildDelegateRegistration(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.delegate.username).toEqual(rawTransaction.username) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(2) + expect(transaction.version).toEqual(2) + }) + }) + describe('when the fee is bigger than the static fee', () => { it('should throw an Error', async () => { - const fee = new BigNumber(fees[1] + 1) - expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee }))).toThrow(/fee/) + const fee = new BigNumber(fees[1][2] + 1) + expect(await errorCapturer(client.buildDelegateRegistration({ fee }))).toThrow(/fee/) }) }) - describe('when the fee is smaller or equal to the static fee (5)', () => { + describe('when the fee is smaller or equal to the static fee (25)', () => { it('should not throw an Error', async () => { - expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee: new BigNumber(fees[1]) }))).not.toThrow(/fee/) - expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee: new BigNumber(fees[1] - 1) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildDelegateRegistration({ fee: new BigNumber(fees[1][2]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildDelegateRegistration({ fee: new BigNumber(fees[1][2] - 1) }))).not.toThrow(/fee/) }) }) }) describe('buildTransfer', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + amount: new BigNumber(100 * 1e8), + fee: new BigNumber(fees[1][0]), + recipientId: address, + vendorField: 'this is a test', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should build a valid v1 transaction', async () => { + setAip11AndSpy(false, false) + + const transaction = await client.buildTransfer(rawTransaction, true) + + expect(transaction.vendorField).toEqual(rawTransaction.vendorField) + expect(transaction.amount).toEqual(rawTransaction.amount.toString()) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(0) + expect(transaction.version).toEqual(1) + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildTransfer(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.vendorField).toEqual(rawTransaction.vendorField) + expect(transaction.amount).toEqual(rawTransaction.amount.toString()) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(0) + expect(transaction.version).toEqual(2) + }) + }) + describe('when a custom network is specified', () => { it('should have the correct version', async () => { + setAip11AndSpy(false, false) const networkId = 'ark.devnet' - const transaction = await client.buildTransfer({ fee: new BigNumber(fees[0]), networkId }, false, true) + const transaction = await client.buildTransfer({ fee: new BigNumber(fees[1][0]), networkId, passphrase: 'test' }, false, true) expect(transaction.data.network).toBe(30) }) }) describe('when the fee is bigger than the static fee', () => { it('should throw an Error', async () => { - const fee = new BigNumber(fees[0] + 1) + const fee = new BigNumber(fees[1][0] + 1) expect(await errorCapturer(client.buildTransfer({ fee }))).toThrow(/fee/) }) }) describe('when the fee is smaller or equal to the static fee (0.1)', () => { it('should not throw an Error', async () => { - expect(await errorCapturer(client.buildTransfer({ fee: new BigNumber(fees[0]) }))).not.toThrow(/fee/) - expect(await errorCapturer(client.buildTransfer({ fee: new BigNumber(fees[0] - 1) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildTransfer({ fee: new BigNumber(fees[1][0]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildTransfer({ fee: new BigNumber(fees[1][0] - 1) }))).not.toThrow(/fee/) }) }) }) - describe('buildVote', () => { - describe('when the fee is bigger than the static fee', () => { - it('should throw an Error', async () => { - const fee = new BigNumber(fees[3] + 1) - expect(await errorCapturer(client.buildVote({ fee }))).toThrow(/fee/) - }) - }) - - describe('when the fee is smaller or equal to the static fee fee (0.1)', () => { + describe('buildSecondSignatureRegistration', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const secondPublicKey = Identities.PublicKey.fromPassphrase('second passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[1][1]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should build a valid v1 transaction', async () => { + setAip11AndSpy(false, false) + + const transaction = await client.buildSecondSignatureRegistration(rawTransaction, true) + + expect(transaction.asset.signature.publicKey).toEqual(secondPublicKey) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(1) + expect(transaction.version).toEqual(1) + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildSecondSignatureRegistration(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.signature.publicKey).toEqual(secondPublicKey) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(1) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const fee = new BigNumber(fees[1][1] + 1) + expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee }))).toThrow(/fee/) + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee: new BigNumber(fees[1][1]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildSecondSignatureRegistration({ fee: new BigNumber(fees[1][1] - 1) }))).not.toThrow(/fee/) + }) + }) + }) + + describe('buildMultiSignature', () => { + const minKeys = 3 + const publicKeys = [] + for (let i = 0; i < 5; i++) { + publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + + const rawTransaction = { + address, + publicKeys, + minKeys, + fee: new BigNumber(fees[1][4]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildMultiSignature(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildMultiSignature(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.multiSignature.publicKeys).toEqual(publicKeys) + expect(transaction.asset.multiSignature.min).toEqual(rawTransaction.minKeys) + expect(transaction.fee + '').toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual('public key of passphrase') + expect(transaction.type).toEqual(4) + expect(transaction.version).toEqual(2) + }) + + it('should return object if required', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildMultiSignature(rawTransaction, true, true) + + spy.mockRestore() + + expect(transaction.constructor.name).toEqual('MultiSignatureBuilder') + expect(transaction.data.asset.multiSignature.publicKeys).toEqual(publicKeys) + expect(transaction.data.asset.multiSignature.min).toEqual(rawTransaction.minKeys) + expect(transaction.data.fee + '').toEqual(rawTransaction.fee.toString()) + expect(transaction.data.senderPublicKey).toEqual('public key of passphrase') + expect(transaction.data.type).toEqual(4) + expect(transaction.data.version).toEqual(2) + }) + + it('should add own wallet to signatures', async () => { + const spy = setAip11AndSpy(true) + const getPublicKeyFromPassphrase = WalletService.getPublicKeyFromPassphrase + WalletService.getPublicKeyFromPassphrase = jest.fn((passphrase) => Identities.PublicKey.fromPassphrase(passphrase)) + + const newPublicKeys = [ + ...publicKeys, + publicKey + ] + const newRawTransaction = { + ...rawTransaction, + publicKeys: newPublicKeys + } + + const transaction = await client.buildMultiSignature(newRawTransaction, true) + + spy.mockRestore() + WalletService.getPublicKeyFromPassphrase = getPublicKeyFromPassphrase + + const publicKeyIndex = newPublicKeys.indexOf(publicKey) + const signature = transaction.signatures.find(s => parseInt(s.substring(0, 2), 16) === publicKeyIndex) + + expect(signature).toBeTruthy() + expect(transaction.asset.multiSignature.publicKeys).toEqual(newPublicKeys) + expect(transaction.asset.multiSignature.min).toEqual(rawTransaction.minKeys) + expect(transaction.fee + '').toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(4) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[1][4] + 1) + expect(await errorCapturer(client.buildMultiSignature({ fee, minKeys, publicKeys }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildMultiSignature({ fee: new BigNumber(fees[1][4]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildMultiSignature({ fee: new BigNumber(fees[1][4] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildIpfs', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[1][5]), + hash: 'QmT9qk3CRYbFDWpDFYeAv8T8H1gnongwKhh5J68NLkLir6', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildIpfs(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should not create transaction with invalid hash', async () => { + const spy = setAip11AndSpy(true) + + const newRawTransaction = { + ...rawTransaction, + hash: 'invalid hash' + } + + expect(await errorCapturer(client.buildIpfs(newRawTransaction, true))).toThrow('Invalid base58 string.') + + spy.mockRestore() + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildIpfs(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.ipfs).toEqual(rawTransaction.hash) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(5) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[1][5] + 1) + expect(await errorCapturer(client.buildIpfs({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildIpfs({ fee: new BigNumber(fees[1][5]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildIpfs({ fee: new BigNumber(fees[1][5] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildMultiPayment', () => { + const recipients = [] + for (let i = 0; i < 5; i++) { + recipients.push({ + address: Identities.Address.fromPassphrase(`passphrase ${i}`, 23), + amount: new BigNumber((i + 1) * 1e8) + }) + } + + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + recipients, + fee: new BigNumber(fees[1][6]), + vendorField: 'this is a test', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildMultiPayment(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildMultiPayment(rawTransaction, true) + + spy.mockRestore() + + for (const recipientIndex in recipients) { + expect(transaction.asset.payments[recipientIndex].recipientId).toEqual(recipients[recipientIndex].address) + expect(transaction.asset.payments[recipientIndex].amount).toEqual(recipients[recipientIndex].amount.toString()) + } + expect(transaction.vendorField).toEqual(rawTransaction.vendorField) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(6) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[1][6] + 1) + expect(await errorCapturer(client.buildMultiPayment({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildMultiPayment({ fee: new BigNumber(fees[1][6]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildMultiPayment({ fee: new BigNumber(fees[1][6] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildDelegateResignation', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[1][7]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildDelegateResignation(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildDelegateResignation(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.type).toEqual(7) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[1][7] + 1) + expect(await errorCapturer(client.buildDelegateResignation({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildDelegateResignation({ fee: new BigNumber(fees[1][7]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildDelegateResignation({ fee: new BigNumber(fees[1][7] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBusinessRegistration', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][0]), + asset: { + name: 'google', + website: 'https://www.google.com', + vat: 'GB123456', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBusinessRegistration(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBusinessRegistration(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.businessRegistration).toEqual(rawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(0) + expect(transaction.version).toEqual(2) + }) + + it('should build a valid v2 transaction without optional data', async () => { + const spy = setAip11AndSpy(true) + + const newRawTransaction = { + ...rawTransaction, + asset: { + name: 'google', + website: 'https://www.google.com' + } + } + const transaction = await client.buildBusinessRegistration(newRawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.businessRegistration).toEqual(newRawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(0) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][0] + 1) + expect(await errorCapturer(client.buildBusinessRegistration({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBusinessRegistration({ fee: new BigNumber(fees[2][0]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBusinessRegistration({ fee: new BigNumber(fees[2][0] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBusinessUpdate', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][2]), + asset: { + name: 'google', + website: 'https://www.google.com', + vat: 'GB123456', + repository: 'https://github.com/arkecosystem/desktop-wallet.git' + }, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBusinessUpdate(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBusinessUpdate(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.businessUpdate).toEqual(rawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(2) + expect(transaction.version).toEqual(2) + }) + + it('should build a valid v2 transaction without optional data', async () => { + const spy = setAip11AndSpy(true) + + const newRawTransaction = { + ...rawTransaction, + asset: { + name: 'google', + website: 'https://www.google.com' + } + } + const transaction = await client.buildBusinessUpdate(newRawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.businessUpdate).toEqual(newRawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(2) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][2] + 1) + expect(await errorCapturer(client.buildBusinessUpdate({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBusinessUpdate({ fee: new BigNumber(fees[2][2]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBusinessUpdate({ fee: new BigNumber(fees[2][2] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBusinessResignation', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][1]), + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBusinessResignation(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBusinessResignation(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(1) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][1] + 1) + expect(await errorCapturer(client.buildBusinessResignation({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBusinessResignation({ fee: new BigNumber(fees[2][1]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBusinessResignation({ fee: new BigNumber(fees[2][1] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBridgechainRegistration', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][3]), + asset: { + name: 'test_bridgechain', + seedNodes: [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4' + ], + ports: { + '@arkecosystem/core-api': 4003 + }, + genesisHash: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + bridgechainRepository: 'https://github.com/arkecosystem/core.git' + }, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBridgechainRegistration(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBridgechainRegistration(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.bridgechainRegistration).toEqual(rawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(3) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][3] + 1) + expect(await errorCapturer(client.buildBridgechainRegistration({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBridgechainRegistration({ fee: new BigNumber(fees[2][3]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBridgechainRegistration({ fee: new BigNumber(fees[2][3] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBridgechainUpdate', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][5]), + asset: { + bridgechainId: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + seedNodes: [ + '1.1.1.1', + '2.2.2.2', + '3.3.3.3', + '4.4.4.4' + ], + ports: { + '@arkecosystem/core-api': 4003 + } + }, + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBridgechainUpdate(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBridgechainUpdate(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.bridgechainUpdate).toEqual(rawTransaction.asset) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(5) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][5] + 1) + expect(await errorCapturer(client.buildBridgechainUpdate({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { + it('should not throw an Error', async () => { + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBridgechainUpdate({ fee: new BigNumber(fees[2][5]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBridgechainUpdate({ fee: new BigNumber(fees[2][5] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('buildBridgechainResignation', () => { + describe('standard transaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const rawTransaction = { + address, + fee: new BigNumber(fees[2][4]), + bridgechainId: '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867', + passphrase: 'passphrase', + secondPassphrase: 'second passphrase', + networkWif: 170 + } + + it('should not build a v1 transaction', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.buildBridgechainResignation(rawTransaction, true))).toThrow('AIP-11 transaction not supported on network') + }) + + it('should build a valid v2 transaction', async () => { + const spy = setAip11AndSpy(true) + + const transaction = await client.buildBridgechainResignation(rawTransaction, true) + + spy.mockRestore() + + expect(transaction.asset.bridgechainResignation.bridgechainId).toEqual(rawTransaction.bridgechainId) + expect(transaction.fee).toEqual(rawTransaction.fee.toString()) + expect(transaction.senderPublicKey).toEqual(publicKey) + expect(transaction.typeGroup).toEqual(2) + expect(transaction.type).toEqual(4) + expect(transaction.version).toEqual(2) + }) + }) + + describe('when the fee is bigger than the static fee', () => { + it('should throw an Error', async () => { + const spy = setAip11AndSpy(true) + const fee = new BigNumber(fees[2][4] + 1) + expect(await errorCapturer(client.buildBridgechainResignation({ fee }))).toThrow(/fee/) + spy.mockRestore() + }) + }) + + describe('when the fee is smaller or equal to the static fee (5)', () => { it('should not throw an Error', async () => { - expect(await errorCapturer(client.buildVote({ fee: new BigNumber(fees[3]) }))).not.toThrow(/fee/) - expect(await errorCapturer(client.buildVote({ fee: new BigNumber(fees[3] - 1) }))).not.toThrow(/fee/) + const spy = setAip11AndSpy(true) + expect(await errorCapturer(client.buildBridgechainResignation({ fee: new BigNumber(fees[2][4]) }))).not.toThrow(/fee/) + expect(await errorCapturer(client.buildBridgechainResignation({ fee: new BigNumber(fees[2][4] - 1) }))).not.toThrow(/fee/) + spy.mockRestore() + }) + }) + }) + + describe('__signTransaction', () => { + const address = Identities.Address.fromPassphrase('passphrase', 23) + const publicKey = Identities.PublicKey.fromPassphrase('passphrase') + const passphrase = 'passphrase' + const secondPassphrase = 'second passphrase' + let transaction + let signData + + beforeEach(() => { + transaction = Transactions.BuilderFactory + .transfer() + .amount(new BigNumber(1 * 1e8)) + .fee(new BigNumber(0.1 * 1e8)) + .recipientId(address) + .vendorField('test vendorfield') + + signData = { + address, + transaction, + passphrase, + secondPassphrase, + networkWif: 170, + networkId: 'ark.mainnet' + } + }) + + it('should sign transaction', async () => { + setAip11AndSpy(false, false) + + const response = await client.__signTransaction(signData) + + expect(response.vendorField).toEqual(transaction.data.vendorField) + expect(response.amount).toBe(new BigNumber(1 * 1e8).toString()) + expect(response.fee).toBe(new BigNumber(0.1 * 1e8).toString()) + expect(response.senderPublicKey).toEqual(publicKey) + expect(response.type).toEqual(0) + expect(response.version).toEqual(1) + }) + + it('should get network from session if no id', async () => { + const networkByIdSpy = jest.spyOn(store.getters, 'network/byId') + + await client.__signTransaction({ + ...signData, + networkId: null + }) + + expect(networkByIdSpy).not.toHaveBeenCalled() + + networkByIdSpy.mockRestore() + }) + + it('should get network by id if provided', async () => { + const networkByIdSpy = jest.spyOn(store.getters, 'network/byId') + + await client.__signTransaction(signData) + + expect(networkByIdSpy).toHaveBeenCalledWith('ark.mainnet') + + networkByIdSpy.mockRestore() + }) + + it('should normalize passphrase if provided', async () => { + const spy = jest.spyOn(client, 'normalizePassphrase') + + await client.__signTransaction(signData) + + expect(spy).toHaveBeenCalledWith(signData.passphrase) + expect(spy).toHaveBeenCalledWith(signData.secondPassphrase) + }) + + it('should not normalize if no passphrase is provided', async () => { + const spy = jest.spyOn(client, 'normalizePassphrase') + const wif = Identities.WIF.fromPassphrase(passphrase, { wif: 170 }) + + await client.__signTransaction({ + ...signData, + passphrase: null, + secondPassphrase: null, + wif + }) + + expect(spy).not.toHaveBeenCalled() + }) + + it('should create v1 transaction if aip11 disabled', async () => { + const response = await client.__signTransaction(signData) + + expect(response.version).toBe(1) + expect(response.nonce).toBeFalsy() + expect(response.timestamp).toBeTruthy() + }) + + it('should create v2 transaction if aip11 enabled', async () => { + const spy = setAip11AndSpy(true) + + const response = await client.__signTransaction(signData) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('1') + }) + + it('should increment nonce of wallet', async () => { + const spy = setAip11AndSpy(true) + nock('http://127.0.0.1:4003') + .get(`/api/v2/wallets/${address}`) + .reply(200, { + data: { + nonce: 3 + } + }) + + const response = await client.__signTransaction(signData) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('4') + }) + + it('should default nonce to 0 if no wallet', async () => { + const spy = setAip11AndSpy(true) + nock('http://127.0.0.1:4003') + .get(`/api/v2/wallets/${address}`) + .reply(200, { + data: {} + }) + + const response = await client.__signTransaction(signData) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('1') + }) + + it('should sign with passphrase', async () => { + const spy = setAip11AndSpy(true) + const spySignPassphrase = jest.spyOn(transaction, 'sign') + + const response = await client.__signTransaction(signData) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('1') + expect(spySignPassphrase).toHaveBeenCalledWith(passphrase) + }) + + it('should sign with second passphrase', async () => { + const spy = setAip11AndSpy(true) + const spySecondSignPassphrase = jest.spyOn(transaction, 'secondSign') + + const response = await client.__signTransaction(signData) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('1') + expect(spySecondSignPassphrase).toHaveBeenCalledWith(secondPassphrase) + }) + + it('should sign with wif', async () => { + const spy = setAip11AndSpy(true) + const spySignWif = jest.spyOn(transaction, 'signWithWif') + const wif = Identities.WIF.fromPassphrase(passphrase, { wif: 170 }) + + const response = await client.__signTransaction({ + ...signData, + passphrase: null, + secondPassphrase: null, + wif + }) + + spy.mockRestore() + + expect(response.version).toBe(2) + expect(response.nonce).toBe('1') + expect(spySignWif).toHaveBeenCalledWith(wif, 170) + }) + + it('should return object', async () => { + const spy = setAip11AndSpy(true) + + const response = await client.__signTransaction(signData, true) + + spy.mockRestore() + + expect(response.data).toBeTruthy() + expect(response.constructor.name).toBe('TransferBuilder') + }) + + describe('multiSignature', () => { + const minKeys = 3 + let multiSignature + let publicKeys + let aip11Spy + + beforeEach(() => { + publicKeys = [] + for (let i = 0; i < 5; i++) { + publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + multiSignature = { + publicKeys, + min: minKeys + } + aip11Spy = setAip11AndSpy(true) + }) + + afterEach(() => { + aip11Spy.mockRestore() + }) + + it('should create transaction for multi-signature wallet when using passphrase', async () => { + const getPublicKeyFromPassphrase = WalletService.getPublicKeyFromPassphrase + WalletService.getPublicKeyFromPassphrase = jest.fn((passphrase) => Identities.PublicKey.fromPassphrase(passphrase)) + + const response = await client.__signTransaction({ + ...signData, + multiSignature + }) + + WalletService.getPublicKeyFromPassphrase = getPublicKeyFromPassphrase + + expect(response.signatures.length).toBe(0) + }) + + it('should return transaction with multiSignature property', async () => { + const getPublicKeyFromPassphrase = WalletService.getPublicKeyFromPassphrase + WalletService.getPublicKeyFromPassphrase = jest.fn((passphrase) => Identities.PublicKey.fromPassphrase(passphrase)) + + const response = await client.__signTransaction({ + ...signData, + multiSignature + }) + + WalletService.getPublicKeyFromPassphrase = getPublicKeyFromPassphrase + + expect(response.multiSignature).toBe(multiSignature) + }) + + describe('own passphrase used', () => { + beforeEach(() => { + publicKeys.push(Identities.PublicKey.fromPassphrase(`${passphrase}`)) + }) + + it('should add signature to list of signatures', async () => { + const spyMultiSign = jest.spyOn(transaction, 'multiSign') + const getPublicKeyFromPassphraseMock = jest.fn((passphrase) => Identities.PublicKey.fromPassphrase(passphrase)) + const getPublicKeyFromPassphrase = WalletService.getPublicKeyFromPassphrase + WalletService.getPublicKeyFromPassphrase = getPublicKeyFromPassphraseMock + + const response = await client.__signTransaction({ + ...signData, + multiSignature + }) + + WalletService.getPublicKeyFromPassphrase = getPublicKeyFromPassphrase + + const publicKeyIndex = publicKeys.indexOf(publicKey) + const signature = response.signatures.find(s => parseInt(s.substring(0, 2), 16) === publicKeyIndex) + + expect(getPublicKeyFromPassphraseMock).toHaveBeenCalledWith(passphrase) + expect(spyMultiSign).toHaveBeenCalledWith(passphrase, publicKeyIndex) + expect(signature).toBeTruthy() + }) + }) + + describe('own wif used', () => { + beforeEach(() => { + publicKeys.push(Identities.PublicKey.fromPassphrase(`${passphrase}`)) + }) + + it('should add signature to list of signatures', async () => { + const spyMultiSignWithWif = jest.spyOn(transaction, 'multiSignWithWif') + const getPublicKeyFromWIFMock = jest.fn((wif) => Identities.PublicKey.fromWIF(wif, { wif: 170 })) + const getPublicKeyFromWIF = WalletService.getPublicKeyFromWIF + WalletService.getPublicKeyFromWIF = getPublicKeyFromWIFMock + + const wif = Identities.WIF.fromPassphrase(passphrase, { wif: 170 }) + const response = await client.__signTransaction({ + ...signData, + multiSignature, + passphrase: null, + secondPassphrase: null, + wif + }) + + WalletService.getPublicKeyFromWIF = getPublicKeyFromWIF + + const publicKeyIndex = publicKeys.indexOf(publicKey) + const signature = response.signatures.find(s => parseInt(s.substring(0, 2), 16) === publicKeyIndex) + + expect(getPublicKeyFromWIFMock).toHaveBeenCalledWith(wif) + expect(spyMultiSignWithWif).toHaveBeenCalledWith(publicKeyIndex, wif, 170) + expect(signature).toBeTruthy() + }) + }) + }) + }) + + describe('multiSign', () => { + const masterPassphrase = 'passphrase' + const address = Identities.Address.fromPassphrase(masterPassphrase, 23) + const publicKey = Identities.PublicKey.fromPassphrase(masterPassphrase) + const minKeys = 3 + let publicKeys + let transaction + let signData + let multiSignature + let aip11Spy + + beforeEach(() => { + publicKeys = [] + for (let i = 0; i < 5; i++) { + publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + multiSignature = { + publicKeys, + min: minKeys + } + aip11Spy = setAip11AndSpy(true) + + transaction = { + amount: new BigNumber(1 * 1e8), + fee: new BigNumber(0.1 * 1e8), + type: 0, + typeGroup: 1, + recipientId: address, + vendorField: 'test vendorfield', + version: 2, + network: 23, + senderPublicKey: publicKey, + nonce: '1', + signatures: [] + } + + signData = { + multiSignature, + networkWif: 170, + passphrase: 'passphrase 1' + } + }) + + afterEach(() => { + aip11Spy.mockRestore() + }) + + it('should throw error if no passphrase or wif', async () => { + expect(await errorCapturer(client.multiSign(transaction, { multiSignature }))).toThrow('No passphrase or wif provided') + }) + + it('should throw error aip11 not enabled', async () => { + setAip11AndSpy(false, false) + + expect(await errorCapturer(client.multiSign(transaction, signData))).toThrow('Multi-Signature Transactions are not supported yet') + }) + + it('should parse transaction from data', async () => { + const spy = jest.spyOn(client, '__transactionFromData') + + await client.multiSign(transaction, signData) + expect(spy).toHaveBeenCalledWith(transaction) + }) + + it('should get keys from passphrase if provided', async () => { + const spy = jest.spyOn(Identities.Keys, 'fromPassphrase') + + await client.multiSign(transaction, signData) + expect(spy).toHaveBeenCalledWith('passphrase 1') + + spy.mockRestore() + }) + + it('should get keys from wif if provided', async () => { + const wif = Identities.WIF.fromPassphrase('passphrase 1', { wif: 170 }) + const spy = jest.spyOn(Identities.Keys, 'fromWIF') + + await client.multiSign(transaction, { + ...signData, + passphrase: null, + wif + }) + expect(spy).toHaveBeenCalledWith(wif, { wif: 170 }) + + spy.mockRestore() + }) + + it('should check if signatures are needed', async () => { + const spy = jest.spyOn(TransactionService, 'isMultiSignatureReady') + + await client.multiSign(transaction, signData) + + expect(spy).toHaveBeenCalledWith({ ...transaction, multiSignature }, true) + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should throw error if passphrase is not required for multi-signature wallet', async () => { + expect(await errorCapturer(client.multiSign(transaction, { ...signData, passphrase: 'not used' }))).toThrow('passphrase/wif is not used to sign this transaction') + }) + + it('should add signature for passphrase', async () => { + const response = await client.multiSign(transaction, signData) + + expect(response.signatures.length).toBe(1) + expect(response.signatures[0]).toBeTruthy() + }) + + it('should add additional signatures upto minimum required', async () => { + for (let i = 0; i < minKeys; i++) { + transaction = await client.multiSign(transaction, { ...signData, passphrase: `passphrase ${i}` }) + } + + expect(transaction.signatures.length).toBe(3) + for (let i = 0; i < minKeys; i++) { + const publicKeyIndex = publicKeys.indexOf(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + const signature = transaction.signatures.find(s => parseInt(s.substring(0, 2), 16) === publicKeyIndex) + + expect(signature).toBeTruthy() + } + }) + + it('should not sign transaction if not primary sender', async () => { + for (let i = 0; i < 4; i++) { + transaction = await client.multiSign(transaction, { ...signData, passphrase: `passphrase ${i}` }) + } + + expect(transaction.signature).toBeFalsy() + }) + + it('should ignore duplicate signatures for passphrase', async () => { + transaction = await client.multiSign(transaction, { ...signData, passphrase: 'passphrase 1' }) + transaction = await client.multiSign(transaction, { ...signData, passphrase: 'passphrase 2' }) + transaction = await client.multiSign(transaction, { ...signData, passphrase: 'passphrase 1' }) + + const publicKeyIndex = publicKeys.indexOf(Identities.PublicKey.fromPassphrase('passphrase 1')) + const signatures = transaction.signatures.filter(s => parseInt(s.substring(0, 2), 16) === publicKeyIndex) + expect(signatures.length).toBe(1) + }) + + it('should only sign transaction with sender passphrase for registration', async () => { + transaction = { + fee: new BigNumber(0.1 * 1e8), + type: 4, + typeGroup: 1, + version: 2, + network: 23, + senderPublicKey: publicKey, + nonce: '1', + signatures: [], + asset: { + multiSignature + } + } + + for (let i = 0; i < 5; i++) { + transaction = await client.multiSign(transaction, { ...signData, passphrase: `passphrase ${i}` }) + } + transaction = await client.multiSign(transaction, { ...signData, passphrase: masterPassphrase }) + + expect(transaction.signature).toBeTruthy() + expect(transaction.signatures.length).toBe(5) + }) + + it('should sign transaction with sender second passphrase for registration', async () => { + transaction = { + fee: new BigNumber(0.1 * 1e8), + type: 4, + typeGroup: 1, + version: 2, + network: 23, + senderPublicKey: publicKey, + nonce: '1', + signatures: [], + asset: { + multiSignature + } + } + + for (let i = 0; i < 5; i++) { + transaction = await client.multiSign(transaction, { ...signData, passphrase: `passphrase ${i}` }) + } + transaction = await client.multiSign(transaction, { + ...signData, + passphrase: masterPassphrase, + secondPassphrase: 'second-passphrase' + }) + + expect(transaction.signature).toBeTruthy() + expect(transaction.secondSignature).toBeTruthy() + expect(transaction.signatures.length).toBe(5) + }) + }) + + describe('__transactionFromData', () => { + let transaction + + beforeEach(() => { + transaction = { + amount: new BigNumber(1 * 1e8), + fee: new BigNumber(0.1 * 1e8), + type: 0, + typeGroup: 1, + recipientId: 'address-1', + vendorField: 'test vendorfield', + version: 2, + network: 23, + senderPublicKey: 'publicKey-1', + timestamp: 100000, + nonce: '1', + signatures: [], + multiSignature: { + min: 3, + publicKeys: [ + 1, + 2, + 3 + ] + } + } + }) + + it('should do a deep clone', () => { + const clonedTransaction = client.__transactionFromData(transaction) + transaction.amount = new BigNumber(2 * 1e8) + + expect(clonedTransaction.amount + '').toEqual('100000000') + }) + + it('should remove unnecessary properties', () => { + const clonedTransaction = client.__transactionFromData(transaction) + + expect(clonedTransaction.timestamp).toBe(undefined) + expect(clonedTransaction.multiSignature).toBe(undefined) + }) + }) + + describe('broadcastTransaction', () => { + beforeEach(() => { + nock('http://127.0.0.1:4003') + .post('/api/v2/transactions') + .reply(200, { + data: 'transaction' + }) + }) + + it('should get current peer', async () => { + const spy = jest.spyOn(store.getters, 'peer/current') + + await client.broadcastTransaction({ network: 23 }) + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should parse current peer if no peer found', async () => { + const spyPeerCurrent = jest.spyOn(store.getters, 'peer/current').mockReturnValue(null) + const spy = jest.spyOn(client, '__parseCurrentPeer') + + await client.broadcastTransaction({ network: 23 }) + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + spyPeerCurrent.mockRestore() + }) + + it('should do nothing if no transactions', async () => { + const spy = jest.spyOn(store.getters, 'peer/current') + + await client.broadcastTransaction([]) + + expect(spy).not.toHaveBeenCalled() + + spy.mockRestore() + }) + + it('should do nothing if empty transaction object', async () => { + const spy = jest.spyOn(store.getters, 'peer/current') + + await client.broadcastTransaction({}) + + expect(spy).not.toHaveBeenCalled() + + spy.mockRestore() + }) + + it('should return transaction response', async () => { + const response = await client.broadcastTransaction({ network: 23 }) + + expect(response.length).toBe(1) + expect(response[0].body).toEqual({ + data: 'transaction' + }) + }) + + describe('broadcast', () => { + let spyDispatch + beforeEach(() => { + spyDispatch = jest.spyOn(store, 'dispatch').mockImplementation((_, peer) => { + // Copied from peer store + const client = new ClientService(false) + const scheme = peer.isHttps ? 'https://' : 'http://' + client.host = `${scheme}${peer.ip}:${peer.port}` + client.client.withOptions({ timeout: 3000 }) + + return client + }) + + for (const ip of ['1.1.1.1', '2.2.2.2']) { + nock(`http://${ip}:8080`) + .post('/api/v2/transactions') + .reply(200, { + data: `broadcast transaction for ${ip}` + }) + } + }) + + afterEach(() => { + spyDispatch.mockRestore() + }) + + it('should get peers to broadcast to', async () => { + const spy = jest.spyOn(store.getters, 'peer/broadcastPeers') + + await client.broadcastTransaction({ network: 23 }, true) + + expect(spy).toHaveBeenCalledTimes(1) + + spy.mockRestore() + }) + + it('should broadcast to all peers and return responses', async () => { + const response = await client.broadcastTransaction({ network: 23 }, true) + + expect(spyDispatch).toHaveBeenCalledWith('peer/clientServiceFromPeer', { + ip: '1.1.1.1', + port: '8080', + isHttps: false + }) + expect(spyDispatch).toHaveBeenCalledWith('peer/clientServiceFromPeer', { + ip: '2.2.2.2', + port: '8080', + isHttps: false + }) + expect(response.length).toBe(2) + expect(response[0].body).toEqual({ + data: 'broadcast transaction for 1.1.1.1' + }) + expect(response[1].body).toEqual({ + data: 'broadcast transaction for 2.2.2.2' + }) + }) + + it('should broadcast to current peer if no broadcast peers', async () => { + const spy = jest.spyOn(store.getters, 'peer/broadcastPeers').mockReturnValue() + const response = await client.broadcastTransaction({ network: 23 }, true) + + spy.mockRestore() + + expect(response.length).toBe(1) + expect(response[0].body).toEqual({ + data: 'transaction' + }) }) }) }) + + // describe('__watchProfile', () => { + // it('should watch current profile', () => { + + // client.__watchProfile() + + // expect(store.watch).toHaveBeenCalledWith() + // }) + // }) }) diff --git a/__tests__/unit/services/ledger-service.spec.js b/__tests__/unit/services/ledger-service.spec.js index 4ebe11801b..7f64774f09 100644 --- a/__tests__/unit/services/ledger-service.spec.js +++ b/__tests__/unit/services/ledger-service.spec.js @@ -19,14 +19,14 @@ describe('LedgerService', () => { }) it('should return false for isConnected', async () => { - const getAddress = ledgerService.ledger.getAddress - ledgerService.ledger.getAddress = jest.fn(() => { + const getPublicKey = ledgerService.ledger.getPublicKey + ledgerService.ledger.getPublicKey = jest.fn(() => { throw new Error('Could not connect') }) expect(await ledgerService.isConnected()).toBe(false) - ledgerService.ledger.getAddress = getAddress + ledgerService.ledger.getPublicKey = getPublicKey }) it('should disconnect', async () => { @@ -36,25 +36,42 @@ describe('LedgerService', () => { expect(ledgerService.transport.close).toHaveBeenCalledTimes(1) }) - it('should run getWallet', async () => { - const response = await ledgerService.getWallet('44\'/1\'/0\'/0/0') + describe('getPublicKey', () => { + it('should run', async () => { + const response = await ledgerService.getPublicKey('44\'/1\'/0\'/0/0') - expect(response).toBeTruthy() - expect(ledgerService.ledger.getAddress).toHaveBeenCalledTimes(1) + expect(response).toBeTruthy() + expect(ledgerService.ledger.getPublicKey).toHaveBeenCalledTimes(1) + }) + }) + + describe('getVersion', () => { + it('should run', async () => { + const response = await ledgerService.getVersion() + + expect(response).toBe('1.0.0') + expect(ledgerService.ledger.getVersion).toHaveBeenCalledTimes(1) + }) }) - it('should run getPublicKey', async () => { - const response = await ledgerService.getPublicKey('44\'/1\'/0\'/0/0') + describe('signTransaction', () => { + it('should run', async () => { + const response = await ledgerService.signTransaction('44\'/1\'/0\'/0/0', '1234') - expect(response).toBeTruthy() - expect(ledgerService.ledger.getAddress).toHaveBeenCalledTimes(1) + expect(response).toBeTruthy() + expect(ledgerService.ledger.signTransaction).toHaveBeenCalledTimes(1) + }) }) - it('should run signTransaction', async () => { - const response = await ledgerService.signTransaction('44\'/1\'/0\'/0/0', '1234') + describe('signMessage', () => { + it('should run', async () => { + ledgerService.ledger.signMessage.mockClear() - expect(response).toBeTruthy() - expect(ledgerService.ledger.signTransaction).toHaveBeenCalledTimes(1) + const response = await ledgerService.signMessage('44\'/1\'/0\'/0/0', Buffer.from('1234')) + + expect(response).toBeTruthy() + expect(ledgerService.ledger.signMessage).toHaveBeenCalledTimes(1) + }) }) it('should queue an action', async () => { diff --git a/__tests__/unit/services/plugin-manager.spec.js b/__tests__/unit/services/plugin-manager.spec.js index 56f33f09c9..fca1e35a54 100644 --- a/__tests__/unit/services/plugin-manager.spec.js +++ b/__tests__/unit/services/plugin-manager.spec.js @@ -78,7 +78,7 @@ describe('Plugin Manager', () => { }) it('should fetch plugins from adapter if forced', async () => { - jest.spyOn(pluginManager, 'fetchPluginsFromAdapter') + jest.spyOn(pluginManager, 'fetchPluginsFromAdapter').mockReturnValue({}) await pluginManager.fetchPlugins(true) expect(pluginManager.fetchPluginsFromAdapter).toHaveBeenCalled() diff --git a/__tests__/unit/services/synchronizer/wallets.spec.js b/__tests__/unit/services/synchronizer/wallets.spec.js index fdef7541f8..dc4e1bb627 100644 --- a/__tests__/unit/services/synchronizer/wallets.spec.js +++ b/__tests__/unit/services/synchronizer/wallets.spec.js @@ -588,7 +588,7 @@ describe('Services > Synchronizer > Wallets', () => { describe('when transactions include votes', () => { beforeEach(() => { - transactions[0].type = config.TRANSACTION_TYPES.VOTE + transactions[0].type = config.TRANSACTION_TYPES.GROUP_1.VOTE }) it('should process the votes', async () => { diff --git a/__tests__/unit/services/transaction.spec.js b/__tests__/unit/services/transaction.spec.js new file mode 100644 index 0000000000..4e9897b57b --- /dev/null +++ b/__tests__/unit/services/transaction.spec.js @@ -0,0 +1,873 @@ +import { Crypto, Identities, Managers, Transactions } from '@arkecosystem/crypto' +import TransactionService from '@/services/transaction' +import transactionFixture from '../__fixtures__/models/transaction' +import currencyMixin from '@/mixins/currency' + +const recipientAddress = Identities.Address.fromPassphrase('recipient passphrase') +const senderPassphrase = 'sender passphrase' +const senderPublicKey = Identities.PublicKey.fromPassphrase(senderPassphrase) + +const mockVm = { + currency_toBuilder: (value) => { + return currencyMixin.methods.currency_toBuilder(value, { + fractionDigits: 8 + }) + } +} + +describe('Services > Transaction', () => { + describe('getId', () => { + it('should return the transaction id', () => { + expect(TransactionService.getId(transactionFixture)).toBe(transactionFixture.id) + }) + }) + + describe('getBytes', () => { + it('should return the transaction bytes', () => { + expect(TransactionService.getBytes(transactionFixture)).toEqual('00f0eb920302275d8577a0ec2b75fc8683282d53c5db76ebc54514a80c2854e419b793ea259a17ee9f689978490631699e01ca0fc2edbecb5ee0390000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000008096980000000000') + }) + }) + + describe('getHash', () => { + const passphrase = 'test passphrase' + let transaction + beforeEach(() => { + Managers.configManager.getMilestone().aip11 = true + transaction = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset({ + min: 1, + publicKeys: [ + Identities.PublicKey.fromPassphrase(passphrase) + ] + }) + .sign('passphrase') + .fee(1) + }) + + it('should return hash excluding multisig signatures', () => { + const hash = TransactionService.getHash(transaction.getStruct()).toString('hex') + transaction.multiSign(passphrase, 0) + + expect(TransactionService.getHash(transaction.getStruct()).toString('hex')).toBe(hash) + }) + + it('should return hash including multisig signatures', () => { + const hash = TransactionService.getHash(transaction.getStruct()).toString('hex') + transaction.multiSign(passphrase, 0) + + expect(TransactionService.getHash(transaction.getStruct(), false).toString('hex')).not.toBe(hash) + }) + }) + + describe('getAmount', () => { + describe('standard transaction', () => { + let transaction + beforeEach(() => { + transaction = Transactions.BuilderFactory + .transfer() + .amount('100000000') + .fee('10000000') + .recipientId(recipientAddress) + .sign('passphrase') + .build() + .toJson() + }) + + it('should get correct amount with fee', () => { + expect(TransactionService.getAmount(mockVm, transaction, true).toFixed()).toEqual('110000000') + }) + + it('should get correct amount without fee', () => { + expect(TransactionService.getAmount(mockVm, transaction).toFixed()).toEqual('100000000') + }) + }) + + describe('multi-payment transaction', () => { + let transaction + beforeEach(() => { + transaction = Transactions.BuilderFactory + .multiPayment() + .addPayment(Identities.Address.fromPassphrase('recipient 1'), '100000000') + .addPayment(Identities.Address.fromPassphrase('recipient 2'), '100000000') + .addPayment(Identities.Address.fromPassphrase('recipient 3'), '100000000') + .fee('10000000') + .recipientId(recipientAddress) + .sign('passphrase') + .build() + .toJson() + }) + + it('should get correct amount with fee', () => { + expect(TransactionService.getAmount(mockVm, transaction, true).toFixed()).toEqual('310000000') + }) + + it('should get correct amount without fee', () => { + expect(TransactionService.getAmount(mockVm, transaction).toFixed()).toEqual('300000000') + }) + }) + }) + + describe('isMultiSignature', () => { + it('should return true if has multiSignature property', () => { + const transaction = { ...transactionFixture, multiSignature: { min: 1, publicKeys: [] } } + + expect(TransactionService.isMultiSignature(transaction)).toBe(true) + }) + + it('should return false if no multiSignature property', () => { + const transaction = { ...transactionFixture } + + expect(TransactionService.isMultiSignature(transaction)).toBe(false) + }) + }) + + describe('isMultiSignatureRegistration', () => { + it('should return true if multiSignature transaction', () => { + Managers.configManager.getMilestone().aip11 = true + + const transaction = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset({ + min: 1, + publicKeys: [] + }) + .sign('passphrase') + .fee(1) + .getStruct() + + expect(TransactionService.isMultiSignatureRegistration(transaction)).toBe(true) + }) + + it('should return false if no multiSignature property', () => { + const transaction = { ...transactionFixture } + + expect(TransactionService.isMultiSignature(transaction)).toBe(false) + }) + }) + + describe('getValidMultiSignatures', () => { + let multiSignatureAsset, passphrases, transaction + + beforeEach(() => { + Managers.configManager.getMilestone().aip11 = true + passphrases = [ + 'passphrase 1', + 'passphrase 2', + 'passphrase 3' + ] + + multiSignatureAsset = { + min: 1, + publicKeys: passphrases.map(passphrase => Identities.PublicKey.fromPassphrase(passphrase)) + } + + transaction = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .fee(1) + + transaction.data.senderPublicKey = senderPublicKey + }) + + it('should return only valid signatures', () => { + transaction.multiSign(passphrases[2], 2) + .multiSign(passphrases[0], 0) + .multiSign('wrong passphrase', 1) + + const transactionJson = transaction.getStruct() + transactionJson.multiSignature = multiSignatureAsset + + const validSignatures = TransactionService.getValidMultiSignatures(transactionJson) + + expect(validSignatures.map(signature => parseInt(signature.substring(0, 2)))).toIncludeSameMembers([0, 2]) + }) + + it('should return empty array if not multiSignature object', () => { + expect(TransactionService.getValidMultiSignatures(transaction)).toEqual([]) + }) + }) + + describe('needsSignatures', () => { + it('should check if all signatures are needed for multisig registration', () => { + const multiSignatureAsset = { + min: 1, + publicKeys: [] + } + + const transaction = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .sign('passphrase') + .fee(1) + .getStruct() + + const spy = jest.spyOn(TransactionService, 'needsAllSignatures') + .mockImplementation() + + transaction.multiSignature = multiSignatureAsset + + TransactionService.needsSignatures(transaction) + + expect(spy).toHaveBeenCalledWith(transaction) + + spy.mockRestore() + }) + + it('should return false if no multiSignature property', () => { + const transaction = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + .sign(senderPassphrase) + .getStruct() + + expect(TransactionService.needsSignatures(transaction)).toBe(false) + }) + + it('should return true when below min required signatures', () => { + const multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + const transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsSignatures(transaction)).toBe(true) + }) + + it('should return false when above min required signatures', () => { + const multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + const transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + for (let i = 1; i <= multiSignatureAsset.min + 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsSignatures(transaction)).toBe(false) + }) + }) + + describe('needsAllSignatures', () => { + let multiSignatureAsset, transactionObject + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .fee(1) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + }) + + it('should call getValidMultiSignatures', () => { + for (let i = 1; i <= multiSignatureAsset.min - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const spy = jest.spyOn(TransactionService, 'getValidMultiSignatures') + + TransactionService.needsAllSignatures(transaction) + + expect(spy).toHaveBeenCalledWith(transaction) + + spy.mockRestore() + }) + + it('should return true when signatures are missing', () => { + for (let i = 1; i <= multiSignatureAsset.min - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsAllSignatures(transaction)).toBe(true) + }) + + it('should return true when incorrect signatures are provided', () => { + for (let i = 1; i <= multiSignatureAsset.min - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transactionObject.multiSign('wrong passphrase') + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsAllSignatures(transaction)).toBe(true) + }) + + it('should return false when no signatures are missing', () => { + for (let i = 1; i <= multiSignatureAsset.min; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + const transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsAllSignatures(transaction)).toBe(true) + }) + }) + + describe('needsWalletSignature', () => { + describe('multi-signature registration', () => { + let multiSignatureAsset, transaction, transactionObject + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .fee(1) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + }) + + it('should check if transaction is ready', () => { + const spy = jest.spyOn(TransactionService, 'isMultiSignatureReady') + .mockImplementation(() => false) + + TransactionService.needsWalletSignature(transaction, transactionObject.data.senderPublicKey) + + expect(spy).toHaveBeenCalledWith(transaction, true) + + spy.mockRestore() + }) + + it('should check for final signature', () => { + const spyReady = jest.spyOn(TransactionService, 'isMultiSignatureReady') + .mockImplementation(() => true) + + const spyFinalSignature = jest.spyOn(TransactionService, 'needsFinalSignature') + .mockImplementation(() => false) + + TransactionService.needsWalletSignature(transaction, transactionObject.data.senderPublicKey) + + expect(spyFinalSignature).toHaveBeenCalledWith(transaction) + + spyReady.mockRestore() + spyFinalSignature.mockRestore() + }) + + it('should match public key', () => { + const spyReady = jest.spyOn(TransactionService, 'isMultiSignatureReady') + .mockImplementation(() => true) + + const spyFinalSignature = jest.spyOn(TransactionService, 'needsFinalSignature') + .mockImplementation(() => false) + + expect(TransactionService.needsWalletSignature(transaction, 'fake public key')).toBe(false) + expect(spyFinalSignature).not.toHaveBeenCalledWith(transaction) + + spyReady.mockRestore() + spyFinalSignature.mockRestore() + }) + }) + + describe('non-registration', () => { + let multiSignatureAsset, transaction, transactionObject + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + }) + + it('should return false if no multiSignature property', () => { + transaction.multiSignature = undefined + + expect(TransactionService.needsWalletSignature(transaction, 'public key')).toBe(false) + }) + + it('should return false if public key is not required for multi-signature', () => { + expect(TransactionService.needsWalletSignature(transaction, 'public key')).toBe(false) + }) + + it('should return true if no signatures property', () => { + transaction.signatures = undefined + + expect(TransactionService.needsWalletSignature(transaction, multiSignatureAsset.publicKeys[0])).toBe(true) + }) + + it('should return true if signature is missing', () => { + expect(TransactionService.needsWalletSignature(transaction, multiSignatureAsset.publicKeys[0])).toBe(true) + }) + + it('should return false if signature exists', () => { + transactionObject.multiSign('passphrase 1') + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + expect(TransactionService.needsWalletSignature(transaction, multiSignatureAsset.publicKeys[0])).toBe(false) + }) + }) + }) + + describe('needsFinalSignature', () => { + let multiSignatureAsset, transactionObject, transaction + + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= 10; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .fee(1) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + }) + + it('should return true if no signature property', () => { + transaction.signature = undefined + + expect(TransactionService.needsFinalSignature(transaction)).toBe(true) + }) + + it('should return true if signed before multi-sign signatures', () => { + transactionObject.sign(senderPassphrase) + for (let i = 1; i <= 10; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transaction = transactionObject.getStruct() + + expect(TransactionService.needsFinalSignature(transaction)).toBe(true) + }) + + it('should return false if signed after multi-sign signatures', () => { + for (let i = 1; i <= 10; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.sign(senderPassphrase) + transaction = transactionObject.getStruct() + + expect(TransactionService.needsFinalSignature(transaction)).toBe(false) + }) + + it('should return false if signed', () => { + transactionObject.sign(senderPassphrase) + transaction = transactionObject.getStruct() + + expect(TransactionService.needsFinalSignature(transaction)).toBe(false) + }) + + it('should return false if multi-signature but not registration', () => { + transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const spy = jest.spyOn(Crypto.Hash, 'verifySchnorr') + + expect(TransactionService.needsFinalSignature(transaction)).toBe(false) + expect(spy).not.toHaveBeenCalled() + + spy.mockRestore() + }) + }) + + describe('isMultiSignatureReady', () => { + describe('multi-signature registration', () => { + const publicKeyCount = 10 + let multiSignatureAsset, spyRegistration, spyNeedsSignatures, transaction, transactionObject + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= publicKeyCount; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .multiSignature() + .multiSignatureAsset(multiSignatureAsset) + .fee(1) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + spyRegistration = jest.spyOn(TransactionService, 'isMultiSignatureRegistration') + spyNeedsSignatures = jest.spyOn(TransactionService, 'needsSignatures') + }) + + afterEach(() => { + spyRegistration.mockRestore() + spyNeedsSignatures.mockRestore() + }) + + it('should return true if multi-signed and with primary passphrase', () => { + for (let i = 1; i <= publicKeyCount; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.sign(senderPassphrase) + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const spyNeedsFinal = jest.spyOn(TransactionService, 'needsFinalSignature') + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(false) + expect(spyNeedsFinal).toHaveReturnedWith(false) + expect(response).toBe(true) + + spyNeedsFinal.mockRestore() + }) + + it('should return true if multi-signed but excluding final', () => { + for (let i = 1; i <= publicKeyCount; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction, true) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(false) + expect(response).toBe(true) + }) + + it('should return false because all multi-sign signatures are required', () => { + for (let i = 1; i <= publicKeyCount - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.sign(senderPassphrase) + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(true) + expect(response).toBe(false) + }) + + it('should return false because primary signature is required', () => { + for (let i = 1; i <= publicKeyCount; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(false) + expect(response).toBe(false) + }) + + it('should return false if a wrong multi-sign passphrase is provided', () => { + for (let i = 1; i <= publicKeyCount - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.multiSign('wrong passphrase') + transactionObject.sign(senderPassphrase) + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(true) + expect(response).toBe(false) + }) + + it('should return false if final passphrase is wrong', () => { + for (let i = 1; i <= publicKeyCount; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.sign('wrong passphrase') + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(true) + expect(spyNeedsSignatures).toHaveReturnedWith(true) + expect(response).toBe(false) + }) + }) + + describe('non-registration', () => { + const publicKeyCount = 10 + let multiSignatureAsset, spyRegistration, spyNeedsSignatures, transaction, transactionObject + beforeEach(() => { + multiSignatureAsset = { + min: 5, + publicKeys: [] + } + + for (let i = 1; i <= publicKeyCount; i++) { + multiSignatureAsset.publicKeys.push(Identities.PublicKey.fromPassphrase(`passphrase ${i}`)) + } + + transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + transactionObject.data.senderPublicKey = senderPublicKey + transactionObject.data.signatures = [] + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + spyRegistration = jest.spyOn(TransactionService, 'isMultiSignatureRegistration') + spyNeedsSignatures = jest.spyOn(TransactionService, 'needsSignatures') + }) + + afterEach(() => { + spyRegistration.mockRestore() + spyNeedsSignatures.mockRestore() + }) + + it('should return true if all multi-signed', () => { + for (let i = 1; i <= publicKeyCount; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(false) + expect(spyNeedsSignatures).toHaveReturnedWith(false) + expect(response).toBe(true) + }) + + it('should return true if multi-signed upto min', () => { + for (let i = 1; i <= multiSignatureAsset.min; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(false) + expect(spyNeedsSignatures).toHaveReturnedWith(false) + expect(response).toBe(true) + }) + + it('should return false because below min required signatures', () => { + for (let i = 1; i <= multiSignatureAsset.min - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(false) + expect(spyNeedsSignatures).toHaveReturnedWith(true) + expect(response).toBe(false) + }) + + it('should return false if a wrong multi-sign passphrase is provided', () => { + for (let i = 1; i <= multiSignatureAsset.min - 1; i++) { + transactionObject.multiSign(`passphrase ${i}`) + } + transactionObject.multiSign('wrong passphrase') + + transaction = transactionObject.getStruct() + transaction.multiSignature = multiSignatureAsset + + const response = TransactionService.isMultiSignatureReady(transaction) + + expect(spyRegistration).toHaveReturnedWith(false) + expect(spyNeedsSignatures).toHaveReturnedWith(true) + expect(response).toBe(false) + }) + }) + }) + + describe('ledgerSign', () => { + const vmMock = { + $store: { + dispatch () {} + }, + $t (translationKey) { + return translationKey + } + } + + let spyDispatch, spyTranslate, transactionObject, wallet + beforeEach(() => { + transactionObject = Transactions.BuilderFactory + .transfer() + .amount(1) + .fee(1) + .recipientId(recipientAddress) + + spyDispatch = jest.spyOn(vmMock.$store, 'dispatch') + spyTranslate = jest.spyOn(vmMock, '$t') + wallet = { + address: Identities.Address.fromPassphrase(senderPassphrase), + publicKey: senderPublicKey, + ledgerIndex: 0 + } + }) + + afterEach(() => { + spyDispatch.mockRestore() + spyTranslate.mockRestore() + }) + + it('should sign the transaction', async () => { + transactionObject.sign(senderPassphrase) + const transactionJson = transactionObject.getStruct() + + const bytes = TransactionService.getBytes(transactionJson) + const id = TransactionService.getId(transactionJson) + const signature = transactionObject.data.signature + + spyDispatch.mockImplementation((key) => { + if (key === 'ledger/signTransaction') { + return signature + } + }) + + const spyGetBytes = jest.spyOn(TransactionService, 'getBytes') + + const transaction = await TransactionService.ledgerSign(wallet, transactionObject, vmMock) + + expect(transaction.id).toEqual(id) + expect(spyGetBytes).toHaveBeenCalledWith(transactionJson) + expect(spyGetBytes).toHaveReturnedWith(bytes) + + spyGetBytes.mockRestore() + }) + + it('should set the recipientId for vote transactions', async () => { + transactionObject = Transactions.BuilderFactory + .vote() + .votesAsset([`+${senderPublicKey}`]) + .fee(1) + .sign(senderPassphrase) + + const signature = transactionObject.data.signature + + spyDispatch.mockImplementation((key) => { + if (key === 'ledger/signTransaction') { + return signature + } + }) + + const transaction = await TransactionService.ledgerSign(wallet, transactionObject, vmMock) + + expect(transaction.recipientId).toEqual(wallet.address) + }) + + it('should throw error if no signature', async () => { + await expect(TransactionService.ledgerSign(wallet, transactionObject, vmMock)).rejects.toThrow('TRANSACTION.LEDGER_USER_DECLINED') + + expect(spyTranslate).toHaveBeenCalledWith('TRANSACTION.LEDGER_USER_DECLINED') + }) + }) +}) diff --git a/__tests__/unit/services/wallet.spec.js b/__tests__/unit/services/wallet.spec.js index f827bc1064..5f17acc429 100644 --- a/__tests__/unit/services/wallet.spec.js +++ b/__tests__/unit/services/wallet.spec.js @@ -1,53 +1,477 @@ +import * as bip39 from 'bip39' +import nock from 'nock' +import { Identities } from '@arkecosystem/crypto' import WalletService from '../../../src/renderer/services/wallet' +jest.mock('@/store', () => ({ + getters: { + 'session/network': { + crypto: require('@arkecosystem/crypto').Managers.configManager.config + }, + 'delegate/byUsername': (username) => { + if (username === 'exists') { + return { + username: 'exists' + } + } + + return false + } + }, + dispatch: jest.fn(), + watch: jest.fn() +})) + +beforeEach(() => { + nock.cleanAll() +}) + describe('Services > Wallet', () => { - describe('generating a passphrase and address', () => { + describe('generate', () => { + it('should generate a wallet in English', () => { + const wallet = WalletService.generate(30, 'english') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.english).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Chinese (Traditional)', () => { + const wallet = WalletService.generate(30, 'chinese_traditional') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.chinese_traditional).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Chinese (Simplified)', () => { + const wallet = WalletService.generate(30, 'chinese_simplified') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.chinese_simplified).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Korean', () => { + const wallet = WalletService.generate(30, 'korean') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.korean).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in French', () => { + const wallet = WalletService.generate(30, 'french') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.french).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Italian', () => { + const wallet = WalletService.generate(30, 'italian') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.italian).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Spanish', () => { + const wallet = WalletService.generate(30, 'spanish') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.spanish).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + + it('should generate a wallet in Japanese', () => { + const wallet = WalletService.generate(30, 'japanese') + + expect(Identities.Address.fromPassphrase(wallet.passphrase)).toEqual(wallet.address) + expect(bip39.wordlists.japanese).toIncludeAllMembers(wallet.passphrase.split(' ')) + }) + }) + + describe('generateSecondPassphrase', () => { + it('should generate second passphrase for wallet in English', () => { + const passphrase = WalletService.generateSecondPassphrase('english') + + expect(bip39.wordlists.english).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Chinese (Traditional)', () => { + const passphrase = WalletService.generateSecondPassphrase('chinese_traditional') + + expect(bip39.wordlists.chinese_traditional).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Chinese (Simplified)', () => { + const passphrase = WalletService.generateSecondPassphrase('chinese_simplified') + + expect(bip39.wordlists.chinese_simplified).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Korean', () => { + const passphrase = WalletService.generateSecondPassphrase('korean') + + expect(bip39.wordlists.korean).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in French', () => { + const passphrase = WalletService.generateSecondPassphrase('french') + + expect(bip39.wordlists.french).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Italian', () => { + const passphrase = WalletService.generateSecondPassphrase('italian') + + expect(bip39.wordlists.italian).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Spanish', () => { + const passphrase = WalletService.generateSecondPassphrase('spanish') + + expect(bip39.wordlists.spanish).toIncludeAllMembers(passphrase.split(' ')) + }) + + it('should generate second passphrase for wallet in Japanese', () => { + const passphrase = WalletService.generateSecondPassphrase('japanese') + + expect(bip39.wordlists.japanese).toIncludeAllMembers(passphrase.split(' ')) + }) + }) + + describe('getAddress', () => { + let normalizeSpy + + beforeEach(() => { + normalizeSpy = jest.spyOn(WalletService, 'normalizePassphrase') + }) + + afterEach(() => { + normalizeSpy.mockRestore() + }) + it('should work in English', () => { const passphrase = 'one video jaguar gap soldier ill hobby motor bundle couple trophy smoke' const address = 'DAy2xDNZLRQsgiJCnF3x4WDxGsBrmsKCsV' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Chinese (Traditional)', () => { const passphrase = '苗 雛 陸 桿 用 腐 爐 詞 鬼 雨 爾 然' const address = 'DS6hPMzbgRkKCZa6fJSmQrG2M7toJAtd5B' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in French', () => { const passphrase = 'galerie notoire prudence mortier soupape cerise argent neurone pommade géranium potager émouvoir' const address = 'DUFdRiUNXt1PiLVakbq4ADo1Ttsx3kH1AT' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Italian', () => { const passphrase = 'mucca comodo imbevuto talismano sconforto cavillo obelisco quota recupero malinteso gergo bipede' const address = 'D8nAGdSCCRMsLPsM4GgzRtgbiTn16rHW6J' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Japanese', () => { const passphrase = 'うかべる くすりゆび ひさしぶり たそがれ そっこう ちけいず ひさしぶり ていか しゃちょう けおりもの ちぬり りきせつ' const address = 'DQquFjRfgA26cut7A8wFC4Bbo4TawWArWr' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Korean with initially decomposed characters', () => { const passphrase = '변명 박수 사건 실컷 목적 비용 가능 시골 수동적 청춘 식량 도망' const address = 'D5FvjRH136fbw8j4thcmKiFiJjfbYHT3zY' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Korean without decomposing', () => { const passphrase = '변명 박수 사건 실컷 목적 비용 가능 시골 수동적 청춘 식량 도망' const address = 'D5FvjRH136fbw8j4thcmKiFiJjfbYHT3zY' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) }) it('should work in Spanish', () => { const passphrase = 'cadena cadáver malla etapa vista alambre burbuja vejez aéreo taco rebaño tauro' const address = 'DNZSrNt7SQ1VBrzx7C17gbPv9FDAxnaor3' expect(WalletService.getAddress(passphrase, 30)).toEqual(address) + expect(normalizeSpy).toHaveBeenCalledWith(passphrase) + }) + }) + + describe('getAddressFromPublicKey', () => { + it('should generate the correct address', () => { + const passphrase = 'one video jaguar gap soldier ill hobby motor bundle couple trophy smoke' + const address = 'DAy2xDNZLRQsgiJCnF3x4WDxGsBrmsKCsV' + expect(WalletService.getAddressFromPublicKey(Identities.PublicKey.fromPassphrase(passphrase), 30)).toEqual(address) + }) + }) + + describe('getAddressFromMultiSignatureAsset', () => { + it('should get address for a multisignature asset', () => { + const multisignatureAsset = { + min: 2, + publicKeys: [ + '037eaa8cb236c40a08fcb9d6220743ee6ae1b5c40e8a77a38f286516c3ff663901', + '0301fd417566397113ba8c55de2f093a572744ed1829b37b56a129058000ef7bce', + '0209d3c0f68994253cee24b23df3266ba1f0ca2f0666cd69a46544d63001cdf150' + ] + } + + expect(WalletService.getAddressFromMultiSignatureAsset(multisignatureAsset)).toEqual('DHBKa1HFsKd9BYMPruYNAoadt5SW8oGggj') + }) + }) + + describe('getPublicKeyFromWallet', () => { + it('should get public key for a standard wallet', () => { + const wallet = { + publicKey: '037eaa8cb236c40a08fcb9d6220743ee6ae1b5c40e8a77a38f286516c3ff663901' + } + + expect(WalletService.getPublicKeyFromWallet(wallet)).toEqual('037eaa8cb236c40a08fcb9d6220743ee6ae1b5c40e8a77a38f286516c3ff663901') + }) + + it('should return null if no public key', () => { + const wallet = {} + + expect(WalletService.getPublicKeyFromWallet(wallet)).toEqual(null) + }) + + it('should get public key from multisignature info if exists', () => { + const multiSignatureSpy = jest.spyOn(WalletService, 'getPublicKeyFromMultiSignatureAsset').mockImplementation(() => true) + const wallet = { + multiSignature: true + } + + WalletService.getPublicKeyFromWallet(wallet) + + expect(multiSignatureSpy).toHaveBeenCalledWith(wallet.multiSignature) + + multiSignatureSpy.mockRestore() + }) + }) + + describe('getPublicKeyFromPassphrase', () => { + it('should get public key for a standard wallet', () => { + const passphrase = 'passphrase 1' + const publicKey = Identities.PublicKey.fromPassphrase(passphrase) + + expect(WalletService.getPublicKeyFromPassphrase(passphrase)).toEqual('03e8021105a6c202097e97e6c6d650942d913099bf6c9f14a6815df1023dde3b87') + expect(WalletService.getPublicKeyFromPassphrase(passphrase)).toEqual(publicKey) + }) + }) + + describe('getPublicKeyFromMultiSignatureAsset', () => { + it('should get public key from multisignature info if exists', () => { + const multiSignatureSpy = jest.spyOn(WalletService, 'getPublicKeyFromMultiSignatureAsset') + const wallet = { + multiSignature: { + min: 2, + publicKeys: [ + '037eaa8cb236c40a08fcb9d6220743ee6ae1b5c40e8a77a38f286516c3ff663901', + '0301fd417566397113ba8c55de2f093a572744ed1829b37b56a129058000ef7bce', + '0209d3c0f68994253cee24b23df3266ba1f0ca2f0666cd69a46544d63001cdf150' + ] + } + } + + expect(WalletService.getPublicKeyFromWallet(wallet)).toEqual('03e8d4175126a39ed7ba803f31705b6f5fb78cbf46455ba778c5f39a32c6adfbd9') + expect(multiSignatureSpy).toHaveBeenCalledWith(wallet.multiSignature) + + multiSignatureSpy.mockRestore() + }) + }) + + describe('isNeoAddress', () => { + const address = Identities.Address.fromPassphrase('test address', 23) + + it('should check if valid version', () => { + const validateAddressSpy = jest.spyOn(WalletService, 'validateAddress').mockImplementation(() => (false)) + + WalletService.isNeoAddress(address) + + expect(validateAddressSpy).toHaveBeenCalledWith(address, 23) + + validateAddressSpy.mockRestore() + }) + + it('should return false if not on NEO network', async () => { + nock('https://neoscan.io') + .persist() + .get(`/api/main_net/v1/get_last_transactions_by_address/${address}`) + .reply(200, []) + + expect(await WalletService.isNeoAddress(address)).toBe(false) + }) + + it('should return true if on NEO network', async () => { + nock('https://neoscan.io') + .persist() + .get(`/api/main_net/v1/get_last_transactions_by_address/${address}`) + .reply(200, [{ + vouts: [], + vin: [] + }]) + + expect(await WalletService.isNeoAddress(address)).toBe(true) + }) + }) + + describe('isBusiness', () => { + it('should return true if wallet has not resigned as a business when checking both scenarios', () => { + const wallet = { + business: { + name: 'mybusiness', + resigned: false + } + } + + expect(WalletService.isBusiness(wallet)).toBe(true) + expect(WalletService.isBusiness(wallet, false)).toBe(true) + }) + + it('should return true if wallet has ever been a business', () => { + const wallet = { + business: { + name: 'mybusiness', + resigned: true + } + } + + expect(WalletService.isBusiness(wallet)).toBe(true) + }) + + it('should return false when checking if wallet has resigned as a business', () => { + const wallet = { + business: { + name: 'mybusiness', + resigned: true + } + } + + expect(WalletService.isBusiness(wallet, false)).toBe(false) + }) + + it('should return false if wallet has never been a business', () => { + const wallet = {} + + expect(WalletService.isBusiness(wallet)).toBe(false) + }) + }) + + describe('canResignBusiness', () => { + it('should return true if business that has not resigned', () => { + const wallet = { + business: { + name: 'mybusiness', + resigned: false + } + } + + expect(WalletService.canResignBusiness(wallet)).toBe(true) + }) + + it('should return false if business has resigned', () => { + const wallet = { + business: { + name: 'mybusiness', + resigned: true + } + } + + expect(WalletService.canResignBusiness(wallet)).toBe(false) + }) + + it('should return false if not a business', () => { + const wallet = {} + + expect(WalletService.canResignBusiness(wallet)).toBe(false) + }) + }) + + describe('Messages', () => { + const message = 'test message' + const passphrase = 'test passphrase' + const publicKey = Identities.PublicKey.fromPassphrase(passphrase) + const messageSignature = '30440220115d561431df663a0e8b2807e0036d08ebdbbd0b77317cf6cd73d861c0983baf0220624792bd66453e666b3f08f4ee62b63736246e45da0bb19c9f747a37ce8b3db7' + + describe('signMessage', () => { + it('should sign a message', () => { + const response = WalletService.signMessage(message, passphrase) + + expect(response.message).toEqual(message) + expect(response.publicKey).toEqual(publicKey) + expect(response.signature).toEqual(messageSignature) + }) + }) + + describe('signMessageWithWif', () => { + it('should sign a message', () => { + const wif = Identities.WIF.fromPassphrase(passphrase) + const response = WalletService.signMessageWithWif(message, wif, { + wif: 170 + }) + + expect(response.message).toEqual(message) + expect(response.publicKey).toEqual(publicKey) + expect(response.signature).toEqual(messageSignature) + }) + }) + + describe('verifyMessage', () => { + it('should return true if verified successfully', () => { + expect(WalletService.verifyMessage(message, publicKey, messageSignature)).toBe(true) + }) + + it('should throw an error for wrong signature', () => { + expect(() => { + WalletService.verifyMessage(message, publicKey, 'wrong') + }).toThrowError() + }) + }) + }) + + describe('validateAddress', () => { + it('should return true if the address is valid', () => { + const address = Identities.Address.fromPassphrase('test passphrase') + + expect(WalletService.validateAddress(address)).toBe(true) + }) + + it('should return true if the address is valid on another network', () => { + const address = Identities.Address.fromPassphrase('test passphrase', 23) + + expect(WalletService.validateAddress(address, 23)).toBe(true) + }) + + it('should return false if the address is not valid', () => { + expect(WalletService.validateAddress('not an address')).toBe(false) + }) + + it('should return false if the address is for a different network', () => { + const address = Identities.Address.fromPassphrase('test passphrase for mainnet', 23) + + expect(WalletService.validateAddress(address)).toBe(false) + }) + }) + + describe('validatePassphrase', () => { + it('should return true when a string is provided', () => { + expect(WalletService.validatePassphrase('test')).toBe(true) + }) + }) + + describe('isBip39Passphrase', () => { + it('should return true for a valid passphrase', () => { + expect(WalletService.isBip39Passphrase('one video jaguar gap soldier ill hobby motor bundle couple trophy smoke', 'english')).toBeTrue() + }) + + it('should return false for an invalid passphrase', () => { + expect(WalletService.isBip39Passphrase('one two three four five six seven eight nine ten eleven twelve', 'english')).toBeFalse() }) }) @@ -76,6 +500,14 @@ describe('Services > Wallet', () => { }) }) + it('should error if username exists', () => { + const username = 'exists' + expect(WalletService.validateUsername(username)).toEqual({ + passes: false, + errors: [{ type: 'exists' }] + }) + }) + it('should not admit uppercase characters', () => { const username = 'eXamPLe' expect(WalletService.validateUsername(username)).toEqual({ @@ -132,13 +564,25 @@ describe('Services > Wallet', () => { }) }) - describe('isBip39Passphrase', () => { - it('should return true for a valid passphrase', () => { - expect(WalletService.isBip39Passphrase('one video jaguar gap soldier ill hobby motor bundle couple trophy smoke', 'english')).toBeTrue() + describe('verifyPassphrase', () => { + it('should return true if address matches passphrase', () => { + const passphrase = 'test passphrase' + const address = Identities.Address.fromPassphrase(passphrase) + + expect(WalletService.verifyPassphrase(address, passphrase)).toBe(true) }) - it('should return false for an invalid passphrase', () => { - expect(WalletService.isBip39Passphrase('one two three four five six seven eight nine ten eleven twelve', 'english')).toBeFalse() + it('should return false if address matches passphrase but wrong network', () => { + const passphrase = 'test passphrase' + const address = Identities.Address.fromPassphrase(passphrase) + + expect(WalletService.verifyPassphrase(address, passphrase, 23)).toBe(false) + }) + + it('should return false if address is not related to passphrase', () => { + const passphrase = 'test passphrase' + + expect(WalletService.verifyPassphrase('wrong address', passphrase)).toBe(false) }) }) }) diff --git a/__tests__/unit/store/modules/ledger.spec.js b/__tests__/unit/store/modules/ledger.spec.js index 3af6cbe1c0..1766befaca 100644 --- a/__tests__/unit/store/modules/ledger.spec.js +++ b/__tests__/unit/store/modules/ledger.spec.js @@ -14,6 +14,8 @@ Vue.use(apiClient) logger.error = jest.fn() let ledgerNameByAddress = () => null +const sessionNetwork = jest.fn() + let ledgerCache = false const nethash = '2a44f340d76ffc3df204c5f38cd355b7496c9065a1ade2ef92071436bd72e867' const store = new Vuex.Store({ @@ -34,10 +36,7 @@ const store = new Vuex.Store({ return 'profile id' }, network () { - return { - id: 'abc', - nethash - } + return sessionNetwork() } } }, @@ -55,12 +54,7 @@ const store = new Vuex.Store({ let spyConnect const disconnectLedger = async () => { - spyConnect = jest.spyOn( - ledgerService, - 'connect' - ).mockImplementation(() => { - return false - }) + spyConnect = jest.spyOn(ledgerService, 'connect').mockImplementation(() => false) await store.dispatch('ledger/disconnect') } @@ -69,24 +63,73 @@ beforeEach(async () => { if (spyConnect) { spyConnect.mockRestore() } + store.replaceState(JSON.parse(JSON.stringify(initialState))) + + sessionNetwork.mockReturnValue({ + id: 'abc', + nethash, + constants: { + aip11: false + } + }) + + store._vm.$error = jest.fn() + ClientService.host = 'http://127.0.0.1' ledgerNameByAddress = () => null nock.cleanAll() }) + describe('ledger store module', () => { - it('should init ledger service', () => { - store.dispatch('ledger/init', 1234) + it('should init ledger service', async () => { + await store.dispatch('ledger/init', 1234) expect(store.state.ledger.slip44).toBe(1234) }) - it('should set slip44 value', () => { - store.dispatch('ledger/init', 4567) + it('should set slip44 value', async () => { + await store.dispatch('ledger/init', 4567) expect(store.state.ledger.slip44).toBe(4567) }) + describe('updateVersion', () => { + it('should not show error if aip11 is false', async () => { + await store.dispatch('ledger/updateVersion') + + expect(store._vm.$error).not.toHaveBeenCalled() + }) + + it('should not show error if aip11 is false', async () => { + sessionNetwork.mockReturnValue({ + id: 'abc', + nethash, + constants: { + aip11: true + } + }) + + await store.dispatch('ledger/updateVersion') + + expect(store._vm.$error).toHaveBeenCalledWith( + 'Ledger update available! Please update the ARK app via Ledger Live to send transactions on this network', + 10000 + ) + }) + }) + + describe('getVersion', () => { + it('should return version', async () => { + expect(await store.dispatch('ledger/getVersion')).toEqual('1.0.0') + }) + + it('should fail when not connected', async () => { + await disconnectLedger() + await expect(store.dispatch('ledger/getVersion')).rejects.toThrow(/.*Ledger not connected$/) + }) + }) + describe('getWallet', () => { it('should return address and publicKey', async () => { expect(await store.dispatch('ledger/getWallet', 1)).toEqual({ @@ -106,6 +149,20 @@ describe('ledger store module', () => { }) describe('getAddress', () => { + it('should call ledger service', async () => { + await store.dispatch('ledger/connect') + await store.dispatch('ledger/setSlip44', 1234) + + const spy = jest.spyOn(ledgerService, 'getPublicKey').mockReturnValue('PUBLIC_KEY') + + const response = await store.dispatch('ledger/getPublicKey', 1) + + expect(response).toBe('PUBLIC_KEY') + expect(spy).toHaveBeenNthCalledWith(1, '44\'/1234\'/1\'/0/0') + + spy.mockRestore() + }) + it('should fail with invalid accountIndex', async () => { await expect(store.dispatch('ledger/getAddress')).rejects.toThrow(/.*accountIndex must be a Number$/) }) @@ -117,6 +174,20 @@ describe('ledger store module', () => { }) describe('getPublicKey', () => { + it('should call ledger service', async () => { + await store.dispatch('ledger/connect') + await store.dispatch('ledger/setSlip44', 1234) + + const spy = jest.spyOn(ledgerService, 'getPublicKey').mockReturnValue('PUBLIC_KEY') + + const response = await store.dispatch('ledger/getPublicKey', 1) + + expect(response).toBe('PUBLIC_KEY') + expect(spy).toHaveBeenNthCalledWith(1, '44\'/1234\'/1\'/0/0') + + spy.mockRestore() + }) + it('should fail with invalid accountIndex', async () => { await expect(store.dispatch('ledger/getPublicKey')).rejects.toThrow(/.*accountIndex must be a Number$/) }) @@ -128,6 +199,23 @@ describe('ledger store module', () => { }) describe('signTransaction', () => { + it('should call ledger service', async () => { + await store.dispatch('ledger/connect') + await store.dispatch('ledger/setSlip44', 1234) + + const spy = jest.spyOn(ledgerService, 'signTransaction').mockReturnValue('SIGNATURE') + + const response = await store.dispatch('ledger/signTransaction', { + accountIndex: 1, + transactionHex: 'abc' + }) + + expect(response).toBe('SIGNATURE') + expect(spy).toHaveBeenNthCalledWith(1, '44\'/1234\'/1\'/0/0', 'abc') + + spy.mockRestore() + }) + it('should fail with invalid accountIndex', async () => { await expect(store.dispatch('ledger/signTransaction')).rejects.toThrow(/.*accountIndex must be a Number$/) }) @@ -141,6 +229,37 @@ describe('ledger store module', () => { }) }) + describe('signMessage', () => { + it('should call ledger service', async () => { + await store.dispatch('ledger/connect') + await store.dispatch('ledger/setSlip44', 1234) + + const spy = jest.spyOn(ledgerService, 'signMessage').mockReturnValue('SIGNATURE') + + const response = await store.dispatch('ledger/signMessage', { + accountIndex: 1, + messageHex: 'abc' + }) + + expect(response).toBe('SIGNATURE') + expect(spy).toHaveBeenNthCalledWith(1, '44\'/1234\'/1\'/0/0', 'abc') + + spy.mockRestore() + }) + + it('should fail with invalid accountIndex', async () => { + await expect(store.dispatch('ledger/signMessage')).rejects.toThrow(/.*accountIndex must be a Number$/) + }) + + it('should fail when not connected', async () => { + await disconnectLedger() + await expect(store.dispatch('ledger/signMessage', { + accountIndex: 1, + messageHex: 'abc' + })).rejects.toThrow(/.*Ledger not connected$/) + }) + }) + describe('reloadWallets', () => { let spyGetWallet let spyCryptoGetAddress @@ -155,11 +274,11 @@ describe('ledger store module', () => { } spyGetWallet = jest.spyOn( ledgerService, - 'getWallet' + 'getPublicKey' ).mockImplementation((path) => { const matches = path.match(/^44'+\/.+'\/([0-9]+)'\/0\/0/) - return testWallets[matches[1]] + return testWallets[matches[1]].publicKey }) spyCryptoGetAddress = jest.spyOn( Identities.Address, diff --git a/config/index.js b/config/index.js index 292c5bbdf3..b96e88128e 100644 --- a/config/index.js +++ b/config/index.js @@ -47,16 +47,36 @@ exports.BIP39 = { ] } +exports.TRANSACTION_GROUPS = { + STANDARD: 1, + MAGISTRATE: 2 +} + exports.TRANSACTION_TYPES = { - TRANSFER: 0, - SECOND_SIGNATURE: 1, - DELEGATE_REGISTRATION: 2, - VOTE: 3, - MULTI_SIGNATURE: 4, - IPFS: 5, - TIMELOCK_TRANSFER: 6, - MULTI_PAYMENT: 7, - DELEGATE_RESIGNATION: 8 + MULTI_SIGN: -1, + + GROUP_1: { + TRANSFER: 0, + SECOND_SIGNATURE: 1, + DELEGATE_REGISTRATION: 2, + VOTE: 3, + MULTI_SIGNATURE: 4, + IPFS: 5, + MULTI_PAYMENT: 6, + DELEGATE_RESIGNATION: 7, + HTLC_LOCK: 8, + HTLC_CLAIM: 9, + HTLC_REFUND: 10 + }, + + GROUP_2: { + BUSINESS_REGISTRATION: 0, + BUSINESS_RESIGNATION: 1, + BUSINESS_UPDATE: 2, + BRIDGECHAIN_REGISTRATION: 3, + BRIDGECHAIN_RESIGNATION: 4, + BRIDGECHAIN_UPDATE: 5 + } } exports.INTERVALS = { @@ -137,15 +157,28 @@ exports.THEMES = [ ] exports.V1 = { - fees: [ - 0.1 * 1e8, // Transfer - 5 * 1e8, // Second signautre - 25 * 1e8, // Delegate registration - 1 * 1e8, // Vote - 5 * 1e8, // Multisignature - 0 * 1e8, // IPFS (not supported yet) - 0 * 1e8, // Timelock transfer (not supported yet) - 0 * 1e8, // Multu-payment (not supported yet) - 0 * 1e8 // Delegate resignation (not supported yet) - ] + fees: { + GROUP_1: [ + 0.1 * 1e8, // Transfer + 5 * 1e8, // Second signautre + 25 * 1e8, // Delegate registration + 1 * 1e8, // Vote + 5 * 1e8, // Multisignature + 5 * 1e8, // IPFS + 1 * 1e8, // Multi-payment + 25 * 1e8, // Delegate resignation + 1 * 1e8, // HTLC Lock + 0 * 1e8, // HTLC Claim + 0 * 1e8 // HTLC Refund + ], + + GROUP_2: [ + 50 * 1e8, // Business Registration + 50 * 1e8, // Business Resignation + 50 * 1e8, // Business Update + 50 * 1e8, // Bridgechain Registration + 50 * 1e8, // Bridgechain Resignation + 50 * 1e8 // Bridgechain Update + ] + } } diff --git a/package.json b/package.json index 1d9d949e77..9f10707a2f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "ark-desktop-wallet", - "version": "2.7.0", + "version": "2.8.0", "repository": { "type": "git", "url": "git+https://github.com/ArkEcosystem/desktop-wallet.git" @@ -22,11 +22,14 @@ "main": "./dist/electron/main.js", "scripts": { "build": "node .electron-vue/build.js && electron-builder", - "build:win": "node .electron-vue/build.js && electron-builder --win --x64 --ia32 --publish onTagOrDraft", + "build:win": "node .electron-vue/build.js && electron-builder --win --x64 --ia32", + "build:win:publish": "node .electron-vue/build.js && electron-builder --win --x64 --ia32 --publish onTagOrDraft", "build:win32": "node .electron-vue/build.js && electron-builder --win --ia32", "build:win64": "node .electron-vue/build.js && electron-builder --win --x64", - "build:mac": "node .electron-vue/build.js && electron-builder --mac --publish onTagOrDraft", - "build:linux": "node .electron-vue/build.js && electron-builder --linux --publish onTagOrDraft", + "build:mac": "node .electron-vue/build.js && electron-builder --mac", + "build:mac:publish": "node .electron-vue/build.js && electron-builder --mac --publish onTagOrDraft", + "build:linux": "node .electron-vue/build.js && electron-builder --linux", + "build:linux:publish": "node .electron-vue/build.js && electron-builder --linux --publish onTagOrDraft", "build:dir": "node .electron-vue/build.js && electron-builder --dir", "build:clean": "cross-env BUILD_TARGET=clean node .electron-vue/build.js", "build:web": "cross-env BUILD_TARGET=web node .electron-vue/build.js", @@ -49,15 +52,16 @@ "test:unit:watch": "jest --config __tests__/unit.jest.conf.js --watch" }, "dependencies": { - "@arkecosystem/client": "^1.0.1", - "@arkecosystem/crypto": "^2.5.7", - "@arkecosystem/ledger-transport": "^0.1.0", + "@arkecosystem/client": "^1.0.5", + "@arkecosystem/core-magistrate-crypto": "^2.6.0-next.9", + "@arkecosystem/crypto": "^2.6.0-next.9", + "@arkecosystem/ledger-transport": "^1.0.3", "@arkecosystem/peers": "^0.2.1", "@babel/runtime": "^7.4.2", "@fortawesome/fontawesome-svg-core": "^1.2.21", "@fortawesome/free-solid-svg-icons": "^5.10.1", "@fortawesome/vue-fontawesome": "^0.1.6", - "@ledgerhq/hw-transport-node-hid-singleton": "^4.68.4", + "@ledgerhq/hw-transport-node-hid-singleton": "^5.7.0", "about-window": "^1.13.1", "animate.css": "^3.7.0", "async": "^3.0.0", @@ -72,6 +76,7 @@ "cycled": "^1.0.0", "dayjs": "^1.8.11", "decompress": "^4.2.0", + "deepmerge": "^4.1.1", "du": "^1.0.0", "electron-dl": "^2.0.0", "electron-log": "^3.0.7", @@ -121,7 +126,7 @@ "autoprefixer": "^9.5.0", "babel-core": "^7.0.0-0", "babel-eslint": "^10.0.1", - "babel-jest": "^24.5.0", + "babel-jest": "^25.0.0", "babel-loader": "^8.0.6", "babel-plugin-require-context-hook": "^1.0.0", "babel-register": "^6.26.0", @@ -129,12 +134,12 @@ "chalk": "^3.0.0", "codecov": "^3.2.0", "copy-webpack-plugin": "^5.0.2", - "cross-env": "^6.0.0", + "cross-env": "^7.0.0", "css-loader": "^3.1.0", "del": "^5.0.0", "devtron": "^1.4.0", "electron": "^6.0.0", - "electron-builder": "22.2.0", + "electron-builder": "22.3.2", "electron-debug": "^3.0.1", "electron-devtools-installer": "^2.2.4", "eslint": "^6.1.0", @@ -142,7 +147,7 @@ "eslint-friendly-formatter": "^4.0.1", "eslint-loader": "^3.0.0", "eslint-plugin-import": "^2.18.2", - "eslint-plugin-node": "^10.0.0", + "eslint-plugin-node": "^11.0.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.0", "eslint-plugin-vue": "^6.0.0", @@ -151,11 +156,11 @@ "html-webpack-plugin": "^3.2.0", "husky": "^4.0.0", "intl": "^1.2.5", - "jest": "^24.5.0", + "jest": "^25.0.0", "jest-extended": "^0.11.1", "jest-serializer-vue": "^2.0.2", "jest-vue-preprocessor": "^1.5.0", - "lint-staged": "^9.2.1", + "lint-staged": "^10.0.0", "mini-css-extract-plugin": "^0.9.0", "mock-socket": "^9.0.0", "multispinner": "^0.2.1", @@ -163,10 +168,10 @@ "node-abi": "^2.11.0", "node-loader": "^0.6.0", "postcss-loader": "^3.0.0", - "purgecss": "^1.1.0", - "purgecss-webpack-plugin": "^1.4.0", + "purgecss": "^2.0.5", + "purgecss-webpack-plugin": "^2.0.5", "rss-parser": "^3.7.0", - "spectron": "^9.0.0", + "spectron": "^10.0.0", "svg-sprite-loader": "^4.1.6", "svgo": "^1.2.0", "svgo-loader": "^2.2.0", @@ -187,6 +192,7 @@ "productName": "Ark Desktop Wallet", "appId": "io.ark.desktop-wallet", "artifactName": "${name}-${os}-${arch}-${version}.${ext}", + "npmRebuild": false, "publish": { "provider": "github", "vPrefixedTagName": false diff --git a/src/renderer/App.vue b/src/renderer/App.vue index 3c4ec981ca..9e9b2379e7 100644 --- a/src/renderer/App.vue +++ b/src/renderer/App.vue @@ -95,6 +95,12 @@ @change="onPortalChange('qr-scan', ...arguments)" /> + +
@@ -308,16 +314,18 @@ export default { ipcRenderer.send('updater:check-for-updates') await this.$store.dispatch('peer/refresh') this.$store.dispatch('peer/connectToBest', {}) + await this.$store.dispatch('network/updateData') if (this.session_network) { this.$store.dispatch('ledger/init', this.session_network.slip44) this.$store.dispatch('delegate/load') } - this.$eventBus.on('client:changed', () => { - this.$store.dispatch('ledger/init', this.session_network.slip44) + this.$eventBus.on('client:changed', async () => { this.$store.dispatch('peer/connectToBest', {}) + this.$store.dispatch('network/updateData') this.$store.dispatch('delegate/load') + await this.$store.dispatch('ledger/init', this.session_network.slip44) if (this.$store.getters['ledger/isConnected']) { this.$store.dispatch('ledger/reloadWallets', { clearFirst: true, forceLoad: true }) } @@ -418,10 +426,6 @@ export default { + + diff --git a/src/renderer/components/Button/ButtonModal.vue b/src/renderer/components/Button/ButtonModal.vue index a7391eb930..6e2274a9fd 100644 --- a/src/renderer/components/Button/ButtonModal.vue +++ b/src/renderer/components/Button/ButtonModal.vue @@ -51,6 +51,12 @@ export default { label: { type: String, required: true + }, + + disabled: { + type: Boolean, + required: false, + default: false } }, @@ -62,7 +68,12 @@ export default { emitToggle () { this.$emit('toggle', this.isOpen) }, + toggle () { + if (this.disabled) { + return + } + this.isOpen = !this.isOpen this.emitToggle() } diff --git a/src/renderer/components/Button/index.js b/src/renderer/components/Button/index.js index f755aed50a..1c2b4cf6f0 100644 --- a/src/renderer/components/Button/index.js +++ b/src/renderer/components/Button/index.js @@ -8,9 +8,13 @@ import ButtonReload from './ButtonReload' import ButtonSwitch from './ButtonSwitch' import ButtonLayout from './ButtonLayout' +// Last because of order +import ButtonDropdown from './ButtonDropdown' + export { ButtonClipboard, ButtonClose, + ButtonDropdown, ButtonGeneric, ButtonIconGeneric, ButtonLetter, diff --git a/src/renderer/components/Input/InputAddress.vue b/src/renderer/components/Input/InputAddress.vue index 06507377cd..7518952c60 100644 --- a/src/renderer/components/Input/InputAddress.vue +++ b/src/renderer/components/Input/InputAddress.vue @@ -16,6 +16,7 @@ :is-disabled="isDisabled" :is-focused="isFocused" :is-invalid="invalid" + :warning-text="warningText" class="InputAddress text-left" >
wallet && !!wallet.address) const addresses = map(source, (wallet) => { const address = { @@ -197,7 +208,7 @@ export default { }) const results = orderBy(addresses, (object) => { - return object.name || object.address.toLowerCase() + return (object.name || object.address).toLowerCase() }) return results.reduce((wallets, wallet, index) => { @@ -370,13 +381,27 @@ export default { this.$nextTick(() => { this.$refs.input.setSelectionRange(this.inputValue.length, this.dropdownValue.length) }) + }, + + reset () { + this.model = '' + this.$nextTick(() => { + this.$v.$reset() + }) } }, validations: { model: { - required, + required (value) { + return this.isRequired ? required(value) : true + }, + isValid (value) { + if (!this.isRequired && value.replace(/\s+/, '') === '') { + return true + } + return WalletService.validateAddress(value, this.pubKeyHash) } } diff --git a/src/renderer/components/Input/InputCurrency.vue b/src/renderer/components/Input/InputCurrency.vue index 9fbee70291..4b7b94a152 100644 --- a/src/renderer/components/Input/InputCurrency.vue +++ b/src/renderer/components/Input/InputCurrency.vue @@ -40,7 +40,6 @@ import { includes, isString } from 'lodash' import { required } from 'vuelidate/lib/validators' import { MARKET } from '@config' -import store from '@/store' import InputField from './InputField' import BigNumber from '@/plugins/bignumber' @@ -365,7 +364,7 @@ export default { * @return {Boolean} */ currencyValidator (currency) { - const currentNetwork = this.walletNetwork || store.getters['session/network'] + const currentNetwork = this.walletNetwork || this.$store.getters['session/network'] const currencies = [ currentNetwork.token, currentNetwork.subunit, @@ -374,6 +373,13 @@ export default { ...Object.values(MARKET.currencies).map(currency => currency.symbol) ] return includes(currencies, currency) + }, + + reset () { + this.inputValue = '' + this.$nextTick(() => { + this.$v.model.$reset() + }) } }, diff --git a/src/renderer/components/Input/InputDelegate.vue b/src/renderer/components/Input/InputDelegate.vue index 91a60ac2b8..76aeafd227 100644 --- a/src/renderer/components/Input/InputDelegate.vue +++ b/src/renderer/components/Input/InputDelegate.vue @@ -66,7 +66,7 @@ import { MenuDropdown } from '@/components/Menu' import Cycled from 'cycled' import InputField from './InputField' import truncate from '@/filters/truncate' -import { includes, isEmpty, map, orderBy } from 'lodash' +import { isEmpty, orderBy } from 'lodash' export default { name: 'InputDelegate', @@ -120,7 +120,7 @@ export default { }, delegates () { - return this.$store.getters['delegate/bySessionNetwork'] + return Object.values(this.$store.getters['delegate/bySessionNetwork'] || {}).filter(delegate => !delegate.isResigned) }, error () { @@ -168,7 +168,7 @@ export default { return [] } - const delegates = map(this.delegates, (object) => { + const delegates = this.delegates.map(object => { const delegate = { name: null, username: object.username, @@ -187,7 +187,7 @@ export default { return results.reduce((delegates, delegate, index) => { Object.values(delegate).forEach(prop => { - if (includes(prop.toLowerCase(), this.inputValue.toLowerCase())) { + if (prop.toLowerCase().includes(this.inputValue.toLowerCase())) { delegates[delegate.username] = delegate.name } }) @@ -243,7 +243,7 @@ export default { // Verifies that the element that generated the blur was a dropdown item if (evt.relatedTarget) { const classList = evt.relatedTarget.classList || [] - const isDropdownItem = includes(classList, 'MenuDropdownItem__button') + const isDropdownItem = classList.includes('MenuDropdownItem__button') if (!isDropdownItem) { this.closeDropdown() diff --git a/src/renderer/components/Input/InputEditableList.vue b/src/renderer/components/Input/InputEditableList.vue new file mode 100644 index 0000000000..13361c9be6 --- /dev/null +++ b/src/renderer/components/Input/InputEditableList.vue @@ -0,0 +1,146 @@ + + + + + diff --git a/src/renderer/components/Input/InputFee.vue b/src/renderer/components/Input/InputFee.vue index 585cb2b9d8..b77446af3a 100644 --- a/src/renderer/components/Input/InputFee.vue +++ b/src/renderer/components/Input/InputFee.vue @@ -92,6 +92,12 @@ export default { required: true }, + transactionGroup: { + type: Number, + required: false, + default: 1 + }, + showInsufficientFunds: { type: Boolean, required: false, @@ -145,8 +151,8 @@ export default { return this.$t('INPUT_FEE.ERROR.NOT_VALID') }, maxV1fee () { - const defaultMaxV1Fee = V1.fees[this.transactionType] - const staticFee = this.$store.getters['transaction/staticFee'](this.transactionType) + const defaultMaxV1Fee = V1.fees[`GROUP_${this.transactionGroup}`][this.transactionType] + const staticFee = this.$store.getters['transaction/staticFee'](this.transactionType, this.transactionGroup) return staticFee || defaultMaxV1Fee }, isStaticFee () { @@ -167,9 +173,19 @@ export default { } const { feeStatistics } = this.feeNetwork - const transactionStatistics = feeStatistics.find(feeConfig => feeConfig.type === this.transactionType) - if (transactionStatistics) { - return transactionStatistics.fees + if (feeStatistics) { + let transactionStatistics + if (feeStatistics[0]) { + transactionStatistics = Object.values(feeStatistics).find(feeConfig => feeConfig.type === this.transactionType) + } else if (feeStatistics[this.transactionGroup]) { + transactionStatistics = Object.values(feeStatistics[this.transactionGroup]).find(feeConfig => { + return feeConfig.type === this.transactionType + }) + } + + if (transactionStatistics) { + return transactionStatistics.fees + } } return { diff --git a/src/renderer/components/Input/InputPublicKey.vue b/src/renderer/components/Input/InputPublicKey.vue new file mode 100644 index 0000000000..c8a87a25f5 --- /dev/null +++ b/src/renderer/components/Input/InputPublicKey.vue @@ -0,0 +1,111 @@ + + + diff --git a/src/renderer/components/Input/InputText.vue b/src/renderer/components/Input/InputText.vue index 93da552198..9f640d47e1 100644 --- a/src/renderer/components/Input/InputText.vue +++ b/src/renderer/components/Input/InputText.vue @@ -171,6 +171,13 @@ export default { blur () { this.$refs.input.blur() + }, + + reset () { + this.model = '' + this.$nextTick(() => { + this.$v.$reset() + }) } }, diff --git a/src/renderer/components/Input/index.js b/src/renderer/components/Input/index.js index 8d202da211..c02e0a6ce9 100644 --- a/src/renderer/components/Input/index.js +++ b/src/renderer/components/Input/index.js @@ -1,10 +1,12 @@ import InputAddress from './InputAddress' import InputCurrency from './InputCurrency' import InputDelegate from './InputDelegate' +import InputEditableList from './InputEditableList' import InputFee from './InputFee' import InputField from './InputField' import InputLanguage from './InputLanguage' import InputPassword from './InputPassword' +import InputPublicKey from './InputPublicKey' import InputSelect from './InputSelect' import InputSwitch from './InputSwitch' import InputText from './InputText' @@ -16,10 +18,12 @@ export { InputAddress, InputCurrency, InputDelegate, + InputEditableList, InputFee, InputField, InputLanguage, InputPassword, + InputPublicKey, InputSelect, InputSwitch, InputText diff --git a/src/renderer/components/Menu/MenuDropdown/MenuDropdownAlternativeHandler.vue b/src/renderer/components/Menu/MenuDropdown/MenuDropdownAlternativeHandler.vue index 96e2a803f6..0ea4f1e1b0 100644 --- a/src/renderer/components/Menu/MenuDropdown/MenuDropdownAlternativeHandler.vue +++ b/src/renderer/components/Menu/MenuDropdown/MenuDropdownAlternativeHandler.vue @@ -8,7 +8,7 @@ 'rotate-vertical': isOpen }" name="arrow-dropdown" - view-box="0 0 18 18" + view-box="0 0 8 8" class="transition align-middle" /> diff --git a/src/renderer/components/Menu/MenuDropdown/MenuDropdownHandler.vue b/src/renderer/components/Menu/MenuDropdown/MenuDropdownHandler.vue index e19b732d83..0688e42f7b 100644 --- a/src/renderer/components/Menu/MenuDropdown/MenuDropdownHandler.vue +++ b/src/renderer/components/Menu/MenuDropdown/MenuDropdownHandler.vue @@ -19,7 +19,7 @@ diff --git a/src/renderer/components/Menu/MenuTab/MenuTab.vue b/src/renderer/components/Menu/MenuTab/MenuTab.vue index 9418f6f520..0aeba59dfb 100644 --- a/src/renderer/components/Menu/MenuTab/MenuTab.vue +++ b/src/renderer/components/Menu/MenuTab/MenuTab.vue @@ -1,7 +1,7 @@