Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Redux Sagas for getting SUSE Manager software updates #2416

Merged
merged 16 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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() {
nelsonkopliku marked this conversation as resolved.
Show resolved Hide resolved
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
Loading