Commit 1deb1dfb authored by Mark Florian's avatar Mark Florian Committed by Natalia Tepluhina

Implement store and environment filter

Part of [WAF statistics reporting][1].

This adds the bulk of the Vuex store for fetching environments and
the WAF statistics, and the filter bar component which allows selection
of the current environment.

This also:

- Aligns the UI with the latest designs, including the help link
  popover, copy, and the alert variant
- Fixes/improves existing tests (avoiding verbose snapshots)

[1]: https://gitlab.com/gitlab-org/gitlab/issues/14707
parent 8feea12e
<script>
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 ThreatMonitoringFilters from './threat_monitoring_filters.vue';
export default {
name: 'ThreatMonitoring',
......@@ -10,14 +11,12 @@ export default {
GlEmptyState,
GlIcon,
GlLink,
GlPopover,
ThreatMonitoringFilters,
},
props: {
isWafSetup: {
type: Boolean,
required: true,
},
endpoint: {
type: String,
defaultEnvironmentId: {
type: Number,
required: true,
},
emptyStateSvgPath: {
......@@ -32,15 +31,25 @@ export default {
data() {
return {
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() {
if (this.isWafSetup) {
this.setEndpoint(this.endpoint);
if (this.isWafMaybeSetUp) {
this.setCurrentEnvironmentId(this.defaultEnvironmentId);
this.fetchEnvironments();
}
},
methods: {
...mapActions('threatMonitoring', ['setEndpoint']),
...mapActions('threatMonitoring', ['fetchEnvironments', 'setCurrentEnvironmentId']),
isValidEnvironmentId(id) {
return Number.isInteger(id) && id >= 0;
},
dismissAlert() {
this.showAlert = false;
},
......@@ -52,28 +61,30 @@ export default {
),
alertText: s__(
`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.`,
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.`,
),
helpPopoverTitle: s__('ThreatMonitoring|At this time, threat monitoring only supports WAF data.'),
};
</script>
<template>
<gl-empty-state
v-if="!isWafSetup"
v-if="!isWafMaybeSetUp"
:title="s__('ThreatMonitoring|Web Application Firewall not enabled')"
:description="$options.emptyStateDescription"
:svg-path="emptyStateSvgPath"
:primary-button-link="documentationPath"
:primary-button-text="__('Learn more')"
:primary-button-text="__('Learn More')"
/>
<section v-else>
<gl-alert
v-if="showAlert"
class="my-3"
variant="tip"
variant="info"
:secondary-button-link="documentationPath"
:secondary-button-text="__('View documentation')"
@dismiss="dismissAlert"
......@@ -84,13 +95,25 @@ export default {
<h2 class="h4 mb-1">
{{ s__('ThreatMonitoring|Threat Monitoring') }}
<gl-link
ref="helpLink"
target="_blank"
:href="documentationPath"
:aria-label="s__('ThreatMonitoring|Threat Monitoring help page link')"
>
<gl-icon name="question" />
</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>
</header>
<threat-monitoring-filters />
</section>
</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 ThreatMonitoringApp from './components/app.vue';
import createStore from './store';
import { parseBoolean } from '~/lib/utils/common_utils';
export default () => {
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();
store.dispatch('threatMonitoring/setEndpoints', {
wafStatisticsEndpoint,
environmentsEndpoint,
});
return new Vue({
el,
......@@ -15,10 +24,9 @@ export default () => {
render(createElement) {
return createElement(ThreatMonitoringApp, {
props: {
isWafSetup: parseBoolean(isWafSetup),
endpoint,
emptyStateSvgPath,
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';
// eslint-disable-next-line import/prefer-default-export
export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint);
export const setEndpoints = ({ commit }, endpoints) => commit(types.SET_ENDPOINTS, endpoints);
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 getters from './getters';
import mutations from './mutations';
import state from './state';
export default () => ({
namespaced: true,
actions,
getters,
mutations,
state,
});
// eslint-disable-next-line import/prefer-default-export
export const SET_ENDPOINT = 'SET_ENDPOINT';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
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';
export default {
[types.SET_ENDPOINT](state, payload) {
state.endpoint = payload;
[types.SET_ENDPOINTS](state, { wafStatisticsEndpoint, environmentsEndpoint }) {
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 () => ({
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")
- 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'),
empty_state_svg_path: image_path('illustrations/security-dashboard-empty-state.svg'),
is_waf_setup: 'false',
endpoint: 'dummy',
waf_statistics_endpoint: 'dummy',
environments_endpoint: project_environments_path(@project),
default_environment_id: default_environment_id,
} }
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThreatMonitoringApp component given the WAF is not set up shows only the empty state 1`] = `
<glemptystate-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."
primarybuttonlink="/docs"
primarybuttontext="Learn more"
svgpath="/svgs"
title="Web Application Firewall not enabled"
/>
`;
exports[`ThreatMonitoringApp component given the WAF is set up dismissing the alert hides the alert 1`] = `
<section>
<!---->
<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.
exports[`ThreatMonitoringApp component given there is a default environment shows the alert 1`] = `
<glalert-stub
class="my-3"
dismissible="true"
dismisslabel="Dismiss"
primarybuttonlink=""
primarybuttontext=""
secondarybuttonlink="/docs"
secondarybuttontext="View documentation"
title=""
variant="info"
>
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.
</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>
</glalert-stub>
`;
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 createStore from 'ee/threat_monitoring/store';
import ThreatMonitoringApp from 'ee/threat_monitoring/components/app.vue';
import ThreatMonitoringFilters from 'ee/threat_monitoring/components/threat_monitoring_filters.vue';
const localVue = createLocalVue();
const endpoint = TEST_HOST;
const emptyStateSvgPath = '/svgs';
const defaultEnvironmentId = 3;
const documentationPath = '/docs';
const emptyStateSvgPath = '/svgs';
const environmentsEndpoint = `${TEST_HOST}/environments`;
const wafStatisticsEndpoint = `${TEST_HOST}/waf`;
describe('ThreatMonitoringApp component', () => {
let store;
......@@ -15,8 +18,12 @@ describe('ThreatMonitoringApp component', () => {
const factory = propsData => {
store = createStore();
Object.assign(store.state.threatMonitoring, {
environmentsEndpoint,
wafStatisticsEndpoint,
});
jest.spyOn(store, 'dispatch');
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(ThreatMonitoringApp, {
localVue,
......@@ -26,54 +33,69 @@ describe('ThreatMonitoringApp component', () => {
});
};
const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
});
describe('given the WAF is not set up', () => {
beforeEach(() => {
factory({
isWafSetup: false,
endpoint,
emptyStateSvgPath,
documentationPath,
describe.each([-1, NaN, Math.PI])(
'given an invalid default environment id of %p',
invalidEnvironmentId => {
beforeEach(() => {
factory({
defaultEnvironmentId: invalidEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
});
});
it('does not dispatch any store actions', () => {
expect(store.dispatch).not.toHaveBeenCalled();
});
it('dispatches no actions', () => {
expect(store.dispatch).not.toHaveBeenCalled();
});
it('shows only the empty state', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('shows only the empty state', () => {
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(() => {
factory({
isWafSetup: true,
endpoint,
defaultEnvironmentId,
emptyStateSvgPath,
documentationPath,
});
});
it('sets the endpoint on creation', () => {
expect(store.dispatch).toHaveBeenCalledWith('threatMonitoring/setEndpoint', endpoint);
it('dispatches the setCurrentEnvironmentId and fetchEnvironments actions', () => {
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', () => {
expect(wrapper.element).toMatchSnapshot();
it('shows the filter bar', () => {
expect(wrapper.find(ThreatMonitoringFilters).exists()).toBe(true);
});
describe('dismissing the alert', () => {
beforeEach(() => {
wrapper.find(GlAlert).vm.$emit('dismiss');
findAlert().vm.$emit('dismiss');
});
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 { TEST_HOST } from 'helpers/test_constants';
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 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('setEndpoint', () => {
it('commits the SET_ENDPOINT mutation', () =>
let state;
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(
actions.setEndpoint,
TEST_HOST,
getInitialState(),
actions.requestWafStatistics,
undefined,
state,
[
{
type: types.SET_ENDPOINT,
payload: TEST_HOST,
type: types.REQUEST_WAF_STATISTICS,
},
],
[],
));
});
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 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', () => {
let state;
beforeEach(() => {
state = getInitialState();
state = {};
});
describe(types.SET_ENDPOINT, () => {
it('sets the endpoint', () => {
mutations[types.SET_ENDPOINT](state, TEST_HOST);
describe(types.SET_ENDPOINTS, () => {
it('sets the endpoints', () => {
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 ""
msgid "Learn GitLab"
msgstr ""
msgid "Learn More"
msgstr ""
msgid "Learn how to %{link_start}contribute to the built-in templates%{link_end}"
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."
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 ""
msgid "ThreatMonitoring|Threat Monitoring"
......@@ -18397,6 +18412,9 @@ msgstr ""
msgid "ThreatMonitoring|Threat Monitoring help page link"
msgstr ""
msgid "ThreatMonitoring|View WAF documentation"
msgstr ""
msgid "ThreatMonitoring|Web Application Firewall not enabled"
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