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: {
......
...@@ -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