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.
@@ -1,17 +1,19 @@
1
- import React from 'react';
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
- import '@testing-library/jest-dom/extend-expect';
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 ? [{ type: 'command', context: ['echo', 'fix_command'] }] : [],
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 waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
92
+ await waitForTable();
79
93
  expect(
80
94
  screen.getByText('Report Entry 1', { selector: 'td' })
81
95
  ).toBeInTheDocument();
82
96
  });
83
97
 
84
- it('expands a row and shows details', async () => {
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 waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
114
+ await waitForTable();
88
115
 
89
- const rowExpandButtons = screen.getAllByLabelText('Details');
90
- fireEvent.click(rowExpandButtons[0]);
116
+ const callCountAfterFirstFetch = APIActions.get.mock.calls.length;
91
117
 
92
- expect(await screen.findByText('Summary')).toBeInTheDocument();
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
- await screen.findByText('Summary for report entry 1')
132
+ screen.getByText('Report Entry 1', { selector: 'td' })
95
133
  ).toBeInTheDocument();
96
134
  });
97
135
 
98
- it('expands all rows', async () => {
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(() => screen.getByText('Report Entry 1', { selector: 'td' }));
161
+ await waitFor(() =>
162
+ expect(
163
+ screen.getByText('The preupgrade report shows no issues.')
164
+ ).toBeInTheDocument()
165
+ );
166
+ });
102
167
 
103
- const expandAllButton = screen.getByLabelText('Expand all rows');
104
- fireEvent.click(expandAllButton);
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 waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
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 waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
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('renders empty state message when no issues found', async () => {
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
- expect(
156
- screen.getByText('The preupgrade report shows no issues.')
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.queryByText('Leapp preupgrade report')
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('displays correct inhibitor status based on flags', async () => {
389
+ it('renders the SelectAll checkbox', async () => {
170
390
  renderComponent();
171
391
  expandSection();
172
- await waitFor(() => screen.getByText('Report Entry 1', { selector: 'td' }));
392
+ await waitForTable();
393
+ expect(screen.getAllByRole('checkbox').length).toBeGreaterThan(0);
394
+ });
173
395
 
174
- const row1 = screen
175
- .getByText('Report Entry 1', { selector: 'td' })
176
- .closest('tr');
177
- const inhibitorCell1 = row1.querySelector('td[data-label="Inhibitor?"]');
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
- const row2 = screen
181
- .getByText('Report Entry 2', { selector: 'td' })
182
- .closest('tr');
183
- const inhibitorCell2 = row2.querySelector('td[data-label="Inhibitor?"]');
184
- expect(within(inhibitorCell2).getByText('No')).toBeInTheDocument();
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
  });