Commit c9ef3f77 authored by Jiaan Louw's avatar Jiaan Louw Committed by Jose Ivan Vargas

Update compliance report to fetch violations from GraphQL

Remove the local resolver and fetch the merge request compliance
violations from our GraphQL API.
parent 40b9f8bc
...@@ -2,10 +2,9 @@ import Vue from 'vue'; ...@@ -2,10 +2,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo'; import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql'; import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils'; import { parseBoolean } from '~/lib/utils/common_utils';
import { queryToObject } from '~/lib/utils/url_utility';
import resolvers from './graphql/resolvers';
import ComplianceDashboard from './components/dashboard.vue'; import ComplianceDashboard from './components/dashboard.vue';
import ComplianceReport from './components/report.vue'; import ComplianceReport from './components/report.vue';
import { buildDefaultFilterParams } from './utils';
export default () => { export default () => {
const el = document.getElementById('js-compliance-report'); const el = document.getElementById('js-compliance-report');
...@@ -22,10 +21,10 @@ export default () => { ...@@ -22,10 +21,10 @@ export default () => {
Vue.use(VueApollo); Vue.use(VueApollo);
const apolloProvider = new VueApollo({ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(resolvers), defaultClient: createDefaultClient(),
}); });
const defaultQuery = queryToObject(window.location.search, { gatherArrays: true }); const defaultFilterParams = buildDefaultFilterParams(window.location.search);
return new Vue({ return new Vue({
el, el,
...@@ -35,7 +34,7 @@ export default () => { ...@@ -35,7 +34,7 @@ export default () => {
props: { props: {
mergeCommitsCsvExportPath, mergeCommitsCsvExportPath,
groupPath, groupPath,
defaultQuery, defaultFilterParams,
}, },
}), }),
}); });
......
...@@ -63,7 +63,7 @@ export default { ...@@ -63,7 +63,7 @@ export default {
<template> <template>
<div> <div>
<gl-dropdown split> <gl-dropdown data-testid="merge-commit-dropdown" split>
<template #button-content> <template #button-content>
<gl-button <gl-button
ref="listMergeCommitsButton" ref="listMergeCommitsButton"
......
...@@ -8,7 +8,7 @@ import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; ...@@ -8,7 +8,7 @@ import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import UrlSync from '~/vue_shared/components/url_sync.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import complianceViolationsQuery from '../graphql/compliance_violations.query.graphql'; import getComplianceViolationsQuery from '../graphql/compliance_violations.query.graphql';
import { mapViolations } from '../graphql/mappers'; import { mapViolations } from '../graphql/mappers';
import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from '../constants'; import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from '../constants';
import { parseViolationsQueryFilter } from '../utils'; import { parseViolationsQueryFilter } from '../utils';
...@@ -44,17 +44,17 @@ export default { ...@@ -44,17 +44,17 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
defaultQuery: { defaultFilterParams: {
type: Object, type: Object,
required: true, required: true,
}, },
}, },
data() { data() {
const sortParam = this.defaultQuery.sort || DEFAULT_SORT; const sortParam = this.defaultFilterParams.sort || DEFAULT_SORT;
const { sortBy, sortDesc } = sortStringToObject(sortParam); const { sortBy, sortDesc } = sortStringToObject(sortParam);
return { return {
urlQuery: { ...this.defaultQuery }, urlQuery: { ...this.defaultFilterParams },
queryError: false, queryError: false,
violations: { violations: {
list: [], list: [],
...@@ -74,11 +74,11 @@ export default { ...@@ -74,11 +74,11 @@ export default {
}, },
apollo: { apollo: {
violations: { violations: {
query: complianceViolationsQuery, query: getComplianceViolationsQuery,
variables() { variables() {
return { return {
fullPath: this.groupPath, fullPath: this.groupPath,
filter: parseViolationsQueryFilter(this.urlQuery), filters: parseViolationsQueryFilter(this.urlQuery),
sort: this.sortParam, sort: this.sortParam,
first: GRAPHQL_PAGE_SIZE, first: GRAPHQL_PAGE_SIZE,
...this.paginationCursors, ...this.paginationCursors,
...@@ -140,12 +140,19 @@ export default { ...@@ -140,12 +140,19 @@ export default {
this.drawerProject = {}; this.drawerProject = {};
}, },
updateUrlQuery({ projectIds = [], ...rest }) { updateUrlQuery({ projectIds = [], ...rest }) {
this.resetPagination();
this.urlQuery = { this.urlQuery = {
// Clear the URL param when the id array is empty // Clear the URL param when the id array is empty
projectIds: projectIds?.length > 0 ? projectIds : null, projectIds: projectIds?.length > 0 ? projectIds : null,
...rest, ...rest,
}; };
}, },
resetPagination() {
this.paginationCursors = {
before: null,
after: null,
};
},
loadPrevPage(startCursor) { loadPrevPage(startCursor) {
this.paginationCursors = { this.paginationCursors = {
before: startCursor, before: startCursor,
...@@ -234,7 +241,7 @@ export default { ...@@ -234,7 +241,7 @@ export default {
</header> </header>
<violation-filter <violation-filter
:group-path="groupPath" :group-path="groupPath"
:default-query="defaultQuery" :default-query="defaultFilterParams"
@filters-changed="updateUrlQuery" @filters-changed="updateUrlQuery"
/> />
<gl-table <gl-table
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
import { GlDaterangePicker } from '@gitlab/ui'; import { GlDaterangePicker } from '@gitlab/ui';
import * as Sentry from '@sentry/browser'; import * as Sentry from '@sentry/browser';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import { getDateInPast, pikadayToString, parsePikadayDate } from '~/lib/utils/datetime_utility'; import { pikadayToString, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale'; import { __ } from '~/locale';
import getGroupProjects from '../../graphql/violation_group_projects.query.graphql'; import getGroupProjects from '../../graphql/violation_group_projects.query.graphql';
...@@ -38,12 +38,10 @@ export default { ...@@ -38,12 +38,10 @@ export default {
}, },
computed: { computed: {
defaultStartDate() { defaultStartDate() {
const startDate = this.defaultQuery.mergedAfter; return parsePikadayDate(this.defaultQuery.mergedAfter);
return startDate ? parsePikadayDate(startDate) : getDateInPast(CURRENT_DATE, 30);
}, },
defaultEndDate() { defaultEndDate() {
const endDate = this.defaultQuery.mergedBefore; return parsePikadayDate(this.defaultQuery.mergedBefore);
return endDate ? parsePikadayDate(endDate) : CURRENT_DATE;
}, },
}, },
async created() { async created() {
...@@ -103,6 +101,7 @@ export default { ...@@ -103,6 +101,7 @@ export default {
}}</label> }}</label>
<projects-dropdown-filter <projects-dropdown-filter
v-if="showProjectFilter" v-if="showProjectFilter"
data-testid="violations-project-dropdown"
class="gl-mb-2 gl-lg-mb-0 compliance-filter-dropdown-input" class="gl-mb-2 gl-lg-mb-0 compliance-filter-dropdown-input"
:group-namespace="groupPath" :group-namespace="groupPath"
:query-params="$options.projectsFilterParams" :query-params="$options.projectsFilterParams"
...@@ -115,6 +114,7 @@ export default { ...@@ -115,6 +114,7 @@ export default {
<gl-daterange-picker <gl-daterange-picker
class="gl-display-flex gl-w-full gl-mb-5" class="gl-display-flex gl-w-full gl-mb-5"
data-testid="violations-date-range-picker"
:default-start-date="defaultStartDate" :default-start-date="defaultStartDate"
:default-end-date="defaultEndDate" :default-end-date="defaultEndDate"
:default-max-date="$options.defaultMaxDate" :default-max-date="$options.defaultMaxDate"
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" #import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
# TODO: Add the correct filter type once it has been added in https://gitlab.com/gitlab-org/gitlab/-/issues/347325
query getComplianceViolations( query getComplianceViolations(
$fullPath: ID! $fullPath: ID!
$filter: Object $filters: ComplianceViolationInput
$sort: String $sort: ComplianceViolationSort
$after: String $after: String
$before: String $before: String
$first: Int $first: Int
) { ) {
group( group(fullPath: $fullPath) {
fullPath: $fullPath
filter: $filter
sort: $sort
after: $after
before: $before
first: $first
) @client {
id id
mergeRequestViolations { mergeRequestViolations(
filters: $filters
sort: $sort
after: $after
before: $before
first: $first
) {
nodes { nodes {
id id
severityLevel severityLevel
...@@ -76,7 +74,7 @@ query getComplianceViolations( ...@@ -76,7 +74,7 @@ query getComplianceViolations(
webUrl webUrl
} }
} }
ref ref: reference
fullRef: reference(full: true) fullRef: reference(full: true)
sourceBranch sourceBranch
sourceBranchExists sourceBranchExists
......
// Note: This is mocking the server response until https://gitlab.com/gitlab-org/gitlab/-/issues/342897 is complete
// These values do not need to be translatable as it will remain behind a development feature flag
// until that issue is merged
/* eslint-disable @gitlab/require-i18n-strings */
export default {
Query: {
group() {
return {
__typename: 'Group',
id: 1,
mergeRequestViolations: {
__typename: 'MergeRequestViolations',
nodes: [
{
__typename: 'MergeRequestViolation',
id: 1,
severityLevel: 'HIGH',
reason: 'APPROVED_BY_COMMITTER',
violatingUser: {
__typename: 'Violator',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
mergeRequest: {
__typename: 'MergeRequest',
id: 24,
title:
'Officiis architecto voluptas ut sit qui qui quisquam sequi consectetur porro.',
mergedAt: '2021-11-25T11:56:52.215Z',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-shell/-/merge_requests/1',
author: {
__typename: 'Author',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
mergeUser: {
__typename: 'MergedBy',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
committers: {
__typename: 'Committers',
nodes: [],
},
participants: {
__typename: 'Participants',
nodes: [
{
__typename: 'User',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
],
},
approvedBy: {
__typename: 'ApprovedBy',
nodes: [
{
__typename: 'User',
id: 49,
name: 'John Doe5',
username: 'user5',
avatarUrl:
'https://secure.gravatar.com/avatar/eaafc9b0f704edaf23cd5cf7727df560?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user5',
},
{
__typename: 'ApprovedBy',
id: 48,
name: 'John Doe4',
username: 'user4',
avatarUrl:
'https://secure.gravatar.com/avatar/5c8881fc63652c86cd4b23101268cf84?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user4',
},
],
},
fullRef: 'gitlab-shell!1',
ref: '!1',
sourceBranch: 'ut-171ad4e263',
sourceBranchExists: false,
targetBranch: 'master',
targetBranchExists: true,
project: {
__typename: 'Project',
id: 1,
avatarUrl: null,
name: 'Gitlab Shell',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-shell',
complianceFrameworks: {
__typename: 'ComplianceFrameworks',
nodes: [
{
__typename: 'ComplianceFrameworks',
id: 1,
name: 'GDPR',
description: 'General Data Protection Regulation',
color: '#009966',
},
],
},
},
},
},
{
__typename: 'MergeRequestViolation',
id: 2,
severityLevel: 'HIGH',
reason: 'APPROVED_BY_INSUFFICIENT_USERS',
violatingUser: {
__typename: 'Violator',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
mergeRequest: {
__typename: 'MergeRequest',
id: 25,
title:
'Officiis architecto voluptas ut sit qui qui quisquam sequi consectetur porro.',
mergedAt: '2021-11-25T11:56:52.215Z',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-test/-/merge_requests/2',
author: {
__typename: 'Author',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
mergeUser: {
__typename: 'MergedBy',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
committers: {
__typename: 'Committers',
nodes: [],
},
participants: {
__typename: 'Participants',
nodes: [
{
__typename: 'User',
id: 50,
name: 'John Doe6',
username: 'user6',
avatarUrl:
'https://secure.gravatar.com/avatar/7ff9b8111da2e2109e7b66f37aa632cc?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user6',
},
],
},
approvedBy: {
__typename: 'ApprovedBy',
nodes: [
{
__typename: 'User',
id: 49,
name: 'John Doe5',
username: 'user5',
avatarUrl:
'https://secure.gravatar.com/avatar/eaafc9b0f704edaf23cd5cf7727df560?s=80&d=identicon',
webUrl: 'https://gdk.localhost:3443/user5',
},
],
},
fullRef: 'gitlab-test!2',
ref: '!2',
sourceBranch: 'ut-171ad4e264',
sourceBranchExists: false,
targetBranch: 'master',
targetBranchExists: true,
project: {
__typename: 'Project',
id: 2,
avatarUrl: null,
name: 'Gitlab Test',
webUrl: 'https://gdk.localhost:3443/gitlab-org/gitlab-test',
complianceFrameworks: {
__typename: 'ComplianceFrameworks',
nodes: [
{
__typename: 'ComplianceFrameworks',
id: 2,
name: 'SOX',
description: 'A framework',
color: '#00FF00',
},
],
},
},
},
},
],
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'eyJpZCI6IjMzMjkwNjMzIn0',
endCursor: 'eyJpZCI6IjMzMjkwNjI5In0',
},
},
};
},
},
};
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { convertToGraphQLIds } from '~/graphql_shared/utils'; import { convertToGraphQLIds } from '~/graphql_shared/utils';
import { TYPE_PROJECT } from '~/graphql_shared/constants'; import { TYPE_PROJECT } from '~/graphql_shared/constants';
import { formatDate } from '~/lib/utils/datetime_utility'; import { formatDate, getDateInPast, pikadayToString } from '~/lib/utils/datetime_utility';
import { ISO_SHORT_FORMAT } from '~/vue_shared/constants'; import { ISO_SHORT_FORMAT } from '~/vue_shared/constants';
import { queryToObject } from '~/lib/utils/url_utility';
import { CURRENT_DATE } from '../audit_events/constants';
export const mapDashboardToDrawerData = (mergeRequest) => ({ export const mapDashboardToDrawerData = (mergeRequest) => ({
id: mergeRequest.id, id: mergeRequest.id,
...@@ -24,8 +26,14 @@ export const convertProjectIdsToGraphQl = (projectIds) => ...@@ -24,8 +26,14 @@ export const convertProjectIdsToGraphQl = (projectIds) =>
projectIds.filter((id) => Boolean(id)), projectIds.filter((id) => Boolean(id)),
); );
export const parseViolationsQueryFilter = ({ createdBefore, createdAfter, projectIds }) => ({ export const parseViolationsQueryFilter = ({ mergedBefore, mergedAfter, projectIds }) => ({
projectIds: projectIds ? convertProjectIdsToGraphQl(projectIds) : [], projectIds: projectIds ? convertProjectIdsToGraphQl(projectIds) : [],
createdBefore: formatDate(createdBefore, ISO_SHORT_FORMAT), mergedBefore: formatDate(mergedBefore, ISO_SHORT_FORMAT),
createdAfter: formatDate(createdAfter, ISO_SHORT_FORMAT), mergedAfter: formatDate(mergedAfter, ISO_SHORT_FORMAT),
});
export const buildDefaultFilterParams = (queryString) => ({
mergedAfter: pikadayToString(getDateInPast(CURRENT_DATE, 30)),
mergedBefore: pikadayToString(CURRENT_DATE),
...queryToObject(queryString, { gatherArrays: true }),
}); });
...@@ -9,14 +9,35 @@ RSpec.describe 'Compliance Dashboard', :js do ...@@ -9,14 +9,35 @@ RSpec.describe 'Compliance Dashboard', :js do
let_it_be(:project) { create(:project, :repository, :public, namespace: group) } let_it_be(:project) { create(:project, :repository, :public, namespace: group) }
let_it_be(:project_2) { create(:project, :repository, :public, namespace: group) } let_it_be(:project_2) { create(:project, :repository, :public, namespace: group) }
shared_examples 'exports a merge commit-specific CSV' do
it 'downloads a commit chain of custory report', :aggregate_failures do
page.within('[data-testid="merge-commit-dropdown"]') do
find('.dropdown-toggle').click
requests = inspect_requests do
page.within('.dropdown-menu') do
find('input[name="commit_sha"]').set(merge_request.merge_commit_sha)
find('button[type="submit"]').click
end
end
csv_request = requests.find { |req| req.url.match(%r{.csv}) }
expect(csv_request.response_headers['Content-Disposition']).to match(%r{.csv})
expect(csv_request.response_headers['Content-Type']).to eq("text/csv; charset=utf-8")
expect(csv_request.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(csv_request.body).to match(%r{#{merge_request.merge_commit_sha}})
expect(csv_request.body).not_to match(%r{#{merge_request_2.merge_commit_sha}})
end
end
end
before do before do
stub_licensed_features(group_level_compliance_dashboard: true) stub_licensed_features(group_level_compliance_dashboard: true)
group.add_owner(user) group.add_owner(user)
sign_in(user) sign_in(user)
end end
# TODO: This should be updated to fully test both with and without the feature flag once closer to feature completion
# https://gitlab.com/gitlab-org/gitlab/-/issues/347302
context 'when compliance_violations_report feature is disabled' do context 'when compliance_violations_report feature is disabled' do
before do before do
stub_feature_flags(compliance_violations_report: false) stub_feature_flags(compliance_violations_report: false)
...@@ -45,24 +66,7 @@ RSpec.describe 'Compliance Dashboard', :js do ...@@ -45,24 +66,7 @@ RSpec.describe 'Compliance Dashboard', :js do
end end
context 'chain of custody report' do context 'chain of custody report' do
it 'exports a merge commit-specific CSV' do it_behaves_like 'exports a merge commit-specific CSV'
find('.dropdown-toggle').click
requests = inspect_requests do
page.within('.dropdown-menu') do
find('input[name="commit_sha"]').set(merge_request.merge_commit_sha)
find('button[type="submit"]').click
end
end
csv_request = requests.find { |req| req.url.match(%r{.csv}) }
expect(csv_request.response_headers['Content-Disposition']).to match(%r{.csv})
expect(csv_request.response_headers['Content-Type']).to eq("text/csv; charset=utf-8")
expect(csv_request.response_headers['Content-Transfer-Encoding']).to eq("binary")
expect(csv_request.body).to match(%r{#{merge_request.merge_commit_sha}})
expect(csv_request.body).not_to match(%r{#{merge_request_2.merge_commit_sha}})
end
end end
end end
end end
...@@ -81,5 +85,95 @@ RSpec.describe 'Compliance Dashboard', :js do ...@@ -81,5 +85,95 @@ RSpec.describe 'Compliance Dashboard', :js do
expect(page).to have_content 'Date merged' expect(page).to have_content 'Date merged'
end end
end end
context 'when there are no compliance violations' do
it 'shows an empty state' do
expect(page).to have_content('No violations found')
end
end
context 'when there are merge requests' do
let_it_be(:merge_request) { create(:merge_request, source_project: project, state: :merged, merge_commit_sha: 'b71a6483b96dc303b66fdcaa212d9db6b10591ce') }
let_it_be(:merge_request_2) { create(:merge_request, source_project: project_2, state: :merged, merge_commit_sha: '24327319d067f4101cd3edd36d023ab5e49a8579') }
context 'chain of custody report' do
it_behaves_like 'exports a merge commit-specific CSV'
end
context 'and there is a compliance violation' do
let_it_be(:violation) { create(:compliance_violation, :approved_by_committer, severity_level: :high, merge_request: merge_request, violating_user: user) }
let_it_be(:violation_2) { create(:compliance_violation, :approved_by_merge_request_author, severity_level: :medium, merge_request: merge_request_2, violating_user: user) }
before do
merge_request.metrics.update!(merged_at: 1.day.ago)
merge_request_2.metrics.update!(merged_at: 7.days.ago)
wait_for_requests
end
it 'shows the compliance violations with details', :aggregate_failures do
expect(all('tbody > tr').count).to eq(2)
expect(first_row).to have_content('High')
expect(first_row).to have_content('Approved by committer')
expect(first_row).to have_content(merge_request.title)
expect(first_row).to have_content('1 day ago')
end
it 'can sort the violations by clicking on a column header' do
click_column_header 'Severity'
expect(first_row).to have_content(merge_request_2.title)
end
context 'violations filter' do
it 'can filter by date range' do
set_date_range(7.days.ago.to_date, 6.days.ago.to_date)
expect(page).to have_content(merge_request_2.title)
expect(page).not_to have_content(merge_request.title)
end
it 'can filter by project id' do
filter_by_project(merge_request_2.project)
expect(page).to have_content(merge_request_2.title)
expect(page).not_to have_content(merge_request.title)
end
end
end
end
end
def first_row
find('tbody tr', match: :first)
end
def set_date_range(start_date, end_date)
page.within('[data-testid="violations-date-range-picker"]') do
all('input')[0].set(start_date)
all('input')[0].native.send_keys(:return)
all('input')[1].set(end_date)
all('input')[1].native.send_keys(:return)
end
end
def filter_by_project(project)
page.within('[data-testid="violations-project-dropdown"]') do
find('.dropdown-toggle').click
find('input[aria-label="Search"]').set(project.name)
wait_for_requests
find('.dropdown-item').click
end
page.find('body').click
end
def click_column_header(name)
page.within('thead') do
find('div', text: name).click
wait_for_requests
end
end end
end end
...@@ -8,9 +8,9 @@ import Reference from 'ee/compliance_dashboard/components/drawer_sections/refere ...@@ -8,9 +8,9 @@ import Reference from 'ee/compliance_dashboard/components/drawer_sections/refere
import Reviewers from 'ee/compliance_dashboard/components/drawer_sections/reviewers.vue'; import Reviewers from 'ee/compliance_dashboard/components/drawer_sections/reviewers.vue';
import { getContentWrapperHeight } from 'ee/threat_monitoring/utils'; import { getContentWrapperHeight } from 'ee/threat_monitoring/utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import resolvers from 'ee/compliance_dashboard/graphql/resolvers';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers'; import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers';
import { createComplianceViolation } from '../mock_data';
jest.mock('ee/threat_monitoring/utils', () => ({ jest.mock('ee/threat_monitoring/utils', () => ({
getContentWrapperHeight: jest.fn(), getContentWrapperHeight: jest.fn(),
...@@ -18,7 +18,7 @@ jest.mock('ee/threat_monitoring/utils', () => ({ ...@@ -18,7 +18,7 @@ jest.mock('ee/threat_monitoring/utils', () => ({
describe('MergeRequestDrawer component', () => { describe('MergeRequestDrawer component', () => {
let wrapper; let wrapper;
const defaultData = mapViolations(resolvers.Query.group().mergeRequestViolations.nodes)[0]; const defaultData = mapViolations([createComplianceViolation()])[0];
const data = { const data = {
id: defaultData.id, id: defaultData.id,
mergeRequest: { mergeRequest: {
......
...@@ -9,8 +9,8 @@ import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue'; ...@@ -9,8 +9,8 @@ import MergeRequestDrawer from 'ee/compliance_dashboard/components/drawer.vue';
import MergeCommitsExportButton from 'ee/compliance_dashboard/components/merge_requests/merge_commits_export_button.vue'; import MergeCommitsExportButton from 'ee/compliance_dashboard/components/merge_requests/merge_commits_export_button.vue';
import ViolationReason from 'ee/compliance_dashboard/components/violations/reason.vue'; import ViolationReason from 'ee/compliance_dashboard/components/violations/reason.vue';
import ViolationFilter from 'ee/compliance_dashboard/components/violations/filter.vue'; import ViolationFilter from 'ee/compliance_dashboard/components/violations/filter.vue';
import getComplianceViolationsQuery from 'ee/compliance_dashboard/graphql/compliance_violations.query.graphql';
import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue'; import SeverityBadge from 'ee/vue_shared/security_reports/components/severity_badge.vue';
import resolvers from 'ee/compliance_dashboard/graphql/resolvers';
import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers'; import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers';
import { stripTypenames } from 'helpers/graphql_helpers'; import { stripTypenames } from 'helpers/graphql_helpers';
import waitForPromises from 'helpers/wait_for_promises'; import waitForPromises from 'helpers/wait_for_promises';
...@@ -21,24 +21,30 @@ import { stubComponent } from 'helpers/stub_component'; ...@@ -21,24 +21,30 @@ import { stubComponent } from 'helpers/stub_component';
import { sortObjectToString } from '~/lib/utils/table_utility'; import { sortObjectToString } from '~/lib/utils/table_utility';
import { parseViolationsQueryFilter } from 'ee/compliance_dashboard/utils'; import { parseViolationsQueryFilter } from 'ee/compliance_dashboard/utils';
import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from 'ee/compliance_dashboard/constants'; import { DEFAULT_SORT, GRAPHQL_PAGE_SIZE } from 'ee/compliance_dashboard/constants';
import { createComplianceViolationsResponse } from '../mock_data';
Vue.use(VueApollo); Vue.use(VueApollo);
describe('ComplianceReport component', () => { describe('ComplianceReport component', () => {
let wrapper; let wrapper;
let mockResolver;
const mergeCommitsCsvExportPath = '/csv'; const mergeCommitsCsvExportPath = '/csv';
const groupPath = 'group-path'; const groupPath = 'group-path';
const mergedAfter = '2021-11-16'; const mergedAfter = '2021-11-16';
const mergedBefore = '2021-12-15'; const mergedBefore = '2021-12-15';
const defaultQuery = { const defaultFilterParams = {
projectIds: ['20'], projectIds: ['20'],
mergedAfter, mergedAfter,
mergedBefore, mergedBefore,
sort: DEFAULT_SORT, sort: DEFAULT_SORT,
}; };
const mockGraphQlError = new Error('GraphQL networkError');
const violationsResponse = createComplianceViolationsResponse({ count: 2 });
const violations = violationsResponse.data.group.mergeRequestViolations.nodes;
const sentryError = new Error('GraphQL networkError');
const mockGraphQlSuccess = jest.fn().mockResolvedValue(violationsResponse);
const mockGraphQlLoading = jest.fn().mockResolvedValue(new Promise(() => {}));
const mockGraphQlError = jest.fn().mockRejectedValue(sentryError);
const findSubheading = () => wrapper.findByTestId('subheading'); const findSubheading = () => wrapper.findByTestId('subheading');
const findErrorMessage = () => wrapper.findComponent(GlAlert); const findErrorMessage = () => wrapper.findComponent(GlAlert);
...@@ -54,8 +60,8 @@ describe('ComplianceReport component', () => { ...@@ -54,8 +60,8 @@ describe('ComplianceReport component', () => {
const findUrlSync = () => wrapper.findComponent(UrlSync); const findUrlSync = () => wrapper.findComponent(UrlSync);
const findTableHeaders = () => findViolationsTable().findAll('th div'); const findTableHeaders = () => findViolationsTable().findAll('th div');
const findTablesFirstRowData = () => const findTableRowData = (idx) =>
findViolationsTable().findAll('tbody > tr').at(0).findAll('td'); findViolationsTable().findAll('tbody > tr').at(idx).findAll('td');
const findSelectedRows = () => findViolationsTable().findAll('tr.b-table-row-selected'); const findSelectedRows = () => findViolationsTable().findAll('tr.b-table-row-selected');
const findRow = (idx) => { const findRow = (idx) => {
...@@ -72,25 +78,22 @@ describe('ComplianceReport component', () => { ...@@ -72,25 +78,22 @@ describe('ComplianceReport component', () => {
await nextTick(); await nextTick();
}; };
const expectApolloVariables = (variables) => [ const createMockApolloProvider = (resolverMock) => {
{}, return createMockApollo([[getComplianceViolationsQuery, resolverMock]]);
variables, };
expect.anything(),
expect.anything(),
];
function createMockApolloProvider() {
return createMockApollo([], { Query: { group: mockResolver } });
}
const createComponent = (mountFn = shallowMount, props = {}) => { const createComponent = (
mountFn = shallowMount,
props = {},
resolverMock = mockGraphQlLoading,
) => {
return extendedWrapper( return extendedWrapper(
mountFn(ComplianceReport, { mountFn(ComplianceReport, {
apolloProvider: createMockApolloProvider(), apolloProvider: createMockApolloProvider(resolverMock),
propsData: { propsData: {
mergeCommitsCsvExportPath, mergeCommitsCsvExportPath,
groupPath, groupPath,
defaultQuery, defaultFilterParams,
...props, ...props,
}, },
stubs: { stubs: {
...@@ -104,7 +107,6 @@ describe('ComplianceReport component', () => { ...@@ -104,7 +107,6 @@ describe('ComplianceReport component', () => {
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
mockResolver = null;
}); });
describe('default behavior', () => { describe('default behavior', () => {
...@@ -131,12 +133,18 @@ describe('ComplianceReport component', () => { ...@@ -131,12 +133,18 @@ describe('ComplianceReport component', () => {
it('does not render an error message', () => { it('does not render an error message', () => {
expect(findErrorMessage().exists()).toBe(false); expect(findErrorMessage().exists()).toBe(false);
}); });
it('configures the filter', () => {
expect(findViolationFilter().props()).toMatchObject({
groupPath,
defaultQuery: defaultFilterParams,
});
});
}); });
describe('when initializing', () => { describe('when initializing', () => {
beforeEach(() => { beforeEach(() => {
mockResolver = jest.fn(); wrapper = createComponent(mount, {}, mockGraphQlLoading);
wrapper = createComponent(mount);
}); });
it('renders the table loading icon', () => { it('renders the table loading icon', () => {
...@@ -145,48 +153,46 @@ describe('ComplianceReport component', () => { ...@@ -145,48 +153,46 @@ describe('ComplianceReport component', () => {
}); });
it('fetches the list of merge request violations with the default filter and sort params', async () => { it('fetches the list of merge request violations with the default filter and sort params', async () => {
expect(mockResolver).toHaveBeenCalledTimes(1); expect(mockGraphQlLoading).toHaveBeenCalledTimes(1);
expect(mockResolver).toHaveBeenCalledWith( expect(mockGraphQlLoading).toHaveBeenCalledWith({
...expectApolloVariables({ fullPath: groupPath,
fullPath: groupPath, filters: parseViolationsQueryFilter(defaultFilterParams),
filter: parseViolationsQueryFilter(defaultQuery), sort: DEFAULT_SORT,
sort: DEFAULT_SORT, first: GRAPHQL_PAGE_SIZE,
first: GRAPHQL_PAGE_SIZE, after: null,
after: null, before: null,
before: null, });
}),
);
}); });
}); });
describe('when the defaultQuery has a sort param', () => { describe('when the defaultFilterParams has a sort param', () => {
const sort = 'SEVERITY_ASC'; const sort = 'VIOLATION_ASC';
beforeEach(() => { beforeEach(() => {
mockResolver = jest.fn(); wrapper = createComponent(
wrapper = createComponent(mount, { defaultQuery: { ...defaultQuery, sort } }); mount,
{ defaultFilterParams: { ...defaultFilterParams, sort } },
mockGraphQlLoading,
);
}); });
it('fetches the list of merge request violations with sort params', async () => { it('fetches the list of merge request violations with sort params', async () => {
expect(mockResolver).toHaveBeenCalledTimes(1); expect(mockGraphQlLoading).toHaveBeenCalledTimes(1);
expect(mockResolver).toHaveBeenCalledWith( expect(mockGraphQlLoading).toHaveBeenCalledWith({
...expectApolloVariables({ fullPath: groupPath,
fullPath: groupPath, filters: parseViolationsQueryFilter(defaultFilterParams),
filter: parseViolationsQueryFilter(defaultQuery), sort,
sort, first: GRAPHQL_PAGE_SIZE,
first: GRAPHQL_PAGE_SIZE, after: null,
after: null, before: null,
before: null, });
}),
);
}); });
}); });
describe('when the query fails', () => { describe('when the query fails', () => {
beforeEach(() => { beforeEach(() => {
jest.spyOn(Sentry, 'captureException'); jest.spyOn(Sentry, 'captureException');
mockResolver = jest.fn().mockRejectedValue(mockGraphQlError); wrapper = createComponent(shallowMount, {}, mockGraphQlError);
wrapper = createComponent();
}); });
it('renders the error message', async () => { it('renders the error message', async () => {
...@@ -196,19 +202,20 @@ describe('ComplianceReport component', () => { ...@@ -196,19 +202,20 @@ describe('ComplianceReport component', () => {
expect(findErrorMessage().text()).toBe( expect(findErrorMessage().text()).toBe(
'Retrieving the compliance report failed. Refresh the page and try again.', 'Retrieving the compliance report failed. Refresh the page and try again.',
); );
expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(mockGraphQlError); expect(Sentry.captureException.mock.calls[0][0].networkError).toBe(sentryError);
}); });
}); });
describe('when there are violations', () => { describe('when there are violations', () => {
beforeEach(() => { beforeEach(() => {
mockResolver = resolvers.Query.group; wrapper = createComponent(mount, {}, mockGraphQlSuccess);
wrapper = createComponent(mount);
return waitForPromises(); return waitForPromises();
}); });
it('does not render the table loading icon', () => { it('does not render the table loading icon', () => {
expect(mockGraphQlSuccess).toHaveBeenCalledTimes(1);
expect(findTableLoadingIcon().exists()).toBe(false); expect(findTableLoadingIcon().exists()).toBe(false);
}); });
...@@ -224,40 +231,37 @@ describe('ComplianceReport component', () => { ...@@ -224,40 +231,37 @@ describe('ComplianceReport component', () => {
]); ]);
}); });
it('has the correct first row data', () => { it.each(Object.keys(violations))('has the correct data for row %s', (idx) => {
const headerTexts = findTablesFirstRowData().wrappers.map((d) => d.text()); const rowTexts = findTableRowData(idx).wrappers.map((d) => d.text());
expect(headerTexts).toEqual([ expect(rowTexts).toEqual([
'High', 'High',
'Approved by committer', 'Approved by committer',
'Officiis architecto voluptas ut sit qui qui quisquam sequi consectetur porro.', `Merge request ${idx}`,
'in 1 year', 'in 1 year',
'View details', 'View details',
]); ]);
}); });
it('renders the violation severity badge', () => { it('renders the violation severity badge', () => {
const { severityLevel } = mapViolations(mockResolver().mergeRequestViolations.nodes)[0]; const { severityLevel } = violations[0];
expect(findSeverityBadge().props()).toStrictEqual({ severity: severityLevel }); expect(findSeverityBadge().props()).toStrictEqual({ severity: severityLevel });
}); });
it('renders the violation reason', () => { it('renders the violation reason', () => {
const { const { violatingUser, reason } = violations[0];
violatingUser: { __typename, ...user },
reason,
} = mockResolver().mergeRequestViolations.nodes[0];
expect(findViolationReason().props()).toMatchObject({ expect(findViolationReason().props()).toMatchObject({
reason, reason,
user, user: stripTypenames(violatingUser),
}); });
}); });
it('renders the time ago tooltip', () => { it('renders the time ago tooltip', () => {
const { const {
mergeRequest: { mergedAt }, mergeRequest: { mergedAt },
} = mockResolver().mergeRequestViolations.nodes[0]; } = violations[0];
expect(findTimeAgoTooltip().props('time')).toBe(mergedAt); expect(findTimeAgoTooltip().props('time')).toBe(mergedAt);
}); });
...@@ -280,7 +284,7 @@ describe('ComplianceReport component', () => { ...@@ -280,7 +284,7 @@ describe('ComplianceReport component', () => {
${selectRow} | ${'row is selected'} ${selectRow} | ${'row is selected'}
`('when a $eventDescription', ({ rowAction, eventDescription }) => { `('when a $eventDescription', ({ rowAction, eventDescription }) => {
it('opens then drawer', async () => { it('opens then drawer', async () => {
const drawerData = mapViolations(mockResolver().mergeRequestViolations.nodes)[0]; const drawerData = mapViolations(violations)[0];
await rowAction(0); await rowAction(0);
...@@ -303,7 +307,7 @@ describe('ComplianceReport component', () => { ...@@ -303,7 +307,7 @@ describe('ComplianceReport component', () => {
}); });
it(`swaps the drawer when another ${eventDescription}`, async () => { it(`swaps the drawer when another ${eventDescription}`, async () => {
const drawerData = mapViolations(mockResolver().mergeRequestViolations.nodes)[1]; const drawerData = mapViolations(violations)[1];
await rowAction(0); await rowAction(0);
await rowAction(1); await rowAction(1);
...@@ -319,59 +323,40 @@ describe('ComplianceReport component', () => { ...@@ -319,59 +323,40 @@ describe('ComplianceReport component', () => {
}); });
}); });
describe('violation filter', () => { describe('when the filters changed', () => {
beforeEach(() => { const query = { mergedAfter, mergedBefore, projectIds: [1, 2, 3] };
mockResolver = jest.fn().mockReturnValue(resolvers.Query.group());
wrapper = createComponent(mount);
return waitForPromises(); beforeEach(() => {
return findViolationFilter().vm.$emit('filters-changed', query);
}); });
it('configures the filter', () => { it('updates the URL query', () => {
expect(findViolationFilter().props()).toMatchObject({ expect(findUrlSync().props('query')).toMatchObject(query);
groupPath,
defaultQuery,
});
}); });
describe('when the filters changed', () => { it('shows the table loading icon', () => {
const query = { mergedAfter, mergedBefore, projectIds: [1, 2, 3] }; expect(findTableLoadingIcon().exists()).toBe(true);
});
beforeEach(() => {
return findViolationFilter().vm.$emit('filters-changed', query);
});
it('updates the URL query', () => {
expect(findUrlSync().props('query')).toMatchObject(query);
});
it('shows the table loading icon', () => {
expect(findTableLoadingIcon().exists()).toBe(true);
});
it('sets the pagination component to disabled', () => { it('sets the pagination component to disabled', () => {
expect(findPagination().props('disabled')).toBe(true); expect(findPagination().props('disabled')).toBe(true);
}); });
it('clears the project URL query param if the project array is empty', async () => { it('clears the project URL query param if the project array is empty', async () => {
await findViolationFilter().vm.$emit('filters-changed', { ...query, projectIds: [] }); await findViolationFilter().vm.$emit('filters-changed', { ...query, projectIds: [] });
expect(findUrlSync().props('query')).toMatchObject({ ...query, projectIds: null }); expect(findUrlSync().props('query')).toMatchObject({ ...query, projectIds: null });
}); });
it('fetches the filtered violations', async () => { it('fetches the filtered violations', async () => {
expect(mockResolver).toHaveBeenCalledTimes(2); expect(mockGraphQlSuccess).toHaveBeenCalledTimes(2);
expect(mockResolver).toHaveBeenNthCalledWith( expect(mockGraphQlSuccess).toHaveBeenNthCalledWith(2, {
2, fullPath: groupPath,
...expectApolloVariables({ filters: parseViolationsQueryFilter(query),
fullPath: groupPath, sort: DEFAULT_SORT,
filter: parseViolationsQueryFilter(query), first: GRAPHQL_PAGE_SIZE,
sort: DEFAULT_SORT, after: null,
first: GRAPHQL_PAGE_SIZE, before: null,
after: null,
before: null,
}),
);
}); });
}); });
}); });
...@@ -380,10 +365,6 @@ describe('ComplianceReport component', () => { ...@@ -380,10 +365,6 @@ describe('ComplianceReport component', () => {
const sortState = { sortBy: 'mergedAt', sortDesc: true }; const sortState = { sortBy: 'mergedAt', sortDesc: true };
beforeEach(async () => { beforeEach(async () => {
mockResolver = jest.fn().mockReturnValue(resolvers.Query.group());
wrapper = createComponent(mount);
await waitForPromises();
await findViolationsTable().vm.$emit('sort-changed', sortState); await findViolationsTable().vm.$emit('sort-changed', sortState);
}); });
...@@ -398,31 +379,23 @@ describe('ComplianceReport component', () => { ...@@ -398,31 +379,23 @@ describe('ComplianceReport component', () => {
}); });
it('fetches the sorted violations', async () => { it('fetches the sorted violations', async () => {
expect(mockResolver).toHaveBeenCalledTimes(2); expect(mockGraphQlSuccess).toHaveBeenCalledTimes(2);
expect(mockResolver).toHaveBeenNthCalledWith( expect(mockGraphQlSuccess).toHaveBeenNthCalledWith(2, {
2, fullPath: groupPath,
...expectApolloVariables({ filters: parseViolationsQueryFilter(defaultFilterParams),
fullPath: groupPath, sort: sortObjectToString(sortState),
filter: parseViolationsQueryFilter(defaultQuery), first: GRAPHQL_PAGE_SIZE,
sort: sortObjectToString(sortState), after: null,
first: GRAPHQL_PAGE_SIZE, before: null,
after: null, });
before: null,
}),
);
}); });
}); });
describe('pagination', () => { describe('pagination', () => {
beforeEach(() => {
mockResolver = jest.fn().mockReturnValue(resolvers.Query.group());
wrapper = createComponent(mount);
return waitForPromises();
});
it('renders and configures the pagination', () => { it('renders and configures the pagination', () => {
const pageInfo = stripTypenames(resolvers.Query.group().mergeRequestViolations.pageInfo); const pageInfo = stripTypenames(
violationsResponse.data.group.mergeRequestViolations.pageInfo,
);
expect(findPagination().props()).toMatchObject({ expect(findPagination().props()).toMatchObject({
...pageInfo, ...pageInfo,
...@@ -440,29 +413,29 @@ describe('ComplianceReport component', () => { ...@@ -440,29 +413,29 @@ describe('ComplianceReport component', () => {
await findPagination().vm.$emit(event, after ?? before); await findPagination().vm.$emit(event, after ?? before);
await waitForPromises(); await waitForPromises();
expect(mockResolver).toHaveBeenCalledTimes(2); expect(mockGraphQlSuccess).toHaveBeenCalledTimes(2);
expect(mockResolver).toHaveBeenNthCalledWith( expect(mockGraphQlSuccess).toHaveBeenNthCalledWith(2, {
2, fullPath: groupPath,
...expectApolloVariables({ filters: parseViolationsQueryFilter(defaultFilterParams),
fullPath: groupPath, first: GRAPHQL_PAGE_SIZE,
filter: parseViolationsQueryFilter(defaultQuery), sort: DEFAULT_SORT,
first: GRAPHQL_PAGE_SIZE, after,
sort: DEFAULT_SORT, before,
after, });
before,
}),
);
}, },
); );
describe('when there are no next or previous pages', () => { describe('when there are no next or previous pages', () => {
beforeEach(() => { beforeEach(() => {
const group = resolvers.Query.group(); const noPagesResponse = createComplianceViolationsResponse({
group.mergeRequestViolations.pageInfo.hasNextPage = false; pageInfo: {
group.mergeRequestViolations.pageInfo.hasPreviousPage = false; hasNextPage: false,
hasPreviousPage: false,
},
});
const mockResolver = jest.fn().mockResolvedValue(noPagesResponse);
mockResolver = () => jest.fn().mockReturnValue(group); wrapper = createComponent(mount, {}, mockResolver);
wrapper = createComponent(mount);
return waitForPromises(); return waitForPromises();
}); });
...@@ -471,20 +444,37 @@ describe('ComplianceReport component', () => { ...@@ -471,20 +444,37 @@ describe('ComplianceReport component', () => {
expect(findPagination().exists()).toBe(false); expect(findPagination().exists()).toBe(false);
}); });
}); });
describe('when the next page has been selected', () => {
beforeEach(() => {
return findPagination().vm.$emit('next', 'foo');
});
it('clears the pagination when the filter is updated', async () => {
const query = { projectIds: [1] };
await findViolationFilter().vm.$emit('filters-changed', query);
expect(mockGraphQlSuccess).toHaveBeenCalledTimes(3);
expect(mockGraphQlSuccess).toHaveBeenNthCalledWith(3, {
fullPath: groupPath,
filters: parseViolationsQueryFilter(query),
first: GRAPHQL_PAGE_SIZE,
sort: DEFAULT_SORT,
after: null,
before: null,
});
});
});
}); });
}); });
describe('when there are no violations', () => { describe('when there are no violations', () => {
beforeEach(() => { beforeEach(() => {
mockResolver = () => ({ const noViolationsResponse = createComplianceViolationsResponse({ count: 0 });
__typename: 'Group', const mockResolver = jest.fn().mockResolvedValue(noViolationsResponse);
id: 1,
mergeRequestViolations: { wrapper = createComponent(mount, {}, mockResolver);
__typename: 'MergeRequestViolations',
nodes: [],
},
});
wrapper = createComponent(mount);
return waitForPromises(); return waitForPromises();
}); });
......
...@@ -3,7 +3,10 @@ import Vue from 'vue'; ...@@ -3,7 +3,10 @@ import Vue from 'vue';
import { GlDaterangePicker } from '@gitlab/ui'; import { GlDaterangePicker } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ViolationFilter from 'ee/compliance_dashboard/components/violations/filter.vue'; import ViolationFilter from 'ee/compliance_dashboard/components/violations/filter.vue';
import { convertProjectIdsToGraphQl } from 'ee/compliance_dashboard/utils'; import {
convertProjectIdsToGraphQl,
buildDefaultFilterParams,
} from 'ee/compliance_dashboard/utils';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue'; import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import { getDateInPast, pikadayToString } from '~/lib/utils/datetime_utility'; import { getDateInPast, pikadayToString } from '~/lib/utils/datetime_utility';
import { CURRENT_DATE } from 'ee/audit_events/constants'; import { CURRENT_DATE } from 'ee/audit_events/constants';
...@@ -16,6 +19,7 @@ Vue.use(VueApollo); ...@@ -16,6 +19,7 @@ Vue.use(VueApollo);
describe('ViolationFilter component', () => { describe('ViolationFilter component', () => {
let wrapper; let wrapper;
const defaultQuery = buildDefaultFilterParams('');
const groupPath = 'group-path'; const groupPath = 'group-path';
const projectIds = ['1', '2']; const projectIds = ['1', '2'];
const startDate = getDateInPast(CURRENT_DATE, 20); const startDate = getDateInPast(CURRENT_DATE, 20);
...@@ -42,7 +46,7 @@ describe('ViolationFilter component', () => { ...@@ -42,7 +46,7 @@ describe('ViolationFilter component', () => {
apolloProvider: mockApollo(mockResponse), apolloProvider: mockApollo(mockResponse),
propsData: { propsData: {
groupPath, groupPath,
defaultQuery: {}, defaultQuery,
...propsData, ...propsData,
}, },
}); });
...@@ -108,7 +112,9 @@ describe('ViolationFilter component', () => { ...@@ -108,7 +112,9 @@ describe('ViolationFilter component', () => {
await findProjectsFilter().vm.$emit('selected', defaultProjects); await findProjectsFilter().vm.$emit('selected', defaultProjects);
expect(wrapper.emitted('filters-changed')).toHaveLength(1); expect(wrapper.emitted('filters-changed')).toHaveLength(1);
expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([{ projectIds: expectedIds }]); expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([
{ ...defaultQuery, projectIds: expectedIds },
]);
}); });
it('emits a query with a start and end date when a date range has been inputted', async () => { it('emits a query with a start and end date when a date range has been inputted', async () => {
...@@ -118,37 +124,29 @@ describe('ViolationFilter component', () => { ...@@ -118,37 +124,29 @@ describe('ViolationFilter component', () => {
expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([{ ...dateRangeQuery }]); expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([{ ...dateRangeQuery }]);
}); });
describe('with a default query', () => { it('emits the existing filter query with mutations on each update', async () => {
const defaultQuery = { projectIds, mergedAfter: '2022-01-01', mergedBefore: '2022-01-31' }; await findProjectsFilter().vm.$emit('selected', []);
beforeEach(() => { expect(wrapper.emitted('filters-changed')).toHaveLength(1);
createComponent({ defaultQuery }); expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([
}); { ...defaultQuery, projectIds: [] },
]);
it('emits the existing filter query with mutations on each update', async () => {
await findProjectsFilter().vm.$emit('selected', []);
expect(wrapper.emitted('filters-changed')).toHaveLength(1);
expect(wrapper.emitted('filters-changed')[0]).toStrictEqual([
{ ...defaultQuery, projectIds: [] },
]);
await findDatePicker().vm.$emit('input', { startDate, endDate }); await findDatePicker().vm.$emit('input', { startDate, endDate });
expect(wrapper.emitted('filters-changed')).toHaveLength(2); expect(wrapper.emitted('filters-changed')).toHaveLength(2);
expect(wrapper.emitted('filters-changed')[1]).toStrictEqual([ expect(wrapper.emitted('filters-changed')[1]).toStrictEqual([
{ {
projectIds: [], projectIds: [],
...dateRangeQuery, ...dateRangeQuery,
}, },
]); ]);
});
}); });
}); });
describe('projects filter', () => { describe('projects filter', () => {
it('fetches the project details when the default query contains projectIds', () => { it('fetches the project details when the default query contains projectIds', () => {
createComponent({ defaultQuery: { projectIds } }); createComponent({ defaultQuery: { ...defaultQuery, projectIds } });
expect(groupProjectsSuccess).toHaveBeenCalledWith({ expect(groupProjectsSuccess).toHaveBeenCalledWith({
groupPath, groupPath,
...@@ -158,7 +156,7 @@ describe('ViolationFilter component', () => { ...@@ -158,7 +156,7 @@ describe('ViolationFilter component', () => {
describe('when the defaultProjects are being fetched', () => { describe('when the defaultProjects are being fetched', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent({ defaultQuery: { projectIds } }, groupProjectsLoading); createComponent({ defaultQuery: { ...defaultQuery, projectIds } }, groupProjectsLoading);
await waitForPromises(); await waitForPromises();
}); });
...@@ -173,7 +171,7 @@ describe('ViolationFilter component', () => { ...@@ -173,7 +171,7 @@ describe('ViolationFilter component', () => {
describe('when the defaultProjects have been fetched', () => { describe('when the defaultProjects have been fetched', () => {
beforeEach(async () => { beforeEach(async () => {
createComponent({ defaultQuery: { projectIds } }); createComponent({ defaultQuery: { ...defaultQuery, projectIds } });
await waitForPromises(); await waitForPromises();
}); });
......
import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers'; import { mapViolations } from 'ee/compliance_dashboard/graphql/mappers';
import resolvers from 'ee/compliance_dashboard/graphql/resolvers'; import { createComplianceViolation } from '../mock_data';
describe('mapViolations', () => { describe('mapViolations', () => {
const mockViolations = resolvers.Query.group().mergeRequestViolations.nodes;
it('returns the expected result', () => { it('returns the expected result', () => {
const { mergeRequest } = mapViolations([{ ...mockViolations[0] }])[0]; const { mergeRequest } = mapViolations([createComplianceViolation()])[0];
expect(mergeRequest).toMatchObject({ expect(mergeRequest).toMatchObject({
reference: mergeRequest.ref, reference: mergeRequest.ref,
......
...@@ -93,3 +93,105 @@ export const createDefaultProjectsResponse = (projects) => ({ ...@@ -93,3 +93,105 @@ export const createDefaultProjectsResponse = (projects) => ({
}, },
}, },
}); });
export const createComplianceViolation = (id) => ({
id: `gid://gitlab/MergeRequests::ComplianceViolation/${id}`,
severityLevel: 'HIGH',
reason: 'APPROVED_BY_COMMITTER',
violatingUser: {
id: 'gid://gitlab/User/21',
name: 'Miranda Friesen',
username: 'karren.medhurst',
avatarUrl: 'https://www.gravatar.com/avatar/9102aef461ba77d0fa0f37daffb834ac?s=80&d=identicon',
webUrl: 'http://gdk.test:3000/karren.medhurst',
__typename: 'UserCore',
},
mergeRequest: {
id: `gid://gitlab/MergeRequest/${id}`,
title: `Merge request ${id}`,
mergedAt: '2022-03-06T16:39:12Z',
webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell/-/merge_requests/56',
author: {
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
webUrl: 'http://gdk.test:3000/root',
__typename: 'UserCore',
},
mergeUser: null,
committers: {
nodes: [],
__typename: 'UserCoreConnection',
},
participants: {
nodes: [
{
id: 'gid://gitlab/User/1',
name: 'Administrator',
username: 'root',
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
webUrl: 'http://gdk.test:3000/root',
__typename: 'UserCore',
},
],
__typename: 'UserCoreConnection',
},
approvedBy: {
nodes: [],
__typename: 'UserCoreConnection',
},
ref: '!56',
fullRef: 'gitlab-org/gitlab-shell!56',
sourceBranch: 'master',
sourceBranchExists: false,
targetBranch: 'feature',
targetBranchExists: false,
project: {
id: 'gid://gitlab/Project/2',
avatarUrl: null,
name: 'Gitlab Shell',
webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-shell',
complianceFrameworks: {
nodes: [
{
id: 'gid://gitlab/ComplianceManagement::Framework/1',
name: 'GDPR',
description: 'asds',
color: '#0000ff',
__typename: 'ComplianceFramework',
},
],
__typename: 'ComplianceFrameworkConnection',
},
__typename: 'Project',
},
__typename: 'MergeRequest',
},
__typename: 'ComplianceViolation',
});
export const createComplianceViolationsResponse = ({ count = 1, pageInfo = {} } = {}) => ({
data: {
group: {
id: 'gid://gitlab/Group/1',
__typename: 'Group',
mergeRequestViolations: {
__typename: 'ComplianceViolationConnection',
nodes: Array(count)
.fill(null)
.map((_, id) => createComplianceViolation(id)),
pageInfo: {
endCursor: 'abc',
hasNextPage: true,
hasPreviousPage: false,
startCursor: 'abc',
__typename: 'PageInfo',
...pageInfo,
},
},
},
},
});
import * as utils from 'ee/compliance_dashboard/utils'; import * as utils from 'ee/compliance_dashboard/utils';
import { queryToObject } from '~/lib/utils/url_utility';
jest.mock('ee/audit_events/constants', () => ({
CURRENT_DATE: new Date('2022 2 28'),
}));
describe('compliance report utils', () => { describe('compliance report utils', () => {
const projectIds = ['1', '2']; const projectIds = ['1', '2'];
...@@ -8,14 +13,14 @@ describe('compliance report utils', () => { ...@@ -8,14 +13,14 @@ describe('compliance report utils', () => {
it('returns the expected result', () => { it('returns the expected result', () => {
const query = { const query = {
projectIds, projectIds,
createdAfter: '2021-12-06', mergedAfter: '2021-12-06',
createdBefore: '2022-01-06', mergedBefore: '2022-01-06',
}; };
expect(utils.parseViolationsQueryFilter(query)).toStrictEqual({ expect(utils.parseViolationsQueryFilter(query)).toStrictEqual({
projectIds: projectGraphQlIds, projectIds: projectGraphQlIds,
createdAfter: query.createdAfter, mergedAfter: query.mergedAfter,
createdBefore: query.createdBefore, mergedBefore: query.mergedBefore,
}); });
}); });
}); });
...@@ -25,4 +30,24 @@ describe('compliance report utils', () => { ...@@ -25,4 +30,24 @@ describe('compliance report utils', () => {
expect(utils.convertProjectIdsToGraphQl(projectIds)).toStrictEqual(projectGraphQlIds); expect(utils.convertProjectIdsToGraphQl(projectIds)).toStrictEqual(projectGraphQlIds);
}); });
}); });
describe('buildDefaultFilterParams', () => {
it('returns the expected result with the default date range of 30 days', () => {
const queryString = 'projectIds[]=20';
expect(utils.buildDefaultFilterParams(queryString)).toStrictEqual({
mergedAfter: '2022-01-29',
mergedBefore: '2022-02-28',
projectIds: ['20'],
});
});
it('return the expected result when the query contains dates', () => {
const queryString = 'mergedAfter=2022-02-09&mergedBefore=2022-03-11&projectIds[]=20';
expect(utils.buildDefaultFilterParams(queryString)).toStrictEqual(
queryToObject(queryString, { gatherArrays: true }),
);
});
});
}); });
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