Commit 621c2a08 authored by Daniel Tian's avatar Daniel Tian Committed by Savas Vedova

Move vulnerabilities count query into count component

parent eb04b06a
<script>
import { GlIntersectionObserver, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql';
import createFlash from '~/flash';
import { s__ } from '~/locale';
......@@ -25,35 +24,13 @@ export default {
inject: ['groupFullPath', 'canViewFalsePositive', 'canAdminVulnerability', 'hasProjects'],
data() {
return {
counts: {},
vulnerabilities: [],
filters: [],
filters: undefined,
sort: undefined,
pageInfo: undefined,
};
},
apollo: {
counts: {
query: countsQuery,
errorPolicy: 'none',
variables() {
return {
fullPath: this.groupFullPath,
isGroup: true,
...this.filters,
};
},
update({ group }) {
return group.vulnerabilitySeveritiesCount;
},
error() {
createFlash({
message: s__(
'SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again.',
),
});
},
},
vulnerabilities: {
query: vulnerabilitiesQuery,
errorPolicy: 'none',
......@@ -76,12 +53,12 @@ export default {
),
});
},
skip() {
return !this.filters;
},
},
computed: {
isLoadingCounts() {
return this.$apollo.queries.counts.loading;
},
computed: {
// Used to show the loading icon at the bottom of the vulnerabilities list.
isLoadingVulnerabilities() {
return this.$apollo.queries.vulnerabilities.loading;
......@@ -148,7 +125,7 @@ export default {
<div v-else>
<vulnerability-report-header />
<vulnerability-counts class="gl-mt-7" :counts="counts" :is-loading="isLoadingCounts" />
<vulnerability-counts class="gl-mt-7" :filters="filters" />
<vulnerability-filters
:filters="$options.filtersToShow"
......
<script>
import { GlCard, GlSkeletonLoading } from '@gitlab/ui';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import { SEVERITIES } from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import { DASHBOARD_TYPES } from 'ee/security_dashboard/store/constants';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import { SEVERITIES } from '~/vulnerabilities/constants';
export default {
components: { GlCard, GlSkeletonLoading, SeverityBadge },
inject: ['fullPath', 'dashboardType'],
props: {
counts: {
filters: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: false,
default: false,
default: null,
},
},
data() {
return {
counts: {},
};
},
apollo: {
counts: {
query: countsQuery,
errorPolicy: 'none',
variables() {
return {
fullPath: this.fullPath,
isProject: this.dashboardType === DASHBOARD_TYPES.PROJECT,
isGroup: this.dashboardType === DASHBOARD_TYPES.GROUP,
isInstance: this.dashboardType === DASHBOARD_TYPES.INSTANCE,
...this.filters,
};
},
update(data) {
return data[this.dashboardType].vulnerabilitySeveritiesCount;
},
error() {
createFlash({
message: s__(
'SecurityReports|Error fetching the vulnerability counts. Please check your network connection and try again.',
),
});
},
skip() {
return !this.filters;
},
},
},
computed: {
isLoadingCounts() {
return !this.filters || this.$apollo.queries.counts.loading;
},
severityCounts() {
return SEVERITIES.map((severity) => ({
severity,
......@@ -39,9 +76,10 @@ export default {
<template #header>
<severity-badge :severity="severity" class="gl-text-center!" />
</template>
<template #default>
<gl-skeleton-loading
v-if="isLoading"
v-if="isLoadingCounts"
:lines="1"
class="gl-display-flex gl-align-items-center"
/>
......
......@@ -77,6 +77,7 @@ export default (el, dashboardType) => {
vulnerabilitiesExportEndpoint,
groupFullPath,
projectFullPath,
fullPath: projectFullPath || groupFullPath,
autoFixDocumentation,
autoFixMrsPath,
canAdminVulnerability: parseBoolean(canAdminVulnerability),
......
......@@ -7,7 +7,6 @@ import VulnerabilityReportDevelopment from 'ee/security_dashboard/components/gro
import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_counts.vue';
import VulnerabilityFilters from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_filters.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import vulnerabilitiesQuery from 'ee/security_dashboard/graphql/queries/group_vulnerabilities.query.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
......@@ -17,17 +16,8 @@ jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
const counts = { critical: 1, high: 2, info: 3, low: 4, medium: 5, unknown: 6 };
const groupFullPath = 'path';
const countsRequestHandler = jest.fn().mockResolvedValue({
data: {
group: {
vulnerabilitySeveritiesCount: counts,
},
},
});
const createVulnerabilitiesRequestHandler = ({ hasNextPage }) =>
jest.fn().mockResolvedValue({
data: {
......@@ -46,79 +36,39 @@ describe('Vulnerability counts component', () => {
let wrapper;
const createWrapper = ({
countsHandler = countsRequestHandler,
vulnerabilitiesHandler = vulnerabilitiesRequestHandler,
canViewFalsePositive = true,
filters = {},
} = {}) => {
// Use the default request handlers if they weren't provided.
const queries = [
[countsQuery, countsHandler],
[vulnerabilitiesQuery, vulnerabilitiesHandler],
];
wrapper = shallowMountExtended(VulnerabilityReportDevelopment, {
localVue,
apolloProvider: createMockApollo(queries),
apolloProvider: createMockApollo([[vulnerabilitiesQuery, vulnerabilitiesHandler]]),
provide: {
groupFullPath,
canViewFalsePositive,
canAdminVulnerability: true,
hasProjects: true,
},
data: () => ({ filters }),
});
};
const findVulnerabilityCounts = () => wrapper.findComponent(VulnerabilityCounts);
const findVulnerabilityFilters = () => wrapper.findComponent(VulnerabilityFilters);
const findVulnerabilityList = () => wrapper.findComponent(VulnerabilityList);
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
afterEach(() => {
wrapper.destroy();
countsRequestHandler.mockClear();
vulnerabilitiesRequestHandler.mockClear();
});
describe('vulnerability counts query', () => {
it('calls the query once with the expected data', () => {
createWrapper();
expect(countsRequestHandler).toHaveBeenCalledTimes(1);
expect(countsRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({
isGroup: true,
fullPath: groupFullPath,
}),
);
});
it('shows an error message if the query fails', async () => {
const countsHandler = jest.fn().mockRejectedValue(new Error());
createWrapper({ countsHandler });
// Have to wait 2 ticks here, one for the query to finish loading, and one more for the
// GraphQL error handler to be called.
await nextTick();
await nextTick();
expect(createFlash).toHaveBeenCalledTimes(1);
});
});
describe('vulnerability counts component', () => {
it('gets the expected isLoading prop from the counts query loading state', async () => {
createWrapper();
// The query will be loading until we use nextTick() to give micro-tasks a chance to run.
expect(findVulnerabilityCounts().props('isLoading')).toBe(true);
await nextTick();
expect(findVulnerabilityCounts().props('isLoading')).toBe(false);
});
it('gets the expected counts prop', async () => {
createWrapper();
await nextTick();
it('receives the filters prop from the filters component', () => {
const filters = {}; // Object itself does not matter, we're only checking that it's passed.
createWrapper({ filters });
expect(findVulnerabilityCounts().props('counts')).toMatchObject(counts);
expect(findVulnerabilityCounts().props('filters')).toBe(filters);
});
});
......@@ -143,6 +93,12 @@ describe('Vulnerability counts component', () => {
},
);
it('does not call the query if filters are not ready', () => {
createWrapper({ filters: null });
expect(vulnerabilitiesRequestHandler).not.toHaveBeenCalled();
});
it('shows an error message if the query fails', async () => {
const vulnerabilitiesHandler = jest.fn().mockRejectedValue(new Error());
createWrapper({ vulnerabilitiesHandler });
......@@ -158,13 +114,11 @@ describe('Vulnerability counts component', () => {
describe('vulnerability list component', () => {
it('gets the expected vulnerabilities prop', async () => {
createWrapper();
await nextTick();
const vulnerabilities = [];
wrapper.setData({ vulnerabilities });
await nextTick();
await wrapper.setData({ vulnerabilities });
expect(findVulnerabilityList().props('vulnerabilities')).toBe(vulnerabilities);
expect(findVulnerabilityList().props('vulnerabilities')).toEqual(vulnerabilities);
});
it('calls the vulnerabilities query with the data from the sort-changed event', async () => {
......@@ -203,6 +157,7 @@ describe('Vulnerability counts component', () => {
createWrapper({
vulnerabilitiesHandler: createVulnerabilitiesRequestHandler({ hasNextPage: false }),
});
await nextTick();
expect(findIntersectionObserver().exists()).toBe(false);
......@@ -210,20 +165,16 @@ describe('Vulnerability counts component', () => {
});
describe('vulnerability filters component', () => {
it('will pass data from filters-changed event to GraphQL queries', async () => {
const countsHandler = jest.fn().mockResolvedValue();
it('will pass data from filters-changed event to vulnerabilities GraphQL query', async () => {
const vulnerabilitiesHandler = jest.fn().mockResolvedValue();
createWrapper({ countsHandler, vulnerabilitiesHandler });
// Sanity check, the report component will call these the first time it's mounted.
expect(countsHandler).toHaveBeenCalledTimes(1);
createWrapper({ vulnerabilitiesHandler });
// Sanity check, the report component will call this the first time it's mounted.
expect(vulnerabilitiesHandler).toHaveBeenCalledTimes(1);
const data = { a: 1 };
wrapper.findComponent(VulnerabilityFilters).vm.$emit('filters-changed', data);
findVulnerabilityFilters().vm.$emit('filters-changed', data);
await nextTick();
expect(countsHandler).toHaveBeenCalledTimes(2);
expect(countsHandler).toHaveBeenCalledWith(expect.objectContaining(data));
expect(vulnerabilitiesHandler).toHaveBeenCalledTimes(2);
expect(vulnerabilitiesHandler).toHaveBeenCalledWith(expect.objectContaining(data));
});
......
import { GlCard, GlSkeletonLoading } from '@gitlab/ui';
import { nextTick } from 'vue';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import VulnerabilityCounts from 'ee/security_dashboard/components/shared/vulnerability_report/vulnerability_counts.vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
CRITICAL,
HIGH,
MEDIUM,
INFO,
LOW,
UNKNOWN,
SEVERITIES,
} from 'ee/security_dashboard/store/modules/vulnerabilities/constants';
import { SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import { DASHBOARD_TYPES, SEVERITY_LEVELS } from 'ee/security_dashboard/store/constants';
import createFlash from '~/flash';
import createMockApollo from 'helpers/mock_apollo_helper';
import countsQuery from 'ee/security_dashboard/graphql/queries/vulnerability_severities_count.query.graphql';
import { SEVERITIES } from '~/vulnerabilities/constants';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
const fullPath = 'path';
const counts = { critical: 1, high: 2, info: 3, low: 4, medium: 5, unknown: 6 };
const getCountsRequestHandler = ({
data = counts,
dashboardType = DASHBOARD_TYPES.PROJECT,
} = {}) => {
return jest.fn().mockResolvedValue({
data: {
[dashboardType]: {
vulnerabilitySeveritiesCount: data,
},
},
});
};
const defaultCountsRequestHandler = getCountsRequestHandler();
describe('Vulnerability counts component', () => {
let wrapper;
const createWrapper = (props) => {
const createWrapper = ({
dashboardType = DASHBOARD_TYPES.PROJECT,
filters = {},
countsHandler = defaultCountsRequestHandler,
} = {}) => {
wrapper = mountExtended(VulnerabilityCounts, {
propsData: props,
localVue,
apolloProvider: createMockApollo([[countsQuery, countsHandler]]),
provide: {
fullPath,
dashboardType,
},
propsData: { filters },
});
};
......@@ -28,17 +59,70 @@ describe('Vulnerability counts component', () => {
wrapper.destroy();
});
it('should show a skeleton loading component for each count when the isLoading prop is true', () => {
createWrapper({ isLoading: true, counts: {} });
describe('vulnerability counts query', () => {
it('calls the query once with the expected data', () => {
const filters = { a: 1, b: 2 };
createWrapper({ filters });
expect(defaultCountsRequestHandler).toHaveBeenCalledTimes(1);
expect(defaultCountsRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({ ...filters, fullPath }),
);
});
it('does not call the query if filters are not ready', () => {
createWrapper({ filters: null });
expect(defaultCountsRequestHandler).not.toHaveBeenCalled();
});
it('shows an error message if the query fails', async () => {
const countsHandler = jest.fn().mockRejectedValue(new Error());
createWrapper({ countsHandler });
// Have to wait 2 ticks here, one for the query to finish loading, and one more for the
// GraphQL error handler to be called.
await nextTick();
await nextTick();
expect(createFlash).toHaveBeenCalledTimes(1);
});
it.each([DASHBOARD_TYPES.PROJECT, DASHBOARD_TYPES.GROUP, DASHBOARD_TYPES.INSTANCE])(
'sets the correct variable for the %s dashboard',
async (dashboardType) => {
createWrapper({ dashboardType });
await nextTick();
expect(defaultCountsRequestHandler).toHaveBeenCalledWith(
expect.objectContaining({
isProject: dashboardType === DASHBOARD_TYPES.PROJECT,
isGroup: dashboardType === DASHBOARD_TYPES.GROUP,
isInstance: dashboardType === DASHBOARD_TYPES.INSTANCE,
}),
);
},
);
});
it('shows a skeleton loading component for each count when the query is loading', () => {
createWrapper();
findCards().wrappers.forEach((card) => {
expect(card.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
});
it('shows a skeleton loading component for each count when there are no filters', () => {
createWrapper({ filters: null });
findCards().wrappers.forEach((card) => {
expect(card.findComponent(GlSkeletonLoading).exists()).toBe(true);
});
});
it('should show a card for each severity with the correct count', () => {
const counts = { [CRITICAL]: 1, [HIGH]: 2, [MEDIUM]: 3, [LOW]: 4, [INFO]: 5, [UNKNOWN]: 6 };
createWrapper({ counts });
it('should show a card for each severity with the correct count', async () => {
createWrapper();
await nextTick();
// Check that there are exactly the same number of cards as there are severities.
expect(findCards()).toHaveLength(Object.keys(counts).length);
......@@ -51,8 +135,13 @@ describe('Vulnerability counts component', () => {
});
});
it('should show zero for the count when there is no value for that severity', () => {
createWrapper({ counts: {} });
it('should show zero for the count when there is no value for that severity', async () => {
const handler = getCountsRequestHandler({ data: {} });
createWrapper({ countsHandler: handler });
// Have to wait 2 ticks here, one for the query to finish loading, and one more for the
// computed property to update.
await nextTick();
await nextTick();
SEVERITIES.forEach((severity) => {
expect(findCardWithSeverity(severity).text()).toContain('0');
......
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