Skip to content

Commit 6ff46be

Browse files
committed
test: add test to ln simulation
1 parent cc7d925 commit 6ff46be

File tree

6 files changed

+595
-2
lines changed

6 files changed

+595
-2
lines changed

src/components/designer/NetworkDesigner.spec.tsx

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

252+
it('should display the Add Simulation modal', async () => {
253+
const { getByText, findByText, store } = renderComponent();
254+
act(() => {
255+
store.getActions().modals.showAddSimulation({});
256+
});
257+
expect(await findByText('Add Simulation')).toBeInTheDocument();
258+
fireEvent.click(getByText('Cancel'));
259+
});
260+
252261
it('should remove a node from the network', async () => {
253262
const { getByText, findByText, queryByText, store } = renderComponent();
254263
// 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: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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+
10+
const mockDockerService = injections.dockerService as jest.Mocked<
11+
typeof injections.dockerService
12+
>;
13+
14+
describe('SimulationDesignerTab', () => {
15+
const renderComponent = (status?: Status, simStatus?: Status, noSim?: boolean) => {
16+
const network = createNetwork({
17+
id: 1,
18+
name: 'test network',
19+
description: 'network description',
20+
lndNodes: 2,
21+
clightningNodes: 0,
22+
eclairNodes: 0,
23+
litdNodes: 0,
24+
bitcoindNodes: 2,
25+
tapdNodes: 0,
26+
repoState: defaultRepoState,
27+
managedImages: testManagedImages,
28+
customImages: [],
29+
status,
30+
});
31+
32+
if (!noSim) {
33+
network.simulation = {
34+
networkId: 1,
35+
source: network.nodes.lightning[0],
36+
destination: network.nodes.lightning[1],
37+
intervalSecs: 10,
38+
amountMsat: 1000000,
39+
status: simStatus ?? Status.Stopped,
40+
};
41+
}
42+
const chart = initChartFromNetwork(network);
43+
44+
const initialState = {
45+
network: {
46+
networks: [network],
47+
},
48+
modals: {
49+
addSimulation: {
50+
visible: false,
51+
},
52+
},
53+
designer: {
54+
allCharts: {
55+
[network.id]: chart,
56+
},
57+
},
58+
};
59+
60+
const result = renderWithProviders(<SimulationDesignerTab network={network} />, {
61+
initialState,
62+
});
63+
64+
return {
65+
...result,
66+
network,
67+
};
68+
};
69+
70+
describe('Simulation Designer Tab', () => {
71+
it('should render the simulation designer tab', async () => {
72+
const { getByText, store } = renderComponent(Status.Started, Status.Started, true);
73+
expect(getByText('No simulations created yet')).toBeInTheDocument();
74+
75+
const addSimulationBtn = getByText('Add a new Simulation');
76+
expect(addSimulationBtn).toBeInTheDocument();
77+
fireEvent.click(addSimulationBtn);
78+
await waitFor(() => {
79+
expect(store.getState().modals.addSimulation.visible).toBe(true);
80+
});
81+
});
82+
83+
it('should show add simulation button if no simulation is defined', async () => {
84+
const { getByText } = renderComponent(Status.Started, Status.Started, true);
85+
expect(getByText('No simulations created yet')).toBeInTheDocument();
86+
87+
const addSimulationBtn = getByText('Add a new Simulation');
88+
expect(addSimulationBtn).toBeInTheDocument();
89+
});
90+
});
91+
92+
describe('Start Simulation', () => {
93+
it('should start simulation successfully', async () => {
94+
const { getByText } = renderComponent(Status.Started);
95+
expect(getByText('Start')).toBeInTheDocument();
96+
97+
fireEvent.click(getByText('Start'));
98+
await waitFor(() => {
99+
expect(mockDockerService.startSimulation).toHaveBeenCalled();
100+
});
101+
});
102+
103+
it('should show error if simulation fails to start', async () => {
104+
// Simulation should fail to start because the network is not started,
105+
// which means the lightning nodes are not running.
106+
const { getByText } = renderComponent(Status.Stopped);
107+
fireEvent.click(getByText('Start'));
108+
109+
await waitFor(() => {
110+
expect(getByText('Unable to start the simulation')).toBeInTheDocument();
111+
});
112+
});
113+
});
114+
115+
describe('Stop Simulation', () => {
116+
it('should stop simulation successfully', async () => {
117+
const { getByText } = renderComponent(Status.Started, Status.Started);
118+
expect(getByText('Stop')).toBeInTheDocument();
119+
fireEvent.click(getByText('Stop'));
120+
await waitFor(() => {
121+
expect(mockDockerService.stopSimulation).toHaveBeenCalled();
122+
});
123+
});
124+
125+
it('should show error if simulation fails to stop', async () => {
126+
mockDockerService.stopSimulation.mockRejectedValue(new Error('simulation-error'));
127+
const { getByText } = renderComponent(Status.Started, Status.Started);
128+
fireEvent.click(getByText('Stop'));
129+
130+
await waitFor(() => {
131+
expect(getByText('Unable to stop the simulation')).toBeInTheDocument();
132+
expect(getByText('simulation-error')).toBeInTheDocument();
133+
});
134+
});
135+
});
136+
137+
describe('Remove Simulation', () => {
138+
it('should remove simulation', async () => {
139+
const { getByText, getByRole } = renderComponent(Status.Started);
140+
fireEvent.click(getByRole('remove'));
141+
await waitFor(() => {
142+
expect(getByText('Remove Simulation')).toBeInTheDocument();
143+
});
144+
145+
fireEvent.click(getByText('Remove'));
146+
await waitFor(() => {
147+
expect(mockDockerService.removeSimulation).toHaveBeenCalled();
148+
});
149+
});
150+
151+
it('should show error if simulation fails to remove', async () => {
152+
mockDockerService.removeSimulation.mockRejectedValue(new Error('simulation-error'));
153+
const { getByRole, getByText } = renderComponent(Status.Started);
154+
fireEvent.click(getByRole('remove'));
155+
await waitFor(() => {
156+
expect(getByText('Remove Simulation')).toBeInTheDocument();
157+
expect(getByText('Remove')).toBeInTheDocument();
158+
expect(getByText('Cancel')).toBeInTheDocument();
159+
});
160+
fireEvent.click(getByText('Remove'));
161+
await waitFor(() => {
162+
expect(getByText('Failed to remove simulation')).toBeInTheDocument();
163+
expect(getByText('simulation-error')).toBeInTheDocument();
164+
});
165+
});
166+
167+
it('should return early if no simulation is defined', async () => {
168+
const { getByText, getByRole, network } = renderComponent(
169+
Status.Started,
170+
Status.Stopped,
171+
);
172+
const sim = {
173+
networkId: 1,
174+
source: network.nodes.lightning[0],
175+
destination: network.nodes.lightning[1],
176+
intervalSecs: 10,
177+
amountMsat: 1000000,
178+
status: Status.Stopped,
179+
};
180+
181+
// This is an unlikely scenario, as the start button is not
182+
// visible if no simulation is defined.
183+
network.simulation = undefined;
184+
expect(getByText('Start')).toBeInTheDocument();
185+
fireEvent.click(getByText('Start'));
186+
expect(getByText('No simulations created yet')).toBeInTheDocument();
187+
188+
network.simulation = { ...sim, status: Status.Started };
189+
await waitFor(() => {
190+
expect(getByText('Stop')).toBeInTheDocument();
191+
});
192+
network.simulation = undefined;
193+
expect(getByText('Stop')).toBeInTheDocument();
194+
fireEvent.click(getByText('Stop'));
195+
expect(getByText('No simulations created yet')).toBeInTheDocument();
196+
197+
network.simulation = { ...sim, status: Status.Stopped };
198+
await waitFor(() => {
199+
expect(getByText('Start')).toBeInTheDocument();
200+
});
201+
expect(getByRole('remove')).toBeInTheDocument();
202+
fireEvent.click(getByRole('remove'));
203+
await waitFor(() => {
204+
expect(getByText('Remove Simulation')).toBeInTheDocument();
205+
});
206+
207+
network.simulation = undefined;
208+
fireEvent.click(getByText('Remove'));
209+
await waitFor(() => {
210+
expect(mockDockerService.removeSimulation).not.toHaveBeenCalled();
211+
});
212+
});
213+
});
214+
});

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)