Commit 3b7e20f7 authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '342897-update-merge-request-violations-to-consume-api' into 'master'

Update compliance violations app to fetch results from GraphQL API

See merge request gitlab-org/gitlab!82397
parents 28ee1ea6 c9ef3f77
...@@ -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