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

Implement sagas for save and upsate of SUMA settings #2338

Merged
merged 3 commits into from
Feb 20, 2024
Merged
Show file tree
Hide file tree
Changes from 2 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
49 changes: 48 additions & 1 deletion assets/js/state/sagas/softwareUpdatesSettings.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import { get } from 'lodash';
import { put, call, takeEvery } from 'redux-saga/effects';
import { getSettings } from '@lib/api/softwareUpdatesSettings';
import {
getSettings,
saveSettings,
updateSettings,
} from '@lib/api/softwareUpdatesSettings';

import {
FETCH_SOFTWARE_UPDATES_SETTINGS,
SAVE_SOFTWARE_UPDATES_SETTINGS,
UPDATE_SOFTWARE_UPDATES_SETTINGS,
startLoadingSoftwareUpdatesSettings,
setSoftwareUpdatesSettings,
setEmptySoftwareUpdatesSettings,
setSoftwareUpdatesSettingsErrors,
} from '@state/softwareUpdatesSettings';

export function* fetchSoftwareUpdatesSettings() {
Expand All @@ -19,9 +27,48 @@ export function* fetchSoftwareUpdatesSettings() {
}
}

export function* saveSoftwareUpdatesSettings({
url,
username,
password,
ca_cert,
}) {
yield put(startLoadingSoftwareUpdatesSettings());

try {
const response = yield call(saveSettings, {
url,
username,
password,
ca_cert,
});
yield put(setSoftwareUpdatesSettings(response.data));
} catch (error) {
const errors = get(error, ['response', 'data', 'errors'], []);
yield put(setSoftwareUpdatesSettingsErrors(errors));
}
}

export function* updateSoftwareUpdatesSettings(payload) {
yield put(startLoadingSoftwareUpdatesSettings());

try {
const response = yield call(updateSettings, payload);
yield put(setSoftwareUpdatesSettings(response.data));
} catch (error) {
const errors = get(error, ['response', 'data', 'errors'], []);
yield put(setSoftwareUpdatesSettingsErrors(errors));
}
}

export function* watchSoftwareUpdateSettings() {
yield takeEvery(
FETCH_SOFTWARE_UPDATES_SETTINGS,
fetchSoftwareUpdatesSettings
);
yield takeEvery(SAVE_SOFTWARE_UPDATES_SETTINGS, saveSoftwareUpdatesSettings);
yield takeEvery(
UPDATE_SOFTWARE_UPDATES_SETTINGS,
updateSoftwareUpdatesSettings
);
}
143 changes: 140 additions & 3 deletions assets/js/state/sagas/softwareUpdatesSettings.test.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { faker } from '@faker-js/faker';

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

import { networkClient } from '@lib/network';
Expand All @@ -7,16 +9,20 @@ import { softwareUpdatesSettingsFactory } from '@lib/test-utils/factories/softwa
import {
startLoadingSoftwareUpdatesSettings,
setSoftwareUpdatesSettings,
setSoftwareUpdatesSettingsErrors,
setEmptySoftwareUpdatesSettings,
} from '@state/softwareUpdatesSettings';

import { fetchSoftwareUpdatesSettings } from './softwareUpdatesSettings';

const axiosMock = new MockAdapter(networkClient);
import {
fetchSoftwareUpdatesSettings,
saveSoftwareUpdatesSettings,
updateSoftwareUpdatesSettings,
} from './softwareUpdatesSettings';

describe('Software Updates Settings saga', () => {
describe('Fetching Software Updates Settings', () => {
it('should successfully fetch software updates settings', async () => {
const axiosMock = new MockAdapter(networkClient);
const successfulResponse = softwareUpdatesSettingsFactory.build();

axiosMock
Expand All @@ -32,6 +38,7 @@ describe('Software Updates Settings saga', () => {
});

it('should empty software updates settings on failed fetching', async () => {
const axiosMock = new MockAdapter(networkClient);
[404, 500].forEach(async (errorStatus) => {
axiosMock.onGet('/settings/suma_credentials').reply(errorStatus);

Expand All @@ -44,4 +51,134 @@ describe('Software Updates Settings saga', () => {
});
});
});

describe('Saving Software Updates settings', () => {
it('should successfully save software updates settings', async () => {
const axiosMock = new MockAdapter(networkClient);
const payload = {
url: faker.internet.url(),
username: faker.word.noun(),
password: faker.word.noun(),
ca_cert: faker.lorem.text(),
};
const caUploadedAt = faker.date.recent().toString();
const successfulResponse = softwareUpdatesSettingsFactory.build({
url: payload.url,
username: payload.username,
ca_uploaded_at: caUploadedAt,
});

axiosMock
.onPost('/settings/suma_credentials')
.reply(201, successfulResponse);

const dispatched = await recordSaga(saveSoftwareUpdatesSettings, payload);

expect(dispatched).toEqual([
startLoadingSoftwareUpdatesSettings(),
setSoftwareUpdatesSettings(successfulResponse),
]);
});

it('should have errors on failed saving', async () => {
const axiosMock = new MockAdapter(networkClient);
const payload = {
url: '',
username: '',
password: faker.word.noun(),
ca_cert: '',
};
const errors = [
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OCD alert: mismatching errors based on the input.
Providing an empty username username: '', would result in an extra error besides url/ca_cert.

Feel free to keep like this, tho 😄 The test still tests what it's supposed to test.

{
detail: "can't be blank",
source: { pointer: '/url' },
title: 'Invalid value',
},
{
detail: "can't be blank",
source: { pointer: '/ca_cert' },
title: 'Invalid value',
},
];

axiosMock.onPost('/settings/suma_credentials', payload).reply(422, {
errors,
});

const dispatched = await recordSaga(saveSoftwareUpdatesSettings, payload);

expect(dispatched).toEqual([
startLoadingSoftwareUpdatesSettings(),
setSoftwareUpdatesSettingsErrors(errors),
]);
});
});

describe('Updating Software Updates settings', () => {
it('should successfully save software updates settings', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a copy/pasta from previous test

Suggested change
it('should successfully save software updates settings', async () => {
it('should successfully change software updates settings', async () => {

const axiosMock = new MockAdapter(networkClient);
const payload = {
url: faker.internet.url(),
username: faker.word.noun(),
password: faker.word.noun(),
ca_cert: faker.lorem.text(),
};
const caUploadedAt = faker.date.recent().toString();
const successfulResponse = softwareUpdatesSettingsFactory.build({
url: payload.url,
username: payload.username,
ca_uploaded_at: caUploadedAt,
});

axiosMock
.onPatch('/settings/suma_credentials')
.reply(200, successfulResponse);

const dispatched = await recordSaga(
updateSoftwareUpdatesSettings,
payload
);

expect(dispatched).toEqual([
startLoadingSoftwareUpdatesSettings(),
setSoftwareUpdatesSettings(successfulResponse),
]);
});

it('should have errors on failed saving', async () => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it('should have errors on failed saving', async () => {
it('should have errors on failed update', async () => {

const axiosMock = new MockAdapter(networkClient);
const payload = {
url: '',
username: '',
password: faker.word.noun(),
ca_cert: '',
};
const errors = [
{
detail: "can't be blank",
source: { pointer: '/url' },
title: 'Invalid value',
},
{
detail: "can't be blank",
source: { pointer: '/ca_cert' },
title: 'Invalid value',
},
];

axiosMock.onPatch('/settings/suma_credentials', payload).reply(422, {
errors,
});

const dispatched = await recordSaga(
updateSoftwareUpdatesSettings,
payload
);

expect(dispatched).toEqual([
startLoadingSoftwareUpdatesSettings(),
setSoftwareUpdatesSettingsErrors(errors),
]);
});
});
});
23 changes: 20 additions & 3 deletions assets/js/state/softwareUpdatesSettings.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ const emptySettings = {
const initialState = {
loading: false,
settings: emptySettings,
error: null,
networkError: null,
errors: [],
};

export const softwareUpdatesSettingsSlice = createSlice({
Expand All @@ -24,27 +25,43 @@ export const softwareUpdatesSettingsSlice = createSlice({
},
setSoftwareUpdatesSettings: (state, { payload: { settings } }) => {
state.loading = false;
state.error = null;
state.networkError = null;
state.settings = settings;
},
setEmptySoftwareUpdatesSettings: (state) => {
state.loading = false;
state.error = null;
state.networkError = null;
state.settings = emptySettings;
},
setSoftwareUpdatesSettingsErrors: (state, { payload: errors }) => {
state.loading = false;
state.networkError = null;
state.errors = errors;
},
},
});

export const FETCH_SOFTWARE_UPDATES_SETTINGS =
'FETCH_SOFTWARE_UPDATES_SETTINGS';
export const SAVE_SOFTWARE_UPDATES_SETTINGS = 'SAVE_SOFTWARE_UPDATES_SETTINGS';
export const UPDATE_SOFTWARE_UPDATES_SETTINGS =
'UPDATE_SOFTWARE_UPDATES_SETTINGS';

export const fetchSoftwareUpdatesSettings = createAction(
FETCH_SOFTWARE_UPDATES_SETTINGS
);
export const saveSoftwareUpdatesSettings = createAction(
SAVE_SOFTWARE_UPDATES_SETTINGS
);
export const updateSoftwareUpdatesSettings = createAction(
UPDATE_SOFTWARE_UPDATES_SETTINGS
);

export const {
startLoadingSoftwareUpdatesSettings,
setSoftwareUpdatesSettings,
setEmptySoftwareUpdatesSettings,
setSoftwareUpdatesSettingsErrors,
} = softwareUpdatesSettingsSlice.actions;

export default softwareUpdatesSettingsSlice.reducer;
55 changes: 51 additions & 4 deletions assets/js/state/softwareUpdatesSettings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import softwareUpdatesSettingsReducer, {
startLoadingSoftwareUpdatesSettings,
setSoftwareUpdatesSettings,
setEmptySoftwareUpdatesSettings,
setSoftwareUpdatesSettingsErrors,
} from './softwareUpdatesSettings';

describe('SoftwareUpdateSettings reducer', () => {
Expand Down Expand Up @@ -29,7 +30,8 @@ describe('SoftwareUpdateSettings reducer', () => {
username: undefined,
ca_uploaded_at: undefined,
},
error: null,
networkError: null,
errors: [],
};

const settings = {
Expand All @@ -45,7 +47,8 @@ describe('SoftwareUpdateSettings reducer', () => {
expect(actual).toEqual({
loading: false,
settings,
error: null,
networkError: null,
errors: [],
});
});

Expand All @@ -57,7 +60,8 @@ describe('SoftwareUpdateSettings reducer', () => {
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
error: null,
networkError: null,
errors: [],
};

const action = setEmptySoftwareUpdatesSettings();
Expand All @@ -71,7 +75,50 @@ describe('SoftwareUpdateSettings reducer', () => {
username: undefined,
ca_uploaded_at: undefined,
},
error: null,
networkError: null,
errors: [],
});
});

it('should set errors upon validation failed', () => {
const initialState = {
loading: false,
settings: {
url: 'https://valid.url',
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
networkError: null,
errors: [],
};

const errors = [
{
detail: "can't be blank",
source: { pointer: '/url' },
title: 'Invalid value',
},
{
detail: "can't be blank",
source: { pointer: '/ca_cert' },
title: 'Invalid value',
},
];

const action = setSoftwareUpdatesSettingsErrors(errors);

const actual = softwareUpdatesSettingsReducer(initialState, action);

expect(actual).toEqual({
loading: false,

settings: {
url: 'https://valid.url',
username: 'username',
ca_uploaded_at: '2021-01-01T00:00:00Z',
},
networkError: null,
errors,
});
});
});
Loading