foreman_rh_cloud 13.0.12 → 13.1.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/lib/foreman_rh_cloud/version.rb +1 -1
- data/package.json +1 -1
- data/webpack/ForemanColumnExtensions/index.js +5 -3
- data/webpack/ForemanRhCloudFills.js +7 -0
- data/webpack/HostsIndexExtensions/VulnerabilityAnalysisActions.js +138 -0
- data/webpack/HostsIndexExtensions/__tests__/VulnerabilityAnalysisActions.test.js +567 -0
- data/webpack/HostsIndexExtensions/hostVulnerabilityStoreUtils.js +41 -0
- data/webpack/InsightsVulnerabilityActionsBar/InsightsVulnerabilityActionsBar.scss +14 -0
- data/webpack/InsightsVulnerabilityActionsBar/InsightsVulnerabilityActionsBarActions.js +169 -0
- data/webpack/InsightsVulnerabilityActionsBar/__tests__/InsightsVulnerabilityActionsBar.test.js +91 -0
- data/webpack/InsightsVulnerabilityActionsBar/__tests__/InsightsVulnerabilityActionsBarActions.test.js +299 -0
- data/webpack/InsightsVulnerabilityActionsBar/index.js +93 -0
- data/webpack/InsightsVulnerabilityHostIndexExtensions/CVECountCell.js +25 -13
- data/webpack/InsightsVulnerabilityHostIndexExtensions/__tests__/CVECountCell.test.js +156 -87
- data/webpack/__mocks__/foremanReact/Root/Context/ForemanContext.js +6 -2
- data/webpack/__mocks__/foremanReact/common/I18n.js +2 -1
- data/webpack/__mocks__/foremanReact/common/helpers.js +11 -0
- data/webpack/__mocks__/foremanReact/components/HostsIndex/index.js +8 -0
- data/webpack/__mocks__/foremanReact/redux.js +10 -0
- data/webpack/common/Hooks/ConfigHooks.js +1 -1
- data/webpack/global_index.js +7 -0
- metadata +11 -1
|
@@ -0,0 +1,567 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
// Create mocks
|
|
3
|
+
const mockGetState = jest.fn();
|
|
4
|
+
const mockDispatch = jest.fn();
|
|
5
|
+
const mockPatch = jest.fn();
|
|
6
|
+
const mockGet = jest.fn();
|
|
7
|
+
|
|
8
|
+
// Mock modules
|
|
9
|
+
jest.mock('../hostVulnerabilityStoreUtils');
|
|
10
|
+
jest.mock(
|
|
11
|
+
'foremanReact/redux',
|
|
12
|
+
() => ({
|
|
13
|
+
__esModule: true,
|
|
14
|
+
default: {
|
|
15
|
+
getState: () => mockGetState(),
|
|
16
|
+
dispatch: action => mockDispatch(action),
|
|
17
|
+
},
|
|
18
|
+
}),
|
|
19
|
+
{ virtual: true }
|
|
20
|
+
);
|
|
21
|
+
jest.mock(
|
|
22
|
+
'foremanReact/redux/API',
|
|
23
|
+
() => ({
|
|
24
|
+
patch: (...args) => mockPatch(...args),
|
|
25
|
+
get: (...args) => mockGet(...args),
|
|
26
|
+
}),
|
|
27
|
+
{ virtual: true }
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
// Import after mocking
|
|
31
|
+
// eslint-disable-next-line import/first
|
|
32
|
+
import * as storeUtils from '../hostVulnerabilityStoreUtils';
|
|
33
|
+
// eslint-disable-next-line import/first
|
|
34
|
+
import getVulnerabilityAnalysisActions from '../VulnerabilityAnalysisActions';
|
|
35
|
+
|
|
36
|
+
describe('VulnerabilityAnalysisActions', () => {
|
|
37
|
+
beforeEach(() => {
|
|
38
|
+
mockGetState.mockReturnValue({ API: {} });
|
|
39
|
+
mockDispatch.mockImplementation(action => action);
|
|
40
|
+
jest.clearAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('getVulnerabilityAnalysisActions', () => {
|
|
44
|
+
it('returns empty array when not in IoP mode', () => {
|
|
45
|
+
const hostDetails = {
|
|
46
|
+
id: 1,
|
|
47
|
+
insights_attributes: {
|
|
48
|
+
use_iop_mode: false,
|
|
49
|
+
},
|
|
50
|
+
subscription_facet_attributes: {
|
|
51
|
+
uuid: 'test-uuid',
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
56
|
+
|
|
57
|
+
expect(actions).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('returns empty array when host has no subscription UUID', () => {
|
|
61
|
+
const hostDetails = {
|
|
62
|
+
id: 1,
|
|
63
|
+
insights_attributes: {
|
|
64
|
+
use_iop_mode: true,
|
|
65
|
+
},
|
|
66
|
+
subscription_facet_attributes: {
|
|
67
|
+
uuid: null,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
72
|
+
|
|
73
|
+
expect(actions).toEqual([]);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('returns empty array when API returned empty data (no system record)', () => {
|
|
77
|
+
mockGetState.mockReturnValue({
|
|
78
|
+
API: {
|
|
79
|
+
'HOST_CVE_COUNT_test-uuid': {
|
|
80
|
+
response: {
|
|
81
|
+
data: [], // Empty array means no system record exists
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const hostDetails = {
|
|
88
|
+
id: 1,
|
|
89
|
+
insights_attributes: {
|
|
90
|
+
use_iop_mode: true,
|
|
91
|
+
},
|
|
92
|
+
subscription_facet_attributes: {
|
|
93
|
+
uuid: 'test-uuid',
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
98
|
+
|
|
99
|
+
expect(actions).toEqual([]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('returns empty array when opt_out is undefined (data not loaded yet)', () => {
|
|
103
|
+
mockGetState.mockReturnValue({
|
|
104
|
+
API: {
|
|
105
|
+
'HOST_CVE_COUNT_test-uuid': {
|
|
106
|
+
response: null,
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const hostDetails = {
|
|
112
|
+
id: 1,
|
|
113
|
+
insights_attributes: {
|
|
114
|
+
use_iop_mode: true,
|
|
115
|
+
},
|
|
116
|
+
subscription_facet_attributes: {
|
|
117
|
+
uuid: 'test-uuid',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
122
|
+
|
|
123
|
+
expect(actions).toEqual([]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('returns "Enable vulnerability analysis" action when opt_out is true', () => {
|
|
127
|
+
const hostDetails = {
|
|
128
|
+
id: 1,
|
|
129
|
+
insights_attributes: {
|
|
130
|
+
use_iop_mode: true,
|
|
131
|
+
vulnerability_opt_out: true,
|
|
132
|
+
},
|
|
133
|
+
subscription_facet_attributes: {
|
|
134
|
+
uuid: 'test-uuid',
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
139
|
+
|
|
140
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
141
|
+
|
|
142
|
+
expect(actions).toHaveLength(1);
|
|
143
|
+
expect(actions[0].title).toBe('Enable vulnerability analysis');
|
|
144
|
+
expect(actions[0].onClick).toBeDefined();
|
|
145
|
+
|
|
146
|
+
// Trigger onClick to verify it dispatches correctly
|
|
147
|
+
actions[0].onClick();
|
|
148
|
+
|
|
149
|
+
expect(mockDispatch).toHaveBeenCalledWith({
|
|
150
|
+
type: 'MOCK_PATCH_ACTION',
|
|
151
|
+
});
|
|
152
|
+
expect(mockPatch).toHaveBeenCalledWith(
|
|
153
|
+
expect.objectContaining({
|
|
154
|
+
key: 'HOST_CVE_COUNT_test-uuid',
|
|
155
|
+
params: {
|
|
156
|
+
inventory_id: 'test-uuid',
|
|
157
|
+
opt_out: false, // Should toggle to false
|
|
158
|
+
},
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('returns "Disable vulnerability analysis" action when opt_out is false', () => {
|
|
164
|
+
const hostDetails = {
|
|
165
|
+
id: 1,
|
|
166
|
+
insights_attributes: {
|
|
167
|
+
use_iop_mode: true,
|
|
168
|
+
vulnerability_opt_out: false,
|
|
169
|
+
},
|
|
170
|
+
subscription_facet_attributes: {
|
|
171
|
+
uuid: 'test-uuid',
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
176
|
+
|
|
177
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
178
|
+
|
|
179
|
+
expect(actions).toHaveLength(1);
|
|
180
|
+
expect(actions[0].title).toBe('Disable vulnerability analysis');
|
|
181
|
+
expect(actions[0].onClick).toBeDefined();
|
|
182
|
+
|
|
183
|
+
// Trigger onClick to verify it dispatches correctly
|
|
184
|
+
actions[0].onClick();
|
|
185
|
+
|
|
186
|
+
expect(mockDispatch).toHaveBeenCalledWith({
|
|
187
|
+
type: 'MOCK_PATCH_ACTION',
|
|
188
|
+
});
|
|
189
|
+
expect(mockPatch).toHaveBeenCalledWith(
|
|
190
|
+
expect.objectContaining({
|
|
191
|
+
key: 'HOST_CVE_COUNT_test-uuid',
|
|
192
|
+
params: {
|
|
193
|
+
inventory_id: 'test-uuid',
|
|
194
|
+
opt_out: true, // Should toggle to true
|
|
195
|
+
},
|
|
196
|
+
})
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('falls back to CVE API cache when opt_out not set on host object', () => {
|
|
201
|
+
mockGetState.mockReturnValue({
|
|
202
|
+
API: {
|
|
203
|
+
'HOST_CVE_COUNT_test-uuid': {
|
|
204
|
+
response: {
|
|
205
|
+
data: [
|
|
206
|
+
{
|
|
207
|
+
attributes: {
|
|
208
|
+
opt_out: true,
|
|
209
|
+
},
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const hostDetails = {
|
|
218
|
+
id: 1,
|
|
219
|
+
insights_attributes: {
|
|
220
|
+
use_iop_mode: true,
|
|
221
|
+
// No vulnerability_opt_out set
|
|
222
|
+
},
|
|
223
|
+
subscription_facet_attributes: {
|
|
224
|
+
uuid: 'test-uuid',
|
|
225
|
+
},
|
|
226
|
+
};
|
|
227
|
+
|
|
228
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
229
|
+
|
|
230
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
231
|
+
|
|
232
|
+
expect(actions).toHaveLength(1);
|
|
233
|
+
expect(actions[0].title).toBe('Enable vulnerability analysis');
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('handleToggle action creator', () => {
|
|
238
|
+
it('creates patch action with correct parameters', () => {
|
|
239
|
+
const mockPatchAction = { type: 'MOCK_PATCH_ACTION' };
|
|
240
|
+
mockPatch.mockReturnValue(mockPatchAction);
|
|
241
|
+
|
|
242
|
+
const hostDetails = {
|
|
243
|
+
id: 123,
|
|
244
|
+
insights_attributes: {
|
|
245
|
+
use_iop_mode: true,
|
|
246
|
+
vulnerability_opt_out: false,
|
|
247
|
+
},
|
|
248
|
+
subscription_facet_attributes: {
|
|
249
|
+
uuid: 'test-uuid-456',
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
254
|
+
actions[0].onClick();
|
|
255
|
+
|
|
256
|
+
expect(mockPatch).toHaveBeenCalledWith(
|
|
257
|
+
expect.objectContaining({
|
|
258
|
+
key: 'HOST_CVE_COUNT_test-uuid-456',
|
|
259
|
+
url: expect.stringContaining('api/vulnerability/v1/systems/opt_out'),
|
|
260
|
+
params: {
|
|
261
|
+
inventory_id: 'test-uuid-456',
|
|
262
|
+
opt_out: true,
|
|
263
|
+
},
|
|
264
|
+
headers: {
|
|
265
|
+
'Content-Type': 'application/vnd.api+json',
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('includes updateData callback when disabling (optimistic update)', () => {
|
|
272
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
273
|
+
|
|
274
|
+
const hostDetails = {
|
|
275
|
+
id: 1,
|
|
276
|
+
insights_attributes: {
|
|
277
|
+
use_iop_mode: true,
|
|
278
|
+
vulnerability_opt_out: false,
|
|
279
|
+
},
|
|
280
|
+
subscription_facet_attributes: {
|
|
281
|
+
uuid: 'test-uuid',
|
|
282
|
+
},
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
286
|
+
actions[0].onClick(); // Clicking "Disable" action
|
|
287
|
+
|
|
288
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
289
|
+
expect(patchCall.updateData).toBeDefined();
|
|
290
|
+
|
|
291
|
+
// Test updateData callback
|
|
292
|
+
const prevState = {
|
|
293
|
+
data: [
|
|
294
|
+
{
|
|
295
|
+
id: 'system-1',
|
|
296
|
+
attributes: {
|
|
297
|
+
cve_count: 5,
|
|
298
|
+
opt_out: false,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
],
|
|
302
|
+
meta: { total: 1 },
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const newState = patchCall.updateData(prevState);
|
|
306
|
+
|
|
307
|
+
expect(newState).toEqual({
|
|
308
|
+
data: [
|
|
309
|
+
{
|
|
310
|
+
id: 'system-1',
|
|
311
|
+
attributes: {
|
|
312
|
+
cve_count: 5,
|
|
313
|
+
opt_out: true, // Should be updated to true
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
],
|
|
317
|
+
meta: { total: 1 },
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('skips updateData when enabling (will refetch instead)', () => {
|
|
322
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
323
|
+
|
|
324
|
+
const hostDetails = {
|
|
325
|
+
id: 1,
|
|
326
|
+
insights_attributes: {
|
|
327
|
+
use_iop_mode: true,
|
|
328
|
+
vulnerability_opt_out: true, // Currently disabled
|
|
329
|
+
},
|
|
330
|
+
subscription_facet_attributes: {
|
|
331
|
+
uuid: 'test-uuid',
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
336
|
+
actions[0].onClick(); // Clicking "Enable" action
|
|
337
|
+
|
|
338
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
339
|
+
// When enabling, updateData should be undefined (no optimistic update)
|
|
340
|
+
expect(patchCall.updateData).toBeUndefined();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('updateData handles missing data gracefully when disabling', () => {
|
|
344
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
345
|
+
|
|
346
|
+
const hostDetails = {
|
|
347
|
+
id: 1,
|
|
348
|
+
insights_attributes: {
|
|
349
|
+
use_iop_mode: true,
|
|
350
|
+
vulnerability_opt_out: false,
|
|
351
|
+
},
|
|
352
|
+
subscription_facet_attributes: {
|
|
353
|
+
uuid: 'test-uuid',
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
358
|
+
actions[0].onClick();
|
|
359
|
+
|
|
360
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
361
|
+
|
|
362
|
+
// Test with undefined prevState
|
|
363
|
+
expect(patchCall.updateData(undefined)).toBeUndefined();
|
|
364
|
+
|
|
365
|
+
// Test with missing data
|
|
366
|
+
expect(patchCall.updateData({})).toEqual({});
|
|
367
|
+
|
|
368
|
+
// Test with null data
|
|
369
|
+
expect(patchCall.updateData({ data: null })).toEqual({ data: null });
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('includes successToast callback that returns correct message for disabling', () => {
|
|
373
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
374
|
+
|
|
375
|
+
const hostDetails = {
|
|
376
|
+
id: 1,
|
|
377
|
+
insights_attributes: {
|
|
378
|
+
use_iop_mode: true,
|
|
379
|
+
vulnerability_opt_out: false,
|
|
380
|
+
},
|
|
381
|
+
subscription_facet_attributes: {
|
|
382
|
+
uuid: 'test-uuid',
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
387
|
+
actions[0].onClick();
|
|
388
|
+
|
|
389
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
390
|
+
expect(patchCall.successToast).toBeDefined();
|
|
391
|
+
|
|
392
|
+
const message = patchCall.successToast();
|
|
393
|
+
expect(message).toBe('Vulnerability analysis disabled');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('includes successToast callback that returns correct message for enabling', () => {
|
|
397
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
398
|
+
|
|
399
|
+
const hostDetails = {
|
|
400
|
+
id: 1,
|
|
401
|
+
insights_attributes: {
|
|
402
|
+
use_iop_mode: true,
|
|
403
|
+
vulnerability_opt_out: true,
|
|
404
|
+
},
|
|
405
|
+
subscription_facet_attributes: {
|
|
406
|
+
uuid: 'test-uuid',
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
411
|
+
actions[0].onClick();
|
|
412
|
+
|
|
413
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
414
|
+
const message = patchCall.successToast();
|
|
415
|
+
expect(message).toBe('Vulnerability analysis enabled');
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('includes handleSuccess callback that updates HOSTS Redux data when disabling', () => {
|
|
419
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
420
|
+
storeUtils.updateHostVulnerabilityOptOut.mockImplementation(() => {});
|
|
421
|
+
|
|
422
|
+
const hostDetails = {
|
|
423
|
+
id: 123,
|
|
424
|
+
insights_attributes: {
|
|
425
|
+
use_iop_mode: true,
|
|
426
|
+
vulnerability_opt_out: false,
|
|
427
|
+
},
|
|
428
|
+
subscription_facet_attributes: {
|
|
429
|
+
uuid: 'test-uuid',
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
|
|
433
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
434
|
+
|
|
435
|
+
// Clear dispatch mock before calling onClick (which dispatches the patch action)
|
|
436
|
+
mockDispatch.mockClear();
|
|
437
|
+
mockGet.mockClear();
|
|
438
|
+
|
|
439
|
+
actions[0].onClick();
|
|
440
|
+
|
|
441
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
442
|
+
expect(patchCall.handleSuccess).toBeDefined();
|
|
443
|
+
|
|
444
|
+
// Call handleSuccess
|
|
445
|
+
patchCall.handleSuccess();
|
|
446
|
+
|
|
447
|
+
expect(storeUtils.updateHostVulnerabilityOptOut).toHaveBeenCalledWith(
|
|
448
|
+
123,
|
|
449
|
+
true
|
|
450
|
+
);
|
|
451
|
+
// When disabling, should NOT dispatch GET (no refetch needed)
|
|
452
|
+
expect(mockGet).not.toHaveBeenCalled();
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
it('includes handleSuccess callback that refetches data when enabling', () => {
|
|
456
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
457
|
+
mockGet.mockReturnValue({ type: 'MOCK_GET_ACTION' });
|
|
458
|
+
storeUtils.updateHostVulnerabilityOptOut.mockImplementation(() => {});
|
|
459
|
+
|
|
460
|
+
const hostDetails = {
|
|
461
|
+
id: 123,
|
|
462
|
+
insights_attributes: {
|
|
463
|
+
use_iop_mode: true,
|
|
464
|
+
vulnerability_opt_out: true, // Currently disabled
|
|
465
|
+
},
|
|
466
|
+
subscription_facet_attributes: {
|
|
467
|
+
uuid: 'test-uuid-456',
|
|
468
|
+
},
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
472
|
+
actions[0].onClick(); // Click "Enable"
|
|
473
|
+
|
|
474
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
475
|
+
expect(patchCall.handleSuccess).toBeDefined();
|
|
476
|
+
|
|
477
|
+
// Call handleSuccess
|
|
478
|
+
patchCall.handleSuccess();
|
|
479
|
+
|
|
480
|
+
// Should update HOSTS Redux data
|
|
481
|
+
expect(storeUtils.updateHostVulnerabilityOptOut).toHaveBeenCalledWith(
|
|
482
|
+
123,
|
|
483
|
+
false // Enabling sets to false
|
|
484
|
+
);
|
|
485
|
+
|
|
486
|
+
// Should dispatch GET to refetch CVE data
|
|
487
|
+
expect(mockDispatch).toHaveBeenCalledWith({ type: 'MOCK_GET_ACTION' });
|
|
488
|
+
expect(mockGet).toHaveBeenCalledWith(
|
|
489
|
+
expect.objectContaining({
|
|
490
|
+
key: 'HOST_CVE_COUNT_test-uuid-456',
|
|
491
|
+
url: expect.stringContaining(
|
|
492
|
+
'api/vulnerability/v1/systems?uuid=test-uuid-456'
|
|
493
|
+
),
|
|
494
|
+
})
|
|
495
|
+
);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
it('includes errorToast callback that extracts JSON:API error detail', () => {
|
|
499
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
500
|
+
|
|
501
|
+
const hostDetails = {
|
|
502
|
+
id: 1,
|
|
503
|
+
insights_attributes: {
|
|
504
|
+
use_iop_mode: true,
|
|
505
|
+
vulnerability_opt_out: false,
|
|
506
|
+
},
|
|
507
|
+
subscription_facet_attributes: {
|
|
508
|
+
uuid: 'test-uuid',
|
|
509
|
+
},
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
513
|
+
actions[0].onClick();
|
|
514
|
+
|
|
515
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
516
|
+
expect(patchCall.errorToast).toBeDefined();
|
|
517
|
+
|
|
518
|
+
// Test with JSON:API error format
|
|
519
|
+
const errorWithDetail = {
|
|
520
|
+
response: {
|
|
521
|
+
data: {
|
|
522
|
+
errors: [
|
|
523
|
+
{
|
|
524
|
+
detail: 'System not found in vulnerability service',
|
|
525
|
+
},
|
|
526
|
+
],
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
const errorMessage = patchCall.errorToast(errorWithDetail);
|
|
532
|
+
expect(errorMessage).toBe('System not found in vulnerability service');
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('includes errorToast callback that falls back to generic message', () => {
|
|
536
|
+
mockPatch.mockReturnValue({ type: 'MOCK_PATCH_ACTION' });
|
|
537
|
+
|
|
538
|
+
const hostDetails = {
|
|
539
|
+
id: 1,
|
|
540
|
+
insights_attributes: {
|
|
541
|
+
use_iop_mode: true,
|
|
542
|
+
vulnerability_opt_out: false,
|
|
543
|
+
},
|
|
544
|
+
subscription_facet_attributes: {
|
|
545
|
+
uuid: 'test-uuid',
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
const actions = getVulnerabilityAnalysisActions(hostDetails);
|
|
550
|
+
actions[0].onClick();
|
|
551
|
+
|
|
552
|
+
const patchCall = mockPatch.mock.calls[0][0];
|
|
553
|
+
|
|
554
|
+
// Test with error that doesn't have JSON:API format
|
|
555
|
+
const genericError = {
|
|
556
|
+
response: {
|
|
557
|
+
data: {},
|
|
558
|
+
},
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
const errorMessage = patchCall.errorToast(genericError);
|
|
562
|
+
expect(errorMessage).toBe(
|
|
563
|
+
'Failed to update vulnerability analysis status'
|
|
564
|
+
);
|
|
565
|
+
});
|
|
566
|
+
});
|
|
567
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import store from 'foremanReact/redux';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Updates a host's vulnerability_opt_out status in the HOSTS Redux cache
|
|
5
|
+
* @param {number} hostId - The host ID to update
|
|
6
|
+
* @param {boolean} optOutValue - The new opt_out status
|
|
7
|
+
* @param {function} dispatch - Redux dispatch function (optional, uses store.dispatch if not provided)
|
|
8
|
+
*/
|
|
9
|
+
export const updateHostVulnerabilityOptOut = (
|
|
10
|
+
hostId,
|
|
11
|
+
optOutValue,
|
|
12
|
+
dispatch = null
|
|
13
|
+
) => {
|
|
14
|
+
const dispatchFn = dispatch || store.dispatch;
|
|
15
|
+
const state = store.getState();
|
|
16
|
+
const hostsData = state.API?.HOSTS;
|
|
17
|
+
|
|
18
|
+
if (!hostsData?.response?.results || !hostId) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
dispatchFn({
|
|
23
|
+
type: 'HOSTS_SUCCESS',
|
|
24
|
+
key: 'HOSTS',
|
|
25
|
+
response: {
|
|
26
|
+
...hostsData.response,
|
|
27
|
+
results: hostsData.response.results.map(host => {
|
|
28
|
+
if (host.id === hostId) {
|
|
29
|
+
return {
|
|
30
|
+
...host,
|
|
31
|
+
insights_attributes: {
|
|
32
|
+
...host.insights_attributes,
|
|
33
|
+
vulnerability_opt_out: optOutValue,
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return host;
|
|
38
|
+
}),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
};
|