Commit 5774f6f5 authored by Russell Dickenson's avatar Russell Dickenson

Merge branch 'philipcunningham-use-graphql-for-dast-profile-in-controller-300995' into 'master'

Use GraphQL to prefetch data for DastProfile#edit

See merge request gitlab-org/gitlab!67809
parents a6183690 82f8afa2
...@@ -12025,6 +12025,18 @@ four standard [pagination arguments](#connection-pagination-arguments): ...@@ -12025,6 +12025,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="projectcontainerrepositoriesname"></a>`name` | [`String`](#string) | Filter the container repositories by their name. | | <a id="projectcontainerrepositoriesname"></a>`name` | [`String`](#string) | Filter the container repositories by their name. |
| <a id="projectcontainerrepositoriessort"></a>`sort` | [`ContainerRepositorySort`](#containerrepositorysort) | Sort container repositories by this criteria. | | <a id="projectcontainerrepositoriessort"></a>`sort` | [`ContainerRepositorySort`](#containerrepositorysort) | Sort container repositories by this criteria. |
##### `Project.dastProfile`
DAST Profile associated with the project.
Returns [`DastProfile`](#dastprofile).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectdastprofileid"></a>`id` | [`DastProfileID!`](#dastprofileid) | ID of the DAST Profile. |
##### `Project.dastSiteProfile` ##### `Project.dastSiteProfile`
DAST Site Profile associated with the project. DAST Site Profile associated with the project.
......
...@@ -143,8 +143,8 @@ export default { ...@@ -143,8 +143,8 @@ export default {
scannerProfiles: [], scannerProfiles: [],
siteProfiles: [], siteProfiles: [],
selectedBranch: this.dastScan?.branch?.name ?? this.defaultBranch, selectedBranch: this.dastScan?.branch?.name ?? this.defaultBranch,
selectedScannerProfileId: this.dastScan?.scannerProfileId || null, selectedScannerProfileId: this.dastScan?.dastScannerProfile.id || null,
selectedSiteProfileId: this.dastScan?.siteProfileId || null, selectedSiteProfileId: this.dastScan?.dastSiteProfile.id || null,
loading: false, loading: false,
errorType: null, errorType: null,
errors: [], errors: [],
......
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
module Projects module Projects
class OnDemandScansController < Projects::ApplicationController class OnDemandScansController < Projects::ApplicationController
include SecurityAndCompliancePermissions include SecurityAndCompliancePermissions
include API::Helpers::GraphqlHelpers
before_action :authorize_read_on_demand_scans!, only: :index before_action :authorize_read_on_demand_scans!, only: :index
before_action :authorize_create_on_demand_dast_scan!, only: [:new, :edit] before_action :authorize_create_on_demand_dast_scan!, only: [:new, :edit]
...@@ -16,16 +17,30 @@ module Projects ...@@ -16,16 +17,30 @@ module Projects
end end
def edit def edit
dast_profile = Dast::ProfilesFinder.new(project_id: @project.id, id: params[:id]).execute.first! # rubocop: disable CodeReuse/ActiveRecord global_id = Gitlab::GlobalId.as_global_id(params[:id], model_name: 'Dast::Profile')
@dast_profile = { query = %(
id: dast_profile.to_global_id.to_s, {
name: dast_profile.name, project(fullPath: "#{project.full_path}") {
description: dast_profile.description, dastProfile(id: "#{global_id}") {
branch: { name: dast_profile.branch_name }, id
site_profile_id: DastSiteProfile.new(id: dast_profile.dast_site_profile_id).to_global_id.to_s, name
scanner_profile_id: DastScannerProfile.new(id: dast_profile.dast_scanner_profile_id).to_global_id.to_s description
branch { name }
dastSiteProfile { id }
dastScannerProfile { id }
}
}
} }
)
@dast_profile = run_graphql!(
query: query,
context: { current_user: current_user },
transform: -> (result) { result.dig('data', 'project', 'dastProfile') }
)
return render_404 unless @dast_profile
end end
end end
end end
...@@ -60,6 +60,12 @@ module EE ...@@ -60,6 +60,12 @@ module EE
description: 'Find iteration cadences.', description: 'Find iteration cadences.',
resolver: ::Resolvers::Iterations::CadencesResolver resolver: ::Resolvers::Iterations::CadencesResolver
field :dast_profile,
::Types::Dast::ProfileType,
null: true,
resolver: ::Resolvers::AppSec::Dast::ProfileResolver.single,
description: 'DAST Profile associated with the project.'
field :dast_profiles, field :dast_profiles,
::Types::Dast::ProfileType.connection_type, ::Types::Dast::ProfileType.connection_type,
null: true, null: true,
......
...@@ -10,6 +10,12 @@ module Resolvers ...@@ -10,6 +10,12 @@ module Resolvers
type ::Types::Dast::ProfileType.connection_type, null: true type ::Types::Dast::ProfileType.connection_type, null: true
when_single do
argument :id, ::Types::GlobalIDType[::Dast::Profile],
required: true,
description: 'ID of the DAST Profile.'
end
def resolve_with_lookahead(**args) def resolve_with_lookahead(**args)
apply_lookahead(find_dast_profiles(args)) apply_lookahead(find_dast_profiles(args))
end end
...@@ -24,7 +30,15 @@ module Resolvers ...@@ -24,7 +30,15 @@ module Resolvers
end end
def find_dast_profiles(args) def find_dast_profiles(args)
::Dast::ProfilesFinder.new(project_id: project.id).execute params = { project_id: project.id }
if args[:id]
# TODO: remove this coercion when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
params[:id] = ::Types::GlobalIDType[::Dast::Profile].coerce_isolated_input(args[:id]).model_id
end
::Dast::ProfilesFinder.new(params).execute
end end
end end
end end
......
...@@ -23,6 +23,7 @@ import { scannerProfiles, siteProfiles } from '../mocks/mock_data'; ...@@ -23,6 +23,7 @@ import { scannerProfiles, siteProfiles } from '../mocks/mock_data';
const helpPagePath = '/application_security/dast/index#on-demand-scans'; const helpPagePath = '/application_security/dast/index#on-demand-scans';
const projectPath = 'group/project'; const projectPath = 'group/project';
const defaultBranch = 'main'; const defaultBranch = 'main';
const selectedBranch = 'some-other-branch';
const profilesLibraryPath = '/security/configuration/dast_scans'; const profilesLibraryPath = '/security/configuration/dast_scans';
const scannerProfilesLibraryPath = '/security/configuration/dast_scans#scanner-profiles'; const scannerProfilesLibraryPath = '/security/configuration/dast_scans#scanner-profiles';
const siteProfilesLibraryPath = '/security/configuration/dast_scans#site-profiles'; const siteProfilesLibraryPath = '/security/configuration/dast_scans#site-profiles';
...@@ -44,8 +45,8 @@ const dastScan = { ...@@ -44,8 +45,8 @@ const dastScan = {
branch: { name: 'dev' }, branch: { name: 'dev' },
name: 'My daily scan', name: 'My daily scan',
description: 'Tests for SQL injections', description: 'Tests for SQL injections',
scannerProfileId: passiveScannerProfile.id, dastScannerProfile: { id: passiveScannerProfile.id },
siteProfileId: validatedSiteProfile.id, dastSiteProfile: { id: validatedSiteProfile.id },
}; };
useLocalStorageSpy(); useLocalStorageSpy();
...@@ -83,9 +84,14 @@ describe('OnDemandScansForm', () => { ...@@ -83,9 +84,14 @@ describe('OnDemandScansForm', () => {
const findCancelButton = () => findByTestId('on-demand-scan-cancel-button'); const findCancelButton = () => findByTestId('on-demand-scan-cancel-button');
const findProfileSummary = () => findByTestId('selected-profile-summary'); const findProfileSummary = () => findByTestId('selected-profile-summary');
const hasSiteProfileAttributes = () => {
expect(findScannerProfilesSelector().attributes('value')).toBe(dastScan.dastScannerProfile.id);
expect(findSiteProfilesSelector().attributes('value')).toBe(dastScan.dastSiteProfile.id);
};
const setValidFormData = () => { const setValidFormData = () => {
findNameInput().vm.$emit('input', 'My daily scan'); findNameInput().vm.$emit('input', 'My daily scan');
findBranchInput().vm.$emit('input', 'some-other-branch'); findBranchInput().vm.$emit('input', selectedBranch);
findScannerProfilesSelector().vm.$emit('input', passiveScannerProfile.id); findScannerProfilesSelector().vm.$emit('input', passiveScannerProfile.id);
findSiteProfilesSelector().vm.$emit('input', nonValidatedSiteProfile.id); findSiteProfilesSelector().vm.$emit('input', nonValidatedSiteProfile.id);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
...@@ -230,6 +236,26 @@ describe('OnDemandScansForm', () => { ...@@ -230,6 +236,26 @@ describe('OnDemandScansForm', () => {
}); });
describe('when editing an existing scan', () => { describe('when editing an existing scan', () => {
describe('when the branch is not present', () => {
/**
* It is possible for pre-fetched data not to have a branch, so we must
* handle this path.
*/
beforeEach(() => {
createShallowComponent({
propsData: {
...dastScan,
branch: null,
},
});
});
it('sets the branch to the default', () => {
expect(findBranchInput().props('value')).toBe(defaultBranch);
});
});
describe('when the branch is present', () => {
beforeEach(() => { beforeEach(() => {
createShallowComponent({ createShallowComponent({
propsData: { propsData: {
...@@ -246,8 +272,8 @@ describe('OnDemandScansForm', () => { ...@@ -246,8 +272,8 @@ describe('OnDemandScansForm', () => {
expect(findNameInput().attributes('value')).toBe(dastScan.name); expect(findNameInput().attributes('value')).toBe(dastScan.name);
expect(findBranchInput().props('value')).toBe(dastScan.branch.name); expect(findBranchInput().props('value')).toBe(dastScan.branch.name);
expect(findDescriptionInput().attributes('value')).toBe(dastScan.description); expect(findDescriptionInput().attributes('value')).toBe(dastScan.description);
expect(findScannerProfilesSelector().attributes('value')).toBe(dastScan.scannerProfileId); hasSiteProfileAttributes();
expect(findSiteProfilesSelector().attributes('value')).toBe(dastScan.siteProfileId); });
}); });
}); });
...@@ -264,7 +290,7 @@ describe('OnDemandScansForm', () => { ...@@ -264,7 +290,7 @@ describe('OnDemandScansForm', () => {
name: 'My daily scan', name: 'My daily scan',
selectedScannerProfileId: 'gid://gitlab/DastScannerProfile/1', selectedScannerProfileId: 'gid://gitlab/DastScannerProfile/1',
selectedSiteProfileId: 'gid://gitlab/DastSiteProfile/1', selectedSiteProfileId: 'gid://gitlab/DastSiteProfile/1',
selectedBranch: 'some-other-branch', selectedBranch,
}), }),
], ],
]); ]);
...@@ -276,8 +302,8 @@ describe('OnDemandScansForm', () => { ...@@ -276,8 +302,8 @@ describe('OnDemandScansForm', () => {
JSON.stringify({ JSON.stringify({
name: dastScan.name, name: dastScan.name,
description: dastScan.description, description: dastScan.description,
selectedScannerProfileId: dastScan.scannerProfileId, selectedScannerProfileId: dastScan.dastScannerProfile.id,
selectedSiteProfileId: dastScan.siteProfileId, selectedSiteProfileId: dastScan.dastSiteProfile.id,
}), }),
); );
...@@ -286,8 +312,7 @@ describe('OnDemandScansForm', () => { ...@@ -286,8 +312,7 @@ describe('OnDemandScansForm', () => {
expect(findNameInput().attributes('value')).toBe(dastScan.name); expect(findNameInput().attributes('value')).toBe(dastScan.name);
expect(findDescriptionInput().attributes('value')).toBe(dastScan.description); expect(findDescriptionInput().attributes('value')).toBe(dastScan.description);
expect(findScannerProfilesSelector().attributes('value')).toBe(dastScan.scannerProfileId); hasSiteProfileAttributes();
expect(findSiteProfilesSelector().attributes('value')).toBe(dastScan.siteProfileId);
}); });
}); });
...@@ -345,7 +370,7 @@ describe('OnDemandScansForm', () => { ...@@ -345,7 +370,7 @@ describe('OnDemandScansForm', () => {
variables: { variables: {
input: { input: {
name: 'My daily scan', name: 'My daily scan',
branchName: 'some-other-branch', branchName: selectedBranch,
dastScannerProfileId: passiveScannerProfile.id, dastScannerProfileId: passiveScannerProfile.id,
dastSiteProfileId: nonValidatedSiteProfile.id, dastSiteProfileId: nonValidatedSiteProfile.id,
fullPath: projectPath, fullPath: projectPath,
...@@ -384,7 +409,7 @@ describe('OnDemandScansForm', () => { ...@@ -384,7 +409,7 @@ describe('OnDemandScansForm', () => {
input: { input: {
id: 1, id: 1,
name: 'My daily scan', name: 'My daily scan',
branchName: 'some-other-branch', branchName: selectedBranch,
description: 'Tests for SQL injections', description: 'Tests for SQL injections',
dastScannerProfileId: passiveScannerProfile.id, dastScannerProfileId: passiveScannerProfile.id,
dastSiteProfileId: nonValidatedSiteProfile.id, dastSiteProfileId: nonValidatedSiteProfile.id,
...@@ -602,16 +627,15 @@ describe('OnDemandScansForm', () => { ...@@ -602,16 +627,15 @@ describe('OnDemandScansForm', () => {
localStorage.setItem( localStorage.setItem(
LOCAL_STORAGE_KEY, LOCAL_STORAGE_KEY,
JSON.stringify({ JSON.stringify({
selectedScannerProfileId: dastScan.scannerProfileId, selectedScannerProfileId: dastScan.dastScannerProfile.id,
selectedSiteProfileId: dastScan.siteProfileId, selectedSiteProfileId: dastScan.dastSiteProfile.id,
}), }),
); );
createShallowComponent(); createShallowComponent();
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(findScannerProfilesSelector().attributes('value')).toBe(dastScan.scannerProfileId); hasSiteProfileAttributes();
expect(findSiteProfilesSelector().attributes('value')).toBe(dastScan.siteProfileId);
}); });
}); });
......
...@@ -20,6 +20,22 @@ RSpec.describe Resolvers::AppSec::Dast::ProfileResolver do ...@@ -20,6 +20,22 @@ RSpec.describe Resolvers::AppSec::Dast::ProfileResolver do
expect(described_class).to have_nullable_graphql_type(Types::Dast::ProfileType.connection_type) expect(described_class).to have_nullable_graphql_type(Types::Dast::ProfileType.connection_type)
end end
context 'when resolving a single DAST profile' do
subject { sync(dast_profile(id: gid)) }
context 'when the DAST profile exists' do
let(:gid) { dast_profile1.to_global_id }
it { is_expected.to eq dast_profile1 }
end
context 'when the DAST profile does not exist' do
let(:gid) { Gitlab::GlobalId.as_global_id(non_existing_record_id, model_name: 'Dast::Profile') }
it { is_expected.to be_nil }
end
end
context 'when resolving multiple DAST profiles' do context 'when resolving multiple DAST profiles' do
subject { sync(dast_profiles) } subject { sync(dast_profiles) }
...@@ -45,4 +61,8 @@ RSpec.describe Resolvers::AppSec::Dast::ProfileResolver do ...@@ -45,4 +61,8 @@ RSpec.describe Resolvers::AppSec::Dast::ProfileResolver do
def dast_profiles def dast_profiles
resolve(described_class, obj: project, ctx: { current_user: current_user }) resolve(described_class, obj: project, ctx: { current_user: current_user })
end end
def dast_profile(id:)
resolve(described_class.single, obj: project, args: { id: id }, ctx: { current_user: current_user })
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query.project(fullPath).dastProfile' do
include GraphqlHelpers
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let(:query) do
fields = all_graphql_fields_for('DastProfile')
graphql_query_for(
:project,
{ full_path: project.full_path },
query_graphql_field(:dast_profile, { id: global_id_of(dast_profile) }, fields)
)
end
subject do
post_graphql(query, current_user: current_user)
end
before do
stub_licensed_features(security_on_demand_scans: true)
end
context 'when a user does not have access to the project' do
it 'returns a null project' do
subject
expect(graphql_data_at(:project)).to be_nil
end
end
context 'when a user does not have access to the dast_profile' do
before do
project.add_guest(current_user)
end
it 'returns a null dast_profile' do
subject
expect(graphql_data_at(:project, :dast_profile)).to be_nil
end
end
context 'when a user has access to the dast_profile' do
before do
project.add_developer(current_user)
end
it 'returns a dast_profile' do
subject
expect(graphql_data_at(:project, :dast_profile, :id)).to eq(dast_profile.to_global_id.to_s)
end
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it 'returns a null dast_profile' do
subject
expect(graphql_data_at(:project, :dast_profile)).to be_nil
end
end
end
end
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe Projects::OnDemandScansController, type: :request do RSpec.describe Projects::OnDemandScansController, type: :request do
include GraphqlHelpers include GraphqlHelpers
let_it_be(:project) { create(:project) } let_it_be(:project) { create(:project, :repository) }
let(:user) { create(:user) } let(:user) { create(:user) }
...@@ -82,7 +82,7 @@ RSpec.describe Projects::OnDemandScansController, type: :request do ...@@ -82,7 +82,7 @@ RSpec.describe Projects::OnDemandScansController, type: :request do
end end
describe 'GET #edit' do describe 'GET #edit' do
let_it_be(:dast_profile) { create(:dast_profile, project: project) } let_it_be(:dast_profile) { create(:dast_profile, project: project, branch_name: project.default_branch_or_main) }
let(:dast_profile_id) { dast_profile.id } let(:dast_profile_id) { dast_profile.id }
let(:edit_path) { edit_project_on_demand_scan_path(project, id: dast_profile_id) } let(:edit_path) { edit_project_on_demand_scan_path(project, id: dast_profile_id) }
...@@ -108,9 +108,9 @@ RSpec.describe Projects::OnDemandScansController, type: :request do ...@@ -108,9 +108,9 @@ RSpec.describe Projects::OnDemandScansController, type: :request do
id: global_id_of(dast_profile), id: global_id_of(dast_profile),
name: dast_profile.name, name: dast_profile.name,
description: dast_profile.description, description: dast_profile.description,
branch: { name: dast_profile.branch_name }, branch: { name: project.default_branch_or_main },
site_profile_id: global_id_of(DastSiteProfile.new(id: dast_profile.dast_site_profile_id)), dastSiteProfile: { id: global_id_of(DastSiteProfile.new(id: dast_profile.dast_site_profile_id)) },
scanner_profile_id: global_id_of(DastScannerProfile.new(id: dast_profile.dast_scanner_profile_id)) dastScannerProfile: { id: global_id_of(DastScannerProfile.new(id: dast_profile.dast_scanner_profile_id)) }
}.to_json }.to_json
on_demand_div = Nokogiri::HTML.parse(response.body).at_css('div#js-on-demand-scans-app') on_demand_div = Nokogiri::HTML.parse(response.body).at_css('div#js-on-demand-scans-app')
......
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