Commit 517d78ff authored by Jose Ivan Vargas's avatar Jose Ivan Vargas

Merge branch '328194-graphql-pipelines-part-1' into 'master'

Part 1 of Pipeline and Merge request artifact downloads

See merge request gitlab-org/gitlab!60771
parents 23750887 784eff81
query securityReportDownloadPaths(
$projectPath: ID!
$iid: String!
$reportTypes: [SecurityReportTypeEnum!]
) {
project(fullPath: $projectPath) {
mergeRequest(iid: $iid) {
headPipeline {
id
jobs(securityReportTypes: $reportTypes) {
nodes {
name
artifacts {
nodes {
downloadPath
fileType
}
}
}
}
}
}
}
}
query getCorpuses($projectPath: ID!, $iid: ID, $reportTypes: [SecurityReportTypeEnum!]) {
project(fullPath: $projectPath) {
pipeline(iid: $iid) {
id
jobs(securityReportTypes: $reportTypes) {
nodes {
name
artifacts {
nodes {
downloadPath
fileType
}
}
}
}
}
}
}
......@@ -16,7 +16,7 @@ import {
import securityReportDownloadPathsQuery from './queries/security_report_download_paths.query.graphql';
import store from './store';
import { MODULE_SAST, MODULE_SECRET_DETECTION } from './store/constants';
import { extractSecurityReportArtifacts } from './utils';
import { extractSecurityReportArtifactsFromMergeRequest } from './utils';
export default {
store,
......@@ -97,7 +97,7 @@ export default {
};
},
update(data) {
return extractSecurityReportArtifacts(this.$options.reportTypes, data);
return extractSecurityReportArtifactsFromMergeRequest(this.$options.reportTypes, data);
},
error(error) {
this.showError(error);
......
......@@ -14,9 +14,7 @@ const addReportTypeIfExists = (acc, reportTypes, reportType, getName, downloadPa
}
};
export const extractSecurityReportArtifacts = (reportTypes, data) => {
const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
const extractSecurityReportArtifacts = (reportTypes, jobs) => {
return jobs.reduce((acc, job) => {
const artifacts = job.artifacts?.nodes ?? [];
......@@ -41,3 +39,13 @@ export const extractSecurityReportArtifacts = (reportTypes, data) => {
return acc;
}, []);
};
export const extractSecurityReportArtifactsFromPipeline = (reportTypes, data) => {
const jobs = data.project?.pipeline?.jobs?.nodes ?? [];
return extractSecurityReportArtifacts(reportTypes, jobs);
};
export const extractSecurityReportArtifactsFromMergeRequest = (reportTypes, data) => {
const jobs = data.project?.mergeRequest?.headPipeline?.jobs?.nodes ?? [];
return extractSecurityReportArtifacts(reportTypes, jobs);
};
......@@ -4,7 +4,7 @@ import createFlash from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils';
export default {
components: {
......@@ -45,7 +45,7 @@ export default {
};
},
update(data) {
return extractSecurityReportArtifacts(this.reportTypes, data);
return extractSecurityReportArtifactsFromMergeRequest(this.reportTypes, data);
},
error(error) {
this.showError(error);
......
<script>
import { reportTypeToSecurityReportTypeEnum } from 'ee/vue_shared/security_reports/constants';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_mr_download_paths.query.graphql';
import { extractSecurityReportArtifactsFromMergeRequest } from '~/vue_shared/security_reports/utils';
export default {
components: {
SecurityReportDownloadDropdown,
},
props: {
reportTypes: {
type: Array,
required: true,
validator: (reportType) => {
return reportType.every((report) => reportTypeToSecurityReportTypeEnum[report]);
},
},
targetProjectFullPath: {
type: String,
required: true,
},
mrIid: {
type: Number,
required: true,
},
},
data() {
return {
reportArtifacts: [],
};
},
apollo: {
reportArtifacts: {
query: securityReportDownloadPathsQuery,
variables() {
return {
projectPath: this.targetProjectFullPath,
iid: String(this.mrIid),
reportTypes: this.reportTypes.map(
(reportType) => reportTypeToSecurityReportTypeEnum[reportType],
),
};
},
update(data) {
return extractSecurityReportArtifactsFromMergeRequest(this.reportTypes, data);
},
error(error) {
this.showError(error);
},
},
},
computed: {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
},
methods: {
showError(error) {
createFlash({
message: this.$options.i18n.apiError,
captureError: true,
error,
});
},
},
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
},
};
</script>
<template>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
/>
</template>
<script>
import { reportTypeToSecurityReportTypeEnum } from 'ee/vue_shared/security_reports/constants';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql';
import { extractSecurityReportArtifactsFromPipeline } from '~/vue_shared/security_reports/utils';
export default {
components: {
SecurityReportDownloadDropdown,
},
props: {
reportTypes: {
type: Array,
required: true,
validator: (reportType) => {
return reportType.every((report) => reportTypeToSecurityReportTypeEnum[report]);
},
},
targetProjectFullPath: {
type: String,
required: true,
},
pipelineIid: {
type: Number,
required: true,
},
},
data() {
return {
reportArtifacts: [],
};
},
apollo: {
reportArtifacts: {
query: securityReportDownloadPathsQuery,
variables() {
return {
projectPath: this.targetProjectFullPath,
iid: String(this.mrIid),
reportTypes: this.reportTypes.map(
(reportType) => reportTypeToSecurityReportTypeEnum[reportType],
),
};
},
update(data) {
return extractSecurityReportArtifactsFromPipeline(this.reportTypes, data);
},
error(error) {
this.showError(error);
},
},
},
computed: {
isLoadingReportArtifacts() {
return this.$apollo.queries.reportArtifacts.loading;
},
},
methods: {
showError(error) {
createFlash({
message: this.$options.i18n.apiError,
captureError: true,
error,
});
},
},
i18n: {
apiError: s__(
'SecurityReports|Failed to get security report information. Please reload the page or try again later.',
),
},
};
</script>
<template>
<security-report-download-dropdown
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
/>
</template>
......@@ -3,6 +3,7 @@ import { invert } from 'lodash';
import {
reportTypeToSecurityReportTypeEnum as reportTypeToSecurityReportTypeEnumCE,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_COVERAGE_FUZZING,
} from '~/vue_shared/security_reports/constants';
export * from '~/vue_shared/security_reports/constants';
......@@ -13,6 +14,7 @@ export * from '~/vue_shared/security_reports/constants';
* These should correspond to the lowercase security scan report types.
*/
export const SECURITY_REPORT_TYPE_ENUM_API_FUZZING = 'API_FUZZING';
export const SECURITY_REPORT_TYPE_ENUM_COVERAGE_FUZZING = 'COVERAGE_FUZZING';
/* Override CE Definitions */
......@@ -22,6 +24,7 @@ export const SECURITY_REPORT_TYPE_ENUM_API_FUZZING = 'API_FUZZING';
export const reportTypeToSecurityReportTypeEnum = {
...reportTypeToSecurityReportTypeEnumCE,
[REPORT_TYPE_API_FUZZING]: SECURITY_REPORT_TYPE_ENUM_API_FUZZING,
[REPORT_TYPE_COVERAGE_FUZZING]: SECURITY_REPORT_TYPE_ENUM_COVERAGE_FUZZING,
};
/**
......
......@@ -20,7 +20,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
......@@ -29,7 +29,7 @@ import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/consta
// Force Jest to transpile and cache
// eslint-disable-next-line no-unused-vars
import _Deployment from '~/vue_merge_request_widget/components/deployment/deployment.vue';
import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_mr_download_paths.query.graphql';
import mockData, {
baseBrowserPerformance,
......@@ -781,6 +781,7 @@ describe('ee merge request widget options', () => {
beforeEach(() => {
gl.mrWidgetData = {
...mockData,
target_project_full_path: '',
enabled_reports: {
coverage_fuzzing: true,
},
......@@ -1037,8 +1038,8 @@ describe('ee merge request widget options', () => {
propsData: { mrData: gl.mrWidgetData },
apolloProvider: createMockApollo([
[
securityReportDownloadPathsQuery,
async () => ({ data: securityReportDownloadPathsQueryResponse }),
securityReportMergeRequestDownloadPathsQuery,
async () => ({ data: securityReportMergeRequestDownloadPathsQueryResponse }),
],
]),
});
......
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Component from 'ee/vue_shared/security_reports/components/artifact_downloads/mr_artifact_download.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from 'ee/vue_shared/security_reports/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
expectedDownloadDropdownProps,
securityReportMergeRequestDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
import createFlash from '~/flash';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_mr_download_paths.query.graphql';
jest.mock('~/flash');
describe('Merge request artifact Download', () => {
let wrapper;
const defaultProps = {
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
targetProjectFullPath: '/path',
mrIid: 123,
};
const createWrapper = ({ propsData, options }) => {
wrapper = shallowMount(Component, {
stubs: {
SecurityReportDownloadDropdown,
},
propsData: {
...defaultProps,
...propsData,
},
...options,
});
};
const pendingHandler = () => new Promise(() => {});
const successHandler = () =>
Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => {
Vue.use(VueApollo);
const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
return createMockApollo(requestHandlers);
};
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
afterEach(() => {
wrapper.destroy();
});
describe('given the query is loading', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(pendingHandler),
},
});
});
it('loading is true', () => {
expect(findDownloadDropdown().props('loading')).toBe(true);
});
});
describe('given the query loads successfully', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(successHandler),
},
});
});
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
describe('given the query fails', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(failureHandler),
},
});
});
it('calls createFlash correctly', () => {
expect(createFlash).toHaveBeenCalledWith({
message: Component.i18n.apiError,
captureError: true,
error: expect.any(Error),
});
});
it('renders nothing', () => {
expect(findDownloadDropdown().props('artifacts')).toEqual([]);
});
});
});
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Component from 'ee/vue_shared/security_reports/components/artifact_downloads/pipeline_artifact_download.vue';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_SECRET_DETECTION,
} from 'ee/vue_shared/security_reports/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
expectedDownloadDropdownProps,
securityReportPipelineDownloadPathsQueryResponse,
} from 'jest/vue_shared/security_reports/mock_data';
import createFlash from '~/flash';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportPipelineDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_pipeline_download_paths.query.graphql';
jest.mock('~/flash');
describe('Pipeline artifact Download', () => {
let wrapper;
const defaultProps = {
reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
targetProjectFullPath: '/path',
pipelineIid: 123,
};
const createWrapper = ({ propsData, options }) => {
wrapper = shallowMount(Component, {
stubs: {
SecurityReportDownloadDropdown,
},
propsData: {
...defaultProps,
...propsData,
},
...options,
});
};
const pendingHandler = () => new Promise(() => {});
const successHandler = () =>
Promise.resolve({ data: securityReportPipelineDownloadPathsQueryResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => {
Vue.use(VueApollo);
const requestHandlers = [[securityReportPipelineDownloadPathsQuery, handler]];
return createMockApollo(requestHandlers);
};
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
afterEach(() => {
wrapper.destroy();
});
describe('given the query is loading', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(pendingHandler),
},
});
});
it('loading is true', () => {
expect(findDownloadDropdown().props('loading')).toBe(true);
});
});
describe('given the query loads successfully', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(successHandler),
},
});
});
it('renders the download dropdown', () => {
expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
describe('given the query fails', () => {
beforeEach(() => {
createWrapper({
options: {
apolloProvider: createMockApolloProvider(failureHandler),
},
});
});
it('calls createFlash correctly', () => {
expect(createFlash).toHaveBeenCalledWith({
message: Component.i18n.apiError,
captureError: true,
error: expect.any(Error),
});
});
it('renders nothing', () => {
expect(findDownloadDropdown().props('artifacts')).toEqual([]);
});
});
});
......@@ -339,7 +339,7 @@ export const securityReportDownloadPathsQueryNoArtifactsResponse = {
},
};
export const securityReportDownloadPathsQueryResponse = {
export const securityReportMergeRequestDownloadPathsQueryResponse = {
project: {
mergeRequest: {
headPipeline: {
......@@ -447,6 +447,114 @@ export const securityReportDownloadPathsQueryResponse = {
},
};
export const securityReportDownloadPathsQueryResponse = securityReportMergeRequestDownloadPathsQueryResponse;
export const securityReportPipelineDownloadPathsQueryResponse = {
project: {
pipeline: {
id: 'gid://gitlab/Ci::Pipeline/176',
jobs: {
nodes: [
{
name: 'secret_detection',
artifacts: {
nodes: [
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
fileType: 'SECRET_DETECTION',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
{
name: 'bandit-sast',
artifacts: {
nodes: [
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
fileType: 'SAST',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
{
name: 'eslint-sast',
artifacts: {
nodes: [
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
fileType: 'SAST',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
{
name: 'all_artifacts',
artifacts: {
nodes: [
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
fileType: 'ARCHIVE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
fileType: 'TRACE',
__typename: 'CiJobArtifact',
},
{
downloadPath:
'/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
fileType: 'METADATA',
__typename: 'CiJobArtifact',
},
],
__typename: 'CiJobArtifactConnection',
},
__typename: 'CiJob',
},
],
__typename: 'CiJobConnection',
},
__typename: 'Pipeline',
},
__typename: 'MergeRequest',
},
__typename: 'Project',
};
/**
* These correspond to SAST jobs in the securityReportDownloadPathsQueryResponse above.
*/
......
......@@ -3,9 +3,13 @@ import {
REPORT_TYPE_SECRET_DETECTION,
REPORT_FILE_TYPES,
} from '~/vue_shared/security_reports/constants';
import { extractSecurityReportArtifacts } from '~/vue_shared/security_reports/utils';
import {
securityReportDownloadPathsQueryResponse,
extractSecurityReportArtifactsFromMergeRequest,
extractSecurityReportArtifactsFromPipeline,
} from '~/vue_shared/security_reports/utils';
import {
securityReportMergeRequestDownloadPathsQueryResponse,
securityReportPipelineDownloadPathsQueryResponse,
sastArtifacts,
secretDetectionArtifacts,
archiveArtifacts,
......@@ -13,7 +17,18 @@ import {
metadataArtifacts,
} from './mock_data';
describe('extractSecurityReportArtifacts', () => {
describe.each([
[
'extractSecurityReportArtifactsFromMergeRequest',
extractSecurityReportArtifactsFromMergeRequest,
securityReportMergeRequestDownloadPathsQueryResponse,
],
[
'extractSecurityReportArtifactsFromPipelines',
extractSecurityReportArtifactsFromPipeline,
securityReportPipelineDownloadPathsQueryResponse,
],
])('%s', (funcName, extractFunc, response) => {
it.each`
reportTypes | expectedArtifacts
${[]} | ${[]}
......@@ -27,9 +42,7 @@ describe('extractSecurityReportArtifacts', () => {
`(
'returns the expected artifacts given report types $reportTypes',
({ reportTypes, expectedArtifacts }) => {
expect(
extractSecurityReportArtifacts(reportTypes, securityReportDownloadPathsQueryResponse),
).toEqual(expectedArtifacts);
expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts);
},
);
});
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