foreman_remote_execution 16.7.0 → 17.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/app/controllers/api/v2/job_invocations_controller.rb +24 -2
- data/app/controllers/job_invocations_controller.rb +1 -28
- data/app/views/api/v2/job_invocations/hosts.json.rabl +8 -0
- data/app/views/api/v2/job_invocations/main.json.rabl +1 -0
- data/app/views/job_templates/_alerts.html.erb +4 -0
- data/config/routes.rb +0 -1
- data/lib/foreman_remote_execution/plugin.rb +3 -3
- data/lib/foreman_remote_execution/version.rb +1 -1
- data/test/functional/api/v2/job_invocations_controller_test.rb +46 -7
- data/webpack/JobInvocationDetail/JobInvocationActions.js +86 -91
- data/webpack/JobInvocationDetail/JobInvocationConstants.js +6 -4
- data/webpack/JobInvocationDetail/JobInvocationDetail.scss +5 -3
- data/webpack/JobInvocationDetail/JobInvocationHostTable.js +92 -46
- data/webpack/JobInvocationDetail/JobInvocationSelectors.js +1 -9
- data/webpack/JobInvocationDetail/JobInvocationSystemStatusChart.js +5 -2
- data/webpack/JobInvocationDetail/JobInvocationToolbarButtons.js +10 -12
- data/webpack/JobInvocationDetail/TemplateInvocation.js +38 -26
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/OutputCodeBlock.js +2 -1
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/TemplateActionButtons.js +17 -6
- data/webpack/JobInvocationDetail/TemplateInvocationComponents/index.scss +0 -1
- data/webpack/JobInvocationDetail/__tests__/JobInvocationHostTablePolling.test.js +369 -0
- data/webpack/JobInvocationDetail/__tests__/JobInvocationPolling.test.js +228 -0
- data/webpack/JobInvocationDetail/__tests__/MainInformation.test.js +205 -154
- data/webpack/JobInvocationDetail/__tests__/TemplateInvocationPolling.test.js +254 -0
- data/webpack/JobInvocationDetail/__tests__/fixtures.js +2 -0
- data/webpack/JobInvocationDetail/index.js +25 -25
- data/webpack/JobWizard/JobWizard.js +5 -1
- data/webpack/JobWizard/JobWizard.scss +21 -0
- data/webpack/JobWizard/JobWizardConstants.js +10 -1
- data/webpack/JobWizard/steps/Schedule/RepeatHour.js +4 -2
- data/webpack/JobWizard/steps/Schedule/RepeatWeek.js +4 -2
- data/webpack/JobWizard/steps/Schedule/ScheduleRecurring.js +2 -1
- data/webpack/JobWizard/steps/Schedule/ScheduleType.js +4 -1
- data/webpack/JobWizard/steps/form/DateTimePicker.js +12 -6
- data/webpack/JobWizard/steps/form/FormHelpers.js +5 -4
- data/webpack/react_app/components/FeaturesDropdown/index.scss +2 -0
- data/webpack/react_app/components/TargetingHosts/TargetingHostsLabelsRow.scss +6 -2
- data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +1 -1
- data/webpack/react_app/components/TargetingHosts/index.js +2 -1
- data/webpack/react_app/redux/actions/jobInvocations/index.js +3 -2
- data/webpack/test_setup.js +1 -13
- metadata +7 -4
- data/webpack/__mocks__/foremanReact/constants.js +0 -25
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, act } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { Provider } from 'react-redux';
|
|
5
|
+
import configureMockStore from 'redux-mock-store';
|
|
6
|
+
import thunk from 'redux-thunk';
|
|
7
|
+
import { Router } from 'react-router-dom';
|
|
8
|
+
import { createMemoryHistory } from 'history';
|
|
9
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
10
|
+
import { createForemanContextWrapper } from './foremanTestHelpers';
|
|
11
|
+
import JobInvocationHostTable from '../JobInvocationHostTable';
|
|
12
|
+
import { JOB_INVOCATION_HOSTS } from '../JobInvocationConstants';
|
|
13
|
+
|
|
14
|
+
jest.useFakeTimers();
|
|
15
|
+
|
|
16
|
+
jest.mock('foremanReact/redux/API/APISelectors', () =>
|
|
17
|
+
jest.requireActual('foremanReact/redux/API/APISelectors')
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
jest.mock('foremanReact/common/hooks/Permissions/permissionHooks', () => ({
|
|
21
|
+
usePermissions: jest.fn(() => true),
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.mock('../TemplateInvocation', () => ({
|
|
25
|
+
TemplateInvocation: () => <div data-testid="template-invocation" />,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock('../TemplateInvocationComponents/TemplateActionButtons', () => ({
|
|
29
|
+
RowActions: () => <div data-testid="row-actions" />,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
const mockStore = configureMockStore([thunk]);
|
|
33
|
+
|
|
34
|
+
const hostsResponse = {
|
|
35
|
+
total: 1,
|
|
36
|
+
subtotal: 1,
|
|
37
|
+
page: 1,
|
|
38
|
+
per_page: 20,
|
|
39
|
+
results: [
|
|
40
|
+
{
|
|
41
|
+
id: 1,
|
|
42
|
+
name: 'host1.example.com',
|
|
43
|
+
operatingsystem_id: 1,
|
|
44
|
+
operatingsystem_name: 'RHEL 9',
|
|
45
|
+
hostgroup_id: 1,
|
|
46
|
+
hostgroup_name: 'default',
|
|
47
|
+
job_status: 'success',
|
|
48
|
+
smart_proxy_id: 1,
|
|
49
|
+
smart_proxy_name: 'proxy1',
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
let apiGetSpy;
|
|
55
|
+
let hostsCalls;
|
|
56
|
+
let pendingCallbacks;
|
|
57
|
+
|
|
58
|
+
const createStore = () =>
|
|
59
|
+
mockStore({
|
|
60
|
+
API: {},
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const flushPendingCallbacks = () => {
|
|
64
|
+
const batch = [...pendingCallbacks];
|
|
65
|
+
pendingCallbacks = [];
|
|
66
|
+
batch.forEach(cb => cb());
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const renderTable = (props = {}) => {
|
|
70
|
+
const store = createStore();
|
|
71
|
+
const history = createMemoryHistory();
|
|
72
|
+
const Wrapper = createForemanContextWrapper();
|
|
73
|
+
|
|
74
|
+
const defaultProps = {
|
|
75
|
+
id: '42',
|
|
76
|
+
targeting: { targeting_type: 'static_query', search_query: '' },
|
|
77
|
+
initialFilter: 'all_statuses',
|
|
78
|
+
jobFinished: false,
|
|
79
|
+
onFilterUpdate: jest.fn(),
|
|
80
|
+
...props,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const result = render(
|
|
84
|
+
<Provider store={store}>
|
|
85
|
+
<Router history={history}>
|
|
86
|
+
<Wrapper>
|
|
87
|
+
<JobInvocationHostTable {...defaultProps} />
|
|
88
|
+
</Wrapper>
|
|
89
|
+
</Router>
|
|
90
|
+
</Provider>
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return { ...result, store, history };
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const setupSuccessMock = () => {
|
|
97
|
+
apiGetSpy = jest
|
|
98
|
+
.spyOn(APIActions, 'get')
|
|
99
|
+
.mockImplementation(opts => dispatch => {
|
|
100
|
+
if (opts.key === JOB_INVOCATION_HOSTS) {
|
|
101
|
+
hostsCalls.push(opts);
|
|
102
|
+
if (opts.handleSuccess) {
|
|
103
|
+
pendingCallbacks.push(() =>
|
|
104
|
+
opts.handleSuccess({ data: hostsResponse })
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const setupErrorMock = () => {
|
|
112
|
+
apiGetSpy = jest
|
|
113
|
+
.spyOn(APIActions, 'get')
|
|
114
|
+
.mockImplementation(opts => dispatch => {
|
|
115
|
+
if (opts.key === JOB_INVOCATION_HOSTS) {
|
|
116
|
+
hostsCalls.push(opts);
|
|
117
|
+
if (opts.handleError) {
|
|
118
|
+
pendingCallbacks.push(() => opts.handleError());
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
describe('JobInvocationHostTable polling', () => {
|
|
125
|
+
beforeEach(() => {
|
|
126
|
+
hostsCalls = [];
|
|
127
|
+
pendingCallbacks = [];
|
|
128
|
+
setupSuccessMock();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
afterEach(() => {
|
|
132
|
+
apiGetSpy.mockRestore();
|
|
133
|
+
jest.clearAllTimers();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('schedules a poll after the initial fetch succeeds', () => {
|
|
137
|
+
renderTable();
|
|
138
|
+
|
|
139
|
+
expect(hostsCalls).toHaveLength(1);
|
|
140
|
+
|
|
141
|
+
act(() => {
|
|
142
|
+
flushPendingCallbacks();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
expect(hostsCalls).toHaveLength(1);
|
|
146
|
+
|
|
147
|
+
act(() => {
|
|
148
|
+
jest.advanceTimersByTime(5000);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
act(() => {
|
|
152
|
+
flushPendingCallbacks();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(hostsCalls).toHaveLength(2);
|
|
156
|
+
|
|
157
|
+
act(() => {
|
|
158
|
+
jest.advanceTimersByTime(5000);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
act(() => {
|
|
162
|
+
flushPendingCallbacks();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
expect(hostsCalls).toHaveLength(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('does not schedule a poll when jobFinished is true', () => {
|
|
169
|
+
renderTable({ jobFinished: true });
|
|
170
|
+
|
|
171
|
+
expect(hostsCalls).toHaveLength(1);
|
|
172
|
+
|
|
173
|
+
act(() => {
|
|
174
|
+
flushPendingCallbacks();
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
act(() => {
|
|
178
|
+
jest.advanceTimersByTime(10000);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
act(() => {
|
|
182
|
+
flushPendingCallbacks();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(hostsCalls).toHaveLength(1);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('stops polling when jobFinished transitions to true', () => {
|
|
189
|
+
const { rerender } = renderTable();
|
|
190
|
+
|
|
191
|
+
act(() => {
|
|
192
|
+
flushPendingCallbacks();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(hostsCalls).toHaveLength(1);
|
|
196
|
+
|
|
197
|
+
const store = createStore();
|
|
198
|
+
const history = createMemoryHistory();
|
|
199
|
+
const Wrapper = createForemanContextWrapper();
|
|
200
|
+
|
|
201
|
+
act(() => {
|
|
202
|
+
rerender(
|
|
203
|
+
<Provider store={store}>
|
|
204
|
+
<Router history={history}>
|
|
205
|
+
<Wrapper>
|
|
206
|
+
<JobInvocationHostTable
|
|
207
|
+
id="42"
|
|
208
|
+
targeting={{
|
|
209
|
+
targeting_type: 'static_query',
|
|
210
|
+
search_query: '',
|
|
211
|
+
}}
|
|
212
|
+
initialFilter="all_statuses"
|
|
213
|
+
jobFinished
|
|
214
|
+
onFilterUpdate={jest.fn()}
|
|
215
|
+
/>
|
|
216
|
+
</Wrapper>
|
|
217
|
+
</Router>
|
|
218
|
+
</Provider>
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
act(() => {
|
|
223
|
+
flushPendingCallbacks();
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const callsAtTransition = hostsCalls.length;
|
|
227
|
+
|
|
228
|
+
act(() => {
|
|
229
|
+
jest.advanceTimersByTime(10000);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
act(() => {
|
|
233
|
+
flushPendingCallbacks();
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
expect(hostsCalls).toHaveLength(callsAtTransition);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('cleans up the poll timer on unmount', () => {
|
|
240
|
+
const { unmount } = renderTable();
|
|
241
|
+
|
|
242
|
+
act(() => {
|
|
243
|
+
flushPendingCallbacks();
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
const callsBeforeUnmount = hostsCalls.length;
|
|
247
|
+
|
|
248
|
+
unmount();
|
|
249
|
+
|
|
250
|
+
act(() => {
|
|
251
|
+
jest.advanceTimersByTime(10000);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
act(() => {
|
|
255
|
+
flushPendingCallbacks();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
expect(hostsCalls).toHaveLength(callsBeforeUnmount);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('restarts polling when filter changes', () => {
|
|
262
|
+
const { rerender } = renderTable({ initialFilter: 'all_statuses' });
|
|
263
|
+
|
|
264
|
+
act(() => {
|
|
265
|
+
flushPendingCallbacks();
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
const callsBeforeFilterChange = hostsCalls.length;
|
|
269
|
+
|
|
270
|
+
const store = createStore();
|
|
271
|
+
const history = createMemoryHistory();
|
|
272
|
+
const Wrapper = createForemanContextWrapper();
|
|
273
|
+
|
|
274
|
+
act(() => {
|
|
275
|
+
rerender(
|
|
276
|
+
<Provider store={store}>
|
|
277
|
+
<Router history={history}>
|
|
278
|
+
<Wrapper>
|
|
279
|
+
<JobInvocationHostTable
|
|
280
|
+
id="42"
|
|
281
|
+
targeting={{
|
|
282
|
+
targeting_type: 'static_query',
|
|
283
|
+
search_query: '',
|
|
284
|
+
}}
|
|
285
|
+
initialFilter="success"
|
|
286
|
+
jobFinished={false}
|
|
287
|
+
onFilterUpdate={jest.fn()}
|
|
288
|
+
/>
|
|
289
|
+
</Wrapper>
|
|
290
|
+
</Router>
|
|
291
|
+
</Provider>
|
|
292
|
+
);
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
expect(hostsCalls.length).toBeGreaterThan(callsBeforeFilterChange);
|
|
296
|
+
|
|
297
|
+
act(() => {
|
|
298
|
+
flushPendingCallbacks();
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
const callsAfterFilterChange = hostsCalls.length;
|
|
302
|
+
|
|
303
|
+
act(() => {
|
|
304
|
+
jest.advanceTimersByTime(5000);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
act(() => {
|
|
308
|
+
flushPendingCallbacks();
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
expect(hostsCalls.length).toBeGreaterThan(callsAfterFilterChange);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('sends include_permissions only on the first request', () => {
|
|
315
|
+
renderTable();
|
|
316
|
+
|
|
317
|
+
expect(hostsCalls).toHaveLength(1);
|
|
318
|
+
expect(hostsCalls[0].params.include_permissions).toBe(true);
|
|
319
|
+
|
|
320
|
+
act(() => {
|
|
321
|
+
flushPendingCallbacks();
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
act(() => {
|
|
325
|
+
jest.advanceTimersByTime(5000);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
act(() => {
|
|
329
|
+
flushPendingCallbacks();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
expect(hostsCalls).toHaveLength(2);
|
|
333
|
+
expect(hostsCalls[1].params.include_permissions).toBeUndefined();
|
|
334
|
+
|
|
335
|
+
act(() => {
|
|
336
|
+
jest.advanceTimersByTime(5000);
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
act(() => {
|
|
340
|
+
flushPendingCallbacks();
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
expect(hostsCalls).toHaveLength(3);
|
|
344
|
+
expect(hostsCalls[2].params.include_permissions).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('stops polling on API error', () => {
|
|
348
|
+
apiGetSpy.mockRestore();
|
|
349
|
+
setupErrorMock();
|
|
350
|
+
|
|
351
|
+
renderTable();
|
|
352
|
+
|
|
353
|
+
expect(hostsCalls).toHaveLength(1);
|
|
354
|
+
|
|
355
|
+
act(() => {
|
|
356
|
+
flushPendingCallbacks();
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
act(() => {
|
|
360
|
+
jest.advanceTimersByTime(10000);
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
act(() => {
|
|
364
|
+
flushPendingCallbacks();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
expect(hostsCalls).toHaveLength(1);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { createStore, applyMiddleware } from 'redux';
|
|
2
|
+
import thunk from 'redux-thunk';
|
|
3
|
+
import { APIActions } from 'foremanReact/redux/API';
|
|
4
|
+
import {
|
|
5
|
+
getJobInvocation,
|
|
6
|
+
stopJobInvocationPolling,
|
|
7
|
+
} from '../JobInvocationActions';
|
|
8
|
+
import {
|
|
9
|
+
JOB_INVOCATION_KEY,
|
|
10
|
+
AUTO_REFRESH_INTERVAL_MS,
|
|
11
|
+
} from '../JobInvocationConstants';
|
|
12
|
+
|
|
13
|
+
jest.useFakeTimers();
|
|
14
|
+
|
|
15
|
+
const reducer = (state = {}) => state;
|
|
16
|
+
const makeStore = () => createStore(reducer, applyMiddleware(thunk));
|
|
17
|
+
const makeRef = () => ({ current: { timeoutId: null, cancel: () => {} } });
|
|
18
|
+
|
|
19
|
+
const runningData = { status_label: 'running' };
|
|
20
|
+
const succeededData = { status_label: 'succeeded' };
|
|
21
|
+
const failedData = { status_label: 'failed' };
|
|
22
|
+
const cancelledData = { status_label: 'cancelled' };
|
|
23
|
+
|
|
24
|
+
let apiGetSpy;
|
|
25
|
+
|
|
26
|
+
const setupGetMock = responseData => {
|
|
27
|
+
apiGetSpy = jest
|
|
28
|
+
.spyOn(APIActions, 'get')
|
|
29
|
+
.mockImplementation(({ handleSuccess, ...action }) => dispatch => {
|
|
30
|
+
handleSuccess && handleSuccess({ data: responseData });
|
|
31
|
+
return dispatch({ type: 'MOCK_GET', ...action });
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const url = '/api/job_invocations/1';
|
|
36
|
+
|
|
37
|
+
describe('job invocation polling', () => {
|
|
38
|
+
let ref;
|
|
39
|
+
|
|
40
|
+
beforeEach(() => {
|
|
41
|
+
ref = makeRef();
|
|
42
|
+
jest.clearAllTimers();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
afterEach(() => {
|
|
46
|
+
if (apiGetSpy) {
|
|
47
|
+
apiGetSpy.mockRestore();
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('sends include_permissions and include_hosts on the initial fetch', () => {
|
|
52
|
+
setupGetMock(succeededData);
|
|
53
|
+
const store = makeStore();
|
|
54
|
+
|
|
55
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
56
|
+
|
|
57
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
58
|
+
expect(apiGetSpy.mock.calls[0][0]).toMatchObject({
|
|
59
|
+
key: JOB_INVOCATION_KEY,
|
|
60
|
+
url,
|
|
61
|
+
params: { include_hosts: false, include_permissions: true },
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('schedules the next poll when job is still running', () => {
|
|
66
|
+
setupGetMock(runningData);
|
|
67
|
+
const store = makeStore();
|
|
68
|
+
|
|
69
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
70
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
71
|
+
|
|
72
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
73
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not include include_permissions on subsequent poll calls', () => {
|
|
77
|
+
setupGetMock(runningData);
|
|
78
|
+
const store = makeStore();
|
|
79
|
+
|
|
80
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
81
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
82
|
+
|
|
83
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
84
|
+
expect(apiGetSpy.mock.calls[1][0].params).toMatchObject({
|
|
85
|
+
include_hosts: false,
|
|
86
|
+
});
|
|
87
|
+
expect(apiGetSpy.mock.calls[1][0].params).not.toHaveProperty(
|
|
88
|
+
'include_permissions'
|
|
89
|
+
);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('stops polling when job succeeds', () => {
|
|
93
|
+
setupGetMock(succeededData);
|
|
94
|
+
const store = makeStore();
|
|
95
|
+
|
|
96
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
97
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
98
|
+
|
|
99
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
100
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('stops polling when job fails', () => {
|
|
104
|
+
setupGetMock(failedData);
|
|
105
|
+
const store = makeStore();
|
|
106
|
+
|
|
107
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
108
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
109
|
+
|
|
110
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('stops polling when job is cancelled', () => {
|
|
114
|
+
setupGetMock(cancelledData);
|
|
115
|
+
const store = makeStore();
|
|
116
|
+
|
|
117
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
118
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
119
|
+
|
|
120
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('stops polling on fetch error', () => {
|
|
124
|
+
apiGetSpy = jest
|
|
125
|
+
.spyOn(APIActions, 'get')
|
|
126
|
+
.mockImplementation(({ handleError, ...action }) => dispatch => {
|
|
127
|
+
handleError && handleError();
|
|
128
|
+
return dispatch({ type: 'MOCK_GET', ...action });
|
|
129
|
+
});
|
|
130
|
+
const store = makeStore();
|
|
131
|
+
|
|
132
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
133
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
134
|
+
|
|
135
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('stopJobInvocationPolling cancels a pending timeout', () => {
|
|
139
|
+
setupGetMock(runningData);
|
|
140
|
+
const store = makeStore();
|
|
141
|
+
|
|
142
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
143
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
144
|
+
|
|
145
|
+
stopJobInvocationPolling(ref);
|
|
146
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
147
|
+
|
|
148
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('cancels pending timeout before starting a new poll when called multiple times', () => {
|
|
152
|
+
setupGetMock(runningData);
|
|
153
|
+
const store = makeStore();
|
|
154
|
+
|
|
155
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
156
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
157
|
+
|
|
158
|
+
// Second call before the timeout fires cancels the first timeout and starts fresh
|
|
159
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
160
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
161
|
+
|
|
162
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS);
|
|
163
|
+
|
|
164
|
+
// Only the second poll chain fires (first was cancelled)
|
|
165
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(3);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('keeps polling as long as the job is running', () => {
|
|
169
|
+
setupGetMock(runningData);
|
|
170
|
+
const store = makeStore();
|
|
171
|
+
|
|
172
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
173
|
+
jest.advanceTimersByTime(AUTO_REFRESH_INTERVAL_MS * 3);
|
|
174
|
+
|
|
175
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(4);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it('cancels in-flight callback when new poll starts before previous resolves', () => {
|
|
179
|
+
let firstCallHandleSuccess;
|
|
180
|
+
let callCount = 0;
|
|
181
|
+
apiGetSpy = jest
|
|
182
|
+
.spyOn(APIActions, 'get')
|
|
183
|
+
.mockImplementation(({ handleSuccess, handleError }) => dispatch => {
|
|
184
|
+
callCount++;
|
|
185
|
+
if (callCount === 1) {
|
|
186
|
+
firstCallHandleSuccess = handleSuccess;
|
|
187
|
+
}
|
|
188
|
+
return dispatch({ type: 'MOCK_GET' });
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
const store = makeStore();
|
|
192
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
193
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
194
|
+
|
|
195
|
+
// Start a new poll cycle before the first resolves
|
|
196
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
197
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(2);
|
|
198
|
+
|
|
199
|
+
// Old callback from first call tries to schedule next poll
|
|
200
|
+
firstCallHandleSuccess({ data: runningData });
|
|
201
|
+
|
|
202
|
+
// Timeout should not be set by the stale callback
|
|
203
|
+
expect(ref.current.timeoutId).toBeNull();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('ignores a stale response resolving after stopJobInvocationPolling (e.g. unmount)', () => {
|
|
207
|
+
let capturedHandleSuccess;
|
|
208
|
+
apiGetSpy = jest
|
|
209
|
+
.spyOn(APIActions, 'get')
|
|
210
|
+
.mockImplementation(({ handleSuccess }) => dispatch => {
|
|
211
|
+
capturedHandleSuccess = handleSuccess;
|
|
212
|
+
return dispatch({ type: 'MOCK_GET' });
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const store = makeStore();
|
|
216
|
+
store.dispatch(getJobInvocation(url, ref));
|
|
217
|
+
expect(apiGetSpy).toHaveBeenCalledTimes(1);
|
|
218
|
+
|
|
219
|
+
// Simulate unmount: stop polling while the request is still in flight.
|
|
220
|
+
stopJobInvocationPolling(ref);
|
|
221
|
+
|
|
222
|
+
// The in-flight request resolves after the "unmount".
|
|
223
|
+
capturedHandleSuccess({ data: runningData });
|
|
224
|
+
|
|
225
|
+
// The stale response must not revive polling.
|
|
226
|
+
expect(ref.current.timeoutId).toBeNull();
|
|
227
|
+
});
|
|
228
|
+
});
|