Browse Source

test(bitcoind): add unit tests for services, store and utils

master
jamaljsr 5 years ago
parent
commit
2351a21235
  1. 1
      src/__mocks__/docker-compose.js
  2. 35
      src/components/designer/lightning/actions/ChangeBackendModal.spec.tsx
  3. 8
      src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx
  4. 31
      src/lib/bitcoin/bitcoindService.spec.ts
  5. 2
      src/lib/bitcoin/bitcoindService.ts
  6. 20
      src/lib/docker/dockerService.spec.ts
  7. 5
      src/lib/lightning/clightning/clightningService.spec.ts
  8. 5
      src/lib/lightning/lnd/lndService.spec.ts
  9. 18
      src/store/models/bitcoind.ts
  10. 138
      src/store/models/designer.spec.ts
  11. 12
      src/store/models/designer.ts
  12. 39
      src/store/models/lightning.spec.ts
  13. 133
      src/store/models/network.spec.ts
  14. 11
      src/utils/async.spec.ts
  15. 10
      src/utils/chart.spec.ts
  16. 3
      src/utils/chart.ts
  17. 43
      src/utils/network.spec.ts
  18. 2
      src/utils/network.ts

1
src/__mocks__/docker-compose.js

@ -1,5 +1,6 @@
module.exports = {
upAll: jest.fn(),
upOne: jest.fn(),
stop: jest.fn(),
stopOne: jest.fn(),
down: jest.fn(),

35
src/components/designer/lightning/actions/ChangeBackendModal.spec.tsx

@ -84,6 +84,28 @@ describe('ChangeBackendModal', () => {
expect(queryByText('Cancel')).not.toBeInTheDocument();
});
it('should remove chart link when cancel is clicked', async () => {
const { getByText, store } = await renderComponent();
const { designer } = store.getActions();
const linkId = 'xxxx';
const link = { linkId, fromNodeId: 'alice', fromPortId: 'backend' } as any;
// create a new link which will open the modal
act(() => {
designer.onLinkStart(link);
});
act(() => {
designer.onLinkComplete({
...link,
toNodeId: 'backend2',
toPortId: 'backend',
} as any);
});
expect(store.getState().designer.activeChart.links[linkId]).toBeTruthy();
fireEvent.click(getByText('Cancel'));
await wait();
expect(store.getState().designer.activeChart.links[linkId]).toBeUndefined();
});
it('should display the compatibility warning for older bitcoin node', async () => {
const { getByText, queryByText, getByLabelText } = await renderComponent();
const warning =
@ -125,6 +147,19 @@ describe('ChangeBackendModal', () => {
).toBeInTheDocument();
});
it('should succeed if a previous link does not exist', async () => {
const { getByText, getByLabelText, store } = await renderComponent();
store.getActions().designer.removeLink('alice-backend1');
fireEvent.click(getByLabelText('Bitcoin Node'));
fireEvent.click(getByText('backend2'));
fireEvent.click(getByText('Change Backend'));
await wait();
expect(store.getState().modals.changeBackend.visible).toBe(false);
expect(
getByText('The alice node will pull chain data from backend2'),
).toBeInTheDocument();
});
it('should restart containers when backend is updated', async () => {
const { getByText, getByLabelText } = await renderComponent(Status.Started);
fireEvent.click(getByLabelText('Bitcoin Node'));

8
src/components/designer/lightning/actions/PayInvoiceModal.spec.tsx

@ -1,5 +1,5 @@
import React from 'react';
import { fireEvent, wait } from '@testing-library/dom';
import { fireEvent, wait } from '@testing-library/react';
import { Status } from 'shared/types';
import { initChartFromNetwork } from 'utils/chart';
import {
@ -58,15 +58,13 @@ describe('PayInvoiceModal', () => {
});
it('should hide modal when cancel is clicked', async () => {
jest.useFakeTimers();
const { getByText, queryByText } = await renderComponent();
const btn = getByText('Cancel');
expect(btn).toBeInTheDocument();
expect(btn.parentElement).toBeInstanceOf(HTMLButtonElement);
await wait(() => fireEvent.click(getByText('Cancel')));
jest.runAllTimers();
fireEvent.click(getByText('Cancel'));
await wait();
expect(queryByText('Cancel')).not.toBeInTheDocument();
jest.useRealTimers();
});
it('should display an error if form is not valid', async () => {

31
src/lib/bitcoin/bitcoindService.spec.ts

@ -1,4 +1,6 @@
import BitcoinCore from 'bitcoin-core';
import { BitcoindVersion } from 'shared/types';
import { createBitcoindNetworkNode } from 'utils/network';
import { getNetwork } from 'utils/tests';
import bitcoindService from './bitcoindService';
@ -6,7 +8,11 @@ jest.mock('bitcoin-core');
const mockBitcoin = (BitcoinCore as unknown) as jest.Mock<BitcoinCore>;
describe('BitcoindService', () => {
const node = getNetwork().nodes.bitcoin[0];
const network = getNetwork();
network.nodes.bitcoin.push(
createBitcoindNetworkNode(network, BitcoindVersion['0.18.1']),
);
const node = network.nodes.bitcoin[0];
const mockProto = BitcoinCore.prototype;
// helper func to get the first instance created during the test
const getInst = () => mockBitcoin.mock.instances[0];
@ -32,6 +38,17 @@ describe('BitcoindService', () => {
expect(info.balance).toEqual(5);
});
it('should connect peers', async () => {
await bitcoindService.connectPeers(node);
expect(mockBitcoin.mock.instances[0].addNode).toBeCalledTimes(1);
});
it('should not throw error if connect peers fails', async () => {
mockProto.addNode = jest.fn().mockRejectedValue('add-error');
await bitcoindService.connectPeers(node);
await expect(bitcoindService.connectPeers(node)).resolves.not.toThrow();
});
it('should mine new blocks', async () => {
const result = await bitcoindService.mine(2, node);
expect(getInst().getNewAddress).toBeCalledTimes(1);
@ -57,10 +74,20 @@ describe('BitcoindService', () => {
expect(txid).toEqual('txid');
});
it('should send funds with sufficient balance above maturity height', async () => {
mockProto.getBlockchainInfo = jest.fn().mockResolvedValue({ blocks: 110 });
mockProto.getWalletInfo = jest.fn().mockResolvedValue({ balance: 0 });
mockProto.listTransactions = jest.fn().mockResolvedValue([{ confirmations: 101 }]);
const txid = await bitcoindService.sendFunds(node, 'destaddr', 100);
expect(getInst().getWalletInfo).toBeCalledTimes(1);
expect(getInst().sendToAddress).toBeCalledWith('destaddr', 100);
expect(txid).toEqual('txid');
});
it('should send funds with insufficient balance above maturity height', async () => {
mockProto.getBlockchainInfo = jest.fn().mockResolvedValue({ blocks: 110 });
mockProto.getWalletInfo = jest.fn().mockResolvedValue({ balance: 0 });
mockProto.listTransactions = jest.fn().mockResolvedValue([]);
mockProto.listTransactions = jest.fn().mockResolvedValue([{ confirmations: 10 }]);
const txid = await bitcoindService.sendFunds(node, 'destaddr', 10);
expect(getInst().getWalletInfo).toBeCalledTimes(1);
expect(getInst().sendToAddress).toBeCalledWith('destaddr', 10);

2
src/lib/bitcoin/bitcoindService.ts

@ -96,7 +96,7 @@ class BitcoindService implements BitcoindLibrary {
const neededConfs = Math.max(0, COINBASE_MATURITY_DELAY - confs);
if (neededConfs > 0) {
await this.mine(neededConfs, node);
// this may mines up to 100 blocks at once, so add a couple second
// this may mine up to 100 blocks at once, so add a couple second
// delay to allow the other nodes to process all of the new blocks
await delay(2 * 1000);
}

20
src/lib/docker/dockerService.spec.ts

@ -274,6 +274,26 @@ describe('DockerService', () => {
);
});
it('should call compose.upOne when a node is started', async () => {
composeMock.upOne.mockResolvedValue(mockResult);
const node = network.nodes.lightning[0];
await dockerService.startNode(network, node);
expect(composeMock.upOne).toBeCalledWith(
node.name,
expect.objectContaining({ cwd: network.path }),
);
});
it('should call compose.stopOne when a node is stopped', async () => {
composeMock.stopOne.mockResolvedValue(mockResult);
const node = network.nodes.lightning[0];
await dockerService.stopNode(network, node);
expect(composeMock.stopOne).toBeCalledWith(
node.name,
expect.objectContaining({ cwd: network.path }),
);
});
it('should call compose.stopOne and compose.rm when a node is removed', async () => {
composeMock.stopOne.mockResolvedValue(mockResult);
composeMock.rm.mockResolvedValue(mockResult);

5
src/lib/lightning/clightning/clightningService.spec.ts

@ -72,6 +72,11 @@ describe('CLightningService', () => {
expect(actual).toEqual(expected);
});
it('should not throw error when connecting to peers', async () => {
clightningApiMock.httpPost.mockRejectedValue(new Error('peer-error'));
await expect(clightningService.connectPeers(node, ['asdf'])).resolves.not.toThrow();
});
it('should close the channel', async () => {
const expected = true;
clightningApiMock.httpDelete.mockResolvedValue(expected);

5
src/lib/lightning/lnd/lndService.spec.ts

@ -67,6 +67,11 @@ describe('LndService', () => {
expect(actual).toEqual(expected);
});
it('should not throw error when connecting to peers', async () => {
lndProxyClient.connectPeer = jest.fn().mockRejectedValue(new Error('peer-error'));
await expect(lndService.connectPeers(node, ['asdf'])).resolves.not.toThrow();
});
it('should close the channel', async () => {
const expected = true;
lndProxyClient.closeChannel = jest.fn().mockResolvedValue(expected);

18
src/store/models/bitcoind.ts

@ -20,8 +20,10 @@ export interface BitcoindNodeModel {
export interface BitcoindModel {
nodes: BitcoindNodeMapping;
removeNode: Action<BitcoindModel, string>;
setChainInfo: Action<BitcoindModel, { node: BitcoinNode; chainInfo: ChainInfo }>;
setWalletinfo: Action<BitcoindModel, { node: BitcoinNode; walletInfo: WalletInfo }>;
setInfo: Action<
BitcoindModel,
{ node: BitcoinNode; chainInfo: ChainInfo; walletInfo: WalletInfo }
>;
getInfo: Thunk<BitcoindModel, BitcoinNode, StoreInjections>;
mine: Thunk<
BitcoindModel,
@ -36,23 +38,17 @@ const bitcoindModel: BitcoindModel = {
nodes: {},
// reducer actions (mutations allowed thx to immer)
removeNode: action((state, name) => {
if (state.nodes[name]) {
delete state.nodes[name];
}
delete state.nodes[name];
}),
setChainInfo: action((state, { node, chainInfo }) => {
setInfo: action((state, { node, chainInfo, walletInfo }) => {
if (!state.nodes[node.name]) state.nodes[node.name] = {};
state.nodes[node.name].chainInfo = chainInfo;
}),
setWalletinfo: action((state, { node, walletInfo }) => {
if (!state.nodes[node.name]) state.nodes[node.name] = {};
state.nodes[node.name].walletInfo = walletInfo;
}),
getInfo: thunk(async (actions, node, { injections }) => {
const chainInfo = await injections.bitcoindService.getBlockchainInfo(node);
actions.setChainInfo({ node, chainInfo });
const walletInfo = await injections.bitcoindService.getWalletInfo(node);
actions.setWalletinfo({ node, walletInfo });
actions.setInfo({ node, chainInfo, walletInfo });
}),
mine: thunk(async (actions, { blocks, node }, { injections, getStoreState }) => {
if (blocks < 0) throw new Error(l('mineError'));

138
src/store/models/designer.spec.ts

@ -1,7 +1,7 @@
import { wait } from '@testing-library/dom';
import { notification } from 'antd';
import { createStore } from 'easy-peasy';
import { LndVersion, Status } from 'shared/types';
import { BitcoindVersion, LndVersion, Status } from 'shared/types';
import { BitcoindLibrary, DockerLibrary } from 'types';
import { LOADING_NODE_ID } from 'utils/constants';
import { injections, lightningServiceMock } from 'utils/tests';
@ -64,7 +64,7 @@ describe('Designer model', () => {
name: 'test',
lndNodes: 2,
clightningNodes: 1,
bitcoindNodes: 1,
bitcoindNodes: 2,
});
});
@ -74,7 +74,7 @@ describe('Designer model', () => {
expect(activeId).toBe(-1);
expect(activeChart).toBeUndefined();
expect(chart).not.toBeUndefined();
expect(Object.keys(chart.nodes)).toHaveLength(4);
expect(Object.keys(chart.nodes)).toHaveLength(5);
});
it('should set the active chart', () => {
@ -82,7 +82,7 @@ describe('Designer model', () => {
const { activeId, activeChart } = store.getState().designer;
expect(activeId).toBe(firstNetwork().id);
expect(activeChart).toBeDefined();
expect(Object.keys(activeChart.nodes)).toHaveLength(4);
expect(Object.keys(activeChart.nodes)).toHaveLength(5);
});
it('should remove the active chart', () => {
@ -222,6 +222,80 @@ describe('Designer model', () => {
}),
);
});
it('should throw an error for bitcoin to bitcoin node links', () => {
const { onLinkStart, onLinkComplete } = store.getActions().designer;
const data = {
...payload,
fromNodeId: 'backend1',
fromPortId: 'peer-right',
toNodeId: 'backend2',
toPortId: 'peer-left',
};
const spy = jest.spyOn(store.getActions().app, 'notify');
onLinkStart(data);
onLinkComplete(data);
expect(spy).toBeCalledWith(
expect.objectContaining({
message: 'Cannot connect nodes',
error: new Error(
'Connections between bitcoin nodes are managed automatically',
),
}),
);
});
it('should throw an error for LN -> backend if backend ports are not used', () => {
const { onLinkStart, onLinkComplete } = store.getActions().designer;
const data = {
...payload,
fromNodeId: 'alice',
fromPortId: 'empty-right',
toNodeId: 'backend2',
toPortId: 'backend',
};
const spy = jest.spyOn(store.getActions().app, 'notify');
onLinkStart(data);
onLinkComplete(data);
expect(spy).toBeCalledWith(
expect.objectContaining({
message: 'Cannot connect nodes',
error: new Error(
'Use the top & bottom ports to connect between bitcoin and lightning nodes',
),
}),
);
});
it('should show the ChangeBackend modal when dragging from LN -> backend', () => {
const { onLinkStart, onLinkComplete } = store.getActions().designer;
const data = {
...payload,
fromNodeId: 'alice',
fromPortId: 'backend',
toNodeId: 'backend2',
toPortId: 'backend',
};
expect(store.getState().modals.changeBackend.visible).toBe(false);
onLinkStart(data);
onLinkComplete(data);
expect(store.getState().modals.changeBackend.visible).toBe(true);
});
it('should show the ChangeBackend modal when dragging from backend -> LN', () => {
const { onLinkStart, onLinkComplete } = store.getActions().designer;
const data = {
...payload,
fromNodeId: 'backend2',
fromPortId: 'backend',
toNodeId: 'alice',
toPortId: 'backend',
};
expect(store.getState().modals.changeBackend.visible).toBe(false);
onLinkStart(data);
onLinkComplete(data);
expect(store.getState().modals.changeBackend.visible).toBe(true);
});
});
describe('onCanvasDrop', () => {
@ -246,16 +320,49 @@ describe('Designer model', () => {
});
});
it('should add a new node to the chart', async () => {
it('should add a new LN node to the chart', async () => {
const { onCanvasDrop } = store.getActions().designer;
expect(Object.keys(firstChart().nodes)).toHaveLength(4);
expect(Object.keys(firstChart().nodes)).toHaveLength(5);
onCanvasDrop({ data, position });
await wait(() => {
expect(Object.keys(firstChart().nodes)).toHaveLength(5);
expect(Object.keys(firstChart().nodes)).toHaveLength(6);
expect(firstChart().nodes['carol']).toBeDefined();
});
});
it('should add a new bitcoin node to the chart', async () => {
const { onCanvasDrop } = store.getActions().designer;
expect(Object.keys(firstChart().nodes)).toHaveLength(5);
const bitcoinData = { type: 'bitcoind', version: BitcoindVersion.latest };
onCanvasDrop({ data: bitcoinData, position });
await wait(() => {
expect(Object.keys(firstChart().nodes)).toHaveLength(6);
expect(firstChart().nodes['backend2']).toBeDefined();
});
});
it('should add a new bitcoin node without a link', async () => {
const { addNetwork } = store.getActions().network;
const { onCanvasDrop, setActiveId } = store.getActions().designer;
await addNetwork({
name: 'test 3',
lndNodes: 0,
clightningNodes: 0,
bitcoindNodes: 0,
});
const newId = store.getState().network.networks[1].id;
setActiveId(newId);
const getChart = () => store.getState().designer.allCharts[newId];
expect(Object.keys(getChart().nodes)).toHaveLength(0);
const bitcoinData = { type: 'bitcoind', version: BitcoindVersion.latest };
onCanvasDrop({ data: bitcoinData, position });
await wait(() => {
expect(Object.keys(getChart().nodes)).toHaveLength(1);
expect(Object.keys(getChart().links)).toHaveLength(0);
expect(getChart().nodes['backend1']).toBeDefined();
});
});
it('should update docker compose file', async () => {
mockDockerService.saveComposeFile.mockReset();
const { onCanvasDrop } = store.getActions().designer;
@ -267,6 +374,23 @@ describe('Designer model', () => {
});
});
it('should throw an error when adding an incompatible LN node', async () => {
const { onCanvasDrop } = store.getActions().designer;
const spy = jest.spyOn(store.getActions().app, 'notify');
const data = { type: 'lnd', version: LndVersion['0.7.1-beta'] };
onCanvasDrop({ data, position });
await wait(() => {
expect(spy).toBeCalledWith(
expect.objectContaining({
message: 'Failed to add node',
error: new Error(
'This network does not contain a Bitcoin Core v0.18.1 (or lower) node which is required for LND v0.7.1-beta',
),
}),
);
});
});
it('should not add the node if the network is transitioning', async () => {
const { setStatus } = store.getActions().network;
setStatus({ id: firstNetwork().id, status: Status.Starting });

12
src/store/models/designer.ts

@ -188,7 +188,7 @@ const designerModel: DesignerModel = {
}),
onLinkCompleteListener: thunkOn(
actions => actions.onLinkComplete,
async (actions, { payload }, { getState, getStoreState, getStoreActions }) => {
(actions, { payload }, { getState, getStoreState, getStoreActions }) => {
const { activeId, activeChart } = getState();
const { linkId, fromNodeId, toNodeId, fromPortId, toPortId } = payload;
if (!activeChart.links[linkId]) return;
@ -232,13 +232,9 @@ const designerModel: DesignerModel = {
return showError(l('linkErrPorts'));
}
try {
const lnName = fromNode.type === 'lightning' ? fromNodeId : toNodeId;
const backendName = fromNode.type === 'lightning' ? toNodeId : fromNodeId;
getStoreActions().modals.showChangeBackend({ lnName, backendName, linkId });
} catch (error) {
return showError(error.message);
}
const lnName = fromNode.type === 'lightning' ? fromNodeId : toNodeId;
const backendName = fromNode.type === 'lightning' ? toNodeId : fromNodeId;
getStoreActions().modals.showChangeBackend({ lnName, backendName, linkId });
}
},
),

39
src/store/models/lightning.spec.ts

@ -13,6 +13,7 @@ import {
getNetwork,
injections,
lightningServiceMock,
mockProperty,
} from 'utils/tests';
import bitcoindModel from './bitcoind';
import designerModel from './designer';
@ -50,7 +51,7 @@ describe('Lightning Model', () => {
// reset the store before each test run
store = createStore(rootModel, { injections, initialState });
asyncUtilMock.delay.mockResolvedValue(true);
asyncUtilMock.delay.mockResolvedValue(Promise.resolve());
bitcoindServiceMock.sendFunds.mockResolvedValue('txid');
lightningServiceMock.getNewAddress.mockResolvedValue({ address: 'bc1aaaa' });
lightningServiceMock.getInfo.mockResolvedValue(
@ -126,6 +127,13 @@ describe('Lightning Model', () => {
expect(balances.total).toEqual('300');
});
it('should not throw an error when connecting peers', async () => {
const { connectAllPeers } = store.getActions().lightning;
lightningServiceMock.getInfo.mockResolvedValue(defaultStateInfo({ rpcUrl: 'asdf' }));
lightningServiceMock.getInfo.mockRejectedValueOnce(new Error('getInfo-error'));
await expect(connectAllPeers(network)).resolves.not.toThrow();
});
it('should open a channel successfully', async () => {
lightningServiceMock.getInfo.mockResolvedValueOnce(
defaultStateInfo({
@ -144,4 +152,33 @@ describe('Lightning Model', () => {
expect(lightningServiceMock.openChannel).toBeCalledTimes(1);
expect(bitcoindServiceMock.mine).toBeCalledTimes(1);
});
it('should open a channel and mine on the first bitcoin node', async () => {
lightningServiceMock.getInfo.mockResolvedValueOnce(
defaultStateInfo({
pubkey: 'abcdef',
syncedToChain: true,
rpcUrl: 'abcdef@1.1.1.1:9735',
}),
);
const [from, to] = store.getState().network.networks[0].nodes.lightning;
from.backendName = 'invalid';
const sats = '1000';
const { openChannel, getInfo } = store.getActions().lightning;
await getInfo(to);
await openChannel({ from, to, sats, autoFund: false });
const btcNode = store.getState().network.networks[0].nodes.bitcoin[0];
expect(bitcoindServiceMock.mine).toBeCalledWith(6, btcNode);
});
it('should cause some delay waiting for nodes', async () => {
mockProperty(process.env, 'NODE_ENV', 'production');
const { waitForNodes } = store.getActions().lightning;
await waitForNodes(network.nodes.lightning);
expect(asyncUtilMock.delay).toBeCalledWith(2000);
mockProperty(process.env, 'NODE_ENV', 'test');
});
});

133
src/store/models/network.spec.ts

@ -1,6 +1,6 @@
import detectPort from 'detect-port';
import { createStore } from 'easy-peasy';
import { CLightningVersion, LndVersion, Status } from 'shared/types';
import { BitcoindVersion, CLightningVersion, LndVersion, Status } from 'shared/types';
import { Network } from 'types';
import { initChartFromNetwork } from 'utils/chart';
import * as files from 'utils/files';
@ -178,31 +178,40 @@ describe('Network model', () => {
"Network with the id '999' was not found.",
);
});
it('should throw an error if the node type is invalid', async () => {
const payload = { id: firstNetwork().id, type: 'abcd', version: LndVersion.latest };
const { addNode: addLndNode } = store.getActions().network;
await expect(addLndNode(payload)).rejects.toThrow(
"Cannot add uknown node type 'abcd' to the network",
);
});
});
describe('Removing a Node', () => {
beforeEach(async () => {
await store.getActions().network.addNetwork(addNetworkArgs);
await store.getActions().network.addNetwork({
...addNetworkArgs,
bitcoindNodes: 2,
});
store.getActions().designer.setActiveId(1);
});
it('should remove a node from an existing network', async () => {
store.getActions().designer.setActiveId(1);
const node = firstNetwork().nodes.lightning[0];
store.getActions().network.removeLightningNode({ node });
await store.getActions().network.removeLightningNode({ node });
const { lightning } = firstNetwork().nodes;
expect(lightning).toHaveLength(2);
expect(lightning[0].name).toBe('bob');
});
it('should remove a c-lightning node from an existing network', async () => {
store.getActions().designer.setActiveId(1);
const node = firstNetwork().nodes.lightning[2];
store.getActions().network.removeLightningNode({ node });
const { lightning } = firstNetwork().nodes;
expect(lightning).toHaveLength(2);
const node = firstNetwork().nodes.lightning[1];
await store.getActions().network.removeLightningNode({ node });
expect(firstNetwork().nodes.lightning).toHaveLength(2);
});
it('should throw an error if the network id is invalid', async () => {
it('should throw an error if the lightning node network id is invalid', async () => {
const node = firstNetwork().nodes.lightning[0];
node.networkId = 999;
const { removeLightningNode } = store.getActions().network;
@ -210,6 +219,105 @@ describe('Network model', () => {
"Network with the id '999' was not found.",
);
});
it('should remove a bitcoin node from an existing network', async () => {
const node = firstNetwork().nodes.bitcoin[0];
await store.getActions().network.removeBitcoinNode({ node });
expect(firstNetwork().nodes.bitcoin).toHaveLength(1);
});
it('should throw an error if the bitcoin node network id is invalid', async () => {
const node = firstNetwork().nodes.bitcoin[0];
node.networkId = 999;
const { removeBitcoinNode } = store.getActions().network;
await expect(removeBitcoinNode({ node })).rejects.toThrow(
"Network with the id '999' was not found.",
);
});
it('should throw an error if only one bitcoin node is in the network', async () => {
const { removeBitcoinNode } = store.getActions().network;
await removeBitcoinNode({ node: firstNetwork().nodes.bitcoin[0] });
const node = firstNetwork().nodes.bitcoin[0];
await expect(removeBitcoinNode({ node })).rejects.toThrow(
'Cannot remove the only bitcoin node',
);
});
it('should throw an error if a LN node depends on the bitcoin node being removed', async () => {
const { removeBitcoinNode, addNode } = store.getActions().network;
const { id } = firstNetwork();
// add old bitcoin and LN nodes
await addNode({ id, type: 'bitcoind', version: BitcoindVersion['0.18.1'] });
await addNode({ id, type: 'lnd', version: LndVersion['0.7.1-beta'] });
// try to remove the old bitcoind version
const node = firstNetwork().nodes.bitcoin[2];
await expect(removeBitcoinNode({ node })).rejects.toThrow(
'There are no other compatible backends for dave to connect to. You must remove the dave node first',
);
});
it('should update peers of surrounding bitcoin nodes', async () => {
const { removeBitcoinNode, addNode } = store.getActions().network;
const { id } = firstNetwork();
await addNode({ id, type: 'bitcoind', version: BitcoindVersion.latest });
const node = firstNetwork().nodes.bitcoin[1];
await removeBitcoinNode({ node });
const { bitcoin } = firstNetwork().nodes;
expect(bitcoin).toHaveLength(2);
expect(bitcoin[0].peers).toEqual(['backend3']);
expect(bitcoin[1].peers).toEqual(['backend1']);
});
});
describe('Updating Backend', () => {
beforeEach(async () => {
await store.getActions().network.addNetwork({
...addNetworkArgs,
bitcoindNodes: 2,
});
store.getActions().designer.setActiveId(1);
});
it('should update the backend node', async () => {
const { updateBackendNode } = store.getActions().network;
expect(firstNetwork().nodes.lightning[0].backendName).toBe('backend1');
const { id } = firstNetwork();
await updateBackendNode({ id, lnName: 'alice', backendName: 'backend2' });
expect(firstNetwork().nodes.lightning[0].backendName).toBe('backend2');
});
it('should throw an error if the network id is not valid', async () => {
const { updateBackendNode } = store.getActions().network;
const args = { id: 999, lnName: 'alice', backendName: 'backend2' };
await expect(updateBackendNode(args)).rejects.toThrow(
"Network with the id '999' was not found.",
);
});
it('should throw an error if the LN node name is not valid', async () => {
const { updateBackendNode } = store.getActions().network;
const args = { id: firstNetwork().id, lnName: 'xxx', backendName: 'backend2' };
await expect(updateBackendNode(args)).rejects.toThrow(
"The node 'xxx' was not found.",
);
});
it('should throw an error if the bitcoin node name is not valid', async () => {
const { updateBackendNode } = store.getActions().network;
const args = { id: firstNetwork().id, lnName: 'alice', backendName: 'xxx' };
await expect(updateBackendNode(args)).rejects.toThrow(
"The node 'xxx' was not found.",
);
});
it('should throw an error if the backend node name is already set on the LN node', async () => {
const { updateBackendNode } = store.getActions().network;
const args = { id: firstNetwork().id, lnName: 'alice', backendName: 'backend1' };
await expect(updateBackendNode(args)).rejects.toThrow(
"The node 'alice' is already connected to 'backend1'",
);
});
});
describe('Starting', () => {
@ -430,5 +538,10 @@ describe('Network model', () => {
const { remove } = store.getActions().network;
await expect(remove(10)).rejects.toThrow();
});
it('should fail to monitor nodes startup with an invalid id', async () => {
const { monitorStartup } = store.getActions().network;
await expect(monitorStartup(10)).rejects.toThrow();
});
});
});

11
src/utils/async.spec.ts

@ -1,4 +1,5 @@
import { delay, waitFor } from './async';
import { mockProperty } from './tests';
describe('Async Util', () => {
describe('delay', () => {
@ -9,6 +10,16 @@ describe('Async Util', () => {
await expect(promise).resolves.toBeTruthy();
expect(spy).toHaveBeenCalledTimes(1);
});
it('should use timeout passed in args', async () => {
mockProperty(process.env, 'NODE_ENV', 'production');
const spy = jest.spyOn(window, 'setTimeout').mockImplementation(cb => cb() as any);
await delay(123);
expect(spy).toHaveBeenCalledWith(expect.any(Function), 123);
mockProperty(process.env, 'NODE_ENV', 'test');
});
});
describe('waitFor', () => {

10
src/utils/chart.spec.ts

@ -59,7 +59,7 @@ describe('Chart Util', () => {
});
});
describe('updateChartFromNetwork', () => {
describe('updateChartFromNodes', () => {
it('should create link for an open channel', () => {
addChannel('alice', 'ln2pubkey');
const result = updateChartFromNodes(chart, network, nodesData);
@ -123,5 +123,13 @@ describe('Chart Util', () => {
expect(size).toBeDefined();
if (size) expect(size.height).toBe(60);
});
it('should keep the node selected', () => {
addChannel('alice', 'ln2pubkey');
chart.selected = { type: 'node', id: 'alice' };
const result = updateChartFromNodes(chart, network, nodesData);
expect(result.selected.id).toEqual('alice');
expect(result.selected.type).toEqual('node');
});
});
});

3
src/utils/chart.ts

@ -86,7 +86,8 @@ export const createBitcoinChartNode = (btc: BitcoinNode) => {
// the first peer is always the prev node unless this is the first node in the network
const peer = btc.peers[0];
if (peer && btc.name > peer) {
// only add one link from left to right (ex: 'backend2' < 'backend3')
// only add one link from right to left (ex: 'backend3' > 'backend2')
// we don't need links if this is the only node
link = {
id: `${peer}-${btc.name}`,
from: { nodeId: peer, portId: 'peer-right' },

43
src/utils/network.spec.ts

@ -1,12 +1,51 @@
import detectPort from 'detect-port';
import { LndNode, Status } from 'shared/types';
import {
BitcoindVersion,
CLightningVersion,
LightningNode,
LndNode,
LndVersion,
Status,
} from 'shared/types';
import { Network } from 'types';
import { getOpenPortRange, getOpenPorts, OpenPorts } from './network';
import {
getOpenPortRange,
getOpenPorts,
getRequiredBackendVersion,
OpenPorts,
} from './network';
import { getNetwork } from './tests';
const mockDetectPort = detectPort as jest.Mock;
describe('Network Utils', () => {
describe('getRequiredBackendVersion', () => {
it('should return the correct version for LND', () => {
expect(getRequiredBackendVersion('LND', LndVersion['0.7.1-beta'])).toEqual(
BitcoindVersion['0.18.1'],
);
expect(getRequiredBackendVersion('LND', LndVersion['0.8.0-beta'])).toEqual(
BitcoindVersion['0.18.1'],
);
expect(getRequiredBackendVersion('LND', LndVersion.latest)).toEqual(
BitcoindVersion.latest,
);
});
it('should return the correct version for c-lightning', () => {
expect(getRequiredBackendVersion('c-lightning', CLightningVersion.latest)).toEqual(
BitcoindVersion.latest,
);
});
it('should return the latest version for unknown implementations', () => {
const unknown = 'asdf' as LightningNode['implementation'];
expect(getRequiredBackendVersion(unknown, CLightningVersion.latest)).toEqual(
BitcoindVersion.latest,
);
});
});
describe('getOpenPortRange', () => {
beforeEach(() => {
let port = 10003;

2
src/utils/network.ts

@ -172,7 +172,7 @@ export const createBitcoindNetworkNode = (
ports: { rpc: BasePorts.bitcoind.rest + id },
};
// peer up with the previous node in both directions
// peer up with the previous node on both sides
if (bitcoin.length > 0) {
const prev = bitcoin[bitcoin.length - 1];
node.peers.push(prev.name);

Loading…
Cancel
Save