Commit 39f2d295 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera

Merge branch '216134_04-geo-settings-form-actions' into 'master'

Geo Settings Form - Validations and Save

See merge request gitlab-org/gitlab!35672
parents 9d90bdf1 1d004e45
...@@ -355,4 +355,9 @@ export default { ...@@ -355,4 +355,9 @@ export default {
const url = Api.buildUrl(this.applicationSettingsPath); const url = Api.buildUrl(this.applicationSettingsPath);
return axios.get(url); return axios.get(url);
}, },
updateApplicationSettings(data) {
const url = Api.buildUrl(this.applicationSettingsPath);
return axios.put(url, data);
},
}; };
<script> <script>
import { mapState } from 'vuex'; import { mapState, mapActions, mapGetters } from 'vuex';
import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { mapComputed } from '~/vuex_shared/bindings';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { validateTimeout, validateAllowedIp } from '../validations';
import { FORM_VALIDATION_FIELDS } from '../constants';
export default { export default {
name: 'GeoSettingsForm', name: 'GeoSettingsForm',
...@@ -11,14 +14,30 @@ export default { ...@@ -11,14 +14,30 @@ export default {
GlButton, GlButton,
}, },
computed: { computed: {
// The real connection between vuex and the component will be implemented in ...mapState(['formErrors']),
// a later MR, this feature is anyhow behind feature flag ...mapGetters(['formHasError']),
...mapState(['timeout', 'allowedIp']), ...mapComputed([
{ key: 'timeout', updateFn: 'setTimeout' },
{ key: 'allowedIp', updateFn: 'setAllowedIp' },
]),
}, },
methods: { methods: {
...mapActions(['updateGeoSettings', 'setFormError']),
redirect() { redirect() {
visitUrl('/admin/geo/nodes'); visitUrl('/admin/geo/nodes');
}, },
checkTimeout() {
this.setFormError({
key: FORM_VALIDATION_FIELDS.TIMEOUT,
error: validateTimeout(this.timeout),
});
},
checkAllowedIp() {
this.setFormError({
key: FORM_VALIDATION_FIELDS.ALLOWED_IP,
error: validateAllowedIp(this.allowedIp),
});
},
}, },
}; };
</script> </script>
...@@ -29,19 +48,32 @@ export default { ...@@ -29,19 +48,32 @@ export default {
:label="__('Connection timeout')" :label="__('Connection timeout')"
label-for="settings-timeout-field" label-for="settings-timeout-field"
:description="__('Time in seconds')" :description="__('Time in seconds')"
:state="Boolean(formErrors.timeout)"
:invalid-feedback="formErrors.timeout"
> >
<gl-form-input id="settings-timeout-field" v-model="timeout" class="col-sm-2" type="number" /> <gl-form-input
id="settings-timeout-field"
v-model="timeout"
class="col-sm-2"
type="number"
:class="{ 'is-invalid': Boolean(formErrors.timeout) }"
@blur="checkTimeout"
/>
</gl-form-group> </gl-form-group>
<gl-form-group <gl-form-group
:label="__('Allowed Geo IP')" :label="__('Allowed Geo IP')"
label-for="settings-allowed-ip-field" label-for="settings-allowed-ip-field"
:description="__('Comma-separated, e.g. \'1.1.1.1, 2.2.2.0/24\'')" :description="__('Comma-separated, e.g. \'1.1.1.1, 2.2.2.0/24\'')"
:state="Boolean(formErrors.allowedIp)"
:invalid-feedback="formErrors.allowedIp"
> >
<gl-form-input <gl-form-input
id="settings-allowed-ip-field" id="settings-allowed-ip-field"
v-model="allowedIp" v-model="allowedIp"
class="col-sm-6" class="col-sm-6"
type="text" type="text"
:class="{ 'is-invalid': Boolean(formErrors.allowedIp) }"
@blur="checkAllowedIp"
/> />
</gl-form-group> </gl-form-group>
<section <section
...@@ -51,6 +83,8 @@ export default { ...@@ -51,6 +83,8 @@ export default {
data-testid="settingsSaveButton" data-testid="settingsSaveButton"
data-qa-selector="add_node_button" data-qa-selector="add_node_button"
variant="success" variant="success"
:disabled="formHasError"
@click="updateGeoSettings"
>{{ __('Save changes') }}</gl-button >{{ __('Save changes') }}</gl-button
> >
<gl-button data-testid="settingsCancelButton" class="gl-ml-auto" @click="redirect">{{ <gl-button data-testid="settingsCancelButton" class="gl-ml-auto" @click="redirect">{{
......
export const DEFAULT_TIMEOUT = 10; export const DEFAULT_TIMEOUT = 10;
export const DEFAULT_ALLOWED_IP = '0.0.0.0/0, ::/0'; export const DEFAULT_ALLOWED_IP = '0.0.0.0/0, ::/0';
export const FORM_VALIDATION_FIELDS = {
TIMEOUT: 'timeout',
ALLOWED_IP: 'allowedIp',
};
...@@ -3,7 +3,6 @@ import createFlash from '~/flash'; ...@@ -3,7 +3,6 @@ import createFlash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import * as types from './mutation_types'; import * as types from './mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const fetchGeoSettings = ({ commit }) => { export const fetchGeoSettings = ({ commit }) => {
commit(types.REQUEST_GEO_SETTINGS); commit(types.REQUEST_GEO_SETTINGS);
Api.getApplicationSettings() Api.getApplicationSettings()
...@@ -18,3 +17,33 @@ export const fetchGeoSettings = ({ commit }) => { ...@@ -18,3 +17,33 @@ export const fetchGeoSettings = ({ commit }) => {
commit(types.RECEIVE_GEO_SETTINGS_ERROR); commit(types.RECEIVE_GEO_SETTINGS_ERROR);
}); });
}; };
export const updateGeoSettings = ({ commit, state }) => {
commit(types.REQUEST_UPDATE_GEO_SETTINGS);
Api.updateApplicationSettings({
geo_status_timeout: state.timeout,
geo_node_allowed_ips: state.allowedIp,
})
.then(({ data }) => {
commit(types.RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS, {
timeout: data.geo_status_timeout,
allowedIp: data.geo_node_allowed_ips,
});
})
.catch(() => {
createFlash(__('There was an error updating the Geo Settings'));
commit(types.RECEIVE_UPDATE_GEO_SETTINGS_ERROR);
});
};
export const setTimeout = ({ commit }, { timeout }) => {
commit(types.SET_TIMEOUT, timeout);
};
export const setAllowedIp = ({ commit }, { allowedIp }) => {
commit(types.SET_ALLOWED_IP, allowedIp);
};
export const setFormError = ({ commit }, { key, error }) => {
commit(types.SET_FORM_ERROR, { key, error });
};
// eslint-disable-next-line import/prefer-default-export
export const formHasError = state =>
Object.keys(state.formErrors)
.map(key => state.formErrors[key])
.some(val => Boolean(val));
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import createState from './state'; import createState from './state';
...@@ -10,5 +11,6 @@ export default () => ...@@ -10,5 +11,6 @@ export default () =>
new Vuex.Store({ new Vuex.Store({
actions, actions,
mutations, mutations,
getters,
state: createState(), state: createState(),
}); });
export const REQUEST_GEO_SETTINGS = 'REQUEST_GEO_SETTINGS'; export const REQUEST_GEO_SETTINGS = 'REQUEST_GEO_SETTINGS';
export const RECEIVE_GEO_SETTINGS_SUCCESS = 'RECEIVE_GEO_SETTINGS_SUCCESS'; export const RECEIVE_GEO_SETTINGS_SUCCESS = 'RECEIVE_GEO_SETTINGS_SUCCESS';
export const RECEIVE_GEO_SETTINGS_ERROR = 'RECEIVE_GEO_SETTINGS_ERROR'; export const RECEIVE_GEO_SETTINGS_ERROR = 'RECEIVE_GEO_SETTINGS_ERROR';
export const REQUEST_UPDATE_GEO_SETTINGS = 'REQUEST_UPDATE_GEO_SETTINGS';
export const RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS = 'RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS';
export const RECEIVE_UPDATE_GEO_SETTINGS_ERROR = 'RECEIVE_UPDATE_GEO_SETTINGS_ERROR';
export const SET_TIMEOUT = 'SET_TIMEOUT';
export const SET_ALLOWED_IP = 'SET_ALLOWED_IP';
export const SET_FORM_ERROR = 'SET_FORM_ERROR';
...@@ -15,4 +15,26 @@ export default { ...@@ -15,4 +15,26 @@ export default {
state.timeout = DEFAULT_TIMEOUT; state.timeout = DEFAULT_TIMEOUT;
state.allowedIp = DEFAULT_ALLOWED_IP; state.allowedIp = DEFAULT_ALLOWED_IP;
}, },
[types.REQUEST_UPDATE_GEO_SETTINGS](state) {
state.isLoading = true;
},
[types.RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS](state, { timeout, allowedIp }) {
state.isLoading = false;
state.timeout = timeout;
state.allowedIp = allowedIp;
},
[types.RECEIVE_UPDATE_GEO_SETTINGS_ERROR](state) {
state.isLoading = false;
state.timeout = DEFAULT_TIMEOUT;
state.allowedIp = DEFAULT_ALLOWED_IP;
},
[types.SET_TIMEOUT](state, timeout) {
state.timeout = timeout;
},
[types.SET_ALLOWED_IP](state, allowedIp) {
state.allowedIp = allowedIp;
},
[types.SET_FORM_ERROR](state, { key, error }) {
state.formErrors[key] = error;
},
}; };
import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP } from '../constants'; import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP, FORM_VALIDATION_FIELDS } from '../constants';
export default () => ({ export default () => ({
isLoading: false, isLoading: false,
timeout: DEFAULT_TIMEOUT, timeout: DEFAULT_TIMEOUT,
allowedIp: DEFAULT_ALLOWED_IP, allowedIp: DEFAULT_ALLOWED_IP,
formErrors: Object.keys(FORM_VALIDATION_FIELDS)
.map(key => FORM_VALIDATION_FIELDS[key])
.reduce((acc, cur) => ({ ...acc, [cur]: '' }), {}),
}); });
import ipaddr from 'ipaddr.js';
import { s__ } from '~/locale';
const validateAddress = address => {
try {
// Checks if Valid IPv4/IPv6 (CIDR) - Throws if not
return Boolean(ipaddr.parseCIDR(address));
} catch (e) {
// Checks if Valid IPv4/IPv6 (Non-CIDR) - Does not Throw
return ipaddr.isValid(address);
}
};
const validateIP = data => {
let addresses = data.replace(/\s/g, '').split(',');
addresses = addresses.map(address => validateAddress(address));
return !addresses.some(a => !a);
};
export const validateTimeout = data => {
if (!data && data !== 0) {
return s__("Geo|Connection timeout can't be blank");
} else if (data && Number.isNaN(Number(data))) {
return s__('Geo|Connection timeout must be a number');
} else if (data < 1 || data > 120) {
return s__('Geo|Connection timeout should be between 1-120');
}
return '';
};
export const validateAllowedIp = data => {
if (!data) {
return s__("Geo|Allowed Geo IP can't be blank");
} else if (data.length > 255) {
return s__('Geo|Allowed Geo IP should be between 1 and 255 characters');
} else if (!validateIP(data)) {
return s__('Geo|Allowed Geo IP should contain valid IP addresses');
}
return '';
};
...@@ -842,10 +842,11 @@ describe('Api', () => { ...@@ -842,10 +842,11 @@ describe('Api', () => {
}); });
}); });
describe('getApplicationSettings', () => { describe('Application Settings', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/application/settings`; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/application/settings`;
const apiResponse = { mock_setting: 1, mock_setting2: 2 }; const apiResponse = { mock_setting: 1, mock_setting2: 2, mock_setting3: 3 };
describe('getApplicationSettings', () => {
it('fetches applications settings', () => { it('fetches applications settings', () => {
jest.spyOn(Api, 'buildUrl').mockReturnValue(expectedUrl); jest.spyOn(Api, 'buildUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get'); jest.spyOn(axios, 'get');
...@@ -857,4 +858,20 @@ describe('Api', () => { ...@@ -857,4 +858,20 @@ describe('Api', () => {
}); });
}); });
}); });
describe('updateApplicationSettings', () => {
const mockReq = { mock_setting: 10 };
it('updates applications settings', () => {
jest.spyOn(Api, 'buildUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'put');
mock.onPut(expectedUrl).replyOnce(201, apiResponse);
return Api.updateApplicationSettings(mockReq).then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.put).toHaveBeenCalledWith(expectedUrl, mockReq);
});
});
});
});
}); });
import Vuex from 'vuex'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, mount } from '@vue/test-utils';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import store from 'ee/geo_settings/store'; import initStore from 'ee/geo_settings/store';
import * as types from 'ee/geo_settings/store/mutation_types';
import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue'; import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue';
import { STRING_OVER_255 } from '../mock_data';
const localVue = createLocalVue(); const localVue = createLocalVue();
localVue.use(Vuex); localVue.use(Vuex);
...@@ -13,9 +15,14 @@ jest.mock('~/lib/utils/url_utility', () => ({ ...@@ -13,9 +15,14 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('GeoSettingsForm', () => { describe('GeoSettingsForm', () => {
let wrapper; let wrapper;
let store;
const createStore = () => {
store = initStore();
};
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(GeoSettingsForm, { wrapper = mount(GeoSettingsForm, {
store, store,
}); });
}; };
...@@ -26,10 +33,13 @@ describe('GeoSettingsForm', () => { ...@@ -26,10 +33,13 @@ describe('GeoSettingsForm', () => {
const findGeoSettingsTimeoutField = () => wrapper.find('#settings-timeout-field'); const findGeoSettingsTimeoutField = () => wrapper.find('#settings-timeout-field');
const findGeoSettingsAllowedIpField = () => wrapper.find('#settings-allowed-ip-field'); const findGeoSettingsAllowedIpField = () => wrapper.find('#settings-allowed-ip-field');
const findSettingsCancelButton = () => wrapper.find('[data-testid="settingsCancelButton"]'); const findGeoSettingsSaveButton = () => wrapper.find('[data-testid="settingsSaveButton"]');
const findGeoSettingsCancelButton = () => wrapper.find('[data-testid="settingsCancelButton"]');
const findErrorMessage = () => wrapper.find('.invalid-feedback');
describe('template', () => { describe('template', () => {
beforeEach(() => { beforeEach(() => {
createStore();
createComponent(); createComponent();
}); });
...@@ -40,18 +50,100 @@ describe('GeoSettingsForm', () => { ...@@ -40,18 +50,100 @@ describe('GeoSettingsForm', () => {
it('renders Geo Node Form Url Field', () => { it('renders Geo Node Form Url Field', () => {
expect(findGeoSettingsAllowedIpField().exists()).toBe(true); expect(findGeoSettingsAllowedIpField().exists()).toBe(true);
}); });
describe('Save Button', () => {
describe('with errors on form', () => {
beforeEach(() => {
store.commit(types.SET_FORM_ERROR, {
key: 'timeout',
error: 'error',
});
});
it('disables button', () => {
expect(findGeoSettingsSaveButton().attributes('disabled')).toBeTruthy();
});
});
describe('with no errors on form', () => {
it('does not disable button', () => {
expect(findGeoSettingsSaveButton().attributes('disabled')).toBeFalsy();
});
});
});
}); });
describe('methods', () => { describe('methods', () => {
describe('redirect', () => {
beforeEach(() => { beforeEach(() => {
createStore();
jest.spyOn(store, 'dispatch').mockImplementation();
createComponent(); createComponent();
}); });
it('calls visitUrl when cancel is clicked', () => { describe('save button', () => {
findSettingsCancelButton().vm.$emit('click'); it('calls updateGeoSettings when clicked', () => {
findGeoSettingsSaveButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('updateGeoSettings');
});
});
describe('cancel button', () => {
it('calls visitUrl when clicked', () => {
findGeoSettingsCancelButton().vm.$emit('click');
expect(visitUrl).toHaveBeenCalledWith('/admin/geo/nodes'); expect(visitUrl).toHaveBeenCalledWith('/admin/geo/nodes');
}); });
}); });
}); });
describe('errors', () => {
describe.each`
data | showError | errorMessage
${null} | ${true} | ${"Connection timeout can't be blank"}
${''} | ${true} | ${"Connection timeout can't be blank"}
${0} | ${true} | ${'Connection timeout should be between 1-120'}
${121} | ${true} | ${'Connection timeout should be between 1-120'}
${10} | ${false} | ${null}
`(`Timeout Field`, ({ data, showError, errorMessage }) => {
beforeEach(() => {
createStore();
createComponent();
findGeoSettingsTimeoutField().vm.$emit('input', data);
findGeoSettingsTimeoutField().trigger('blur');
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoSettingsTimeoutField().classes('is-invalid')).toBe(showError);
if (showError) {
expect(findErrorMessage().text()).toBe(errorMessage);
}
});
});
describe.each`
data | showError | errorMessage
${null} | ${true} | ${"Allowed Geo IP can't be blank"}
${''} | ${true} | ${"Allowed Geo IP can't be blank"}
${STRING_OVER_255} | ${true} | ${'Allowed Geo IP should be between 1 and 255 characters'}
${'asdf'} | ${true} | ${'Allowed Geo IP should contain valid IP addresses'}
${'1.1.1.1, asdf'} | ${true} | ${'Allowed Geo IP should contain valid IP addresses'}
${'asdf, 1.1.1.1'} | ${true} | ${'Allowed Geo IP should contain valid IP addresses'}
${'1.1.1.1'} | ${false} | ${null}
${'::/0'} | ${false} | ${null}
${'1.1.1.1, ::/0'} | ${false} | ${null}
`(`Allowed Geo IP Field`, ({ data, showError, errorMessage }) => {
beforeEach(() => {
createStore();
createComponent();
findGeoSettingsAllowedIpField().vm.$emit('input', data);
findGeoSettingsAllowedIpField().trigger('blur');
});
it(`${showError ? 'shows' : 'hides'} error when data is ${data}`, () => {
expect(findGeoSettingsAllowedIpField().classes('is-invalid')).toBe(showError);
if (showError) {
expect(findErrorMessage().text()).toBe(errorMessage);
}
});
});
});
}); });
...@@ -7,3 +7,5 @@ export const MOCK_BASIC_SETTINGS_DATA = { ...@@ -7,3 +7,5 @@ export const MOCK_BASIC_SETTINGS_DATA = {
timeout: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_status_timeout, timeout: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_status_timeout,
allowedIp: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_node_allowed_ips, allowedIp: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_node_allowed_ips,
}; };
export const STRING_OVER_255 = new Array(257).join('a');
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import flash from '~/flash'; import flash from '~/flash';
import Api from 'ee/api'; import axios from '~/lib/utils/axios_utils';
import * as actions from 'ee/geo_settings/store/actions'; import * as actions from 'ee/geo_settings/store/actions';
import * as types from 'ee/geo_settings/store/mutation_types'; import * as types from 'ee/geo_settings/store/mutation_types';
import state from 'ee/geo_settings/store/state'; import state from 'ee/geo_settings/store/state';
...@@ -9,45 +10,50 @@ import { MOCK_BASIC_SETTINGS_DATA, MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE } fr ...@@ -9,45 +10,50 @@ import { MOCK_BASIC_SETTINGS_DATA, MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE } fr
jest.mock('~/flash'); jest.mock('~/flash');
describe('GeoSettings Store Actions', () => { describe('GeoSettings Store Actions', () => {
describe('fetchGeoSettings', () => { let mock;
describe('on success', () => {
const noCallback = () => {};
const flashCallback = () => {
expect(flash).toHaveBeenCalledTimes(1);
flash.mockClear();
};
beforeEach(() => { beforeEach(() => {
jest mock = new MockAdapter(axios);
.spyOn(Api, 'getApplicationSettings') });
.mockResolvedValue({ data: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE });
afterEach(() => {
mock.restore();
}); });
it('should commit the request and success actions', done => { describe.each`
testAction( action | data | mutationName | mutationCall | callback
actions.fetchGeoSettings, ${actions.setTimeout} | ${{ timeout: MOCK_BASIC_SETTINGS_DATA.timeout }} | ${types.SET_TIMEOUT} | ${{ type: types.SET_TIMEOUT, payload: MOCK_BASIC_SETTINGS_DATA.timeout }} | ${noCallback}
{}, ${actions.setAllowedIp} | ${{ allowedIp: MOCK_BASIC_SETTINGS_DATA.allowedIp }} | ${types.SET_ALLOWED_IP} | ${{ type: types.SET_ALLOWED_IP, payload: MOCK_BASIC_SETTINGS_DATA.allowedIp }} | ${noCallback}
state, ${actions.setFormError} | ${{ key: 'timeout', error: 'error' }} | ${types.SET_FORM_ERROR} | ${{ type: types.SET_FORM_ERROR, payload: { key: 'timeout', error: 'error' } }} | ${noCallback}
[ `(`non-axios calls`, ({ action, data, mutationName, mutationCall, callback }) => {
{ type: types.REQUEST_GEO_SETTINGS }, describe(action.name, () => {
{ type: types.RECEIVE_GEO_SETTINGS_SUCCESS, payload: MOCK_BASIC_SETTINGS_DATA }, it(`should commit mutation ${mutationName}`, () => {
], return testAction(action, data, state, [mutationCall], []).then(() => callback());
[], });
done,
);
}); });
}); });
describe('on error', () => { describe.each`
action | axiosMock | type | mutationCalls | callback
${actions.fetchGeoSettings} | ${{ method: 'onGet', code: 200, res: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE }} | ${'success'} | ${[{ type: types.REQUEST_GEO_SETTINGS }, { type: types.RECEIVE_GEO_SETTINGS_SUCCESS, payload: MOCK_BASIC_SETTINGS_DATA }]} | ${noCallback}
${actions.fetchGeoSettings} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GEO_SETTINGS }, { type: types.RECEIVE_GEO_SETTINGS_ERROR }]} | ${flashCallback}
${actions.updateGeoSettings} | ${{ method: 'onPut', code: 200, res: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE }} | ${'success'} | ${[{ type: types.REQUEST_UPDATE_GEO_SETTINGS }, { type: types.RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS, payload: MOCK_BASIC_SETTINGS_DATA }]} | ${noCallback}
${actions.updateGeoSettings} | ${{ method: 'onPut', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_UPDATE_GEO_SETTINGS }, { type: types.RECEIVE_UPDATE_GEO_SETTINGS_ERROR }]} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, mutationCalls, callback }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Api, 'getApplicationSettings').mockRejectedValue(new Error(500)); mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
return testAction(action, null, state, mutationCalls, []).then(() => callback());
}); });
it('should commit the request and error actions', () => {
testAction(
actions.fetchGeoSettings,
{},
state,
[{ type: types.REQUEST_GEO_SETTINGS }, { type: types.RECEIVE_GEO_SETTINGS_ERROR }],
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
},
);
}); });
}); });
}); });
......
import * as getters from 'ee/geo_settings/store/getters';
import createState from 'ee/geo_settings/store/state';
describe('Geo Settings Store Getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('formHasError', () => {
it('with error returns true', () => {
state.formErrors.timeout = 'Error';
expect(getters.formHasError(state)).toBeTruthy();
});
it('without error returns false', () => {
state.formErrors.timeout = '';
expect(getters.formHasError(state)).toBeFalsy();
});
});
});
...@@ -11,55 +11,88 @@ describe('GeoSettings Store Mutations', () => { ...@@ -11,55 +11,88 @@ describe('GeoSettings Store Mutations', () => {
state = createState(); state = createState();
}); });
describe('REQUEST_GEO_SETTINGS', () => { describe.each`
it('sets isLoading to true', () => { mutation | data | loadingBefore | loadingAfter
mutations[types.REQUEST_GEO_SETTINGS](state); ${types.REQUEST_GEO_SETTINGS} | ${null} | ${false} | ${true}
${types.RECEIVE_GEO_SETTINGS_SUCCESS} | ${MOCK_BASIC_SETTINGS_DATA} | ${true} | ${false}
expect(state.isLoading).toEqual(true); ${types.RECEIVE_GEO_SETTINGS_ERROR} | ${null} | ${true} | ${false}
${types.REQUEST_UPDATE_GEO_SETTINGS} | ${null} | ${false} | ${true}
${types.RECEIVE_UPDATE_GEO_SETTINGS_ERROR} | ${null} | ${true} | ${false}
`(`Loading Mutations: `, ({ mutation, data, loadingBefore, loadingAfter }) => {
describe(`${mutation}`, () => {
it(`sets isLoading to ${loadingAfter}`, () => {
state.isLoading = loadingBefore;
mutations[mutation](state, data);
expect(state.isLoading).toEqual(loadingAfter);
});
}); });
}); });
describe('RECEIVE_GEO_SETTINGS_SUCCESS', () => { describe('RECEIVE_GEO_SETTINGS_SUCCESS', () => {
const mockData = MOCK_BASIC_SETTINGS_DATA; it('sets timeout and allowedIp array with data', () => {
mutations[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, MOCK_BASIC_SETTINGS_DATA);
expect(state.timeout).toBe(MOCK_BASIC_SETTINGS_DATA.timeout);
expect(state.allowedIp).toBe(MOCK_BASIC_SETTINGS_DATA.allowedIp);
});
});
describe('RECEIVE_GEO_SETTINGS_ERROR', () => {
beforeEach(() => { beforeEach(() => {
state.isLoading = true; state.timeout = MOCK_BASIC_SETTINGS_DATA.timeout;
state.allowedIp = MOCK_BASIC_SETTINGS_DATA.allowedIp;
}); });
it('sets isLoading to false', () => { it('resets timeout and allowedIp array', () => {
mutations[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, mockData); mutations[types.RECEIVE_GEO_SETTINGS_ERROR](state);
expect(state.isLoading).toEqual(false); expect(state.timeout).toBe(DEFAULT_TIMEOUT);
expect(state.allowedIp).toBe(DEFAULT_ALLOWED_IP);
});
}); });
describe('RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS', () => {
it('sets timeout and allowedIp array with data', () => { it('sets timeout and allowedIp array with data', () => {
mutations[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, mockData); mutations[types.RECEIVE_UPDATE_GEO_SETTINGS_SUCCESS](state, MOCK_BASIC_SETTINGS_DATA);
expect(state.timeout).toBe(mockData.timeout); expect(state.timeout).toBe(MOCK_BASIC_SETTINGS_DATA.timeout);
expect(state.allowedIp).toBe(mockData.allowedIp); expect(state.allowedIp).toBe(MOCK_BASIC_SETTINGS_DATA.allowedIp);
}); });
}); });
describe('RECEIVE_GEO_SETTINGS_ERROR', () => { describe('RECEIVE_UPDATE_GEO_SETTINGS_ERROR', () => {
const mockData = MOCK_BASIC_SETTINGS_DATA;
beforeEach(() => { beforeEach(() => {
state.isLoading = true; state.timeout = MOCK_BASIC_SETTINGS_DATA.timeout;
state.timeout = mockData.timeout; state.allowedIp = MOCK_BASIC_SETTINGS_DATA.allowedIp;
state.allowedIp = mockData.allowedIp;
});
it('sets isLoading to false', () => {
mutations[types.RECEIVE_GEO_SETTINGS_ERROR](state);
expect(state.isLoading).toEqual(false);
}); });
it('resets timeout and allowedIp array', () => { it('resets timeout and allowedIp array', () => {
mutations[types.RECEIVE_GEO_SETTINGS_ERROR](state); mutations[types.RECEIVE_UPDATE_GEO_SETTINGS_ERROR](state);
expect(state.timeout).toBe(DEFAULT_TIMEOUT); expect(state.timeout).toBe(DEFAULT_TIMEOUT);
expect(state.allowedIp).toBe(DEFAULT_ALLOWED_IP); expect(state.allowedIp).toBe(DEFAULT_ALLOWED_IP);
}); });
}); });
describe('SET_TIMEOUT', () => {
it('sets error for field', () => {
mutations[types.SET_TIMEOUT](state, 1);
expect(state.timeout).toBe(1);
});
});
describe('SET_ALLOWED_IP', () => {
it('sets error for field', () => {
mutations[types.SET_ALLOWED_IP](state, '0.0.0.0');
expect(state.allowedIp).toBe('0.0.0.0');
});
});
describe('SET_FORM_ERROR', () => {
it('sets error for field', () => {
mutations[types.SET_FORM_ERROR](state, { key: 'timeout', error: 'error' });
expect(state.formErrors.timeout).toBe('error');
});
});
}); });
import { validateTimeout, validateAllowedIp } from 'ee/geo_settings/validations';
import { STRING_OVER_255 } from './mock_data';
describe('Geo Settings Validations', () => {
let res = '';
describe.each`
data | errorMessage
${null} | ${"Connection timeout can't be blank"}
${''} | ${"Connection timeout can't be blank"}
${'asdf'} | ${'Connection timeout must be a number'}
${0} | ${'Connection timeout should be between 1-120'}
${121} | ${'Connection timeout should be between 1-120'}
${10} | ${''}
`(`validateTimeout`, ({ data, errorMessage }) => {
beforeEach(() => {
res = validateTimeout(data);
});
it(`return ${errorMessage} when data is ${data}`, () => {
expect(res).toBe(errorMessage);
});
});
describe.each`
data | errorMessage
${null} | ${"Allowed Geo IP can't be blank"}
${''} | ${"Allowed Geo IP can't be blank"}
${STRING_OVER_255} | ${'Allowed Geo IP should be between 1 and 255 characters'}
${'asdf'} | ${'Allowed Geo IP should contain valid IP addresses'}
${'1.1.1.1, asdf'} | ${'Allowed Geo IP should contain valid IP addresses'}
${'asdf, 1.1.1.1'} | ${'Allowed Geo IP should contain valid IP addresses'}
${'1.1.1.1'} | ${''}
${'::/0'} | ${''}
${'1.1.1.1, ::/0'} | ${''}
`(`validateAllowedIp`, ({ data, errorMessage }) => {
beforeEach(() => {
res = validateAllowedIp(data);
});
it(`return ${errorMessage} when data is ${data}`, () => {
expect(res).toBe(errorMessage);
});
});
});
...@@ -10666,6 +10666,24 @@ msgstr "" ...@@ -10666,6 +10666,24 @@ msgstr ""
msgid "Geo|All projects are being scheduled for reverify" msgid "Geo|All projects are being scheduled for reverify"
msgstr "" msgstr ""
msgid "Geo|Allowed Geo IP can't be blank"
msgstr ""
msgid "Geo|Allowed Geo IP should be between 1 and 255 characters"
msgstr ""
msgid "Geo|Allowed Geo IP should contain valid IP addresses"
msgstr ""
msgid "Geo|Connection timeout can't be blank"
msgstr ""
msgid "Geo|Connection timeout must be a number"
msgstr ""
msgid "Geo|Connection timeout should be between 1-120"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project." msgid "Geo|Could not remove tracking entry for an existing project."
msgstr "" msgstr ""
...@@ -23425,6 +23443,9 @@ msgstr "" ...@@ -23425,6 +23443,9 @@ msgstr ""
msgid "There was an error trying to validate your query" msgid "There was an error trying to validate your query"
msgstr "" msgstr ""
msgid "There was an error updating the Geo Settings"
msgstr ""
msgid "There was an error updating the dashboard, branch name is invalid." msgid "There was an error updating the dashboard, branch name is invalid."
msgstr "" msgstr ""
......
...@@ -6130,11 +6130,16 @@ ip@^1.1.0, ip@^1.1.5: ...@@ -6130,11 +6130,16 @@ ip@^1.1.0, ip@^1.1.5:
resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" resolved "https://registry.yarnpkg.com/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a"
integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=
ipaddr.js@1.9.0, ipaddr.js@^1.9.0: ipaddr.js@1.9.0:
version "1.9.0" version "1.9.0"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65" resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.0.tgz#37df74e430a0e47550fe54a2defe30d8acd95f65"
integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA== integrity sha512-M4Sjn6N/+O6/IXSJseKqHoFc+5FdGJ22sXqnjTpdZweHK64MzEPAyQZyEU3R/KRv2GLoa7nNtg/C2Ev6m7z+eA==
ipaddr.js@^1.9.0, ipaddr.js@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
is-absolute-url@^3.0.3: is-absolute-url@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698" resolved "https://registry.yarnpkg.com/is-absolute-url/-/is-absolute-url-3.0.3.tgz#96c6a22b6a23929b11ea0afb1836c36ad4a5d698"
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment