Skip to content

Commit 95fce5a

Browse files
committed
test: add test to ln simulation
1 parent c684a61 commit 95fce5a

File tree

6 files changed

+609
-2
lines changed

6 files changed

+609
-2
lines changed

src/components/designer/NetworkDesigner.spec.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,15 @@ describe('NetworkDesigner Component', () => {
259259
fireEvent.click(getByText('Cancel'));
260260
});
261261

262+
it('should display the Add Simulation modal', async () => {
263+
const { getByText, findByText, store } = renderComponent();
264+
act(() => {
265+
store.getActions().modals.showAddSimulation({});
266+
});
267+
expect(await findByText('Add Simulation')).toBeInTheDocument();
268+
fireEvent.click(getByText('Cancel'));
269+
});
270+
262271
it('should remove a node from the network', async () => {
263272
const { getByText, findByText, queryByText, store } = renderComponent();
264273
// add a new LN node that doesn't have a tap node connected
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
import React from 'react';
2+
import AddSimulationModal from './AddSimulationModal';
3+
import { createNetwork } from 'utils/network';
4+
import { defaultRepoState } from 'utils/constants';
5+
import { renderWithProviders, testManagedImages } from 'utils/tests';
6+
import { Status } from 'shared/types';
7+
import { fireEvent, waitFor } from '@testing-library/react';
8+
9+
describe('AddSimulationModal', () => {
10+
let unmount: () => void;
11+
12+
const renderComponent = async (status?: Status) => {
13+
const network = createNetwork({
14+
id: 1,
15+
name: 'test network',
16+
description: 'network description',
17+
lndNodes: 2,
18+
clightningNodes: 0,
19+
eclairNodes: 0,
20+
litdNodes: 0,
21+
bitcoindNodes: 2,
22+
tapdNodes: 0,
23+
repoState: defaultRepoState,
24+
managedImages: testManagedImages,
25+
customImages: [],
26+
status,
27+
});
28+
const initialState = {
29+
network: {
30+
networks: [network],
31+
},
32+
modals: {
33+
addSimulation: {
34+
visible: true,
35+
},
36+
},
37+
};
38+
39+
const cmp = <AddSimulationModal network={network} />;
40+
const result = renderWithProviders(cmp, { initialState });
41+
unmount = result.unmount;
42+
return {
43+
...result,
44+
network,
45+
};
46+
};
47+
48+
afterEach(() => unmount());
49+
50+
it('should render labels', async () => {
51+
const { getByText } = await renderComponent();
52+
expect(getByText('Add Simulation')).toBeInTheDocument();
53+
expect(getByText('Source')).toBeInTheDocument();
54+
expect(getByText('Destination')).toBeInTheDocument();
55+
expect(getByText('Interval (secs)')).toBeInTheDocument();
56+
expect(getByText('Amount (msat)')).toBeInTheDocument();
57+
});
58+
59+
it('should render form inputs', async () => {
60+
const { getByLabelText } = await renderComponent();
61+
expect(getByLabelText('Source')).toBeInTheDocument();
62+
expect(getByLabelText('Destination')).toBeInTheDocument();
63+
expect(getByLabelText('Interval (secs)')).toBeInTheDocument();
64+
expect(getByLabelText('Amount (msat)')).toBeInTheDocument();
65+
});
66+
67+
it('should render button', async () => {
68+
const { getByText } = await renderComponent();
69+
expect(getByText('Create')).toBeInTheDocument();
70+
});
71+
72+
it('should hide modal when cancel is clicked', async () => {
73+
const { getByText, queryByText } = await renderComponent();
74+
const btn = getByText('Cancel');
75+
expect(btn).toBeInTheDocument();
76+
expect(btn.parentElement).toBeInstanceOf(HTMLButtonElement);
77+
fireEvent.click(getByText('Cancel'));
78+
expect(queryByText('Cancel')).not.toBeInTheDocument();
79+
});
80+
81+
it('should do nothing if an invalid node name is used', async () => {
82+
const { getByText } = await renderComponent();
83+
fireEvent.click(getByText('Create'));
84+
await waitFor(() => {
85+
expect(getByText('Create')).toBeInTheDocument();
86+
});
87+
});
88+
89+
describe('with form submitted', () => {
90+
it('should create a simulation', async () => {
91+
const { getByText, getByLabelText, store } = await renderComponent();
92+
fireEvent.change(getByLabelText('Source'), { target: { value: 'alice' } });
93+
fireEvent.change(getByLabelText('Destination'), { target: { value: 'bob' } });
94+
fireEvent.change(getByLabelText('Interval (secs)'), { target: { value: '1000' } });
95+
fireEvent.change(getByLabelText('Amount (msat)'), { target: { value: '1000000' } });
96+
fireEvent.click(getByText('Create'));
97+
await waitFor(() => {
98+
expect(store.getState().modals.addSimulation.visible).toBe(false);
99+
expect(store.getState().network.networks[0].simulation).toBeDefined();
100+
});
101+
});
102+
103+
it('should throw an error if source or destination node is not found', async () => {
104+
const { getByText, getByLabelText, network, findByText } = await renderComponent(
105+
Status.Started,
106+
);
107+
fireEvent.change(getByLabelText('Source'), { target: { value: 'alice' } });
108+
fireEvent.change(getByLabelText('Destination'), { target: { value: 'bob' } });
109+
fireEvent.change(getByLabelText('Interval (secs)'), { target: { value: '10' } });
110+
fireEvent.change(getByLabelText('Amount (msat)'), { target: { value: '1000000' } });
111+
network.nodes.lightning = [];
112+
fireEvent.click(getByText('Create'));
113+
expect(
114+
await findByText('Source or destination node not found'),
115+
).toBeInTheDocument();
116+
});
117+
});
118+
});
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import React from 'react';
2+
import { fireEvent, waitFor } from '@testing-library/react';
3+
import SimulationDesignerTab from './SimulationDesignerTab';
4+
import { Status } from 'shared/types';
5+
import { injections, renderWithProviders, testManagedImages } from 'utils/tests';
6+
import { createNetwork } from 'utils/network';
7+
import { defaultRepoState } from 'utils/constants';
8+
import { initChartFromNetwork } from 'utils/chart';
9+
import { ipcChannels } from 'shared';
10+
11+
const mockDockerService = injections.dockerService as jest.Mocked<
12+
typeof injections.dockerService
13+
>;
14+
15+
describe('SimulationDesignerTab', () => {
16+
const renderComponent = (status?: Status, simStatus?: Status, noSim?: boolean) => {
17+
const network = createNetwork({
18+
id: 1,
19+
name: 'test network',
20+
description: 'network description',
21+
lndNodes: 2,
22+
clightningNodes: 0,
23+
eclairNodes: 0,
24+
litdNodes: 0,
25+
bitcoindNodes: 2,
26+
tapdNodes: 0,
27+
repoState: defaultRepoState,
28+
managedImages: testManagedImages,
29+
customImages: [],
30+
status,
31+
});
32+
33+
if (!noSim) {
34+
network.simulation = {
35+
networkId: 1,
36+
source: network.nodes.lightning[0],
37+
destination: network.nodes.lightning[1],
38+
intervalSecs: 10,
39+
amountMsat: 1000000,
40+
status: simStatus ?? Status.Stopped,
41+
};
42+
}
43+
const chart = initChartFromNetwork(network);
44+
45+
const initialState = {
46+
network: {
47+
networks: [network],
48+
},
49+
modals: {
50+
addSimulation: {
51+
visible: false,
52+
},
53+
},
54+
designer: {
55+
allCharts: {
56+
[network.id]: chart,
57+
},
58+
},
59+
};
60+
61+
const result = renderWithProviders(<SimulationDesignerTab network={network} />, {
62+
initialState,
63+
});
64+
65+
return {
66+
...result,
67+
network,
68+
};
69+
};
70+
71+
describe('Simulation Designer Tab', () => {
72+
it('should render the simulation designer tab', async () => {
73+
const { getByText, store } = renderComponent(Status.Started, Status.Started, true);
74+
expect(getByText('No simulations created yet')).toBeInTheDocument();
75+
76+
const addSimulationBtn = getByText('Add a new Simulation');
77+
expect(addSimulationBtn).toBeInTheDocument();
78+
fireEvent.click(addSimulationBtn);
79+
await waitFor(() => {
80+
expect(store.getState().modals.addSimulation.visible).toBe(true);
81+
});
82+
});
83+
84+
it('should show add simulation button if no simulation is defined', async () => {
85+
const { getByText } = renderComponent(Status.Started, Status.Started, true);
86+
expect(getByText('No simulations created yet')).toBeInTheDocument();
87+
88+
const addSimulationBtn = getByText('Add a new Simulation');
89+
expect(addSimulationBtn).toBeInTheDocument();
90+
});
91+
});
92+
93+
describe('Start Simulation', () => {
94+
it('should start simulation successfully', async () => {
95+
const { getByText } = renderComponent(Status.Started);
96+
expect(getByText('Start')).toBeInTheDocument();
97+
98+
fireEvent.click(getByText('Start'));
99+
await waitFor(() => {
100+
expect(mockDockerService.startSimulation).toHaveBeenCalled();
101+
});
102+
});
103+
104+
it('should show error if simulation fails to start', async () => {
105+
// Simulation should fail to start because the network is not started,
106+
// which means the lightning nodes are not running.
107+
const { getByText } = renderComponent(Status.Stopped);
108+
fireEvent.click(getByText('Start'));
109+
110+
await waitFor(() => {
111+
expect(getByText('Unable to start the simulation')).toBeInTheDocument();
112+
});
113+
});
114+
});
115+
116+
describe('Stop Simulation', () => {
117+
it('should stop simulation successfully', async () => {
118+
const { getByText } = renderComponent(Status.Started, Status.Started);
119+
expect(getByText('Stop')).toBeInTheDocument();
120+
fireEvent.click(getByText('Stop'));
121+
await waitFor(() => {
122+
expect(mockDockerService.stopSimulation).toHaveBeenCalled();
123+
});
124+
});
125+
126+
it('should show error if simulation fails to stop', async () => {
127+
mockDockerService.stopSimulation.mockRejectedValue(new Error('simulation-error'));
128+
const { getByText } = renderComponent(Status.Started, Status.Started);
129+
fireEvent.click(getByText('Stop'));
130+
131+
await waitFor(() => {
132+
expect(getByText('Unable to stop the simulation')).toBeInTheDocument();
133+
expect(getByText('simulation-error')).toBeInTheDocument();
134+
});
135+
});
136+
});
137+
138+
describe('Remove Simulation', () => {
139+
it('should remove simulation', async () => {
140+
const { getByText, getByLabelText, findByText } = renderComponent(Status.Started);
141+
fireEvent.mouseOver(getByLabelText('more'));
142+
fireEvent.click(await findByText('Delete'));
143+
await waitFor(() => {
144+
expect(getByText('Remove Simulation')).toBeInTheDocument();
145+
});
146+
147+
fireEvent.click(getByText('Remove'));
148+
await waitFor(() => {
149+
expect(mockDockerService.removeSimulation).toHaveBeenCalled();
150+
});
151+
});
152+
153+
it('should show error if simulation fails to remove', async () => {
154+
mockDockerService.removeSimulation.mockRejectedValue(new Error('simulation-error'));
155+
const { getByText, getByLabelText, findByText } = renderComponent(Status.Started);
156+
fireEvent.mouseOver(getByLabelText('more'));
157+
fireEvent.click(await findByText('Delete'));
158+
await waitFor(() => {
159+
expect(getByText('Remove Simulation')).toBeInTheDocument();
160+
expect(getByText('Remove')).toBeInTheDocument();
161+
expect(getByText('Cancel')).toBeInTheDocument();
162+
});
163+
164+
fireEvent.click(getByText('Remove'));
165+
await waitFor(() => {
166+
expect(getByText('Failed to remove simulation')).toBeInTheDocument();
167+
expect(getByText('simulation-error')).toBeInTheDocument();
168+
});
169+
});
170+
171+
it('should return early if no simulation is defined', async () => {
172+
const { getByText, network, findByText, getByLabelText } = renderComponent(
173+
Status.Started,
174+
Status.Stopped,
175+
);
176+
const sim = {
177+
networkId: 1,
178+
source: network.nodes.lightning[0],
179+
destination: network.nodes.lightning[1],
180+
intervalSecs: 10,
181+
amountMsat: 1000000,
182+
status: Status.Stopped,
183+
};
184+
185+
// This is an unlikely scenario, as the start button is not
186+
// visible if no simulation is defined.
187+
network.simulation = undefined;
188+
expect(getByText('Start')).toBeInTheDocument();
189+
fireEvent.click(getByText('Start'));
190+
expect(getByText('No simulations created yet')).toBeInTheDocument();
191+
192+
network.simulation = { ...sim, status: Status.Started };
193+
await waitFor(() => {
194+
expect(getByText('Stop')).toBeInTheDocument();
195+
});
196+
network.simulation = undefined;
197+
expect(getByText('Stop')).toBeInTheDocument();
198+
fireEvent.click(getByText('Stop'));
199+
expect(getByText('No simulations created yet')).toBeInTheDocument();
200+
201+
network.simulation = { ...sim, status: Status.Stopped };
202+
await waitFor(() => {
203+
expect(getByText('Start')).toBeInTheDocument();
204+
});
205+
206+
network.simulation = undefined;
207+
fireEvent.mouseOver(getByLabelText('more'));
208+
fireEvent.click(await findByText('Delete'));
209+
fireEvent.click(await findByText('Remove'));
210+
await waitFor(() => {
211+
expect(mockDockerService.removeSimulation).not.toHaveBeenCalled();
212+
});
213+
});
214+
215+
it('should send an ipc message when the button is clicked', async () => {
216+
const { getByText, getByLabelText, findByText } = renderComponent();
217+
fireEvent.mouseOver(getByLabelText('more'));
218+
fireEvent.click(await findByText('Delete'));
219+
const ipcMock = injections.ipc as jest.Mock;
220+
ipcMock.mockResolvedValue(true);
221+
fireEvent.click(getByText('View Logs'));
222+
const url = '/logs/simln/polar-n1-simln';
223+
await waitFor(() => {
224+
expect(ipcMock).toBeCalledWith(ipcChannels.openWindow, { url });
225+
});
226+
});
227+
});
228+
});

src/lib/docker/composeFile.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,8 @@ describe('ComposeFile', () => {
4141
it('should add multiple services', () => {
4242
composeFile.addBitcoind(btcNode);
4343
composeFile.addLnd(lndNode, btcNode);
44-
expect(Object.keys(composeFile.content.services).length).toEqual(2);
44+
composeFile.addSimln(1);
45+
expect(Object.keys(composeFile.content.services).length).toEqual(3);
4546
});
4647

4748
it('should add a bitcoind config', () => {
@@ -190,4 +191,17 @@ describe('ComposeFile', () => {
190191
expect(service.image).toBe('my-image');
191192
expect(service.command).toBe('my-command');
192193
});
194+
195+
it('should add a simln config', () => {
196+
composeFile.addSimln(1);
197+
expect(composeFile.content.services['simln']).not.toBeUndefined();
198+
});
199+
200+
it('should create the correct simln docker compose values', () => {
201+
composeFile.addSimln(1);
202+
const service = composeFile.content.services['simln'];
203+
expect(service.image).toContain('simln');
204+
expect(service.container_name).toEqual('polar-n1-simln');
205+
expect(service.command).toBe('');
206+
});
193207
});

0 commit comments

Comments
 (0)