Browse Source

test(eclair): add additional unit tests for eclair

master
jamaljsr 5 years ago
parent
commit
305a68c87a
  1. 38
      src/components/designer/lightning/ConnectTab.tsx
  2. 21
      src/components/designer/lightning/LightningDetails.spec.tsx
  3. 2
      src/components/designer/lightning/connect/BasicAuth.tsx
  4. 60
      src/components/designer/lightning/connect/EncodedStrings.spec.tsx
  5. 6
      src/lib/bitcoin/bitcoindService.spec.ts
  6. 22
      src/lib/docker/dockerService.spec.ts
  7. 42
      src/lib/lightning/eclair/eclairApi.spec.ts
  8. 5
      src/lib/lightning/eclair/eclairApi.ts
  9. 85
      src/lib/lightning/eclair/eclairService.spec.ts

38
src/components/designer/lightning/ConnectTab.tsx

@ -43,6 +43,7 @@ export interface ConnectionInfo {
basicAuth?: string; basicAuth?: string;
}; };
p2pUriExternal: string; p2pUriExternal: string;
authTypes: string[];
} }
interface Props { interface Props {
@ -74,6 +75,7 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
cert: lnd.paths.tlsCert, cert: lnd.paths.tlsCert,
}, },
p2pUriExternal: `${pubkey}@127.0.0.1:${lnd.ports.p2p}`, p2pUriExternal: `${pubkey}@127.0.0.1:${lnd.ports.p2p}`,
authTypes: ['paths', 'hex', 'base64', 'lndc'],
}; };
} else if (node.implementation === 'c-lightning') { } else if (node.implementation === 'c-lightning') {
const cln = node as CLightningNode; const cln = node as CLightningNode;
@ -84,6 +86,7 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
admin: cln.paths.macaroon, admin: cln.paths.macaroon,
}, },
p2pUriExternal: `${pubkey}@127.0.0.1:${cln.ports.p2p}`, p2pUriExternal: `${pubkey}@127.0.0.1:${cln.ports.p2p}`,
authTypes: ['paths', 'hex', 'base64'],
}; };
} else if (node.implementation === 'eclair') { } else if (node.implementation === 'eclair') {
const eln = node as EclairNode; const eln = node as EclairNode;
@ -94,6 +97,7 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
basicAuth: eclairCredentials.pass, basicAuth: eclairCredentials.pass,
}, },
p2pUriExternal: `${pubkey}@127.0.0.1:${eln.ports.p2p}`, p2pUriExternal: `${pubkey}@127.0.0.1:${eln.ports.p2p}`,
authTypes: ['basic'],
}; };
} }
} }
@ -102,9 +106,19 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
restUrl: '', restUrl: '',
restDocsUrl: '', restDocsUrl: '',
credentials: {}, credentials: {},
p2pUriExternal: '',
authTypes: [],
} as ConnectionInfo; } as ConnectionInfo;
}, [node, pubkey]); }, [node, pubkey]);
// ensure an appropriate auth type is used when switching nodes
const nodeAuthType = useMemo(() => {
if (!info.authTypes.includes(authType)) {
return info.authTypes[0];
}
return authType;
}, [authType, info.authTypes]);
if (node.status !== Status.Started) { if (node.status !== Status.Started) {
return <>{l('notStarted')}</>; return <>{l('notStarted')}</>;
} }
@ -157,21 +171,19 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
<DetailsList details={hosts} /> <DetailsList details={hosts} />
<Styled.RadioGroup <Styled.RadioGroup
name="authType" name="authType"
defaultValue={authType} value={nodeAuthType}
size="small" size="small"
onChange={e => setAuthType(e.target.value)} onChange={e => setAuthType(e.target.value)}
> >
{credentials.admin && [ <Radio.Button key="paths" value="paths">
<Radio.Button key="paths" value="paths"> {l('filePaths')}
{l('filePaths')} </Radio.Button>
</Radio.Button>, <Radio.Button key="hex" value="hex">
<Radio.Button key="hex" value="hex"> {l('hexStrings')}
{l('hexStrings')} </Radio.Button>
</Radio.Button>, <Radio.Button key="base64" value="base64">
<Radio.Button key="base64" value="base64"> {l('base64Strings')}
{l('base64Strings')} </Radio.Button>
</Radio.Button>,
]}
{node.implementation === 'LND' && ( {node.implementation === 'LND' && (
<Radio.Button value="lndc">{l('lndConnect')}</Radio.Button> <Radio.Button value="lndc">{l('lndConnect')}</Radio.Button>
)} )}
@ -179,7 +191,7 @@ const ConnectTab: React.FC<Props> = ({ node }) => {
<Radio.Button value="basic">{l('basicAuth')}</Radio.Button> <Radio.Button value="basic">{l('basicAuth')}</Radio.Button>
)} )}
</Styled.RadioGroup> </Styled.RadioGroup>
{authCmps[authType]} {authCmps[nodeAuthType]}
</> </>
); );
}; };

21
src/components/designer/lightning/LightningDetails.spec.tsx

@ -307,6 +307,27 @@ describe('LightningDetails', () => {
}); });
}); });
describe('eclair', () => {
beforeEach(() => {
node = network.nodes.lightning[2];
});
it('should display the REST Host', async () => {
const { getByText, findByText } = renderComponent(Status.Started);
fireEvent.click(await findByText('Connect'));
expect(getByText('REST Host')).toBeInTheDocument();
expect(getByText('http://127.0.0.1:8283')).toBeInTheDocument();
});
it('should open API Doc links in the browser', async () => {
shell.openExternal = jest.fn().mockResolvedValue(true);
const { getByText, findByText } = renderComponent(Status.Started);
fireEvent.click(await findByText('Connect'));
await wait(() => fireEvent.click(getByText('REST')));
expect(shell.openExternal).toBeCalledWith('https://acinq.github.io/eclair');
});
});
describe('connect options', () => { describe('connect options', () => {
const toggle = (container: HTMLElement, value: string) => { const toggle = (container: HTMLElement, value: string) => {
fireEvent.click( fireEvent.click(

2
src/components/designer/lightning/connect/BasicAuth.tsx

@ -10,7 +10,7 @@ interface Props {
const BasicAuth: React.FC<Props> = ({ password }) => { const BasicAuth: React.FC<Props> = ({ password }) => {
const { l } = usePrefixedTranslation('cmps.designer.lightning.connect.BasicAuth'); const { l } = usePrefixedTranslation('cmps.designer.lightning.connect.BasicAuth');
const base64pass = new Buffer(`:${password}`).toString('base64'); const base64pass = Buffer.from(`:${password}`).toString('base64');
const auth = `Basic ${base64pass}`; const auth = `Basic ${base64pass}`;
const details: DetailValues = [ const details: DetailValues = [

60
src/components/designer/lightning/connect/EncodedStrings.spec.tsx

@ -0,0 +1,60 @@
import React from 'react';
import { wait } from '@testing-library/react';
import { LndNode } from 'shared/types';
import * as files from 'utils/files';
import { getNetwork, renderWithProviders } from 'utils/tests';
import { ConnectionInfo } from '../ConnectTab';
import { EncodedStrings } from './';
jest.mock('utils/files');
const filesMock = files as jest.Mocked<typeof files>;
describe('EncodedStrings', () => {
const network = getNetwork();
const renderComponent = (
credentials: ConnectionInfo['credentials'],
encoding: 'hex' | 'base64',
) => {
const cmp = <EncodedStrings credentials={credentials} encoding={encoding} />;
return renderWithProviders(cmp);
};
beforeEach(() => {
filesMock.read.mockResolvedValue('file-content');
});
it('should display credentials', async () => {
const lnd = network.nodes.lightning[0] as LndNode;
const lndCreds: ConnectionInfo['credentials'] = {
admin: lnd.paths.adminMacaroon,
readOnly: lnd.paths.readonlyMacaroon,
cert: lnd.paths.tlsCert,
};
const { getByText } = renderComponent(lndCreds, 'hex');
await wait();
expect(getByText('TLS Cert')).toBeInTheDocument();
expect(getByText('Admin Macaroon')).toBeInTheDocument();
expect(getByText('Read-only Macaroon')).toBeInTheDocument();
expect(filesMock.read).toBeCalledWith(expect.stringContaining('tls.cert'), 'hex');
expect(filesMock.read).toBeCalledWith(
expect.stringContaining('admin.macaroon'),
'hex',
);
expect(filesMock.read).toBeCalledWith(
expect.stringContaining('readonly.macaroon'),
'hex',
);
});
it('should handle all missing credentials', async () => {
const missingCreds = {} as ConnectionInfo['credentials'];
const { queryByText } = renderComponent(missingCreds, 'hex');
await wait();
expect(queryByText('TLS Cert')).not.toBeInTheDocument();
expect(queryByText('Admin Macaroon')).not.toBeInTheDocument();
expect(queryByText('Read-only Macaroon')).not.toBeInTheDocument();
expect(filesMock.read).not.toBeCalled();
});
});

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

@ -38,6 +38,12 @@ describe('BitcoindService', () => {
expect(info.balance).toEqual(5); expect(info.balance).toEqual(5);
}); });
it('should get new address', async () => {
const addr = await bitcoindService.getNewAddress(node);
expect(mockBitcoin.mock.instances[0].getNewAddress).toBeCalledTimes(1);
expect(addr).toEqual('abcdef');
});
it('should connect peers', async () => { it('should connect peers', async () => {
await bitcoindService.connectPeers(node); await bitcoindService.connectPeers(node);
expect(mockBitcoin.mock.instances[0].addNode).toBeCalledTimes(1); expect(mockBitcoin.mock.instances[0].addNode).toBeCalledTimes(1);

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

@ -222,6 +222,28 @@ describe('DockerService', () => {
); );
}); });
it('should save the eclair node with the first bitcoin node as backend', () => {
const net = createNetwork({
id: 1,
name: 'my network',
lndNodes: 0,
clightningNodes: 0,
eclairNodes: 1,
bitcoindNodes: 1,
repoState: defaultRepoState,
managedImages: testManagedImages,
customImages: [],
});
net.nodes.lightning[0].backendName = 'invalid';
dockerService.saveComposeFile(net);
expect(filesMock.write).toBeCalledWith(
expect.stringContaining('docker-compose.yml'),
expect.stringContaining(
`container_name: polar-n1-${network.nodes.lightning[0].name}`,
),
);
});
it('should not save unknown lightning implementation', () => { it('should not save unknown lightning implementation', () => {
network.nodes.lightning[0].implementation = 'unknown' as any; network.nodes.lightning[0].implementation = 'unknown' as any;
dockerService.saveComposeFile(network); dockerService.saveComposeFile(network);

42
src/lib/lightning/eclair/eclairApi.spec.ts

@ -0,0 +1,42 @@
import { EclairNode } from 'shared/types';
import * as ipc from 'lib/ipc/ipcService';
import { getNetwork } from 'utils/tests';
import { httpPost } from './eclairApi';
jest.mock('lib/ipc/ipcService');
const ipcMock = ipc as jest.Mocked<typeof ipc>;
describe('EclairApi', () => {
const node = getNetwork().nodes.lightning[2] as EclairNode;
it('should throw an error for an incorrect node implementation', async () => {
const lnd = getNetwork().nodes.lightning[0];
await expect(httpPost(lnd, 'get-ok')).rejects.toThrow(
"EclairService cannot be used for 'LND' nodes",
);
});
it('should perform an unsuccessful httpPost', async () => {
const sender = jest.fn().mockRejectedValue(new Error('api-error'));
ipcMock.createIpcSender.mockReturnValue(sender);
await expect(httpPost(node, 'getinfo')).rejects.toThrow('api-error');
});
it('should perform a successful httpPost', async () => {
const sender = jest.fn().mockResolvedValue('asdf');
ipcMock.createIpcSender.mockReturnValue(sender);
await expect(httpPost(node, 'getinfo')).resolves.toBe('asdf');
expect(sender).toBeCalledWith(
'http',
expect.objectContaining({
url: 'http://127.0.0.1:8283/getinfo',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: 'Basic OmVjbGFpcnB3',
},
}),
);
});
});

5
src/lib/lightning/eclair/eclairApi.ts

@ -5,8 +5,6 @@ import { eclairCredentials } from 'utils/constants';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
const ipc = createIpcSender('EclairApi', 'app');
const request = async <T>( const request = async <T>(
node: LightningNode, node: LightningNode,
method: HttpMethod, method: HttpMethod,
@ -17,7 +15,7 @@ const request = async <T>(
throw new Error(`EclairService cannot be used for '${node.implementation}' nodes`); throw new Error(`EclairService cannot be used for '${node.implementation}' nodes`);
// there is no username for Ecalir API so left of the colon is blank // there is no username for Ecalir API so left of the colon is blank
const base64auth = new Buffer(`:${eclairCredentials.pass}`).toString('base64'); const base64auth = Buffer.from(`:${eclairCredentials.pass}`).toString('base64');
const args = { const args = {
url: `http://127.0.0.1:${node.ports.rest}/${path}`, url: `http://127.0.0.1:${node.ports.rest}/${path}`,
method, method,
@ -27,6 +25,7 @@ const request = async <T>(
Authorization: `Basic ${base64auth}`, Authorization: `Basic ${base64auth}`,
}, },
}; };
const ipc = createIpcSender('EclairApi', 'app');
const res = await ipc(ipcChannels.http, args); const res = await ipc(ipcChannels.http, args);
return res as T; return res as T;
}; };

85
src/lib/lightning/eclair/eclairService.spec.ts

@ -0,0 +1,85 @@
import { WalletInfo } from 'bitcoin-core';
import bitcoindService from 'lib/bitcoin/bitcoindService';
import { defaultStateBalances, defaultStateInfo, getNetwork } from 'utils/tests';
import { eclairService } from './';
import * as eclairApi from './eclairApi';
import * as ELN from './types';
jest.mock('./eclairApi');
jest.mock('lib/bitcoin/bitcoindService');
const eclairApiMock = eclairApi as jest.Mocked<typeof eclairApi>;
const bitcoindServiceMock = bitcoindService as jest.Mocked<typeof bitcoindService>;
describe('EclairService', () => {
const network = getNetwork();
const node = network.nodes.lightning[2];
const backend = network.nodes.bitcoin[0];
it('should get node info', async () => {
const infoResponse: Partial<ELN.GetInfoResponse> = {
nodeId: 'asdf',
alias: '',
publicAddresses: ['1.1.1.1:9735'],
blockHeight: 0,
};
eclairApiMock.httpPost.mockResolvedValue(infoResponse);
const expected = defaultStateInfo({
pubkey: 'asdf',
rpcUrl: 'asdf@1.1.1.1:9735',
syncedToChain: true,
});
const actual = await eclairService.getInfo(node);
expect(actual).toEqual(expected);
});
it('should get wallet balance', async () => {
const ballanceResponse: Partial<WalletInfo> = {
balance: 0.00001,
// eslint-disable-next-line @typescript-eslint/camelcase
unconfirmed_balance: 0,
// eslint-disable-next-line @typescript-eslint/camelcase
immature_balance: 0,
};
bitcoindServiceMock.getWalletInfo.mockResolvedValue(ballanceResponse as any);
const expected = defaultStateBalances({ confirmed: '1000', total: '1000' });
const actual = await eclairService.getBalances(node, backend);
expect(actual).toEqual(expected);
});
it('should get new address', async () => {
const expected = { address: 'abcdef' };
eclairApiMock.httpPost.mockResolvedValue(expected.address);
const actual = await eclairService.getNewAddress(node);
expect(actual).toEqual(expected);
});
it('should get a list of channels', async () => {
const chanResponse: ELN.ChannelResponse = {
nodeId: 'abcdef',
channelId: '65sdfd7',
state: ELN.ChannelState.NORMAL,
data: {
commitments: {
localParams: {
isFunder: true,
},
localCommit: {
spec: {
toLocal: 100000000,
toRemote: 50000000,
},
},
commitInput: {
amountSatoshis: 150000,
},
},
},
};
eclairApiMock.httpPost.mockResolvedValue([chanResponse]);
const expected = [expect.objectContaining({ pubkey: 'abcdef' })];
const actual = await eclairService.getChannels(node);
expect(actual).toEqual(expected);
});
});
Loading…
Cancel
Save