foreman_leapp 3.3.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Rakefile +0 -15
- data/lib/foreman_leapp/engine.rb +1 -1
- data/lib/foreman_leapp/version.rb +1 -1
- data/webpack/components/PreupgradeReports/PreupgradeReports.js +11 -4
- data/webpack/components/PreupgradeReports/__tests__/PreupgradeReports.fixtures.js +43 -0
- data/webpack/components/PreupgradeReports/__tests__/PreupgradeReports.test.js +151 -46
- data/webpack/components/PreupgradeReports/components/__snapshots__/NoReports.test.js.snap +2 -0
- data/webpack/components/PreupgradeReportsTable/PreupgradeReportsTable.scss +9 -0
- data/webpack/components/PreupgradeReportsTable/__tests__/PreupgradeReportsTable.test.js +276 -53
- data/webpack/components/PreupgradeReportsTable/index.js +319 -92
- metadata +3 -18
- data/webpack/components/PreupgradeReports/__tests__/__snapshots__/PreupgradeReports.test.js.snap +0 -121
|
@@ -1,17 +1,19 @@
|
|
|
1
|
-
import
|
|
1
|
+
import '@testing-library/jest-dom/extend-expect';
|
|
2
|
+
|
|
2
3
|
import {
|
|
4
|
+
fireEvent,
|
|
3
5
|
render,
|
|
4
6
|
screen,
|
|
5
7
|
waitFor,
|
|
6
|
-
fireEvent,
|
|
7
8
|
within,
|
|
8
9
|
} from '@testing-library/react';
|
|
9
|
-
|
|
10
|
+
|
|
11
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
12
|
+
import PreupgradeReportsTable from '../index';
|
|
10
13
|
import { Provider } from 'react-redux';
|
|
14
|
+
import React from 'react';
|
|
11
15
|
import configureMockStore from 'redux-mock-store';
|
|
12
16
|
import thunk from 'redux-thunk';
|
|
13
|
-
import { APIActions } from 'foremanReact/redux/API';
|
|
14
|
-
import PreupgradeReportsTable from '../index';
|
|
15
17
|
|
|
16
18
|
jest.mock('foremanReact/redux/API');
|
|
17
19
|
|
|
@@ -24,17 +26,25 @@ const mockJobData = {
|
|
|
24
26
|
template_name: 'Run preupgrade via Leapp',
|
|
25
27
|
};
|
|
26
28
|
|
|
29
|
+
// Entry 0 (id=1): command remediation + inhibitor flag → fixable + selectable
|
|
30
|
+
// Entry 1 (id=2): hint-only remediation → has_remediation=Yes, NOT selectable
|
|
31
|
+
// Entries 2-11: no remediations → has_remediation=No, NOT selectable
|
|
27
32
|
const mockEntries = Array.from({ length: 12 }, (_, i) => ({
|
|
28
33
|
id: i + 1,
|
|
29
34
|
title: `Report Entry ${i + 1}`,
|
|
30
35
|
hostname: 'example.com',
|
|
36
|
+
host_id: 100 + i,
|
|
31
37
|
severity: i === 0 ? 'high' : 'low',
|
|
32
38
|
summary: `Summary for report entry ${i + 1}`,
|
|
33
39
|
tags: i === 0 ? ['security', 'network'] : [],
|
|
34
40
|
flags: i === 0 ? ['inhibitor'] : [],
|
|
35
41
|
detail: {
|
|
36
42
|
remediations:
|
|
37
|
-
i === 0
|
|
43
|
+
i === 0
|
|
44
|
+
? [{ type: 'command', context: ['echo', 'fix_command'] }]
|
|
45
|
+
: i === 1
|
|
46
|
+
? [{ type: 'hint', context: 'Do something manually' }]
|
|
47
|
+
: null,
|
|
38
48
|
external:
|
|
39
49
|
i === 0 ? [{ url: 'http://example.com', title: 'External Link' }] : [],
|
|
40
50
|
},
|
|
@@ -59,6 +69,8 @@ describe('PreupgradeReportsTable', () => {
|
|
|
59
69
|
return { type: 'MOCK_API_SUCCESS' };
|
|
60
70
|
};
|
|
61
71
|
});
|
|
72
|
+
|
|
73
|
+
APIActions.post.mockImplementation(() => () => ({ type: 'MOCK_API_POST' }));
|
|
62
74
|
});
|
|
63
75
|
|
|
64
76
|
const renderComponent = (data = mockJobData) =>
|
|
@@ -68,41 +80,106 @@ describe('PreupgradeReportsTable', () => {
|
|
|
68
80
|
</Provider>
|
|
69
81
|
);
|
|
70
82
|
|
|
71
|
-
const expandSection = () =>
|
|
83
|
+
const expandSection = () =>
|
|
72
84
|
fireEvent.click(screen.getByText('Leapp preupgrade report'));
|
|
73
|
-
|
|
85
|
+
|
|
86
|
+
const waitForTable = () =>
|
|
87
|
+
waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
|
|
74
88
|
|
|
75
89
|
it('renders data', async () => {
|
|
76
90
|
renderComponent();
|
|
77
91
|
expandSection();
|
|
78
|
-
await
|
|
92
|
+
await waitForTable();
|
|
79
93
|
expect(
|
|
80
94
|
screen.getByText('Report Entry 1', { selector: 'td' })
|
|
81
95
|
).toBeInTheDocument();
|
|
82
96
|
});
|
|
83
97
|
|
|
84
|
-
it('
|
|
85
|
-
renderComponent();
|
|
98
|
+
it('does not render anything for non-Leapp jobs', () => {
|
|
99
|
+
renderComponent({ id: 55, template_name: 'Standard RHEL Update' });
|
|
100
|
+
expect(
|
|
101
|
+
screen.queryByText('Leapp preupgrade report')
|
|
102
|
+
).not.toBeInTheDocument();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('refetches when status_label transitions (e.g. Running → Succeeded)', async () => {
|
|
106
|
+
const { rerender } = render(
|
|
107
|
+
<Provider store={store}>
|
|
108
|
+
<PreupgradeReportsTable
|
|
109
|
+
data={{ ...mockJobData, status_label: 'Running' }}
|
|
110
|
+
/>
|
|
111
|
+
</Provider>
|
|
112
|
+
);
|
|
86
113
|
expandSection();
|
|
87
|
-
await
|
|
114
|
+
await waitForTable();
|
|
88
115
|
|
|
89
|
-
const
|
|
90
|
-
fireEvent.click(rowExpandButtons[0]);
|
|
116
|
+
const callCountAfterFirstFetch = APIActions.get.mock.calls.length;
|
|
91
117
|
|
|
92
|
-
|
|
118
|
+
rerender(
|
|
119
|
+
<Provider store={store}>
|
|
120
|
+
<PreupgradeReportsTable
|
|
121
|
+
data={{ ...mockJobData, status_label: 'Succeeded' }}
|
|
122
|
+
/>
|
|
123
|
+
</Provider>
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
await waitFor(() =>
|
|
127
|
+
expect(APIActions.get.mock.calls.length).toBeGreaterThan(
|
|
128
|
+
callCountAfterFirstFetch
|
|
129
|
+
)
|
|
130
|
+
);
|
|
93
131
|
expect(
|
|
94
|
-
|
|
132
|
+
screen.getByText('Report Entry 1', { selector: 'td' })
|
|
95
133
|
).toBeInTheDocument();
|
|
96
134
|
});
|
|
97
135
|
|
|
98
|
-
it('
|
|
136
|
+
it('does not refetch on collapse/re-expand when status_label is unchanged', async () => {
|
|
137
|
+
renderComponent({ ...mockJobData, status_label: 'Succeeded' });
|
|
138
|
+
expandSection();
|
|
139
|
+
await waitForTable();
|
|
140
|
+
|
|
141
|
+
const callCountAfterFirstFetch = APIActions.get.mock.calls.length;
|
|
142
|
+
|
|
143
|
+
fireEvent.click(screen.getByText('Leapp preupgrade report')); // collapse
|
|
144
|
+
fireEvent.click(screen.getByText('Leapp preupgrade report')); // re-expand
|
|
145
|
+
|
|
146
|
+
expect(APIActions.get.mock.calls.length).toBe(callCountAfterFirstFetch);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('renders empty state message when no issues found', async () => {
|
|
150
|
+
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
|
|
151
|
+
return () => {
|
|
152
|
+
if (key.includes('GET_LEAPP_REPORT_LIST'))
|
|
153
|
+
handleSuccess({ results: [{ id: mockReportId }] });
|
|
154
|
+
if (key.includes('GET_LEAPP_REPORT_DETAIL'))
|
|
155
|
+
handleSuccess({ id: mockReportId, preupgrade_report_entries: [] });
|
|
156
|
+
return { type: 'EMPTY' };
|
|
157
|
+
};
|
|
158
|
+
});
|
|
99
159
|
renderComponent();
|
|
100
160
|
expandSection();
|
|
101
|
-
await waitFor(() =>
|
|
161
|
+
await waitFor(() =>
|
|
162
|
+
expect(
|
|
163
|
+
screen.getByText('The preupgrade report shows no issues.')
|
|
164
|
+
).toBeInTheDocument()
|
|
165
|
+
);
|
|
166
|
+
});
|
|
102
167
|
|
|
103
|
-
|
|
104
|
-
|
|
168
|
+
it('expands a row and shows details', async () => {
|
|
169
|
+
renderComponent();
|
|
170
|
+
expandSection();
|
|
171
|
+
await waitForTable();
|
|
172
|
+
fireEvent.click(screen.getAllByLabelText('Details')[0]);
|
|
173
|
+
expect(
|
|
174
|
+
await screen.findByText('Summary for report entry 1')
|
|
175
|
+
).toBeInTheDocument();
|
|
176
|
+
});
|
|
105
177
|
|
|
178
|
+
it('expands all rows', async () => {
|
|
179
|
+
renderComponent();
|
|
180
|
+
expandSection();
|
|
181
|
+
await waitForTable();
|
|
182
|
+
fireEvent.click(screen.getByLabelText('Expand all rows'));
|
|
106
183
|
expect(
|
|
107
184
|
await screen.findByText('Summary for report entry 1')
|
|
108
185
|
).toBeInTheDocument();
|
|
@@ -114,11 +191,9 @@ describe('PreupgradeReportsTable', () => {
|
|
|
114
191
|
it('paginates to the next page', async () => {
|
|
115
192
|
renderComponent();
|
|
116
193
|
expandSection();
|
|
117
|
-
await
|
|
118
|
-
|
|
194
|
+
await waitForTable();
|
|
119
195
|
fireEvent.click(screen.getAllByLabelText('Go to next page')[0]);
|
|
120
196
|
await waitFor(() => screen.getByText('Report Entry 6', { selector: 'td' }));
|
|
121
|
-
|
|
122
197
|
expect(
|
|
123
198
|
screen.getByText('Report Entry 10', { selector: 'td' })
|
|
124
199
|
).toBeInTheDocument();
|
|
@@ -127,19 +202,168 @@ describe('PreupgradeReportsTable', () => {
|
|
|
127
202
|
it('changes perPage limit to 10', async () => {
|
|
128
203
|
renderComponent();
|
|
129
204
|
expandSection();
|
|
130
|
-
await
|
|
131
|
-
|
|
205
|
+
await waitForTable();
|
|
132
206
|
fireEvent.click(screen.getAllByLabelText('Items per page')[0]);
|
|
133
207
|
fireEvent.click(screen.getAllByText('10 per page')[0]);
|
|
134
|
-
|
|
135
|
-
await waitFor(() => {
|
|
208
|
+
await waitFor(() =>
|
|
136
209
|
expect(
|
|
137
210
|
screen.getByText('Report Entry 10', { selector: 'td' })
|
|
138
|
-
).toBeInTheDocument()
|
|
139
|
-
|
|
211
|
+
).toBeInTheDocument()
|
|
212
|
+
);
|
|
140
213
|
});
|
|
141
214
|
|
|
142
|
-
it('
|
|
215
|
+
it('displays correct inhibitor status based on flags', async () => {
|
|
216
|
+
renderComponent();
|
|
217
|
+
expandSection();
|
|
218
|
+
await waitForTable();
|
|
219
|
+
|
|
220
|
+
const row1 = screen
|
|
221
|
+
.getByText('Report Entry 1', { selector: 'td' })
|
|
222
|
+
.closest('tr');
|
|
223
|
+
expect(
|
|
224
|
+
within(row1.querySelector('td[data-label="Inhibitor?"]')).getByText('Yes')
|
|
225
|
+
).toBeInTheDocument();
|
|
226
|
+
|
|
227
|
+
const row2 = screen
|
|
228
|
+
.getByText('Report Entry 2', { selector: 'td' })
|
|
229
|
+
.closest('tr');
|
|
230
|
+
expect(
|
|
231
|
+
within(row2.querySelector('td[data-label="Inhibitor?"]')).getByText('No')
|
|
232
|
+
).toBeInTheDocument();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('shows Has Remediation? Yes for any remediation type, not only command', async () => {
|
|
236
|
+
renderComponent();
|
|
237
|
+
expandSection();
|
|
238
|
+
await waitForTable();
|
|
239
|
+
|
|
240
|
+
// id=1: command → Yes
|
|
241
|
+
const row1 = screen
|
|
242
|
+
.getByText('Report Entry 1', { selector: 'td' })
|
|
243
|
+
.closest('tr');
|
|
244
|
+
expect(
|
|
245
|
+
within(row1.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
246
|
+
'Yes'
|
|
247
|
+
)
|
|
248
|
+
).toBeInTheDocument();
|
|
249
|
+
|
|
250
|
+
// id=2: hint-only → still Yes (display column shows any remediations)
|
|
251
|
+
const row2 = screen
|
|
252
|
+
.getByText('Report Entry 2', { selector: 'td' })
|
|
253
|
+
.closest('tr');
|
|
254
|
+
expect(
|
|
255
|
+
within(row2.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
256
|
+
'Yes'
|
|
257
|
+
)
|
|
258
|
+
).toBeInTheDocument();
|
|
259
|
+
|
|
260
|
+
// id=3: no remediations → No
|
|
261
|
+
const row3 = screen
|
|
262
|
+
.getByText('Report Entry 3', { selector: 'td' })
|
|
263
|
+
.closest('tr');
|
|
264
|
+
expect(
|
|
265
|
+
within(row3.querySelector('td[data-label="Has Remediation?"]')).getByText(
|
|
266
|
+
'No'
|
|
267
|
+
)
|
|
268
|
+
).toBeInTheDocument();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('renders Fix Selected button disabled initially', async () => {
|
|
272
|
+
renderComponent();
|
|
273
|
+
expandSection();
|
|
274
|
+
await waitForTable();
|
|
275
|
+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('enables Fix Selected after selecting a fixable (command) row', async () => {
|
|
279
|
+
renderComponent();
|
|
280
|
+
expandSection();
|
|
281
|
+
await waitForTable();
|
|
282
|
+
|
|
283
|
+
// checkboxes[0] = SelectAll, checkboxes[1] = entry id=1 (command remediation)
|
|
284
|
+
fireEvent.click(screen.getAllByRole('checkbox')[1]);
|
|
285
|
+
|
|
286
|
+
await waitFor(() =>
|
|
287
|
+
expect(
|
|
288
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
289
|
+
).not.toBeDisabled()
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('Fix Selected dispatches APIActions.post with correct feature, host_ids and remediation_ids', async () => {
|
|
294
|
+
renderComponent();
|
|
295
|
+
expandSection();
|
|
296
|
+
await waitForTable();
|
|
297
|
+
|
|
298
|
+
fireEvent.click(screen.getAllByRole('checkbox')[1]); // select entry id=1 (host_id=100)
|
|
299
|
+
|
|
300
|
+
await waitFor(() =>
|
|
301
|
+
expect(
|
|
302
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
303
|
+
).not.toBeDisabled()
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
fireEvent.click(screen.getByRole('button', { name: 'Fix Selected' }));
|
|
307
|
+
|
|
308
|
+
expect(APIActions.post).toHaveBeenCalledWith(
|
|
309
|
+
expect.objectContaining({
|
|
310
|
+
url: expect.stringContaining('/api/job_invocations'),
|
|
311
|
+
params: {
|
|
312
|
+
job_invocation: {
|
|
313
|
+
feature: 'leapp_remediation_plan',
|
|
314
|
+
host_ids: [100],
|
|
315
|
+
inputs: { remediation_ids: '1' },
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
})
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('hint-only row checkbox is disabled and does not enable Fix Selected', async () => {
|
|
323
|
+
renderComponent();
|
|
324
|
+
expandSection();
|
|
325
|
+
await waitForTable();
|
|
326
|
+
|
|
327
|
+
// checkboxes[2] = entry id=2, which has hint-only remediation — must be disabled
|
|
328
|
+
const hintCheckbox = screen.getAllByRole('checkbox')[2];
|
|
329
|
+
expect(hintCheckbox).toBeDisabled();
|
|
330
|
+
|
|
331
|
+
fireEvent.click(hintCheckbox);
|
|
332
|
+
expect(screen.getByRole('button', { name: 'Fix Selected' })).toBeDisabled();
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('renders Run Upgrade button enabled when entries are present', async () => {
|
|
336
|
+
renderComponent();
|
|
337
|
+
expandSection();
|
|
338
|
+
await waitForTable();
|
|
339
|
+
expect(
|
|
340
|
+
screen.getByRole('button', { name: 'Run Upgrade' })
|
|
341
|
+
).not.toBeDisabled();
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it('Run Upgrade dispatches APIActions.post with correct feature and all host_ids', async () => {
|
|
345
|
+
renderComponent();
|
|
346
|
+
expandSection();
|
|
347
|
+
await waitForTable();
|
|
348
|
+
|
|
349
|
+
fireEvent.click(screen.getByRole('button', { name: 'Run Upgrade' }));
|
|
350
|
+
|
|
351
|
+
expect(APIActions.post).toHaveBeenCalledWith(
|
|
352
|
+
expect.objectContaining({
|
|
353
|
+
url: expect.stringContaining('/api/job_invocations'),
|
|
354
|
+
params: {
|
|
355
|
+
job_invocation: {
|
|
356
|
+
feature: 'leapp_upgrade',
|
|
357
|
+
host_ids: expect.arrayContaining([100, 101, 102]),
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
const callParams = APIActions.post.mock.calls[0][0].params.job_invocation;
|
|
363
|
+
expect(callParams.inputs).toBeUndefined();
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
it('does not render toolbar buttons when report has no entries', async () => {
|
|
143
367
|
APIActions.get.mockImplementation(({ key, handleSuccess }) => {
|
|
144
368
|
return () => {
|
|
145
369
|
if (key.includes('GET_LEAPP_REPORT_LIST'))
|
|
@@ -151,36 +375,35 @@ describe('PreupgradeReportsTable', () => {
|
|
|
151
375
|
});
|
|
152
376
|
renderComponent();
|
|
153
377
|
expandSection();
|
|
154
|
-
await waitFor(() =>
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
).toBeInTheDocument();
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('does not render anything for non-Leapp jobs', () => {
|
|
162
|
-
const nonLeappData = { id: 55, template_name: 'Standard RHEL Update' };
|
|
163
|
-
renderComponent(nonLeappData);
|
|
378
|
+
await waitFor(() =>
|
|
379
|
+
screen.getByText('The preupgrade report shows no issues.')
|
|
380
|
+
);
|
|
164
381
|
expect(
|
|
165
|
-
screen.
|
|
382
|
+
screen.queryByRole('button', { name: 'Fix Selected' })
|
|
383
|
+
).not.toBeInTheDocument();
|
|
384
|
+
expect(
|
|
385
|
+
screen.queryByRole('button', { name: 'Run Upgrade' })
|
|
166
386
|
).not.toBeInTheDocument();
|
|
167
387
|
});
|
|
168
388
|
|
|
169
|
-
it('
|
|
389
|
+
it('renders the SelectAll checkbox', async () => {
|
|
170
390
|
renderComponent();
|
|
171
391
|
expandSection();
|
|
172
|
-
await
|
|
392
|
+
await waitForTable();
|
|
393
|
+
expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
|
|
394
|
+
});
|
|
173
395
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
expect(within(inhibitorCell1).getByText('Yes')).toBeInTheDocument();
|
|
396
|
+
it('selecting all fixable rows enables Fix Selected', async () => {
|
|
397
|
+
renderComponent();
|
|
398
|
+
expandSection();
|
|
399
|
+
await waitForTable();
|
|
179
400
|
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
401
|
+
fireEvent.click(screen.getByLabelText('Select all'));
|
|
402
|
+
|
|
403
|
+
await waitFor(() =>
|
|
404
|
+
expect(
|
|
405
|
+
screen.getByRole('button', { name: 'Fix Selected' })
|
|
406
|
+
).not.toBeDisabled()
|
|
407
|
+
);
|
|
185
408
|
});
|
|
186
409
|
});
|