Commit 2607e05d authored by Markus Koller's avatar Markus Koller

Merge branch '349830-on-demand-scan-form-redirect' into 'master'

Redirect to on-demand scan form after editing a DAST profile

See merge request gitlab-org/gitlab!77819
parents d5d459d6 d5edfabb
......@@ -145,8 +145,11 @@ export default {
};
},
computed: {
dastScanId() {
return this.dastScan?.id ?? null;
},
isEdit() {
return Boolean(this.dastScan?.id);
return Boolean(this.dastScanId);
},
title() {
return this.isEdit
......@@ -443,6 +446,7 @@ export default {
:profiles="scannerProfiles"
:selected-profile="selectedScannerProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
<site-profile-selector
v-model="selectedSiteProfileId"
......@@ -450,6 +454,7 @@ export default {
:profiles="siteProfiles"
:selected-profile="selectedSiteProfile"
:has-conflict="hasProfilesConflict"
:dast-scan-id="dastScanId"
/>
<scan-schedule v-model="profileSchedule" class="gl-mb-5" />
......
......@@ -10,6 +10,9 @@ import {
} from '@gitlab/ui';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { setUrlParams, relativePathToAbsolute, getBaseURL } from '~/lib/utils/url_utility';
import { FROM_ONDEMAND_SCAN_ID_QUERY_PARAM } from '../../settings';
export default {
i18n: {
......@@ -46,6 +49,11 @@ export default {
required: false,
default: null,
},
dastScanId: {
type: String,
required: false,
default: null,
},
},
data() {
return { searchTerm: '' };
......@@ -65,6 +73,30 @@ export default {
filteredProfilesEmpty() {
return this.filteredProfiles.length === 0;
},
editProfilePath() {
if (!this.selectedProfile) {
return '';
}
const {
selectedProfile: { editPath },
dastScanId,
} = this;
return this.pathWithDastScanId(editPath, dastScanId);
},
actualNewProfilePath() {
const { newProfilePath, dastScanId } = this;
return this.pathWithDastScanId(newProfilePath, dastScanId);
},
},
methods: {
pathWithDastScanId(path, dastScanId = null) {
return dastScanId
? setUrlParams(
{ [FROM_ONDEMAND_SCAN_ID_QUERY_PARAM]: getIdFromGraphQLId(dastScanId) },
relativePathToAbsolute(path, getBaseURL()),
)
: path;
},
},
};
</script>
......@@ -110,7 +142,7 @@ export default {
{{ __('No matching results...') }}
</div>
<template #footer>
<gl-dropdown-item :href="newProfilePath" data-testid="create-profile-option">
<gl-dropdown-item :href="actualNewProfilePath" data-testid="create-profile-option">
<slot name="new-profile"></slot>
</gl-dropdown-item>
<gl-dropdown-item :href="libraryPath" data-testid="manage-profiles-option">
......@@ -127,11 +159,12 @@ export default {
<gl-button
v-if="selectedProfile"
v-gl-tooltip
data-testid="selected-profile-edit-link"
category="primary"
icon="pencil"
:title="$options.i18n.editProfileLabel"
:aria-label="$options.i18n.editProfileLabel"
:href="selectedProfile.editPath"
:href="editProfilePath"
class="gl-absolute gl-right-7 gl-z-index-1"
/>
<slot name="summary"></slot>
......@@ -142,7 +175,7 @@ export default {
<slot name="no-profiles"></slot>
</p>
<gl-button
:href="newProfilePath"
:href="actualNewProfilePath"
variant="confirm"
category="secondary"
data-testid="create-profile-link"
......
......@@ -87,3 +87,5 @@ export const SCAN_CADENCE_OPTIONS = [
},
},
];
export const FROM_ONDEMAND_SCAN_ID_QUERY_PARAM = 'from_on_demand_scan_id';
......@@ -13,7 +13,7 @@ export default () => {
const {
projectFullPath,
profilesLibraryPath,
onDemandScansPath,
onDemandScanFormPath,
dastConfigurationPath,
} = el.dataset;
......@@ -24,7 +24,7 @@ export default () => {
}
const factoryParams = {
allowedPaths: [onDemandScansPath, dastConfigurationPath],
allowedPaths: [onDemandScanFormPath, dastConfigurationPath],
profilesLibraryPath,
urlParamKey: 'scanner_profile_id',
};
......
......@@ -13,7 +13,7 @@ export default () => {
const {
projectFullPath,
profilesLibraryPath,
onDemandScansPath,
onDemandScanFormPath,
dastConfigurationPath,
} = el.dataset;
......@@ -24,7 +24,7 @@ export default () => {
}
const factoryParams = {
allowedPaths: [onDemandScansPath, dastConfigurationPath],
allowedPaths: [onDemandScanFormPath, dastConfigurationPath],
profilesLibraryPath,
urlParamKey: 'site_profile_id',
};
......
......@@ -9,4 +9,47 @@ module Projects::Security::DastProfilesHelper
'timezones' => timezone_data(format: :abbr).to_json
}
end
def dast_scanner_profile_form_data(project)
dast_profile_forms_common_data(project).merge({
profiles_library_path: project_security_configuration_dast_scans_path(project, anchor: 'scanner-profiles')
})
end
def edit_dast_scanner_profile_form_data(project, scanner_profile)
dast_scanner_profile_form_data(project).merge({
scanner_profile: {
id: scanner_profile.to_global_id.to_s,
profile_name: scanner_profile.name,
spider_timeout: scanner_profile.spider_timeout,
target_timeout: scanner_profile.target_timeout,
scan_type: scanner_profile.scan_type.upcase,
use_ajax_spider: scanner_profile.use_ajax_spider,
show_debug_messages: scanner_profile.show_debug_messages,
referenced_in_security_policies: scanner_profile.referenced_in_security_policies
}.to_json
})
end
def dast_site_profile_form_data(project)
dast_profile_forms_common_data(project).merge({
profiles_library_path: project_security_configuration_dast_scans_path(project, anchor: 'site-profiles')
})
end
def edit_dast_site_profile_form_data(project, site_profile)
dast_site_profile_form_data(project).merge({
site_profile: site_profile.to_json
})
end
private
def dast_profile_forms_common_data(project)
{
project_full_path: project.path_with_namespace,
on_demand_scan_form_path: params&.dig(:from_on_demand_scan_id) ? edit_project_on_demand_scan_path(project, id: params[:from_on_demand_scan_id]) : new_project_on_demand_scan_path(project),
dast_configuration_path: project_security_configuration_dast_path(project)
}
end
end
......@@ -3,11 +3,4 @@
- breadcrumb_title s_('DastProfiles|Edit scanner profile')
- page_title s_('DastProfiles|Edit scanner profile')
.js-dast-scanner-profile-form{ data: { project_full_path: @project.path_with_namespace,
profiles_library_path: project_security_configuration_dast_scans_path(@project, anchor: 'scanner-profiles'),
scanner_profile: { id: @scanner_profile.to_global_id.to_s, profile_name: @scanner_profile.name,
spider_timeout: @scanner_profile.spider_timeout, target_timeout: @scanner_profile.target_timeout,
scan_type: @scanner_profile.scan_type.upcase, use_ajax_spider: @scanner_profile.use_ajax_spider,
show_debug_messages: @scanner_profile.show_debug_messages, referenced_in_security_policies: @scanner_profile.referenced_in_security_policies }.to_json,
on_demand_scans_path: new_project_on_demand_scan_path(@project),
dast_configuration_path: project_security_configuration_dast_path(@project) } }
.js-dast-scanner-profile-form{ data: edit_dast_scanner_profile_form_data(@project, @scanner_profile) }
......@@ -3,7 +3,4 @@
- breadcrumb_title s_('DastProfiles|New scanner profile')
- page_title s_('DastProfiles|New scanner profile')
.js-dast-scanner-profile-form{ data: { project_full_path: @project.path_with_namespace,
profiles_library_path: project_security_configuration_dast_scans_path(@project, anchor: 'scanner-profiles'),
on_demand_scans_path: new_project_on_demand_scan_path(@project),
dast_configuration_path: project_security_configuration_dast_path(@project) } }
.js-dast-scanner-profile-form{ data: dast_scanner_profile_form_data(@project) }
......@@ -3,8 +3,4 @@
- breadcrumb_title s_('DastProfiles|Edit site profile')
- page_title s_('DastProfiles|Edit site profile')
.js-dast-site-profile-form{ data: { project_full_path: @project.path_with_namespace,
profiles_library_path: project_security_configuration_dast_scans_path(@project, anchor: 'site-profiles'),
site_profile: @site_profile.to_json,
on_demand_scans_path: new_project_on_demand_scan_path(@project),
dast_configuration_path: project_security_configuration_dast_path(@project) } }
.js-dast-site-profile-form{ data: edit_dast_site_profile_form_data(@project, @site_profile) }
......@@ -3,7 +3,4 @@
- breadcrumb_title s_('DastProfiles|New site profile')
- page_title s_('DastProfiles|New site profile')
.js-dast-site-profile-form{ data: { project_full_path: @project.path_with_namespace,
profiles_library_path: project_security_configuration_dast_scans_path(@project, anchor: 'site-profiles'),
on_demand_scans_path: new_project_on_demand_scan_path(@project),
dast_configuration_path: project_security_configuration_dast_path(@project) } }
.js-dast-site-profile-form{ data: dast_site_profile_form_data(@project) }
......@@ -76,8 +76,8 @@ describe('OnDemandScansForm', () => {
const findNameInput = () => findByTestId('dast-scan-name-input');
const findBranchInput = () => findByTestId('dast-scan-branch-input');
const findDescriptionInput = () => findByTestId('dast-scan-description-input');
const findScannerProfilesSelector = () => wrapper.find(ScannerProfileSelector);
const findSiteProfilesSelector = () => wrapper.find(SiteProfileSelector);
const findScannerProfilesSelector = () => wrapper.findComponent(ScannerProfileSelector);
const findSiteProfilesSelector = () => wrapper.findComponent(SiteProfileSelector);
const findAlert = () => findByTestId('on-demand-scan-error');
const findProfilesConflictAlert = () => findByTestId('on-demand-scans-profiles-conflict-alert');
const findSubmitButton = () => findByTestId('on-demand-scan-submit-button');
......@@ -420,6 +420,13 @@ describe('OnDemandScansForm', () => {
actionFunction();
});
it('passes the scan ID to the profile selectors', () => {
const dastScanId = String(dastScan.id);
expect(findScannerProfilesSelector().attributes('dast-scan-id')).toBe(dastScanId);
expect(findSiteProfilesSelector().attributes('dast-scan-id')).toBe(dastScanId);
});
it(`triggers dastProfileUpdateMutation mutation with runAfterUpdate set to ${runAfter}`, async () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: dastProfileUpdateMutation,
......@@ -531,8 +538,8 @@ describe('OnDemandScansForm', () => {
'profiles conflict prevention',
({ description, selectedScannerProfile, selectedSiteProfile, hasConflict }) => {
const setFormData = () => {
wrapper.find(ScannerProfileSelector).vm.$emit('input', selectedScannerProfile.id);
wrapper.find(SiteProfileSelector).vm.$emit('input', selectedSiteProfile.id);
findScannerProfilesSelector().vm.$emit('input', selectedScannerProfile.id);
findSiteProfilesSelector().vm.$emit('input', selectedSiteProfile.id);
return wrapper.vm.$nextTick();
};
......@@ -622,14 +629,14 @@ describe('OnDemandScansForm', () => {
setWindowLocation(`?scanner_profile_id=${getIdFromGraphQLId(scannerProfile.id)}`);
createShallowComponent();
expect(wrapper.find(ScannerProfileSelector).attributes('value')).toBe(scannerProfile.id);
expect(findScannerProfilesSelector().attributes('value')).toBe(scannerProfile.id);
});
it('site profile', () => {
setWindowLocation(`?site_profile_id=${getIdFromGraphQLId(siteProfile.id)}`);
createShallowComponent();
expect(wrapper.find(SiteProfileSelector).attributes('value')).toBe(siteProfile.id);
expect(findSiteProfilesSelector().attributes('value')).toBe(siteProfile.id);
});
it('both scanner & site profile', () => {
......@@ -640,8 +647,8 @@ describe('OnDemandScansForm', () => {
);
createShallowComponent();
expect(wrapper.find(SiteProfileSelector).attributes('value')).toBe(siteProfile.id);
expect(wrapper.find(ScannerProfileSelector).attributes('value')).toBe(scannerProfile.id);
expect(findSiteProfilesSelector().attributes('value')).toBe(siteProfile.id);
expect(findScannerProfilesSelector().attributes('value')).toBe(scannerProfile.id);
});
it('when local storage data is available', async () => {
......
import { GlDropdownItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { merge } from 'lodash';
import dastProfilesMock from 'test_fixtures/graphql/on_demand_scans/graphql/dast_profiles.query.graphql.json';
import OnDemandScansProfileSelector from 'ee/on_demand_scans_form/components/profile_selector/profile_selector.vue';
import { FROM_ONDEMAND_SCAN_ID_QUERY_PARAM } from 'ee/on_demand_scans_form/settings';
import { scannerProfiles } from 'ee_jest/security_configuration/dast_profiles/mocks/mock_data';
import { TEST_HOST } from 'helpers/test_constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
describe('OnDemandScansProfileSelector', () => {
let wrapper;
......@@ -24,12 +28,16 @@ describe('OnDemandScansProfileSelector', () => {
},
];
// Finders
const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findCreateProfileOption = () => findByTestId('create-profile-option');
const findManageProfilesOption = () => findByTestId('manage-profiles-option');
const findProfilesDropdown = () => findByTestId('profiles-dropdown');
const findCreateNewProfileLink = () => findByTestId('create-profile-link');
const findSelectedProfileSummary = () => findByTestId('selected-profile-summary');
const findSelectedProfileEditLink = () => findByTestId('selected-profile-edit-link');
// Helpers
const parseDropdownItems = () =>
findProfilesDropdown()
.findAll(GlDropdownItem)
......@@ -45,6 +53,7 @@ describe('OnDemandScansProfileSelector', () => {
wrapper = mount(
OnDemandScansProfileSelector,
merge(
{},
{
propsData: defaultProps,
slots: {
......@@ -163,5 +172,82 @@ describe('OnDemandScansProfileSelector', () => {
...defaultDropdownItems,
]);
});
it('shows an edit link', () => {
const editLink = findSelectedProfileEditLink();
expect(editLink.exists()).toBe(true);
expect(editLink.attributes('href')).toBe(selectedProfile.editPath);
});
});
describe('when editing an on-demand scan', () => {
const dastScanId = dastProfilesMock.data.project.pipelines.nodes[0].id;
describe('without profiles', () => {
beforeEach(() => {
createFullComponent({
propsData: {
dastScanId,
},
});
});
it('shows a link to create a new profile including the scan ID', () => {
const link = findCreateNewProfileLink();
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe(
`${TEST_HOST}/path/to/new/profile/form?${FROM_ONDEMAND_SCAN_ID_QUERY_PARAM}=${getIdFromGraphQLId(
dastScanId,
)}`,
);
});
});
describe('with profiles', () => {
beforeEach(() => {
createFullComponent({
propsData: {
profiles: scannerProfiles,
dastScanId,
},
});
});
it('shows an option to create a new profile including the scan ID', () => {
expect(findCreateProfileOption().exists()).toBe(true);
expect(findCreateProfileOption().attributes('href')).toBe(
`${TEST_HOST}/path/to/new/profile/form?${FROM_ONDEMAND_SCAN_ID_QUERY_PARAM}=${getIdFromGraphQLId(
dastScanId,
)}`,
);
});
});
describe('when a profile is selected', () => {
const [selectedProfile] = scannerProfiles;
beforeEach(() => {
createFullComponent({
propsData: {
profiles: scannerProfiles,
value: selectedProfile.id,
dastScanId,
},
});
});
it('shows an edit link', () => {
const editLink = findSelectedProfileEditLink();
expect(editLink.exists()).toBe(true);
expect(editLink.attributes('href')).toBe(
`${TEST_HOST}${
selectedProfile.editPath
}?${FROM_ONDEMAND_SCAN_ID_QUERY_PARAM}=${getIdFromGraphQLId(dastScanId)}`,
);
});
});
});
});
......@@ -97,6 +97,7 @@ describe('OnDemandScansScannerProfileSelector', () => {
});
const sel = findProfileSelector();
expect(sel.props()).toEqual({
dastScanId: null,
libraryPath: TEST_LIBRARY_PATH,
newProfilePath: TEST_NEW_PATH,
profiles,
......
......@@ -111,6 +111,7 @@ describe('OnDemandScansSiteProfileSelector', () => {
const sel = findProfileSelector();
expect(sel.props()).toEqual({
dastScanId: null,
libraryPath: TEST_LIBRARY_PATH,
newProfilePath: TEST_NEW_PATH,
profiles,
......
......@@ -4,12 +4,13 @@ import * as urlUtility from '~/lib/utils/url_utility';
const fullPath = '/group/project';
const profilesLibraryPath = `${fullPath}/-/security/configuration/dast_scans`;
const onDemandScansPath = `${fullPath}/-/on_demand_scans`;
const newOnDemandScanPath = `${fullPath}/-/on_demand_scans`;
const editOnDemandScanPath = `${fullPath}/-/on_demand_scans/3/edit`;
const dastConfigPath = `${fullPath}/-/security/configuration/dast`;
const urlParamKey = 'site_profile_id';
const originalReferrer = document.referrer;
const allowedPaths = [onDemandScansPath, dastConfigPath];
const allowedPaths = [newOnDemandScanPath, editOnDemandScanPath, dastConfigPath];
const disallowedPaths = [profilesLibraryPath, fullPath];
const defaultRedirectionPath = profilesLibraryPath;
......
......@@ -3,12 +3,16 @@
require 'spec_helper'
RSpec.describe Projects::Security::DastProfilesHelper do
let_it_be(:project) { create(:project) }
before do
allow(project).to receive(:path_with_namespace).and_return("foo/bar")
end
describe '#dast_profiles_list_data' do
let_it_be(:project) { create(:project) }
let_it_be(:timezones) { [{ identifier: "Europe/Paris" }] }
before do
allow(project).to receive(:path_with_namespace).and_return("foo/bar")
allow(helper).to receive(:timezone_data).with(format: :abbr).and_return(timezones)
end
......@@ -23,4 +27,115 @@ RSpec.describe Projects::Security::DastProfilesHelper do
)
end
end
shared_examples 'passes on-demand scan edit path when from_on_demand_scan_id param is present' do
before do
allow(helper).to receive(:params).and_return({ from_on_demand_scan_id: '1' })
end
it 'returns edit path as on_demand_scan_form_path' do
expect(subject[:on_demand_scan_form_path]).to eq(
"/#{project.full_path}/-/on_demand_scans/1/edit"
)
end
end
describe "#dast_scanner_profile_form_data" do
subject { helper.dast_scanner_profile_form_data(project) }
before do
allow(helper).to receive(:params).and_return({})
end
it 'returns proper data' do
expect(subject).to eq(
{
project_full_path: "foo/bar",
on_demand_scan_form_path: "/#{project.full_path}/-/on_demand_scans/new",
dast_configuration_path: "/#{project.full_path}/-/security/configuration/dast",
profiles_library_path: "/#{project.full_path}/-/security/configuration/dast_scans#scanner-profiles"
}
)
end
it_behaves_like 'passes on-demand scan edit path when from_on_demand_scan_id param is present'
end
describe "#edit_dast_scanner_profile_form_data" do
subject { helper.edit_dast_scanner_profile_form_data(project, scanner_profile) }
let_it_be(:scanner_profile) { create(:dast_scanner_profile, project: project) }
before do
allow(helper).to receive(:params).and_return({})
end
it 'returns proper data' do
expect(subject).to eq(
{
project_full_path: "foo/bar",
on_demand_scan_form_path: "/#{project.full_path}/-/on_demand_scans/new",
dast_configuration_path: "/#{project.full_path}/-/security/configuration/dast",
profiles_library_path: "/#{project.full_path}/-/security/configuration/dast_scans#scanner-profiles",
scanner_profile: {
id: scanner_profile.to_global_id.to_s,
profile_name: scanner_profile.name,
spider_timeout: scanner_profile.spider_timeout,
target_timeout: scanner_profile.target_timeout,
scan_type: scanner_profile.scan_type.upcase,
use_ajax_spider: scanner_profile.use_ajax_spider,
show_debug_messages: scanner_profile.show_debug_messages,
referenced_in_security_policies: scanner_profile.referenced_in_security_policies
}.to_json
}
)
end
it_behaves_like 'passes on-demand scan edit path when from_on_demand_scan_id param is present'
end
describe "#dast_site_profile_form_data" do
subject { helper.dast_site_profile_form_data(project) }
before do
allow(helper).to receive(:params).and_return({})
end
it 'returns proper data' do
expect(subject).to eq(
{
project_full_path: "foo/bar",
on_demand_scan_form_path: "/#{project.full_path}/-/on_demand_scans/new",
dast_configuration_path: "/#{project.full_path}/-/security/configuration/dast",
profiles_library_path: "/#{project.full_path}/-/security/configuration/dast_scans#site-profiles"
}
)
end
it_behaves_like 'passes on-demand scan edit path when from_on_demand_scan_id param is present'
end
describe "#edit_dast_site_profile_form_data" do
subject { helper.edit_dast_site_profile_form_data(project, site_profile) }
let_it_be(:site_profile) { create(:dast_site_profile, project: project) }
before do
allow(helper).to receive(:params).and_return({})
end
it 'returns proper data' do
expect(subject).to eq(
{
project_full_path: "foo/bar",
on_demand_scan_form_path: "/#{project.full_path}/-/on_demand_scans/new",
dast_configuration_path: "/#{project.full_path}/-/security/configuration/dast",
profiles_library_path: "/#{project.full_path}/-/security/configuration/dast_scans#site-profiles",
site_profile: site_profile.to_json
}
)
end
it_behaves_like 'passes on-demand scan edit path when from_on_demand_scan_id param is present'
end
end
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