Commit 925e1dd2 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '14707-add-threat-monitoring-filters-ee' into 'master'

Implement bulk of Threat Monitoring store and filters component

See merge request gitlab-org/gitlab!21689
parents 8feea12e 1deb1dfb
<script> <script>
import { mapActions } from 'vuex'; import { mapActions } from 'vuex';
import { GlAlert, GlEmptyState, GlIcon, GlLink } from '@gitlab/ui'; import { GlAlert, GlEmptyState, GlIcon, GlLink, GlPopover } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import ThreatMonitoringFilters from './threat_monitoring_filters.vue';
export default { export default {
name: 'ThreatMonitoring', name: 'ThreatMonitoring',
...@@ -10,14 +11,12 @@ export default { ...@@ -10,14 +11,12 @@ export default {
GlEmptyState, GlEmptyState,
GlIcon, GlIcon,
GlLink, GlLink,
GlPopover,
ThreatMonitoringFilters,
}, },
props: { props: {
isWafSetup: { defaultEnvironmentId: {
type: Boolean, type: Number,
required: true,
},
endpoint: {
type: String,
required: true, required: true,
}, },
emptyStateSvgPath: { emptyStateSvgPath: {
...@@ -32,15 +31,25 @@ export default { ...@@ -32,15 +31,25 @@ export default {
data() { data() {
return { return {
showAlert: true, showAlert: true,
// WAF requires the project to have at least one available environment.
// An invalid default environment id means there there are no available
// environments, therefore the WAF cannot be set up. A valid default
// environment id only means that WAF *might* be set up.
isWafMaybeSetUp: this.isValidEnvironmentId(this.defaultEnvironmentId),
}; };
}, },
created() { created() {
if (this.isWafSetup) { if (this.isWafMaybeSetUp) {
this.setEndpoint(this.endpoint); this.setCurrentEnvironmentId(this.defaultEnvironmentId);
this.fetchEnvironments();
} }
}, },
methods: { methods: {
...mapActions('threatMonitoring', ['setEndpoint']), ...mapActions('threatMonitoring', ['fetchEnvironments', 'setCurrentEnvironmentId']),
isValidEnvironmentId(id) {
return Number.isInteger(id) && id >= 0;
},
dismissAlert() { dismissAlert() {
this.showAlert = false; this.showAlert = false;
}, },
...@@ -52,28 +61,30 @@ export default { ...@@ -52,28 +61,30 @@ export default {
), ),
alertText: s__( alertText: s__(
`ThreatMonitoring|The graph below is an overview of traffic coming to your `ThreatMonitoring|The graph below is an overview of traffic coming to your
application. View the documentation for instructions on how to access the application as tracked by the Web Application Firewall (WAF). View the docs
WAF logs to see what type of malicious traffic is trying to access your for instructions on how to access the WAF logs to see what type of
app.`, malicious traffic is trying to access your app. The docs link is also
accessible by clicking the "?" icon next to the title below.`,
), ),
helpPopoverTitle: s__('ThreatMonitoring|At this time, threat monitoring only supports WAF data.'),
}; };
</script> </script>
<template> <template>
<gl-empty-state <gl-empty-state
v-if="!isWafSetup" v-if="!isWafMaybeSetUp"
:title="s__('ThreatMonitoring|Web Application Firewall not enabled')" :title="s__('ThreatMonitoring|Web Application Firewall not enabled')"
:description="$options.emptyStateDescription" :description="$options.emptyStateDescription"
:svg-path="emptyStateSvgPath" :svg-path="emptyStateSvgPath"
:primary-button-link="documentationPath" :primary-button-link="documentationPath"
:primary-button-text="__('Learn more')" :primary-button-text="__('Learn More')"
/> />
<section v-else> <section v-else>
<gl-alert <gl-alert
v-if="showAlert" v-if="showAlert"
class="my-3" class="my-3"
variant="tip" variant="info"
:secondary-button-link="documentationPath" :secondary-button-link="documentationPath"
:secondary-button-text="__('View documentation')" :secondary-button-text="__('View documentation')"
@dismiss="dismissAlert" @dismiss="dismissAlert"
...@@ -84,13 +95,25 @@ export default { ...@@ -84,13 +95,25 @@ export default {
<h2 class="h4 mb-1"> <h2 class="h4 mb-1">
{{ s__('ThreatMonitoring|Threat Monitoring') }} {{ s__('ThreatMonitoring|Threat Monitoring') }}
<gl-link <gl-link
ref="helpLink"
target="_blank" target="_blank"
:href="documentationPath" :href="documentationPath"
:aria-label="s__('ThreatMonitoring|Threat Monitoring help page link')" :aria-label="s__('ThreatMonitoring|Threat Monitoring help page link')"
> >
<gl-icon name="question" /> <gl-icon name="question" />
</gl-link> </gl-link>
<gl-popover
:target="() => $refs.helpLink"
triggers="hover focus"
:title="$options.helpPopoverTitle"
>
<gl-link :href="documentationPath">{{
s__('ThreatMonitoring|View WAF documentation')
}}</gl-link>
</gl-popover>
</h2> </h2>
</header> </header>
<threat-monitoring-filters />
</section> </section>
</template> </template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import { GlFormGroup, GlDropdown, GlDropdownItem } from '@gitlab/ui';
export default {
name: 'ThreatMonitoringFilters',
components: {
GlFormGroup,
GlDropdown,
GlDropdownItem,
},
computed: {
...mapState('threatMonitoring', ['environments', 'currentEnvironmentId']),
...mapGetters('threatMonitoring', ['currentEnvironmentName']),
},
methods: {
...mapActions('threatMonitoring', ['setCurrentEnvironmentId']),
},
environmentFilterId: 'threat-monitoring-environment-filter',
};
</script>
<template>
<div class="pt-3 px-3 bg-gray-light">
<div class="row">
<gl-form-group
:label="s__('ThreatMonitoring|Environment')"
label-size="sm"
:label-for="$options.environmentFilterId"
class="col-sm-6 col-md-4 col-lg-3 col-xl-2"
>
<gl-dropdown
:id="$options.environmentFilterId"
class="mb-0 d-flex"
toggle-class="d-flex justify-content-between"
:text="currentEnvironmentName"
:disabled="environments.length === 0"
>
<gl-dropdown-item
v-for="environment in environments"
:key="environment.id"
@click="setCurrentEnvironmentId(environment.id)"
>{{ environment.name }}</gl-dropdown-item
>
</gl-dropdown>
</gl-form-group>
</div>
</div>
</template>
import Vue from 'vue'; import Vue from 'vue';
import ThreatMonitoringApp from './components/app.vue'; import ThreatMonitoringApp from './components/app.vue';
import createStore from './store'; import createStore from './store';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => { export default () => {
const el = document.querySelector('#js-threat-monitoring-app'); const el = document.querySelector('#js-threat-monitoring-app');
const { isWafSetup, endpoint, emptyStateSvgPath, documentationPath } = el.dataset; const {
wafStatisticsEndpoint,
environmentsEndpoint,
emptyStateSvgPath,
documentationPath,
defaultEnvironmentId,
} = el.dataset;
const store = createStore(); const store = createStore();
store.dispatch('threatMonitoring/setEndpoints', {
wafStatisticsEndpoint,
environmentsEndpoint,
});
return new Vue({ return new Vue({
el, el,
...@@ -15,10 +24,9 @@ export default () => { ...@@ -15,10 +24,9 @@ export default () => {
render(createElement) { render(createElement) {
return createElement(ThreatMonitoringApp, { return createElement(ThreatMonitoringApp, {
props: { props: {
isWafSetup: parseBoolean(isWafSetup),
endpoint,
emptyStateSvgPath, emptyStateSvgPath,
documentationPath, documentationPath,
defaultEnvironmentId: parseInt(defaultEnvironmentId, 10),
}, },
}); });
}, },
......
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as types from './mutation_types'; import * as types from './mutation_types';
// eslint-disable-next-line import/prefer-default-export export const setEndpoints = ({ commit }, endpoints) => commit(types.SET_ENDPOINTS, endpoints);
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const requestEnvironments = ({ commit }) => commit(types.REQUEST_ENVIRONMENTS);
export const receiveEnvironmentsSuccess = ({ commit }, environments) =>
commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments);
export const receiveEnvironmentsError = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_ERROR);
createFlash(s__('ThreatMonitoring|Something went wrong, unable to fetch environments'));
};
const getAllEnvironments = (url, page = 1) =>
axios({
method: 'GET',
url,
params: {
per_page: 100,
page,
},
}).then(({ headers, data }) => {
const nextPage = headers && headers['x-next-page'];
return nextPage
? // eslint-disable-next-line promise/no-nesting
getAllEnvironments(url, nextPage).then(environments => [
...data.environments,
...environments,
])
: data.environments;
});
export const fetchEnvironments = ({ state, dispatch }) => {
if (!state.environmentsEndpoint) {
return dispatch('receiveEnvironmentsError');
}
dispatch('requestEnvironments');
return getAllEnvironments(state.environmentsEndpoint)
.then(environments => dispatch('receiveEnvironmentsSuccess', environments))
.catch(() => dispatch('receiveEnvironmentsError'));
};
export const setCurrentEnvironmentId = ({ commit, dispatch }, environmentId) => {
commit(types.SET_CURRENT_ENVIRONMENT_ID, environmentId);
return dispatch('fetchWafStatistics');
};
export const requestWafStatistics = ({ commit }) => commit(types.REQUEST_WAF_STATISTICS);
export const receiveWafStatisticsSuccess = ({ commit }, statistics) =>
commit(types.RECEIVE_WAF_STATISTICS_SUCCESS, statistics);
export const receiveWafStatisticsError = ({ commit }) => {
commit(types.RECEIVE_WAF_STATISTICS_ERROR);
createFlash(s__('ThreatMonitoring|Something went wrong, unable to fetch WAF statistics'));
};
export const fetchWafStatistics = ({ state, dispatch }) => {
if (!state.wafStatisticsEndpoint) {
return dispatch('receiveWafStatisticsError');
}
dispatch('requestWafStatistics');
return axios
.get(state.wafStatisticsEndpoint, {
params: {
environment_id: state.currentEnvironmentId,
},
})
.then(({ data }) => dispatch('receiveWafStatisticsSuccess', data))
.catch(() => dispatch('receiveWafStatisticsError'));
};
// eslint-disable-next-line import/prefer-default-export
export const INVALID_CURRENT_ENVIRONMENT_NAME = '';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from './constants';
// eslint-disable-next-line import/prefer-default-export
export const currentEnvironmentName = ({ currentEnvironmentId, environments }) => {
const environment = environments.find(({ id }) => id === currentEnvironmentId);
return environment ? environment.name : INVALID_CURRENT_ENVIRONMENT_NAME;
};
import * as actions from './actions'; import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations'; import mutations from './mutations';
import state from './state'; import state from './state';
export default () => ({ export default () => ({
namespaced: true, namespaced: true,
actions, actions,
getters,
mutations, mutations,
state, state,
}); });
// eslint-disable-next-line import/prefer-default-export export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
export const RECEIVE_ENVIRONMENTS_ERROR = 'RECEIVE_ENVIRONMENTS_ERROR';
export const SET_CURRENT_ENVIRONMENT_ID = 'SET_CURRENT_ENVIRONMENT_ID';
export const REQUEST_WAF_STATISTICS = 'REQUEST_WAF_STATISTICS';
export const RECEIVE_WAF_STATISTICS_SUCCESS = 'RECEIVE_WAF_STATISTICS_SUCCESS';
export const RECEIVE_WAF_STATISTICS_ERROR = 'RECEIVE_WAF_STATISTICS_ERROR';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
[types.SET_ENDPOINT](state, payload) { [types.SET_ENDPOINTS](state, { wafStatisticsEndpoint, environmentsEndpoint }) {
state.endpoint = payload; state.wafStatisticsEndpoint = wafStatisticsEndpoint;
state.environmentsEndpoint = environmentsEndpoint;
},
[types.REQUEST_ENVIRONMENTS](state) {
state.isLoadingEnvironments = true;
state.errorLoadingEnvironments = false;
},
[types.RECEIVE_ENVIRONMENTS_SUCCESS](state, payload) {
state.environments = payload;
state.isLoadingEnvironments = false;
state.errorLoadingEnvironments = false;
},
[types.RECEIVE_ENVIRONMENTS_ERROR](state) {
state.isLoadingEnvironments = false;
state.errorLoadingEnvironments = true;
},
[types.SET_CURRENT_ENVIRONMENT_ID](state, payload) {
state.currentEnvironmentId = payload;
},
[types.REQUEST_WAF_STATISTICS](state) {
state.isLoadingWafStatistics = true;
state.errorLoadingWafStatistics = false;
},
[types.RECEIVE_WAF_STATISTICS_SUCCESS](state, payload) {
state.wafStatistics = convertObjectPropsToCamelCase(payload);
state.isLoadingWafStatistics = false;
state.errorLoadingWafStatistics = false;
},
[types.RECEIVE_WAF_STATISTICS_ERROR](state) {
state.isLoadingWafStatistics = false;
state.errorLoadingWafStatistics = true;
}, },
}; };
export default () => ({ export default () => ({
endpoint: '', environmentsEndpoint: '',
environments: [],
isLoadingEnvironments: false,
errorLoadingEnvironments: false,
currentEnvironmentId: -1,
wafStatisticsEndpoint: '',
wafStatistics: {
totalTraffic: 0,
trafficAllowed: 0,
trafficBlocked: 0,
history: {
allowed: [],
blocked: [],
},
},
isWafStatisticsLoading: false,
errorLoadingWafStatistics: false,
}); });
- breadcrumb_title s_("ThreatMonitoring|Threat Monitoring") - breadcrumb_title s_("ThreatMonitoring|Threat Monitoring")
- page_title s_("ThreatMonitoring|Threat Monitoring") - page_title s_("ThreatMonitoring|Threat Monitoring")
- default_environment_id = @project.default_environment&.id || -1
#js-threat-monitoring-app{ data: { documentation_path: help_page_path('user/clusters/applications', anchor: 'web-application-firewall-modsecurity'), #js-threat-monitoring-app{ data: { documentation_path: help_page_path('user/clusters/applications', anchor: 'web-application-firewall-modsecurity'),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'), empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
is_waf_setup: 'false', waf_statistics_endpoint: 'dummy',
endpoint: 'dummy', environments_endpoint: project_environments_path(@project),
default_environment_id: default_environment_id,
} } } }
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThreatMonitoringApp component given the WAF is not set up shows only the empty state 1`] = ` exports[`ThreatMonitoringApp component given there is a default environment shows the alert 1`] = `
<glemptystate-stub <glalert-stub
description="A Web Application Firewall (WAF) provides monitoring and rules to protect production applications. GitLab adds the modsecurity WAF plug-in when you install the Ingress app in your Kubernetes cluster." class="my-3"
primarybuttonlink="/docs" dismissible="true"
primarybuttontext="Learn more" dismisslabel="Dismiss"
svgpath="/svgs" primarybuttonlink=""
title="Web Application Firewall not enabled" primarybuttontext=""
/> secondarybuttonlink="/docs"
`; secondarybuttontext="View documentation"
title=""
exports[`ThreatMonitoringApp component given the WAF is set up dismissing the alert hides the alert 1`] = ` variant="info"
<section> >
<!---->
The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the "?" icon next to the title below.
<header
class="my-3"
>
<h2
class="h4 mb-1"
>
Threat Monitoring
<gllink-stub
aria-label="Threat Monitoring help page link"
href="/docs"
target="_blank"
>
<glicon-stub
name="question"
size="16"
/>
</gllink-stub>
</h2>
</header>
</section>
`;
exports[`ThreatMonitoringApp component given the WAF is set up shows the alert and header 1`] = `
<section>
<glalert-stub
class="my-3"
dismissible="true"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink="/docs"
secondarybuttontext="View documentation"
title=""
variant="tip"
>
The graph below is an overview of traffic coming to your application. View the documentation for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app.
</glalert-stub> </glalert-stub>
<header
class="my-3"
>
<h2
class="h4 mb-1"
>
Threat Monitoring
<gllink-stub
aria-label="Threat Monitoring help page link"
href="/docs"
target="_blank"
>
<glicon-stub
name="question"
size="16"
/>
</gllink-stub>
</h2>
</header>
</section>
`; `;
import { createLocalVue, shallowMount } from '@vue/test-utils'; import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlEmptyState } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import createStore from 'ee/threat_monitoring/store'; import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue'; import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
const localVue = createLocalVue(); const localVue = createLocalVue();
const endpoint = TEST_HOST; const defaultEnvironmentId = 3;
const emptyStateSvgPath = '/svgs';
const documentationPath = '/docs'; const documentationPath = '/docs';
const emptyStateSvgPath = '/svgs';
const environmentsEndpoint = `${TEST_HOST}/environments`;
const wafStatisticsEndpoint = `${TEST_HOST}/waf`;
describe('ThreatMonitoringApp component', () => { describe('ThreatMonitoringApp component', () => {
let store; let store;
...@@ -15,8 +18,12 @@ describe('ThreatMonitoringApp component', () => { ...@@ -15,8 +18,12 @@ describe('ThreatMonitoringApp component', () => {
const factory = propsData => { const factory = propsData => {
store = createStore(); store = createStore();
Object.assign(store.state.threatMonitoring, {
environmentsEndpoint,
wafStatisticsEndpoint,
});
jest.spyOn(store, 'dispatch'); jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(ThreatMonitoringApp, { wrapper = shallowMount(ThreatMonitoringApp, {
localVue, localVue,
...@@ -26,54 +33,69 @@ describe('ThreatMonitoringApp component', () => { ...@@ -26,54 +33,69 @@ describe('ThreatMonitoringApp component', () => {
}); });
}; };
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('given the WAF is not set up', () => { describe.each([-1, NaN, Math.PI])(
beforeEach(() => { 'given an invalid default environment id of %p',
factory({ invalidEnvironmentId => {
isWafSetup: false, beforeEach(() => {
endpoint, factory({
emptyStateSvgPath, defaultEnvironmentId: invalidEnvironmentId,
documentationPath, emptyStateSvgPath,
documentationPath,
});
}); });
});
it('does not dispatch any store actions', () => { it('dispatches no actions', () => {
expect(store.dispatch).not.toHaveBeenCalled(); expect(store.dispatch).not.toHaveBeenCalled();
}); });
it('shows only the empty state', () => { it('shows only the empty state', () => {
expect(wrapper.element).toMatchSnapshot(); const emptyState = wrapper.find(GlEmptyState);
}); expect(wrapper.element).toBe(emptyState.element);
}); expect(emptyState.props()).toMatchObject({
svgPath: emptyStateSvgPath,
primaryButtonLink: documentationPath,
});
});
},
);
describe('given the WAF is set up', () => { describe('given there is a default environment', () => {
beforeEach(() => { beforeEach(() => {
factory({ factory({
isWafSetup: true, defaultEnvironmentId,
endpoint,
emptyStateSvgPath, emptyStateSvgPath,
documentationPath, documentationPath,
}); });
}); });
it('sets the endpoint on creation', () => { it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
expect(store.dispatch).toHaveBeenCalledWith('threatMonitoring/setEndpoint', endpoint); expect(store.dispatch.mock.calls).toEqual([
['threatMonitoring/setCurrentEnvironmentId', defaultEnvironmentId],
['threatMonitoring/fetchEnvironments', undefined],
]);
});
it('shows the alert', () => {
expect(findAlert().element).toMatchSnapshot();
}); });
it('shows the alert and header', () => { it('shows the filter bar', () => {
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.find(ThreatMonitoringFilters).exists()).toBe(true);
}); });
describe('dismissing the alert', () => { describe('dismissing the alert', () => {
beforeEach(() => { beforeEach(() => {
wrapper.find(GlAlert).vm.$emit('dismiss'); findAlert().vm.$emit('dismiss');
}); });
it('hides the alert', () => { it('hides the alert', () => {
expect(wrapper.element).toMatchSnapshot(); expect(findAlert().exists()).toBe(false);
}); });
}); });
}); });
......
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/store/modules/threat_monitoring/constants';
import { mockEnvironmentsResponse } from '../mock_data';
const localVue = createLocalVue();
describe('ThreatMonitoringFilters component', () => {
let store;
let wrapper;
const factory = state => {
store = createStore();
Object.assign(store.state.threatMonitoring, state);
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(ThreatMonitoringFilters, {
localVue,
store,
sync: false,
});
};
const findEnvironmentsDropdown = () => wrapper.find(GlDropdown);
const findEnvironmentsDropdownItems = () => wrapper.findAll(GlDropdownItem).wrappers;
afterEach(() => {
wrapper.destroy();
});
describe('given there are no environments', () => {
beforeEach(() => {
factory();
});
describe('the environments dropdown', () => {
it('is disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe('true');
});
it('has text set to the INVALID_CURRENT_ENVIRONMENT_NAME', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(INVALID_CURRENT_ENVIRONMENT_NAME);
});
it('has no dropdown items', () => {
expect(findEnvironmentsDropdownItems()).toHaveLength(0);
});
});
});
describe('given there are environments', () => {
const { environments } = mockEnvironmentsResponse;
const currentEnvironment = environments[1];
beforeEach(() => {
factory({
environments,
currentEnvironmentId: currentEnvironment.id,
});
});
describe('the environments dropdown', () => {
it('is not disabled', () => {
expect(findEnvironmentsDropdown().attributes().disabled).toBe(undefined);
});
it('has text set to the current environment', () => {
expect(findEnvironmentsDropdown().attributes().text).toBe(currentEnvironment.name);
});
it('has dropdown items for each environment', () => {
const dropdownItems = findEnvironmentsDropdownItems();
environments.forEach((environment, i) => {
expect(dropdownItems[i].text()).toBe(environment.name);
dropdownItems[i].vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith(
'threatMonitoring/setCurrentEnvironmentId',
environment.id,
);
});
});
});
});
});
export const mockEnvironmentsResponse = {
environments: [
{
id: 1129970,
name: 'production',
state: 'available',
},
{
id: 1156094,
name: 'review/enable-blocking-waf',
state: 'available',
},
],
available_count: 2,
stopped_count: 5,
};
export const mockWafStatisticsResponse = {
total_traffic: 31500,
traffic_allowed: 0.11,
traffic_blocked: 0.89,
history: {
allowed: [['<timestamp>', 25], ['<timestamp>', 30]],
blocked: [['<timestamp>', 15], ['<timestamp>', 20]],
},
};
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper'; import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/actions'; import * as actions from 'ee/threat_monitoring/store/modules/threat_monitoring/actions';
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types'; import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types';
import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state'; import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
import { mockEnvironmentsResponse, mockWafStatisticsResponse } from '../../../mock_data';
jest.mock('~/flash', () => jest.fn());
const environmentsEndpoint = 'environmentsEndpoint';
const wafStatisticsEndpoint = 'wafStatisticsEndpoint';
describe('Threat Monitoring actions', () => { describe('Threat Monitoring actions', () => {
describe('setEndpoint', () => { let state;
it('commits the SET_ENDPOINT mutation', () =>
beforeEach(() => {
state = getInitialState();
});
afterEach(() => {
createFlash.mockClear();
});
describe('setEndpoints', () => {
it('commits the SET_ENDPOINTS mutation', () =>
testAction(
actions.setEndpoints,
{ environmentsEndpoint, wafStatisticsEndpoint },
state,
[
{
type: types.SET_ENDPOINTS,
payload: { environmentsEndpoint, wafStatisticsEndpoint },
},
],
[],
));
});
describe('requestEnvironments', () => {
it('commits the REQUEST_ENVIRONMENTS mutation', () =>
testAction(
actions.requestEnvironments,
undefined,
state,
[
{
type: types.REQUEST_ENVIRONMENTS,
},
],
[],
));
});
describe('receiveEnvironmentsSuccess', () => {
const environments = [{ id: 1, name: 'production' }];
it('commits the RECEIVE_ENVIRONMENTS_SUCCESS mutation', () =>
testAction(
actions.receiveEnvironmentsSuccess,
environments,
state,
[
{
type: types.RECEIVE_ENVIRONMENTS_SUCCESS,
payload: environments,
},
],
[],
));
});
describe('receiveEnvironmentsError', () => {
it('commits the RECEIVE_ENVIRONMENTS_ERROR mutation', () =>
testAction(
actions.receiveEnvironmentsError,
undefined,
state,
[
{
type: types.RECEIVE_ENVIRONMENTS_ERROR,
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('fetchEnvironments', () => {
let mock;
beforeEach(() => {
state.environmentsEndpoint = environmentsEndpoint;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onGet(environmentsEndpoint).replyOnce(200, mockEnvironmentsResponse);
});
it('should dispatch the request and success actions', () =>
testAction(
actions.fetchEnvironments,
undefined,
state,
[],
[
{ type: 'requestEnvironments' },
{
type: 'receiveEnvironmentsSuccess',
payload: mockEnvironmentsResponse.environments,
},
],
));
});
describe('given more than one page of environments', () => {
beforeEach(() => {
const oneEnvironmentPerPage = ({ totalPages }) => config => {
const { page } = config.params;
const response = [200, { environments: [{ id: page }] }];
if (page < totalPages) {
response.push({ 'x-next-page': page + 1 });
}
return response;
};
mock.onGet(environmentsEndpoint).reply(oneEnvironmentPerPage({ totalPages: 3 }));
});
it('should fetch all pages and dispatch the request and success actions', () =>
testAction(
actions.fetchEnvironments,
undefined,
state,
[],
[
{ type: 'requestEnvironments' },
{
type: 'receiveEnvironmentsSuccess',
payload: [{ id: 1 }, { id: 2 }, { id: 3 }],
},
],
));
});
describe('on error', () => {
beforeEach(() => {
mock.onGet(environmentsEndpoint).replyOnce(500);
});
it('should dispatch the request and error actions', () =>
testAction(
actions.fetchEnvironments,
undefined,
state,
[],
[{ type: 'requestEnvironments' }, { type: 'receiveEnvironmentsError' }],
));
});
describe('with an empty endpoint', () => {
beforeEach(() => {
state.environmentsEndpoint = '';
});
it('should dispatch receiveEnvironmentsError', () =>
testAction(
actions.fetchEnvironments,
undefined,
state,
[],
[{ type: 'receiveEnvironmentsError' }],
));
});
});
describe('setCurrentEnvironmentId', () => {
const environmentId = 1;
it('commits the SET_CURRENT_ENVIRONMENT_ID mutation and dispatches fetchWafStatistics', () =>
testAction(
actions.setCurrentEnvironmentId,
environmentId,
state,
[{ type: types.SET_CURRENT_ENVIRONMENT_ID, payload: environmentId }],
[{ type: 'fetchWafStatistics' }],
));
});
describe('requestWafStatistics', () => {
it('commits the REQUEST_WAF_STATISTICS mutation', () =>
testAction( testAction(
actions.setEndpoint, actions.requestWafStatistics,
TEST_HOST, undefined,
getInitialState(), state,
[ [
{ {
type: types.SET_ENDPOINT, type: types.REQUEST_WAF_STATISTICS,
payload: TEST_HOST,
}, },
], ],
[], [],
)); ));
}); });
describe('receiveWafStatisticsSuccess', () => {
it('commits the RECEIVE_WAF_STATISTICS_SUCCESS mutation', () =>
testAction(
actions.receiveWafStatisticsSuccess,
mockWafStatisticsResponse,
state,
[
{
type: types.RECEIVE_WAF_STATISTICS_SUCCESS,
payload: mockWafStatisticsResponse,
},
],
[],
));
});
describe('receiveWafStatisticsError', () => {
it('commits the RECEIVE_WAF_STATISTICS_ERROR mutation', () =>
testAction(
actions.receiveWafStatisticsError,
undefined,
state,
[
{
type: types.RECEIVE_WAF_STATISTICS_ERROR,
},
],
[],
).then(() => {
expect(createFlash).toHaveBeenCalled();
}));
});
describe('fetchWafStatistics', () => {
let mock;
const currentEnvironmentId = 3;
beforeEach(() => {
state.wafStatisticsEndpoint = wafStatisticsEndpoint;
state.currentEnvironmentId = currentEnvironmentId;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock
.onGet(wafStatisticsEndpoint, { params: { environment_id: currentEnvironmentId } })
.replyOnce(200, mockWafStatisticsResponse);
});
it('should dispatch the request and success actions', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[
{ type: 'requestWafStatistics' },
{
type: 'receiveWafStatisticsSuccess',
payload: mockWafStatisticsResponse,
},
],
));
});
describe('on error', () => {
beforeEach(() => {
mock.onGet(wafStatisticsEndpoint).replyOnce(500);
});
it('should dispatch the request and error actions', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[{ type: 'requestWafStatistics' }, { type: 'receiveWafStatisticsError' }],
));
});
describe('with an empty endpoint', () => {
beforeEach(() => {
state.wafStatisticsEndpoint = '';
});
it('should dispatch receiveWafStatisticsError', () =>
testAction(
actions.fetchWafStatistics,
undefined,
state,
[],
[{ type: 'receiveWafStatisticsError' }],
));
});
});
}); });
import createState from 'ee/threat_monitoring/store/modules/threat_monitoring/state';
import * as getters from 'ee/threat_monitoring/store/modules/threat_monitoring/getters';
import { INVALID_CURRENT_ENVIRONMENT_NAME } from 'ee/threat_monitoring/store/modules/threat_monitoring/constants';
describe('threatMonitoring module getters', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('currentEnvironmentName', () => {
describe.each`
context | currentEnvironmentId | environments | expectedName
${'no environments'} | ${1} | ${[]} | ${INVALID_CURRENT_ENVIRONMENT_NAME}
${'a non-existent environment id'} | ${2} | ${[{ id: 1 }]} | ${INVALID_CURRENT_ENVIRONMENT_NAME}
${'an existing environment id'} | ${3} | ${[{ id: 3, name: 'foo' }]} | ${'foo'}
`('given $context', ({ currentEnvironmentId, environments, expectedName }) => {
beforeEach(() => {
state.currentEnvironmentId = currentEnvironmentId;
state.environments = environments;
});
it('returns the expected name', () => {
expect(getters.currentEnvironmentName(state)).toBe(expectedName);
});
});
});
});
import { TEST_HOST } from 'helpers/test_constants';
import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types'; import * as types from 'ee/threat_monitoring/store/modules/threat_monitoring/mutation_types';
import mutations from 'ee/threat_monitoring/store/modules/threat_monitoring/mutations'; import mutations from 'ee/threat_monitoring/store/modules/threat_monitoring/mutations';
import getInitialState from 'ee/threat_monitoring/store/modules/threat_monitoring/state'; import { mockWafStatisticsResponse } from '../../../mock_data';
describe('Threat Monitoring mutations', () => { describe('Threat Monitoring mutations', () => {
let state; let state;
beforeEach(() => { beforeEach(() => {
state = getInitialState(); state = {};
}); });
describe(types.SET_ENDPOINT, () => { describe(types.SET_ENDPOINTS, () => {
it('sets the endpoint', () => { it('sets the endpoints', () => {
mutations[types.SET_ENDPOINT](state, TEST_HOST); const endpoints = { wafStatisticsEndpoint: 'waf', environmentsEndpoint: 'envs' };
expect(state.endpoint).toBe(TEST_HOST); mutations[types.SET_ENDPOINTS](state, endpoints);
expect(state).toEqual(expect.objectContaining(endpoints));
});
});
describe(types.REQUEST_ENVIRONMENTS, () => {
beforeEach(() => {
mutations[types.REQUEST_ENVIRONMENTS](state);
});
it('sets isLoadingEnvironments to true', () => {
expect(state.isLoadingEnvironments).toBe(true);
});
it('sets errorLoadingEnvironments to false', () => {
expect(state.errorLoadingEnvironments).toBe(false);
});
});
describe(types.RECEIVE_ENVIRONMENTS_SUCCESS, () => {
let environments;
beforeEach(() => {
environments = [{ id: 1, name: 'production' }];
mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments);
});
it('sets environments to the payload', () => {
expect(state.environments).toBe(environments);
});
it('sets isLoadingEnvironments to false', () => {
expect(state.isLoadingEnvironments).toBe(false);
});
it('sets errorLoadingEnvironments to false', () => {
expect(state.errorLoadingEnvironments).toBe(false);
});
});
describe(types.RECEIVE_ENVIRONMENTS_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_ENVIRONMENTS_ERROR](state);
});
it('sets isLoadingEnvironments to false', () => {
expect(state.isLoadingEnvironments).toBe(false);
});
it('sets errorLoadingEnvironments to true', () => {
expect(state.errorLoadingEnvironments).toBe(true);
});
});
describe(types.SET_CURRENT_ENVIRONMENT_ID, () => {
const environmentId = 3;
beforeEach(() => {
mutations[types.SET_CURRENT_ENVIRONMENT_ID](state, environmentId);
});
it('sets currentEnvironmentId', () => {
expect(state.currentEnvironmentId).toBe(environmentId);
});
});
describe(types.REQUEST_WAF_STATISTICS, () => {
beforeEach(() => {
mutations[types.REQUEST_WAF_STATISTICS](state);
});
it('sets isLoadingWafStatistics to true', () => {
expect(state.isLoadingWafStatistics).toBe(true);
});
it('sets errorLoadingWafStatistics to false', () => {
expect(state.errorLoadingWafStatistics).toBe(false);
});
});
describe(types.RECEIVE_WAF_STATISTICS_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_WAF_STATISTICS_SUCCESS](state, mockWafStatisticsResponse);
});
it('sets wafStatistics according to the payload', () => {
expect(state.wafStatistics).toEqual({
totalTraffic: mockWafStatisticsResponse.total_traffic,
trafficAllowed: mockWafStatisticsResponse.traffic_allowed,
trafficBlocked: mockWafStatisticsResponse.traffic_blocked,
history: mockWafStatisticsResponse.history,
});
});
it('sets isLoadingWafStatistics to false', () => {
expect(state.isLoadingWafStatistics).toBe(false);
});
it('sets errorLoadingWafStatistics to false', () => {
expect(state.errorLoadingWafStatistics).toBe(false);
});
});
describe(types.RECEIVE_WAF_STATISTICS_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_WAF_STATISTICS_ERROR](state);
});
it('sets isLoadingWafStatistics to false', () => {
expect(state.isLoadingWafStatistics).toBe(false);
});
it('sets errorLoadingWafStatistics to true', () => {
expect(state.errorLoadingWafStatistics).toBe(true);
}); });
}); });
}); });
...@@ -10318,6 +10318,9 @@ msgstr "" ...@@ -10318,6 +10318,9 @@ msgstr ""
msgid "Learn GitLab" msgid "Learn GitLab"
msgstr "" msgstr ""
msgid "Learn More"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}" msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
msgstr "" msgstr ""
...@@ -18388,7 +18391,19 @@ msgstr "" ...@@ -18388,7 +18391,19 @@ msgstr ""
msgid "ThreatMonitoring|A Web Application Firewall (WAF) provides monitoring and rules to protect production applications. GitLab adds the modsecurity WAF plug-in when you install the Ingress app in your Kubernetes cluster." msgid "ThreatMonitoring|A Web Application Firewall (WAF) provides monitoring and rules to protect production applications. GitLab adds the modsecurity WAF plug-in when you install the Ingress app in your Kubernetes cluster."
msgstr "" msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application. View the documentation for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app." msgid "ThreatMonitoring|At this time, threat monitoring only supports WAF data."
msgstr ""
msgid "ThreatMonitoring|Environment"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch WAF statistics"
msgstr ""
msgid "ThreatMonitoring|Something went wrong, unable to fetch environments"
msgstr ""
msgid "ThreatMonitoring|The graph below is an overview of traffic coming to your application as tracked by the Web Application Firewall (WAF). View the docs for instructions on how to access the WAF logs to see what type of malicious traffic is trying to access your app. The docs link is also accessible by clicking the \"?\" icon next to the title below."
msgstr "" msgstr ""
msgid "ThreatMonitoring|Threat Monitoring" msgid "ThreatMonitoring|Threat Monitoring"
...@@ -18397,6 +18412,9 @@ msgstr "" ...@@ -18397,6 +18412,9 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link" msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr "" msgstr ""
msgid "ThreatMonitoring|View WAF documentation"
msgstr ""
msgid "ThreatMonitoring|Web Application Firewall not enabled" msgid "ThreatMonitoring|Web Application Firewall not enabled"
msgstr "" msgstr ""
......
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