Commit 8ab82896 authored by samdbeckham's avatar samdbeckham

Adds the counts to the security dashboard

- Improves the vulnerabilities store to accomodate the counts
- Adds Components for the vulnerability counts
- Adds extra actions, getters, and methods for the counts
- Adds skeletal loading to the vulnerabilities tablet
- Adds integration tests for the components that use stores
parent 22192158
......@@ -7,7 +7,7 @@ import { BYTES_IN_KIB } from './constants';
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
*/
*/
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
......@@ -80,3 +80,22 @@ export function numberToHumanSize(size) {
}
return `${bytesToGiB(size).toFixed(2)} GiB`;
}
/**
* A simple method that returns the value of a + b
* It seems unessesary, but when combined with a reducer it
* adds up all the values in an array.
*
* e.g. `[1, 2, 3, 4, 5].reduce(sum) // => 15`
*
* @param {Float} a
* @param {Float} b
* @example
* // return 15
* [1, 2, 3, 4, 5].reduce(sum);
*
* // returns 6
* Object.values([{a: 1, b: 2, c: 3].reduce(sum);
* @returns {Float} The summed value
*/
export const sum = (a = 0, b = 0) => a + b;
<script>
import { mapActions, mapGetters } from 'vuex';
import Tabs from '~/vue_shared/components/tabs/tabs';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import SecurityDashboardTable from './security_dashboard_table.vue';
import VulnerabilityCountList from './vulnerability_count_list.vue';
export default {
name: 'SecurityDashboardApp',
......@@ -9,31 +11,34 @@ export default {
Tabs,
Tab,
SecurityDashboardTable,
VulnerabilityCountList,
},
computed: {
count() {
// TODO: Get the count from the overview API
return {
sast: null,
};
},
showSastCount() {
return this.count && this.count.sast;
...mapGetters('vulnerabilities', ['vulnerabilitiesCountByReportType']),
sastCount() {
return this.vulnerabilitiesCountByReportType('sast');
},
},
created() {
this.fetchVulnerabilitiesCount();
},
methods: {
...mapActions('vulnerabilities', ['fetchVulnerabilitiesCount']),
},
};
</script>
<template>
<div>
<vulnerability-count-list />
<tabs stop-propagation>
<tab active>
<template slot="title">
{{ __('SAST') }}
<span
v-if="showSastCount"
v-if="sastCount"
class="badge badge-pill">
{{ count.sast }}
{{ sastCount }}
</span>
</template>
......@@ -42,4 +47,3 @@ export default {
</tabs>
</div>
</template>
<script>
import { mapGetters, mapActions } from 'vuex';
import { mapActions, mapState } from 'vuex';
import Pagination from '~/vue_shared/components/pagination_links.vue';
import SecurityDashboardTableRow from './security_dashboard_table_row.vue';
export default {
name: 'SecurityDashboardTable',
components: {
SecurityDashboardTableRow,
Pagination,
SecurityDashboardTableRow,
},
computed: {
...mapGetters(['vulnerabilities', 'pageInfo', 'isLoading']),
...mapState('vulnerabilities', ['vulnerabilities', 'pageInfo', 'isLoadingVulnerabilities']),
showPagination() {
return this.pageInfo && this.pageInfo.total;
},
......@@ -19,7 +19,7 @@ export default {
this.fetchVulnerabilities();
},
methods: {
...mapActions(['fetchVulnerabilities']),
...mapActions('vulnerabilities', ['fetchVulnerabilities']),
},
};
</script>
......@@ -27,7 +27,7 @@ export default {
<template>
<div class="ci-table">
<div
class="gl-responsive-table-row table-row-header"
class="gl-responsive-table-row table-row-header vulnerabilities-row-header"
role="row"
>
<div
......@@ -50,10 +50,13 @@ export default {
</div>
</div>
<gl-loading-icon
v-if="isLoading"
:size="2"
/>
<div v-if="isLoadingVulnerabilities">
<security-dashboard-table-row
v-for="n in 10"
:key="n"
:is-loading="true"
/>
</div>
<div v-else>
<security-dashboard-table-row
......@@ -72,3 +75,10 @@ export default {
</div>
</template>
<style>
.vulnerabilities-row-header {
color: #707070;
padding-left: 0.4em;
padding-right: 0.4em;
}
</style>
<script>
import { SkeletonLoading } from '@gitlab-org/gitlab-ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import SecurityDashboardActionButtons from './security_dashboard_action_buttons.vue';
......@@ -7,11 +8,18 @@ export default {
components: {
SeverityBadge,
SecurityDashboardActionButtons,
SkeletonLoading,
},
props: {
vulnerability: {
type: Object,
required: true,
required: false,
default: () => ({}),
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
......@@ -19,10 +27,7 @@ export default {
return this.vulnerability.confidence || '';
},
severity() {
return this.vulnerability.severity || '';
},
description() {
return this.vulnerability.description;
return this.vulnerability.severity || ' ';
},
projectNamespace() {
const { project } = this.vulnerability;
......@@ -54,13 +59,20 @@ export default {
{{ s__('Reports|Vulnerability') }}
</div>
<div class="table-mobile-content">
<span>{{ description }}</span>
<br />
<span
v-if="projectNamespace"
class="vulnerability-namespace">
{{ projectNamespace }}
</span>
<skeleton-loading
v-if="isLoading"
class="mt-2 js-skeleton-loader"
:lines="2"
/>
<div v-else>
<span>{{ vulnerability.description }}</span>
<br />
<span
v-if="projectNamespace"
class="vulnerability-namespace">
{{ projectNamespace }}
</span>
</div>
</div>
</div>
......@@ -71,7 +83,7 @@ export default {
>
{{ s__('Reports|Confidence') }}
</div>
<div class="table-mobile-content">
<div class="table-mobile-content text-capitalize">
{{ confidence }}
</div>
</div>
......@@ -97,7 +109,7 @@ export default {
<style>
@media (min-width: 768px) {
.vulnerabilities-row {
padding: .6em .4em;
padding: 0.6em 0.4em;
}
.vulnerabilities-row:hover,
......@@ -126,6 +138,6 @@ export default {
.vulnerability-namespace {
color: #707070;
font-size: .8em;
font-size: 0.8em;
}
</style>
<script>
export default {
name: 'VulnerabilityCount',
props: {
severity: {
type: String,
required: true,
},
count: {
type: Number,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
className() {
return `vulnerability-count-${this.severity}`;
},
},
};
</script>
<template>
<div
class="vulnerability-count"
:class="className"
>
<div class="vulnerability-count-header">
{{ severity }}
</div>
<div class="vulnerability-count-body">
<span v-if="isLoading">&nbsp;</span>
<span v-else>{{ count }}</span>
</div>
</div>
</template>
<style>
.vulnerability-count {
background-color: #fafafa;
border-radius: 0.6em;
color: #505050;
display: block;
font-weight: bold;
margin-bottom: 1em;
overflow: hidden;
text-align: center;
}
.vulnerability-count-header {
background-color: #f2f2f2;
display: block;
padding: 0.4em;
text-transform: capitalize;
}
.vulnerability-count-body {
display: block;
font-size: 2em;
padding: 0.8em;
}
.vulnerability-count-critical {
background-color: #fff6f5;
color: #c0341e;
}
.vulnerability-count-critical .vulnerability-count-header {
background-color: #fae5e1;
}
.vulnerability-count-high {
background-color: #fffaf3;
color: #de7e00;
}
.vulnerability-count-high .vulnerability-count-header {
background-color: #fff1de;
}
.vulnerability-count-medium {
background-color: #f9f7fd;
color: #6d49cb;
}
.vulnerability-count-medium .vulnerability-count-header {
background-color: #ede8fb;
}
.vulnerability-count-unknown {
background-color: #ffffff;
border: 1px solid;
color: #707070;
}
.vulnerability-count-unknown .vulnerability-count-header {
background-color: #ffffff;
border-bottom: 1px solid;
}
</style>
<script>
import { mapGetters, mapState } from 'vuex';
import VulnerabilityCount from './vulnerability_count.vue';
import { CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN } from '../store/modules/vulnerabilities/constants';
const SEVERITIES = [CRITICAL, HIGH, MEDIUM, LOW, UNKNOWN];
export default {
name: 'VulnerabilityCountList',
components: {
VulnerabilityCount,
},
computed: {
...mapGetters('vulnerabilities', ['vulnerabilitiesCountBySeverity']),
...mapState('vulnerabilities', ['isLoadingVulnerabilitiesCount']),
counts() {
return SEVERITIES.map(severity => {
const count = this.vulnerabilitiesCountBySeverity(severity);
return { severity, count };
});
},
},
};
</script>
<template>
<div class="vulnerabilities-count-list">
<div class="row">
<div
v-for="count in counts"
:key="count.severity"
class="col-md col-sm-6 js-count"
>
<vulnerability-count
:severity="count.severity"
:count="count.count"
:is-loading="isLoadingVulnerabilitiesCount"
/>
</div>
</div>
</div>
</template>
<style>
.vulnerabilities-count-list {
display: block;
padding: 2.5em 0 1.5em;
border-bottom: 1px solid #e5e5e5;
margin-bottom: 3px;
}
</style>
import axios from '~/lib/utils/axios_utils';
import * as types from './mutation_types';
import mockData from './mock_data.json';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
export const fetchVulnerabilities = ({ dispatch }, params = {}) => {
export const fetchVulnerabilitiesCount = ({ state, dispatch }) => {
dispatch('requestVulnerabilitiesCount');
axios({
method: 'GET',
url: state.vulnerabilitiesCountEndpoint,
})
.then(response => {
const { data } = response;
dispatch('receiveVulnerabilitiesCountSuccess', { data });
})
.catch(() => {
dispatch('receiveVulnerabilitiesCountError');
});
};
export const requestVulnerabilitiesCount = ({ commit }) => {
commit(types.REQUEST_VULNERABILITIES_COUNT);
};
export const receiveVulnerabilitiesCountSuccess = ({ commit }, response) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, response.data);
};
export const receiveVulnerabilitiesCountError = ({ commit }) => {
commit(types.RECEIVE_VULNERABILITIES_COUNT_ERROR);
};
export const fetchVulnerabilities = ({ state, dispatch }, page = 1) => {
dispatch('requestVulnerabilities');
// TODO: Replace with axios when we can use the actual API
Promise.resolve({
data: mockData,
headers: {
'X-Page': params.page || 1,
'X-Next-Page': 2,
'X-Prev-Page': 1,
'X-Per-Page': 20,
'X-Total': 100,
'X-Total-Pages': 5,
} })
axios({
method: 'GET',
url: state.vulnerabilitiesEndpoint,
params: { page },
})
.then(response => {
dispatch('receiveVulnerabilitiesSuccess', response);
const { headers, data } = response;
dispatch('receiveVulnerabilitiesSuccess', { headers, data });
})
.catch(error => {
dispatch('receiveVulnerabilitiesError', error);
.catch(() => {
dispatch('receiveVulnerabilitiesError');
});
};
export const requestVulnerabilities = ({ commit }) => {
commit(types.SET_LOADING, true);
commit(types.REQUEST_VULNERABILITIES);
};
export const receiveVulnerabilitiesSuccess = ({ commit }, response = {}) => {
const normalizedHeaders = normalizeHeaders(response.headers);
const paginationInformation = parseIntPagination(normalizedHeaders);
const pageInfo = parseIntPagination(normalizedHeaders);
const vulnerabilities = response.data;
commit(types.SET_LOADING, false);
commit(types.SET_VULNERABILITIES, response.data);
commit(types.SET_PAGINATION, paginationInformation);
commit(types.RECEIVE_VULNERABILITIES_SUCCESS, { pageInfo, vulnerabilities });
};
export const receiveVulnerabilitiesError = ({ commit }) => {
// TODO: Show error state when we get it from UX
commit(types.SET_LOADING, false);
commit(types.RECEIVE_VULNERABILITIES_ERROR);
};
export default () => {};
export const CRITICAL = 'critical';
export const HIGH = 'high';
export const MEDIUM = 'medium';
export const LOW = 'low';
export const UNKNOWN = 'unknown';
export const isLoading = state => state.isLoading;
export const pageInfo = state => state.pageInfo;
export const vulnerabilities = state => state.vulnerabilities || [];
import { sum } from '~/lib/utils/number_utils';
export const vulnerabilitiesCountBySeverity = state => severity =>
Object.values(state.vulnerabilitiesCount)
.map(count => count[severity])
.reduce(sum, 0);
export const vulnerabilitiesCountByReportType = state => type => {
const counts = state.vulnerabilitiesCount[type];
return counts ? Object.values(counts).reduce(sum, 0) : 0;
};
export default () => {};
......@@ -4,6 +4,7 @@ import * as getters from './getters';
import * as actions from './actions';
export default {
namespaced: true,
state,
mutations,
getters,
......
{
"sast": {
"critical": 2,
"high": 4,
"low": 7,
"medium": 8,
"unknown": 9
},
"container_scanning": {
"critical": 3,
"high": 3,
"low": 2,
"medium": 9,
"unknown": 7
},
"dependency_scanning": {
"critical": 2,
"high": 3,
"low": 9,
"medium": 4,
"unknown": 7
},
"dast": {
"critical": 2,
"high": 3,
"low": 9,
"medium": 4,
"unknown": 7
}
}
export const SET_LOADING = 'SET_LOADING';
export const SET_PAGINATION = 'SET_PAGINATION';
export const SET_VULNERABILITIES = 'SET_VULNERABILITIES';
export const REQUEST_VULNERABILITIES = 'REQUEST_VULNERABILITIES';
export const RECEIVE_VULNERABILITIES_SUCCESS = 'RECEIVE_VULNERABILITIES_SUCCESS';
export const RECEIVE_VULNERABILITIES_ERROR = 'RECEIVE_VULNERABILITIES_ERROR';
export const REQUEST_VULNERABILITIES_COUNT = 'REQUEST_VULNERABILITIES_COUNT';
export const RECEIVE_VULNERABILITIES_COUNT_SUCCESS = 'RECEIVE_VULNERABILITIES_COUNT_SUCCESS';
export const RECEIVE_VULNERABILITIES_COUNT_ERROR = 'RECEIVE_VULNERABILITIES_COUNT_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_LOADING](state, payload) {
state.isLoading = payload;
[types.REQUEST_VULNERABILITIES](state) {
state.isLoadingVulnerabilities = true;
},
[types.SET_PAGINATION](state, payload) {
state.pageInfo = payload;
[types.RECEIVE_VULNERABILITIES_SUCCESS](state, payload) {
state.isLoadingVulnerabilities = false;
state.errorLoadingVulnerabilities = false;
state.pageInfo = payload.pageInfo;
state.vulnerabilities = payload.vulnerabilities;
},
[types.SET_VULNERABILITIES](state, payload) {
state.vulnerabilities = payload;
[types.RECEIVE_VULNERABILITIES_ERROR](state) {
state.isLoadingVulnerabilities = false;
state.errorLoadingVulnerabilities = true;
},
[types.REQUEST_VULNERABILITIES_COUNT](state) {
state.isLoadingVulnerabilitiesCount = true;
},
[types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS](state, payload) {
state.isLoadingVulnerabilitiesCount = false;
state.errorLoadingVulnerabilities = false;
state.vulnerabilitiesCount = payload;
},
[types.RECEIVE_VULNERABILITIES_COUNT_ERROR](state) {
state.isLoadingVulnerabilitiesCount = false;
state.errorLoadingVulnerabilities = true;
},
};
export default () => ({
vulnerabilitiesUrl: false,
vulnerabilities: [],
isLoadingVulnerabilities: false,
isLoadingVulnerabilitiesCount: false,
pageInfo: {},
isLoading: false,
vulnerabilities: [],
vulnerabilitiesCount: {},
errorLoadingVulnerabilities: false,
});
<script>
export default {
name: 'SeverityBadge',
props: {
......@@ -10,7 +9,7 @@ export default {
},
computed: {
className() {
return `severity-badge severity-badge-${this.severity}`;
return `severity-badge-${this.severity}`;
},
},
};
......@@ -18,6 +17,7 @@ export default {
<template>
<div
class="severity-badge"
:class="className"
>{{ severity }}</div>
</template>
......@@ -25,13 +25,13 @@ export default {
<style>
.severity-badge {
background-color: #f2f2f2;
border-radius: .3em;
border-radius: 0.3em;
color: #505050;
display: inline-block;
font-size: .9em;
font-size: 0.9em;
font-weight: bold;
line-height: 1em;
padding: .6em .4em .4em;
padding: 0.6em 0.4em 0.4em;
text-transform: uppercase;
}
......@@ -56,4 +56,3 @@ export default {
color: #707070;
}
</style>
---
title: Adds group-level Security Dashboard counts
merge_request: 7564
author:
type: added
......@@ -4,117 +4,73 @@ import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Security Dashboard Table Row', () => {
let vm;
let vulnerability;
let props;
const Component = Vue.extend(component);
afterEach(() => {
vm.$destroy();
});
describe('severity', () => {
it('should pass high severity down to the component', () => {
vulnerability = { severity: 'high' };
vm = mountComponent(Component, { vulnerability });
expect(vm.severity).toBe(vulnerability.severity);
});
it('should compute a `–` when no severity is passed', () => {
vulnerability = {};
vm = mountComponent(Component, { vulnerability });
expect(vm.severity).toBe('');
});
});
describe('description', () => {
it('should pass high confidence down to the component', () => {
vulnerability = { description: 'high' };
vm = mountComponent(Component, { vulnerability });
expect(vm.description).toBe(vulnerability.description);
});
});
describe('project namespace', () => {
it('should get the project namespace from the vulnerability', () => {
vulnerability = {
project: { name_with_namespace: 'project name' },
};
vm = mountComponent(Component, { vulnerability });
expect(vm.projectNamespace).toBe(vulnerability.project.name_with_namespace);
});
it('should return null when no namespace is set', () => {
vulnerability = { project: {} };
vm = mountComponent(Component, { vulnerability });
expect(vm.projectNamespace).toBeNull();
describe('when loading', () => {
beforeEach(() => {
props = { isLoading: true };
vm = mountComponent(Component, props);
});
it('should return null when no project is set', () => {
vulnerability = {};
vm = mountComponent(Component, { vulnerability });
expect(vm.projectNamespace).toBeNull();
it('should display the skeleton loader', () => {
expect(vm.$el.querySelector('.js-skeleton-loader')).not.toBeNull();
});
});
describe('confidence', () => {
it('should pass high confidence down to the component', () => {
vulnerability = { confidence: 'high' };
vm = mountComponent(Component, { vulnerability });
expect(vm.confidence).toBe(vulnerability.confidence);
it('should render a ` ` for severity', () => {
expect(vm.severity).toEqual(' ');
expect(vm.$el.querySelectorAll('.table-mobile-content')[0].textContent).toContain(' ');
});
it('should compute a `–` when no confidence is passed', () => {
vulnerability = {};
vm = mountComponent(Component, { vulnerability });
expect(vm.confidence).toBe('');
it('should render a `–` for confidence', () => {
expect(vm.confidence).toEqual('');
expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent).toContain('');
});
});
describe('rendered output', () => {
describe('when loaded', () => {
beforeEach(() => {
vulnerability = {
const vulnerability = {
severity: 'high',
description: 'Test vulnerability',
confidence: 'medium',
project: { name_with_namespace: 'project name' },
confidence: 'high',
description: 'this is a description',
severity: 'low',
};
vm = mountComponent(Component, { vulnerability });
props = { vulnerability };
vm = mountComponent(Component, props);
});
it('should not display the skeleton loader', () => {
expect(vm.$el.querySelector('.js-skeleton-loader')).not.toExist();
});
it('should render the severity', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[0].textContent)
.toContain(vulnerability.severity);
expect(vm.$el.querySelectorAll('.table-mobile-content')[0].textContent).toContain(
props.vulnerability.severity,
);
});
it('should render the description', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent)
.toContain(vulnerability.description);
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.description,
);
});
it('should render the project namespace', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent)
.toContain(vulnerability.project.name_with_namespace);
expect(vm.$el.querySelectorAll('.table-mobile-content')[1].textContent).toContain(
props.vulnerability.project.name_with_namespace,
);
});
it('should render the confidence', () => {
expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent)
.toContain(vulnerability.confidence);
expect(vm.$el.querySelectorAll('.table-mobile-content')[2].textContent).toContain(
props.vulnerability.confidence,
);
});
});
});
import Vue from 'vue';
import Vuex from 'vuex';
import MockAdapater from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import component from 'ee/security_dashboard/components/security_dashboard_table.vue';
import createStore from 'ee/security_dashboard/store';
import mockDataVulnerabilities from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities.json';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import waitForPromises from 'spec/helpers/wait_for_promises';
import { resetStore } from '../helpers';
describe('Security Dashboard Table', () => {
const vulnerabilities = [{ id: 0 }, { id: 1 }, { id: 2 }];
const Component = Vue.extend(component);
const vulnerabilitiesEndpoint = '/vulnerabilitiesEndpoint.json';
let store;
let mock;
let vm;
let getters;
let actions;
beforeEach(() => {
const Component = Vue.extend(component);
getters = {
vulnerabilities: () => vulnerabilities,
pageInfo: () => null,
};
actions = {
fetchVulnerabilities: jasmine.createSpy('fetchVulnerabilities'),
};
const store = new Vuex.Store({ actions, getters });
vm = mountComponentWithStore(Component, { store });
mock = new MockAdapater(axios);
store = createStore();
store.state.vulnerabilities.vulnerabilitiesEndpoint = vulnerabilitiesEndpoint;
});
afterEach(() => {
actions.fetchVulnerabilities.calls.reset();
resetStore(store);
vm.$destroy();
mock.restore();
});
it('should dispatch a `fetchVulnerabilities` action on creation', () => {
expect(actions.fetchVulnerabilities).toHaveBeenCalledTimes(1);
describe('while loading', () => {
beforeEach(() => {
store.dispatch('vulnerabilities/requestVulnerabilities');
vm = mountComponentWithStore(Component, { store });
});
it('should render 10 skeleton rows in the table', () => {
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(10);
});
});
it('should render a row for each vulnerability', () => {
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(vulnerabilities.length);
describe('with success result', () => {
beforeEach(() => {
mock.onGet(vulnerabilitiesEndpoint).replyOnce(200, mockDataVulnerabilities);
vm = mountComponentWithStore(Component, { store });
});
it('should render a row for each vulnerability', done => {
waitForPromises()
.then(() => {
expect(vm.$el.querySelectorAll('.vulnerabilities-row')).toHaveLength(
mockDataVulnerabilities.length,
);
done();
})
.catch(done.fail);
});
});
});
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_count_list.vue';
import createStore from 'ee/security_dashboard/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
describe('Vulnerability Count List', () => {
const Component = Vue.extend(component);
const store = createStore();
const counts = {
sast: {
critical: 22,
},
};
let vm;
beforeEach(() => {
store.dispatch('vulnerabilities/receiveVulnerabilitiesCountSuccess', { data: counts });
vm = mountComponentWithStore(Component, { store });
});
afterEach(() => {
vm.$destroy();
resetStore(store);
});
it('should fetch the counts for each severity', () => {
expect(vm.counts[0]).toEqual({ severity: 'critical', count: 22 });
});
it('should render a counter for each severity', () => {
expect(vm.$el.querySelectorAll('.js-count')).toHaveLength(vm.counts.length);
});
});
import Vue from 'vue';
import component from 'ee/security_dashboard/components/vulnerability_count.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Vulnerability Count', () => {
const Component = Vue.extend(component);
let vm;
let props;
beforeEach(() => {
const severity = 'high';
const count = 100;
props = { severity, count };
vm = mountComponent(Component, props);
});
afterEach(() => {
vm.$destroy();
});
it('should render the severity label', () => {
const header = vm.$el.querySelector('.vulnerability-count-header');
expect(header.textContent).toMatch(props.severity);
});
it('should render the count', () => {
const body = vm.$el.querySelector('.vulnerability-count-body');
expect(body.textContent).toMatch(props.count.toString());
});
});
import vulnerabilitiesState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
// eslint-disable-next-line import/prefer-default-export
export const resetStore = store => {
const newState = {
vulnerabilities: vulnerabilitiesState(),
};
store.replaceState(newState);
};
......@@ -3,13 +3,117 @@ import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import mockData from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data.json';
import mockDataVulnerabilities from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities.json';
import mockDataVulnerabilitiesCount from 'ee/security_dashboard/store/modules/vulnerabilities/mock_data_vulnerabilities_count.json';
import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/mutation_types';
import * as actions from 'ee/security_dashboard/store/modules/vulnerabilities/actions';
describe('vulnerabilities module actions', () => {
const data = mockData;
describe('vulnerabiliites count actions', () => {
const data = mockDataVulnerabilitiesCount;
describe('fetchVulnerabilitesCount', () => {
let mock;
const state = initialState;
beforeEach(() => {
state.vulnerabilitiesCountEndpoint = `${TEST_HOST}/vulnerabilities_count.json`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('on success', () => {
beforeEach(() => {
mock.onGet(state.vulnerabilitiesCountEndpoint).replyOnce(200, data);
});
it('should dispatch the request and success actions', done => {
testAction(
actions.fetchVulnerabilitiesCount,
{},
state,
[],
[
{ type: 'requestVulnerabilitiesCount' },
{
type: 'receiveVulnerabilitiesCountSuccess',
payload: { data },
},
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onGet(state.vulnerabilitiesCountEndpoint).replyOnce(404, {});
});
it('should dispatch the request and error actions', done => {
testAction(
actions.fetchVulnerabilitiesCount,
{},
state,
[],
[{ type: 'requestVulnerabilitiesCount' }, { type: 'receiveVulnerabilitiesCountError' }],
done,
);
});
});
});
describe('requestVulnerabilitesCount', () => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestVulnerabilitiesCount,
{},
state,
[{ type: types.REQUEST_VULNERABILITIES_COUNT }],
[],
done,
);
});
});
describe('receiveVulnerabilitesCountSuccess', () => {
it('should commit the success mutation', done => {
const state = initialState;
testAction(
actions.receiveVulnerabilitiesCountSuccess,
{ data },
state,
[{ type: types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS, payload: data }],
[],
done,
);
});
});
describe('receivetVulnerabilitesCountError', () => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveVulnerabilitiesCountError,
{},
state,
[{ type: types.RECEIVE_VULNERABILITIES_COUNT_ERROR }],
[],
done,
);
});
});
});
describe('vulnerabilities actions', () => {
const data = mockDataVulnerabilities;
const pageInfo = {
page: 1,
nextPage: 2,
......@@ -27,12 +131,12 @@ describe('vulnerabilities module actions', () => {
'X-Total-Pages': pageInfo.totalPages,
};
describe('fetch vulnerabilities', () => {
describe('fetchVulnerabilities', () => {
let mock;
const state = initialState;
beforeEach(() => {
state.vulnerabilitiesUrl = `${TEST_HOST}/vulnerabilities.json`;
state.vulnerabilitiesEndpoint = `${TEST_HOST}/vulnerabilities.json`;
mock = new MockAdapter(axios);
});
......@@ -42,9 +146,7 @@ describe('vulnerabilities module actions', () => {
describe('on success', () => {
beforeEach(() => {
mock
.onGet(state.vulnerabilitiesUrl)
.replyOnce(200, data, headers);
mock.onGet(state.vulnerabilitiesEndpoint).replyOnce(200, data, headers);
});
it('should dispatch the request and success actions', done => {
......@@ -65,14 +167,9 @@ describe('vulnerabilities module actions', () => {
});
});
// NOTE: This will fail as we're currently mocking the API call in the action
// so the mock adaptor can't pick it up.
// eslint-disable-next-line
xdescribe('on error', () => {
describe('on error', () => {
beforeEach(() => {
mock
.onGet(state.vulnerabilitiesUrl)
.replyOnce(404, {});
mock.onGet(state.vulnerabilitiesEndpoint).replyOnce(404, {});
});
it('should dispatch the request and error actions', done => {
......@@ -81,13 +178,7 @@ describe('vulnerabilities module actions', () => {
{},
state,
[],
[
{ type: 'requestVulnerabilities' },
{
type: 'receiveVulnerabilitiesError',
payload: {},
},
],
[{ type: 'requestVulnerabilities' }, { type: 'receiveVulnerabilitiesError' }],
done,
);
});
......@@ -95,7 +186,7 @@ describe('vulnerabilities module actions', () => {
});
describe('receiveVulnerabilitiesSuccess', () => {
it('should commit the required mutations', done => {
it('should commit the success mutation', done => {
const state = initialState;
testAction(
......@@ -103,9 +194,10 @@ describe('vulnerabilities module actions', () => {
{ headers, data },
state,
[
{ type: types.SET_LOADING, payload: false },
{ type: types.SET_VULNERABILITIES, payload: data },
{ type: types.SET_PAGINATION, payload: pageInfo },
{
type: types.RECEIVE_VULNERABILITIES_SUCCESS,
payload: { pageInfo, vulnerabilities: data },
},
],
[],
done,
......@@ -114,16 +206,14 @@ describe('vulnerabilities module actions', () => {
});
describe('receiveVulnerabilitiesError', () => {
it('should commit the loading mutation', done => {
it('should commit the error mutation', done => {
const state = initialState;
testAction(
actions.receiveVulnerabilitiesError,
{},
state,
[
{ type: types.SET_LOADING, payload: false },
],
[{ type: types.RECEIVE_VULNERABILITIES_ERROR }],
[],
done,
);
......@@ -131,16 +221,14 @@ describe('vulnerabilities module actions', () => {
});
describe('requestVulnerabilities', () => {
it('should commit the loading mutation', done => {
it('should commit the request mutation', done => {
const state = initialState;
testAction(
actions.requestVulnerabilities,
{},
state,
[
{ type: types.SET_LOADING, payload: true },
],
[{ type: types.REQUEST_VULNERABILITIES }],
[],
done,
);
......
import initialState from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import State from 'ee/security_dashboard/store/modules/vulnerabilities/state';
import * as getters from 'ee/security_dashboard/store/modules/vulnerabilities/getters';
describe('vulnerabilities module getters', () => {
describe('vulnerabilities', () => {
it('should get the vulnerabilities from the state', () => {
const vulnerabilities = [1, 2, 3, 4, 5];
const state = { vulnerabilities };
const result = getters.vulnerabilities(state);
const initialState = State();
describe('vulnerabilitiesCountBySeverity', () => {
const sast = { critical: 10 };
const dast = { critical: 66 };
const expectedValue = sast.critical + dast.critical;
const vulnerabilitiesCount = { sast, dast };
const state = { vulnerabilitiesCount };
expect(result).toBe(vulnerabilities);
it('should add up all the counts with `high` severity', () => {
const result = getters.vulnerabilitiesCountBySeverity(state)('critical');
expect(result).toBe(expectedValue);
});
it('should return 0 if no counts match the severity name', () => {
const result = getters.vulnerabilitiesCountBySeverity(state)('medium');
expect(result).toBe(0);
});
it('should get an empty array when there are no vulnerabilities in the state', () => {
const result = getters.vulnerabilities(initialState);
it('should return 0 if there are no counts at all', () => {
const result = getters.vulnerabilitiesCountBySeverity(initialState)('critical');
expect(result).toEqual([]);
expect(result).toBe(0);
});
});
describe('pageInfo', () => {
it('should get the pageInfo object from the state', () => {
const pageInfo = { page: 1 };
const state = { pageInfo };
const result = getters.pageInfo(state);
describe('vulnerabilitiesCountByReportType', () => {
const sast = { critical: 10, medium: 22 };
const dast = { critical: 66 };
const expectedValue = sast.critical + sast.medium;
const vulnerabilitiesCount = { sast, dast };
const state = { vulnerabilitiesCount };
it('should add up all the counts in the sast report', () => {
const result = getters.vulnerabilitiesCountByReportType(state)('sast');
expect(result).toBe(expectedValue);
});
it('should return 0 if there are no reports for a severity type', () => {
const result = getters.vulnerabilitiesCountByReportType(initialState)('sast');
expect(result).toBe(pageInfo);
expect(result).toBe(0);
});
});
});
......@@ -3,43 +3,96 @@ import * as types from 'ee/security_dashboard/store/modules/vulnerabilities/muta
import mutations from 'ee/security_dashboard/store/modules/vulnerabilities/mutations';
describe('vulnerabilities module mutations', () => {
describe('SET_PAGINATION', () => {
it('should apply the payload to `pageInfo` in the state', () => {
describe('REQUEST_VULNERABILITIES', () => {
it('should set `isLoadingVulnerabilities` to `true`', () => {
const state = initialState;
const payload = { page: 2 };
mutations[types.SET_PAGINATION](state, payload);
mutations[types.REQUEST_VULNERABILITIES](state);
expect(state.pageInfo).toEqual(payload);
expect(state.isLoadingVulnerabilities).toBeTruthy();
});
});
describe('SET_VULNERABILITIES', () => {
it('should apply the payload to `pageInfo` in the state', () => {
describe('RECEIVE_VULNERABILITIES_SUCCESS', () => {
let payload;
let state;
beforeEach(() => {
payload = {
vulnerabilities: [1, 2, 3, 4, 5],
pageInfo: { a: 1, b: 2, c: 3 },
};
state = initialState;
mutations[types.RECEIVE_VULNERABILITIES_SUCCESS](state, payload);
});
it('should set `isLoadingVulnerabilities` to `false`', () => {
expect(state.isLoadingVulnerabilities).toBeFalsy();
});
it('should set `errorLoadingData` to `false`', () => {
expect(state.errorLoadingData).toBeFalsy();
});
it('should set `pageInfo`', () => {
expect(state.pageInfo).toBe(payload.pageInfo);
});
it('should set `vulnerabilities`', () => {
expect(state.vulnerabilities).toBe(payload.vulnerabilities);
});
});
describe('RECEIVE_VULNERABILITIES_ERROR', () => {
it('should set `isLoadingVulnerabilities` to `false`', () => {
const state = initialState;
const payload = [1, 2, 3, 4, 5];
mutations[types.SET_VULNERABILITIES](state, payload);
mutations[types.RECEIVE_VULNERABILITIES_ERROR](state);
expect(state.vulnerabilities).toEqual(payload);
expect(state.isLoadingVulnerabilities).toBeFalsy();
});
});
describe('SET_LOADING', () => {
it('should set loading to true', () => {
describe('REQUEST_VULNERABILITIES_COUNT', () => {
it('should set `isLoadingVulnerabilitiesCount` to `true`', () => {
const state = initialState;
mutations[types.SET_LOADING](state, true);
mutations[types.REQUEST_VULNERABILITIES_COUNT](state);
expect(state.isLoading).toBeTruthy();
expect(state.isLoadingVulnerabilitiesCount).toBeTruthy();
});
});
describe('RECEIVE_VULNERABILITIES_COUNT_SUCCESS', () => {
let payload;
let state;
beforeEach(() => {
payload = { a: 1, b: 2, c: 3 };
state = initialState;
mutations[types.RECEIVE_VULNERABILITIES_COUNT_SUCCESS](state, payload);
});
it('should set `isLoadingVulnerabilitiesCount` to `false`', () => {
expect(state.isLoadingVulnerabilitiesCount).toBeFalsy();
});
it('should set `errorLoadingData` to `false`', () => {
expect(state.errorLoadingData).toBeFalsy();
});
it('should set `vulnerabilitiesCount`', () => {
expect(state.vulnerabilitiesCount).toBe(payload);
});
});
it('should not modify loading values are the same', () => {
describe('RECEIVE_VULNERABILITIES_COUNT_ERROR', () => {
it('should set `isLoadingVulnerabilitiesCount` to `false`', () => {
const state = initialState;
mutations[types.SET_LOADING](state, false);
mutations[types.RECEIVE_VULNERABILITIES_COUNT_ERROR](state);
expect(state.isLoading).toBeFalsy();
expect(state.isLoadingVulnerabilitiesCount).toBeFalsy();
});
});
});
import { formatRelevantDigits, bytesToKiB, bytesToMiB, bytesToGiB, numberToHumanSize } from '~/lib/utils/number_utils';
import {
formatRelevantDigits,
bytesToKiB,
bytesToMiB,
bytesToGiB,
numberToHumanSize,
sum,
} from '~/lib/utils/number_utils';
describe('Number Utils', () => {
describe('formatRelevantDigits', () => {
......@@ -77,4 +84,14 @@ describe('Number Utils', () => {
expect(numberToHumanSize(10737418240)).toEqual('10.00 GiB');
});
});
describe('sum', () => {
it('should add up two values', () => {
expect(sum(1, 2)).toEqual(3);
});
it('should add up all the values in an array when passed to a reducer', () => {
expect([1, 2, 3, 4, 5].reduce(sum)).toEqual(15);
});
});
});
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