• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / gui / 1493849842

13 Oct 2024 07:39AM UTC coverage: 83.457% (-16.5%) from 99.965%
1493849842

Pull #4531

gitlab-ci

web-flow
chore: Bump send and express in /tests/e2e_tests

Bumps [send](https://github.com/pillarjs/send) and [express](https://github.com/expressjs/express). These dependencies needed to be updated together.

Updates `send` from 0.18.0 to 0.19.0
- [Release notes](https://github.com/pillarjs/send/releases)
- [Changelog](https://github.com/pillarjs/send/blob/master/HISTORY.md)
- [Commits](https://github.com/pillarjs/send/compare/0.18.0...0.19.0)

Updates `express` from 4.19.2 to 4.21.1
- [Release notes](https://github.com/expressjs/express/releases)
- [Changelog](https://github.com/expressjs/express/blob/4.21.1/History.md)
- [Commits](https://github.com/expressjs/express/compare/4.19.2...4.21.1)

---
updated-dependencies:
- dependency-name: send
  dependency-type: indirect
- dependency-name: express
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #4531: chore: Bump send and express in /tests/e2e_tests

4486 of 6422 branches covered (69.85%)

8551 of 10246 relevant lines covered (83.46%)

151.3 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.53
/src/js/actions/deploymentActions.js
1
/*eslint import/namespace: ['error', { allowComputed: true }]*/
2
import isUUID from 'validator/lib/isUUID';
3

4
import { commonErrorHandler, setSnackbar } from '../actions/appActions';
5
import GeneralApi, { apiUrl, headerNames } from '../api/general-api';
6
import { SORTING_OPTIONS, TIMEOUTS } from '../constants/appConstants';
7
import * as DeploymentConstants from '../constants/deploymentConstants';
8
import { DEVICE_LIST_DEFAULTS, RECEIVE_DEVICE } from '../constants/deviceConstants';
9
import { deepCompare, isEmpty, standardizePhases, startTimeSort } from '../helpers';
10
import {
11
  getDeploymentsByStatus as getDeploymentsByStatusSelector,
12
  getDevicesById,
13
  getGlobalSettings,
14
  getOrganization,
15
  getUserCapabilities
16
} from '../selectors';
17
import Tracking from '../tracking';
18
import { getDeviceAuth, getDeviceById, mapTermsToFilters } from './deviceActions';
19
import { saveGlobalSettings } from './userActions';
20

21
export const deploymentsApiUrl = `${apiUrl.v1}/deployments`;
184✔
22
export const deploymentsApiUrlV2 = `${apiUrl.v2}/deployments`;
184✔
23

24
const {
25
  CREATE_DEPLOYMENT,
26
  DEPLOYMENT_ROUTES,
27
  DEPLOYMENT_STATES,
28
  DEPLOYMENT_TYPES,
29
  deploymentPrototype,
30
  RECEIVE_DEPLOYMENT_DEVICE_LOG,
31
  RECEIVE_DEPLOYMENT_DEVICES,
32
  RECEIVE_DEPLOYMENT,
33
  RECEIVE_DEPLOYMENTS,
34
  REMOVE_DEPLOYMENT,
35
  SET_DEPLOYMENTS_STATE
36
} = DeploymentConstants;
184✔
37

38
// default per page until pagination and counting integrated
39
const { page: defaultPage, perPage: defaultPerPage } = DEVICE_LIST_DEFAULTS;
184✔
40

41
export const deriveDeploymentGroup = ({ filter = {}, group, groups = [], name }) => (group || (groups.length === 1 && !isUUID(name)) ? groups[0] : filter.name);
974!
42

43
const transformDeployments = (deployments, deploymentsById) =>
184✔
44
  deployments.sort(startTimeSort).reduce(
492✔
45
    (accu, item) => {
46
      const filter = item.filter ?? {};
974✔
47
      let deployment = {
974✔
48
        ...deploymentPrototype,
49
        ...deploymentsById[item.id],
50
        ...item,
51
        filter: item.filter ? { ...filter, name: filter.name ?? filter.id, filters: mapTermsToFilters(filter.terms) } : undefined,
974!
52
        name: decodeURIComponent(item.name)
53
      };
54
      // deriving the group in a second step to potentially make use of the merged data from the existing group state + the decoded name
55
      deployment = { ...deployment, group: deriveDeploymentGroup(deployment) };
974✔
56
      accu.deployments[item.id] = deployment;
974✔
57
      accu.deploymentIds.push(item.id);
974✔
58
      return accu;
974✔
59
    },
60
    { deployments: {}, deploymentIds: [] }
61
  );
62

63
/*Deployments */
64
export const getDeploymentsByStatus =
65
  (status, page = defaultPage, per_page = defaultPerPage, startDate, endDate, group, type, shouldSelect = true, sort = SORTING_OPTIONS.desc) =>
184✔
66
  (dispatch, getState) => {
482✔
67
    const created_after = startDate ? `&created_after=${startDate}` : '';
482✔
68
    const created_before = endDate ? `&created_before=${endDate}` : '';
482!
69
    const search = group ? `&search=${group}` : '';
482✔
70
    const typeFilter = type ? `&type=${type}` : '';
482✔
71
    return GeneralApi.get(
482✔
72
      `${deploymentsApiUrl}/deployments?status=${status}&per_page=${per_page}&page=${page}${created_after}${created_before}${search}${typeFilter}&sort=${sort}`
73
    ).then(res => {
74
      const { deployments, deploymentIds } = transformDeployments(res.data, getState().deployments.byId);
482✔
75
      const total = Number(res.headers[headerNames.total]);
482✔
76
      let tasks = [
482✔
77
        dispatch({ type: RECEIVE_DEPLOYMENTS, deployments }),
78
        dispatch({
79
          type: DeploymentConstants[`RECEIVE_${status.toUpperCase()}_DEPLOYMENTS`],
80
          deploymentIds,
81
          status,
82
          total: !(startDate || endDate || group || type) ? total : getState().deployments.byStatus[status].total
2,391✔
83
        })
84
      ];
85
      tasks = deploymentIds.reduce((accu, deploymentId) => {
482✔
86
        if (deployments[deploymentId].type === DEPLOYMENT_TYPES.configuration) {
964!
87
          accu.push(dispatch(getSingleDeployment(deploymentId)));
×
88
        }
89
        return accu;
964✔
90
      }, tasks);
91
      if (shouldSelect) {
482✔
92
        tasks.push(dispatch({ type: DeploymentConstants[`SELECT_${status.toUpperCase()}_DEPLOYMENTS`], deploymentIds, status, total }));
463✔
93
      }
94
      tasks.push({ deploymentIds, total });
482✔
95
      return Promise.all(tasks);
482✔
96
    });
97
  };
98

99
const isWithinFirstMonth = expirationDate => {
184✔
100
  if (!expirationDate) {
1!
101
    return false;
1✔
102
  }
103
  const endOfFirstMonth = new Date(expirationDate);
×
104
  endOfFirstMonth.setMonth(endOfFirstMonth.getMonth() - 11);
×
105
  return endOfFirstMonth > new Date();
×
106
};
107

108
const trackDeploymentCreation = (totalDeploymentCount, hasDeployments, trial_expiration) => {
184✔
109
  Tracking.event({ category: 'deployments', action: 'create' });
5✔
110
  if (!totalDeploymentCount) {
5✔
111
    if (!hasDeployments) {
1!
112
      Tracking.event({ category: 'deployments', action: 'create_initial_deployment' });
1✔
113
      if (isWithinFirstMonth(trial_expiration)) {
1!
114
        Tracking.event({ category: 'deployments', action: 'create_initial_deployment_first_month' });
×
115
      }
116
    }
117
    Tracking.event({ category: 'deployments', action: 'create_initial_deployment_user' });
1✔
118
  }
119
};
120

121
const MAX_PREVIOUS_PHASES_COUNT = 5;
184✔
122
export const createDeployment =
123
  (newDeployment, hasNewRetryDefault = false) =>
184✔
124
  (dispatch, getState) => {
5✔
125
    let request;
126
    if (newDeployment.filter_id) {
5✔
127
      request = GeneralApi.post(`${deploymentsApiUrlV2}/deployments`, newDeployment);
1✔
128
    } else if (newDeployment.group) {
4✔
129
      request = GeneralApi.post(`${deploymentsApiUrl}/deployments/group/${newDeployment.group}`, newDeployment);
1✔
130
    } else {
131
      request = GeneralApi.post(`${deploymentsApiUrl}/deployments`, newDeployment);
3✔
132
    }
133
    const totalDeploymentCount = Object.values(getDeploymentsByStatusSelector(getState())).reduce((accu, item) => accu + item.total, 0);
20✔
134
    const { hasDeployments } = getGlobalSettings(getState());
5✔
135
    const { trial_expiration } = getOrganization(getState());
5✔
136
    return request
5✔
137
      .catch(err => commonErrorHandler(err, 'Error creating deployment.', dispatch))
×
138
      .then(data => {
139
        const lastslashindex = data.headers.location.lastIndexOf('/');
5✔
140
        const deploymentId = data.headers.location.substring(lastslashindex + 1);
5✔
141
        const deployment = {
5✔
142
          ...newDeployment,
143
          devices: newDeployment.devices ? newDeployment.devices.map(id => ({ id, status: 'pending' })) : [],
1✔
144
          statistics: { status: {} }
145
        };
146
        let tasks = [
5✔
147
          dispatch({ type: CREATE_DEPLOYMENT, deployment, deploymentId }),
148
          dispatch(getSingleDeployment(deploymentId)),
149
          dispatch(setSnackbar('Deployment created successfully', TIMEOUTS.fiveSeconds))
150
        ];
151
        // track in GA
152
        trackDeploymentCreation(totalDeploymentCount, hasDeployments, trial_expiration);
5✔
153

154
        const { canManageUsers } = getUserCapabilities(getState());
5✔
155
        if (canManageUsers) {
5!
156
          const { phases, retries } = newDeployment;
5✔
157
          const { previousPhases = [], retries: previousRetries = 0 } = getGlobalSettings(getState());
5✔
158
          let newSettings = { retries: hasNewRetryDefault ? retries : previousRetries, hasDeployments: true };
5✔
159
          if (phases) {
5✔
160
            const standardPhases = standardizePhases(phases);
1✔
161
            let prevPhases = previousPhases.map(standardizePhases);
1✔
162
            if (!prevPhases.find(previousPhaseList => previousPhaseList.every(oldPhase => standardPhases.find(phase => deepCompare(phase, oldPhase))))) {
3!
163
              prevPhases.push(standardPhases);
1✔
164
            }
165
            newSettings.previousPhases = prevPhases.slice(-1 * MAX_PREVIOUS_PHASES_COUNT);
1✔
166
          }
167
          tasks.push(dispatch(saveGlobalSettings(newSettings)));
5✔
168
        }
169
        return Promise.all(tasks);
5✔
170
      });
171
  };
172

173
export const getDeploymentDevices =
174
  (id, options = {}) =>
184✔
175
  (dispatch, getState) => {
3✔
176
    const { page = defaultPage, perPage = defaultPerPage } = options;
3✔
177
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}/devices/list?deployment_id=${id}&page=${page}&per_page=${perPage}`).then(response => {
3✔
178
      const { devices: deploymentDevices = {} } = getState().deployments.byId[id] || {};
3!
179
      const devices = response.data.reduce((accu, item) => {
3✔
180
        accu[item.id] = item;
3✔
181
        const log = (deploymentDevices[item.id] || {}).log;
3!
182
        if (log) {
3!
183
          accu[item.id].log = log;
×
184
        }
185
        return accu;
3✔
186
      }, {});
187
      const selectedDeviceIds = Object.keys(devices);
3✔
188
      let tasks = [
3✔
189
        dispatch({
190
          type: RECEIVE_DEPLOYMENT_DEVICES,
191
          deploymentId: id,
192
          devices,
193
          selectedDeviceIds,
194
          totalDeviceCount: Number(response.headers[headerNames.total])
195
        })
196
      ];
197
      const devicesById = getDevicesById(getState());
3✔
198
      // only update those that have changed & lack data
199
      const lackingData = selectedDeviceIds.reduce((accu, deviceId) => {
3✔
200
        const device = devicesById[deviceId];
3✔
201
        if (!device || !device.identity_data || !device.attributes || Object.keys(device.attributes).length === 0) {
3!
202
          accu.push(deviceId);
×
203
        }
204
        return accu;
3✔
205
      }, []);
206
      // get device artifact, inventory and identity details not listed in schedule data
207
      tasks = lackingData.reduce((accu, deviceId) => [...accu, dispatch(getDeviceById(deviceId)), dispatch(getDeviceAuth(deviceId))], tasks);
3✔
208
      return Promise.all(tasks);
3✔
209
    });
210
  };
211

212
const parseDeviceDeployment = ({
184✔
213
  deployment: { id, artifact_name: release, status: deploymentStatus },
214
  device: { created, deleted, id: deviceId, finished, status, log }
215
}) => ({
4✔
216
  id,
217
  release,
218
  created,
219
  deleted,
220
  deviceId,
221
  finished,
222
  status,
223
  log,
224
  route: Object.values(DEPLOYMENT_ROUTES).reduce((accu, { key, states }) => {
225
    if (!accu) {
12✔
226
      return states.includes(deploymentStatus) ? key : accu;
4!
227
    }
228
    return accu;
8✔
229
  }, '')
230
});
231

232
export const getDeviceDeployments =
233
  (deviceId, options = {}) =>
184✔
234
  (dispatch, getState) => {
4✔
235
    const { filterSelection = [], page = defaultPage, perPage = defaultPerPage } = options;
4✔
236
    const filters = filterSelection.map(item => `&status=${item}`).join('');
4✔
237
    return GeneralApi.get(`${deploymentsApiUrl}/deployments/devices/${deviceId}?page=${page}&per_page=${perPage}${filters}`)
4✔
238
      .then(({ data, headers }) =>
239
        Promise.resolve(
4✔
240
          dispatch({
241
            type: RECEIVE_DEVICE,
242
            device: {
243
              ...getState().devices.byId[deviceId],
244
              deviceDeployments: data.map(parseDeviceDeployment),
245
              deploymentsCount: Number(headers[headerNames.total])
246
            }
247
          })
248
        )
249
      )
250
      .catch(err => commonErrorHandler(err, 'There was an error retrieving the device deployment history:', dispatch));
×
251
  };
252

253
export const resetDeviceDeployments = deviceId => dispatch =>
184✔
254
  GeneralApi.delete(`${deploymentsApiUrl}/deployments/devices/${deviceId}/history`)
1✔
255
    .then(() => Promise.resolve(dispatch(getDeviceDeployments(deviceId))))
1✔
256
    .catch(err => commonErrorHandler(err, 'There was an error resetting the device deployment history:', dispatch));
×
257

258
export const getSingleDeployment = id => (dispatch, getState) =>
184✔
259
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${id}`).then(({ data }) => {
10✔
260
    const { deployments } = transformDeployments([data], getState().deployments.byId);
10✔
261
    return Promise.resolve(dispatch({ type: RECEIVE_DEPLOYMENT, deployment: deployments[id] }));
10✔
262
  });
263

264
export const getDeviceLog = (deploymentId, deviceId) => (dispatch, getState) =>
184✔
265
  GeneralApi.get(`${deploymentsApiUrl}/deployments/${deploymentId}/devices/${deviceId}/log`)
1✔
266
    .catch(e => {
267
      console.log('no log here', e);
×
268
      return Promise.reject();
×
269
    })
270
    .then(({ data: log }) => {
271
      const stateDeployment = getState().deployments.byId[deploymentId];
1✔
272
      const deployment = {
1✔
273
        ...stateDeployment,
274
        devices: {
275
          ...stateDeployment.devices,
276
          [deviceId]: {
277
            ...stateDeployment.devices[deviceId],
278
            log
279
          }
280
        }
281
      };
282
      return Promise.all([
1✔
283
        Promise.resolve(
284
          dispatch({
285
            type: RECEIVE_DEPLOYMENT_DEVICE_LOG,
286
            deployment
287
          })
288
        ),
289
        Promise.resolve(log)
290
      ]);
291
    });
292

293
export const abortDeployment = deploymentId => (dispatch, getState) =>
184✔
294
  GeneralApi.put(`${deploymentsApiUrl}/deployments/${deploymentId}/status`, { status: 'aborted' })
2✔
295
    .then(() => {
296
      const state = getState();
1✔
297
      let status = DEPLOYMENT_STATES.pending;
1✔
298
      let index = state.deployments.byStatus.pending.deploymentIds.findIndex(id => id === deploymentId);
1✔
299
      if (index < 0) {
1!
300
        status = DEPLOYMENT_STATES.inprogress;
1✔
301
        index = state.deployments.byStatus.inprogress.deploymentIds.findIndex(id => id === deploymentId);
1✔
302
      }
303
      const deploymentIds = [
1✔
304
        ...state.deployments.byStatus[status].deploymentIds.slice(0, index),
305
        ...state.deployments.byStatus[status].deploymentIds.slice(index + 1)
306
      ];
307
      const deployments = deploymentIds.reduce((accu, id) => {
1✔
308
        accu[id] = state.deployments.byId[id];
×
309
        return accu;
×
310
      }, {});
311
      const total = Math.max(state.deployments.byStatus[status].total - 1, 0);
1✔
312
      return Promise.all([
1✔
313
        dispatch({ type: RECEIVE_DEPLOYMENTS, deployments }),
314
        dispatch({ type: DeploymentConstants[`RECEIVE_${status.toUpperCase()}_DEPLOYMENTS`], deploymentIds, status, total }),
315
        dispatch({
316
          type: REMOVE_DEPLOYMENT,
317
          deploymentId
318
        }),
319
        dispatch(setSnackbar('The deployment was successfully aborted'))
320
      ]);
321
    })
322
    .catch(err => commonErrorHandler(err, 'There was an error while aborting the deployment:', dispatch));
1✔
323

324
export const updateDeploymentControlMap = (deploymentId, update_control_map) => dispatch =>
184✔
325
  GeneralApi.patch(`${deploymentsApiUrl}/deployments/${deploymentId}`, { update_control_map })
1✔
326
    .catch(err => commonErrorHandler(err, 'There was an error while updating the deployment status:', dispatch))
×
327
    .then(() => Promise.resolve(dispatch(getSingleDeployment(deploymentId))));
1✔
328

329
export const setDeploymentsState = selection => (dispatch, getState) => {
184✔
330
  // eslint-disable-next-line no-unused-vars
331
  const { page, perPage, ...selectionState } = selection;
27✔
332
  const currentState = getState().deployments.selectionState;
27✔
333
  let nextState = {
27✔
334
    ...currentState,
335
    ...selectionState,
336
    ...Object.keys(DEPLOYMENT_STATES).reduce((accu, item) => {
337
      accu[item] = {
108✔
338
        ...currentState[item],
339
        ...selectionState[item]
340
      };
341
      return accu;
108✔
342
    }, {}),
343
    general: {
344
      ...currentState.general,
345
      ...selectionState.general
346
    }
347
  };
348
  let tasks = [dispatch({ type: SET_DEPLOYMENTS_STATE, state: nextState })];
27✔
349
  if (nextState.selectedId && currentState.selectedId !== nextState.selectedId) {
27✔
350
    tasks.push(dispatch(getSingleDeployment(nextState.selectedId)));
2✔
351
  }
352
  return Promise.all(tasks);
27✔
353
};
354

355
const deltaAttributeMappings = [
184✔
356
  { here: 'compressionLevel', there: 'compression_level' },
357
  { here: 'disableChecksum', there: 'disable_checksum' },
358
  { here: 'disableDecompression', there: 'disable_external_decompression' },
359
  { here: 'sourceWindow', there: 'source_window_size' },
360
  { here: 'inputWindow', there: 'input_window_size' },
361
  { here: 'duplicatesWindow', there: 'compression_duplicates_window' },
362
  { here: 'instructionBuffer', there: 'instruction_buffer_size' }
363
];
364

365
const mapExternalDeltaConfig = (config = {}) =>
184!
366
  deltaAttributeMappings.reduce((accu, { here, there }) => {
12✔
367
    if (config[there] !== undefined) {
84✔
368
      accu[here] = config[there];
66✔
369
    }
370
    return accu;
84✔
371
  }, {});
372

373
export const getDeploymentsConfig = () => (dispatch, getState) =>
184✔
374
  GeneralApi.get(`${deploymentsApiUrl}/config`).then(({ data }) => {
6✔
375
    const oldConfig = getState().deployments.config;
6✔
376
    const { delta = {} } = data;
6!
377
    const { binary_delta = {}, binary_delta_limits = {} } = delta;
6!
378
    const { xdelta_args = {}, timeout: timeoutConfig = oldConfig.binaryDelta.timeout } = binary_delta;
6!
379
    const { xdelta_args_limits = {}, timeout: timeoutLimit = oldConfig.binaryDeltaLimits.timeout } = binary_delta_limits;
6!
380
    const config = {
6✔
381
      ...oldConfig,
382
      hasDelta: Boolean(delta.enabled),
383
      binaryDelta: {
384
        ...oldConfig.binaryDelta,
385
        timeout: timeoutConfig,
386
        ...mapExternalDeltaConfig(xdelta_args)
387
      },
388
      binaryDeltaLimits: {
389
        ...oldConfig.binaryDeltaLimits,
390
        timeout: timeoutLimit,
391
        ...mapExternalDeltaConfig(xdelta_args_limits)
392
      }
393
    };
394
    return Promise.resolve(dispatch({ type: DeploymentConstants.SET_DEPLOYMENTS_CONFIG, config }));
6✔
395
  });
396

397
// traverse a source object and remove undefined & empty object properties to only return an attribute if there really is content worth sending
398
const deepClean = source =>
184✔
399
  Object.entries(source).reduce((accu, [key, value]) => {
2✔
400
    if (value !== undefined) {
9!
401
      let cleanedValue = typeof value === 'object' ? deepClean(value) : value;
9✔
402
      if (cleanedValue === undefined || (typeof cleanedValue === 'object' && isEmpty(cleanedValue))) {
9!
403
        return accu;
×
404
      }
405
      accu = { ...(accu ?? {}), [key]: cleanedValue };
9✔
406
    }
407
    return accu;
9✔
408
  }, undefined);
409

410
export const saveDeltaDeploymentsConfig = config => (dispatch, getState) => {
184✔
411
  const configChange = {
1✔
412
    timeout: config.timeout,
413
    xdelta_args: deltaAttributeMappings.reduce((accu, { here, there }) => {
414
      if (config[here] !== undefined) {
7!
415
        accu[there] = config[here];
7✔
416
      }
417
      return accu;
7✔
418
    }, {})
419
  };
420
  const result = deepClean(configChange);
1✔
421
  if (!result) {
1!
422
    return Promise.resolve();
×
423
  }
424
  return GeneralApi.put(`${deploymentsApiUrl}/config/binary_delta`, result)
1✔
425
    .catch(err => commonErrorHandler(err, 'There was a problem storing your delta artifact generation configuration.', dispatch))
×
426
    .then(() => {
427
      const oldConfig = getState().deployments.config;
1✔
428
      const newConfig = {
1✔
429
        ...oldConfig,
430
        binaryDelta: {
431
          ...oldConfig.binaryDelta,
432
          ...config
433
        }
434
      };
435
      return Promise.all([
1✔
436
        dispatch({ type: DeploymentConstants.SET_DEPLOYMENTS_CONFIG, config: newConfig }),
437
        dispatch(setSnackbar('Settings saved successfully'))
438
      ]);
439
    });
440
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc