Skip to content

Commit

Permalink
Add Redux Sagas for getting SUSE Manager software updates (#2416)
Browse files Browse the repository at this point in the history
  • Loading branch information
jamie-suse authored Mar 19, 2024
1 parent 0a5ac04 commit 2d08e21
Show file tree
Hide file tree
Showing 9 changed files with 518 additions and 0 deletions.
4 changes: 4 additions & 0 deletions assets/js/lib/api/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { networkClient } from '@lib/network';

export const getSoftwareUpdates = (hostId) =>
networkClient.get(`/api/v1/hosts/${hostId}/software_updates`);
2 changes: 2 additions & 0 deletions assets/js/state/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import catalogReducer from './catalog';
import lastExecutionsReducer from './lastExecutions';
import settingsReducer from './settings';
import userReducer from './user';
import softwareUpdatesReducer from './softwareUpdates';
import softwareUpdatesSettingsReducer from './softwareUpdatesSettings';
import rootSaga from './sagas';

Expand All @@ -30,6 +31,7 @@ export const store = configureStore({
lastExecutions: lastExecutionsReducer,
settings: settingsReducer,
user: userReducer,
softwareUpdates: softwareUpdatesReducer,
softwareUpdatesSettings: softwareUpdatesSettingsReducer,
},
middleware: [sagaMiddleware],
Expand Down
2 changes: 2 additions & 0 deletions assets/js/state/sagas/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ import { watchSapSystemEvents } from '@state/sagas/sapSystems';
import { watchPerformLogin } from '@state/sagas/user';
import { watchChecksSelectionEvents } from '@state/sagas/checksSelection';
import { watchSoftwareUpdateSettings } from '@state/sagas/softwareUpdatesSettings';
import { watchSoftwareUpdates } from '@state/sagas/softwareUpdates';

import { initSocketConnection } from '@lib/network/socket';
import processChannelEvents from '@state/channels';
Expand Down Expand Up @@ -248,5 +249,6 @@ export default function* rootSaga() {
watchSapSystemEvents(),
watchUserLoggedIn(),
watchSoftwareUpdateSettings(),
watchSoftwareUpdates(),
]);
}
28 changes: 28 additions & 0 deletions assets/js/state/sagas/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { get } from 'lodash';
import { put, call, takeEvery } from 'redux-saga/effects';
import { getSoftwareUpdates } from '@lib/api/softwareUpdates';

import {
FETCH_SOFTWARE_UPDATES,
startLoadingSoftwareUpdates,
setSoftwareUpdates,
setEmptySoftwareUpdates,
setSoftwareUpdatesErrors,
} from '@state/softwareUpdates';

export function* fetchSoftwareUpdates({ payload: { hostId } }) {
yield put(startLoadingSoftwareUpdates());

try {
const response = yield call(getSoftwareUpdates, hostId);
yield put(setSoftwareUpdates({ hostId, ...response.data }));
} catch (error) {
yield put(setEmptySoftwareUpdates({ hostId }));
const errors = get(error, ['response', 'data'], []);
yield put(setSoftwareUpdatesErrors(errors));
}
}

export function* watchSoftwareUpdates() {
yield takeEvery(FETCH_SOFTWARE_UPDATES, fetchSoftwareUpdates);
}
90 changes: 90 additions & 0 deletions assets/js/state/sagas/softwareUpdates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { faker } from '@faker-js/faker';
import MockAdapter from 'axios-mock-adapter';

import { recordSaga } from '@lib/test-utils';

import { networkClient } from '@lib/network';

import {
startLoadingSoftwareUpdates,
setSoftwareUpdates,
setSoftwareUpdatesErrors,
setEmptySoftwareUpdates,
} from '@state/softwareUpdates';

import { fetchSoftwareUpdates } from './softwareUpdates';

describe('Software Updates saga', () => {
describe('Fetching Software Updates', () => {
it('should successfully fetch software updates', async () => {
const axiosMock = new MockAdapter(networkClient);
const hostId = faker.string.uuid();
const response = {
relevant_patches: [
{
date: '2023-05-18',
advisory_name: 'SUSE-15-SP4-2023-2245',
advisory_type: 'bugfix',
advisory_status: 'stable',
id: 2192,
advisory_synopsis: 'Recommended update for libzypp, zypper',
update_date: '2023-05-18',
},
],
upgradable_packages: [
{
from_epoch: ' ',
to_release: '150400.7.60.2',
name: 'openssl-1_1',
from_release: '150400.7.25.1',
to_epoch: ' ',
arch: 'x86_64',
to_package_id: 37454,
from_version: '1.1.1l',
to_version: '1.1.1l',
from_arch: 'x86_64',
to_arch: 'x86_64',
},
],
};

axiosMock
.onGet(`/api/v1/hosts/${hostId}/software_updates`)
.reply(200, response);

const dispatched = await recordSaga(fetchSoftwareUpdates, {
payload: { hostId, ...response },
});

expect(dispatched).toEqual([
startLoadingSoftwareUpdates(),
setSoftwareUpdates({ hostId, ...response }),
]);
});

it.each([
{ status: 404, body: { message: '404 Not found' } },
{ status: 500, body: { message: 'java.lang.NullPointerException' } },
])(
'should empty software updates settings on failed fetching',
async ({ status, body }) => {
const axiosMock = new MockAdapter(networkClient);
const hostId = faker.string.uuid();

axiosMock
.onGet(`/api/v1/hosts/${hostId}/software_updates`)
.reply(status, body);

const dispatched = await recordSaga(fetchSoftwareUpdates, {
payload: { hostId },
});

expect(dispatched).toEqual([
startLoadingSoftwareUpdates(),
setEmptySoftwareUpdates({ hostId }),
setSoftwareUpdatesErrors(body),
]);
}
);
});
});
14 changes: 14 additions & 0 deletions assets/js/state/selectors/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { createSelector } from '@reduxjs/toolkit';

export const getSoftwareUpdates = (state) => state?.softwareUpdates;

export const getSoftwareUpdatesForHost = (id) => (state) =>
state?.softwareUpdates.softwareUpdates[id];

export const getSoftwareUpdatesStats = createSelector(
[(state, id) => getSoftwareUpdatesForHost(id)(state)],
(softwareUpdates) => ({
numRelevantPatches: softwareUpdates?.relevant_patches?.length,
numUpgradablePackages: softwareUpdates?.upgradable_packages?.length,
})
);
119 changes: 119 additions & 0 deletions assets/js/state/selectors/softwareUpdates.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { faker } from '@faker-js/faker';
import { getSoftwareUpdates, getSoftwareUpdatesStats } from './softwareUpdates';

describe('Software Updates selector', () => {
const hostId = faker.string.uuid();
const softwareUpdates = {
[hostId]: {
relevant_patches: [
{
date: '2024-03-13',
advisory_name: 'SUSE-15-SP4-2024-877',
advisory_type: 'security_advisory',
advisory_status: 'stable',
id: 4263,
advisory_synopsis: 'important: Security update for sudo',
update_date: '2024-03-13',
},
{
date: '2024-03-13',
advisory_name: 'SUSE-15-SP4-2024-871',
advisory_type: 'security_advisory',
advisory_status: 'stable',
id: 4261,
advisory_synopsis: 'important: Security update for vim',
update_date: '2024-03-13',
},
{
date: '2024-03-13',
advisory_name: 'SUSE-15-SP4-2024-870',
advisory_type: 'security_advisory',
advisory_status: 'stable',
id: 4260,
advisory_synopsis: 'moderate: Security update for glibc',
update_date: '2024-03-13',
},
{
date: '2024-02-28',
advisory_name: 'SUSE-15-SP4-2024-641',
advisory_type: 'bugfix',
advisory_status: 'stable',
id: 4189,
advisory_synopsis: 'Recommended update for gcc7',
update_date: '2024-02-28',
},
],
upgradable_packages: [
{
from_epoch: ' ',
to_release: '150400.6.10.1',
name: 'libQt5Network5',
from_release: '150400.6.3.1',
to_epoch: ' ',
arch: 'x86_64',
to_package_id: 38289,
from_version: '5.15.2+kde294',
to_version: '5.15.2+kde294',
from_arch: 'x86_64',
to_arch: 'x86_64',
},
{
from_epoch: ' ',
to_release: '150300.10.51.1',
name: 'libpython3_6m1_0',
from_release: '150300.10.40.1',
to_epoch: ' ',
arch: 'x86_64',
to_package_id: 36262,
from_version: '3.6.15',
to_version: '3.6.15',
from_arch: 'x86_64',
to_arch: 'x86_64',
},
{
from_epoch: ' ',
to_release: '150400.6.10.1',
name: 'libQt5Gui5',
from_release: '150400.6.3.1',
to_epoch: ' ',
arch: 'x86_64',
to_package_id: 38391,
from_version: '5.15.2+kde294',
to_version: '5.15.2+kde294',
from_arch: 'x86_64',
to_arch: 'x86_64',
},
],
},
};
const state = {
softwareUpdates: {
loading: false,
softwareUpdates,
errors: [],
},
};

it('should return the software updates', () => {
expect(getSoftwareUpdates(state)).toEqual({
loading: false,
softwareUpdates,
errors: [],
});
});

it('should return the correct software updates statistics', () => {
expect(getSoftwareUpdatesStats(state, hostId)).toEqual({
numRelevantPatches: 4,
numUpgradablePackages: 3,
});
});

it('should return undefined stats when there is no data for a host', () => {
const unknownHost = faker.string.uuid();
expect(getSoftwareUpdatesStats(state, unknownHost)).toEqual({
numRelevantPatches: undefined,
numUpgradablePackages: undefined,
});
});
});
52 changes: 52 additions & 0 deletions assets/js/state/softwareUpdates.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { createAction, createSlice } from '@reduxjs/toolkit';

const initialState = {
loading: false,
softwareUpdates: {},
errors: [],
};

export const softwareUpdatesSlice = createSlice({
name: 'softwareUpdates',
initialState,
reducers: {
startLoadingSoftwareUpdates: (state) => {
state.loading = true;
},
setSoftwareUpdates: (
state,
{ payload: { hostId, relevant_patches, upgradable_packages } }
) => {
state.loading = false;

state.softwareUpdates = {
...state.softwareUpdates,
[hostId]: {
relevant_patches,
upgradable_packages,
},
};
},
setEmptySoftwareUpdates: (state, { payload: { hostId } }) => {
state.loading = false;
state.softwareUpdates = { ...state.softwareUpdates, [hostId]: {} };
},
setSoftwareUpdatesErrors: (state, { payload: errors }) => {
state.loading = false;
state.errors = errors;
},
},
});

export const FETCH_SOFTWARE_UPDATES = 'FETCH_SOFTWARE_UPDATES';

export const fetchSoftwareUpdates = createAction(FETCH_SOFTWARE_UPDATES);

export const {
startLoadingSoftwareUpdates,
setSoftwareUpdates,
setEmptySoftwareUpdates,
setSoftwareUpdatesErrors,
} = softwareUpdatesSlice.actions;

export default softwareUpdatesSlice.reducer;
Loading

0 comments on commit 2d08e21

Please sign in to comment.