Commit 8f7c21a3 authored by Rémy Coutable's avatar Rémy Coutable

Merge branch...

Merge branch '233453-dast-scanner-profile-library-implementation-iteration-1-add-scanner-profiles-to-library-view' into 'master'

Add scanner profiles to DAST on-demand profiles library view

Closes #233453

See merge request gitlab-org/gitlab!40819
parents 5ae4a69f aa965b0c
...@@ -4,8 +4,6 @@ import { GlDropdown, GlDropdownItem, GlTab, GlTabs } from '@gitlab/ui'; ...@@ -4,8 +4,6 @@ import { GlDropdown, GlDropdownItem, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ProfilesList from './dast_profiles_list.vue'; import ProfilesList from './dast_profiles_list.vue';
import dastSiteProfilesQuery from '../graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from '../graphql/dast_site_profiles_delete.mutation.graphql';
import * as cacheUtils from '../graphql/cache_utils'; import * as cacheUtils from '../graphql/cache_utils';
import { getProfileSettings } from '../settings/profiles'; import { getProfileSettings } from '../settings/profiles';
...@@ -32,39 +30,11 @@ export default { ...@@ -32,39 +30,11 @@ export default {
}, },
data() { data() {
return { return {
siteProfiles: [], profileTypes: {},
siteProfilesPageInfo: {},
errorMessage: '', errorMessage: '',
errorDetails: [], errorDetails: [],
}; };
}, },
apollo: {
siteProfiles() {
return {
query: dastSiteProfilesQuery,
variables: {
fullPath: this.projectFullPath,
first: this.$options.profilesPerPage,
},
result({ data, error }) {
if (!error) {
this.siteProfilesPageInfo = data.project.siteProfiles.pageInfo;
}
},
update(data) {
const siteProfileEdges = data?.project?.siteProfiles?.edges ?? [];
return siteProfileEdges.map(({ node }) => node);
},
error(error) {
this.handleError({
exception: error,
message: this.$options.i18n.errorMessages.fetchNetworkError,
});
},
};
},
},
computed: { computed: {
profileSettings() { profileSettings() {
const { glFeatures, createNewProfilePaths } = this; const { glFeatures, createNewProfilePaths } = this;
...@@ -76,14 +46,66 @@ export default { ...@@ -76,14 +46,66 @@ export default {
glFeatures, glFeatures,
); );
}, },
hasMoreSiteProfiles() { },
return this.siteProfilesPageInfo.hasNextPage; created() {
}, this.addSmartQueriesForEnabledProfileTypes();
isLoadingSiteProfiles() {
return this.$apollo.queries.siteProfiles.loading;
},
}, },
methods: { methods: {
addSmartQueriesForEnabledProfileTypes() {
Object.values(this.profileSettings).forEach(({ profileType, graphQL: { query } }) => {
this.makeProfileTypeReactive(profileType);
this.$apollo.addSmartQuery(
profileType,
this.createQuery({
profileType,
query,
variables: {
fullPath: this.projectFullPath,
first: this.$options.profilesPerPage,
},
}),
);
});
},
makeProfileTypeReactive(profileType) {
this.$set(this.profileTypes, profileType, {
profiles: [],
pageInfo: {},
});
},
hasMoreProfiles(profileType) {
return this.profileTypes[profileType]?.pageInfo?.hasNextPage;
},
isLoadingProfiles(profileType) {
return this.$apollo.queries[profileType].loading;
},
createQuery({ profileType, query, variables }) {
return {
query,
variables,
manual: true,
result({ data, error }) {
if (!error) {
const { project } = data;
const profileEdges = project?.[profileType]?.edges ?? [];
const profiles = profileEdges.map(({ node }) => node);
const pageInfo = project?.[profileType].pageInfo;
this.profileTypes[profileType] = {
profiles,
pageInfo,
};
}
},
error(error) {
this.handleError({
exception: error,
message: this.profileSettings[profileType].i18n.errorMessages.fetchNetworkError,
});
},
};
},
handleError({ exception, message = '', details = [] }) { handleError({ exception, message = '', details = [] }) {
Sentry.captureException(exception); Sentry.captureException(exception);
this.errorMessage = message; this.errorMessage = message;
...@@ -93,32 +115,37 @@ export default { ...@@ -93,32 +115,37 @@ export default {
this.errorMessage = ''; this.errorMessage = '';
this.errorDetails = []; this.errorDetails = [];
}, },
fetchMoreProfiles() { fetchMoreProfiles(profileType) {
const { const {
$apollo, $apollo,
siteProfilesPageInfo,
$options: { i18n }, $options: { i18n },
} = this; } = this;
const { pageInfo } = this.profileTypes[profileType];
this.resetErrors(); this.resetErrors();
$apollo.queries.siteProfiles $apollo.queries[profileType]
.fetchMore({ .fetchMore({
variables: { after: siteProfilesPageInfo.endCursor }, variables: { after: pageInfo.endCursor },
updateQuery: cacheUtils.appendToPreviousResult, updateQuery: cacheUtils.appendToPreviousResult(profileType),
}) })
.catch(error => { .catch(error => {
this.handleError({ exception: error, message: i18n.errorMessages.fetchNetworkError }); this.handleError({ exception: error, message: i18n.errorMessages.fetchNetworkError });
}); });
}, },
deleteSiteProfile(profileToBeDeletedId) { deleteProfile(profileType, profileId) {
const { const {
projectFullPath, projectFullPath,
handleError, handleError,
$options: { i18n }, profileSettings: {
[profileType]: {
i18n,
graphQL: { deletion },
},
},
$apollo: { $apollo: {
queries: { queries: {
siteProfiles: { options: siteProfilesQueryOptions }, [profileType]: { options: queryOptions },
}, },
}, },
} = this; } = this;
...@@ -127,27 +154,23 @@ export default { ...@@ -127,27 +154,23 @@ export default {
this.$apollo this.$apollo
.mutate({ .mutate({
mutation: dastSiteProfilesDelete, mutation: deletion.mutation,
variables: { variables: {
projectFullPath, projectFullPath,
profileId: profileToBeDeletedId, profileId,
}, },
update( update(store, { data = {} }) {
store, const errors = data[`${profileType}Delete`]?.errors ?? [];
{
data: {
dastSiteProfileDelete: { errors = [] },
},
},
) {
if (errors.length === 0) { if (errors.length === 0) {
cacheUtils.removeProfile({ cacheUtils.removeProfile({
profileId,
profileType,
store, store,
queryBody: { queryBody: {
query: siteProfilesQueryOptions.query, query: queryOptions.query,
variables: siteProfilesQueryOptions.variables, variables: queryOptions.variables,
}, },
profileToBeDeletedId,
}); });
} else { } else {
handleError({ handleError({
...@@ -156,7 +179,7 @@ export default { ...@@ -156,7 +179,7 @@ export default {
}); });
} }
}, },
optimisticResponse: cacheUtils.dastSiteProfilesDeleteResponse(), optimisticResponse: deletion.optimisticResponse,
}) })
.catch(error => { .catch(error => {
this.handleError({ this.handleError({
...@@ -173,15 +196,6 @@ export default { ...@@ -173,15 +196,6 @@ export default {
subHeading: s__( subHeading: s__(
'DastProfiles|Save commonly used configurations for target sites and scan specifications as profiles. Use these with an on-demand scan.', 'DastProfiles|Save commonly used configurations for target sites and scan specifications as profiles. Use these with an on-demand scan.',
), ),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete site profile. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete site profiles:'),
},
}, },
}; };
</script> </script>
...@@ -200,11 +214,11 @@ export default { ...@@ -200,11 +214,11 @@ export default {
class="gl-ml-auto" class="gl-ml-auto"
> >
<gl-dropdown-item <gl-dropdown-item
v-for="{ i18n, createNewProfilePath, key } in profileSettings" v-for="{ i18n, createNewProfilePath, profileType } in profileSettings"
:key="key" :key="profileType"
:href="createNewProfilePath" :href="createNewProfilePath"
> >
{{ i18n.title }} {{ i18n.createNewLinkText }}
</gl-dropdown-item> </gl-dropdown-item>
</gl-dropdown> </gl-dropdown>
</div> </div>
...@@ -214,20 +228,22 @@ export default { ...@@ -214,20 +228,22 @@ export default {
</header> </header>
<gl-tabs> <gl-tabs>
<gl-tab> <gl-tab v-for="(data, profileType) in profileSettings" :key="profileType">
<template #title> <template #title>
<span>{{ s__('DastProfiles|Site Profiles') }}</span> <span>{{ profileSettings[profileType].i18n.tabName }}</span>
</template> </template>
<profiles-list <profiles-list
:data-testid="`${profileType}List`"
:error-message="errorMessage" :error-message="errorMessage"
:error-details="errorDetails" :error-details="errorDetails"
:has-more-profiles-to-load="hasMoreSiteProfiles" :has-more-profiles-to-load="hasMoreProfiles(profileType)"
:is-loading="isLoadingSiteProfiles" :is-loading="isLoadingProfiles(profileType)"
:profiles-per-page="$options.profilesPerPage" :profiles-per-page="$options.profilesPerPage"
:profiles="siteProfiles" :profiles="profileTypes[profileType].profiles"
@loadMoreProfiles="fetchMoreProfiles" :fields="profileSettings[profileType].tableFields"
@deleteProfile="deleteSiteProfile" @load-more-profiles="fetchMoreProfiles(profileType)"
@delete-profile="deleteProfile(profileType, $event)"
/> />
</gl-tab> </gl-tab>
</gl-tabs> </gl-tabs>
......
...@@ -27,6 +27,10 @@ export default { ...@@ -27,6 +27,10 @@ export default {
type: Array, type: Array,
required: true, required: true,
}, },
fields: {
type: Array,
required: true,
},
errorMessage: { errorMessage: {
type: String, type: String,
required: false, required: false,
...@@ -76,10 +80,17 @@ export default { ...@@ -76,10 +80,17 @@ export default {
modalId() { modalId() {
return `dast-profiles-list-${uniqueId()}`; return `dast-profiles-list-${uniqueId()}`;
}, },
tableFields() {
const defaultClasses = ['gl-word-break-all'];
const dataFields = this.fields.map(key => ({ key, class: defaultClasses }));
const staticFields = [{ key: 'actions' }];
return [...dataFields, ...staticFields];
},
}, },
methods: { methods: {
handleDelete() { handleDelete() {
this.$emit('deleteProfile', this.toBeDeletedProfileId); this.$emit('delete-profile', this.toBeDeletedProfileId);
}, },
prepareProfileDeletion(profileId) { prepareProfileDeletion(profileId) {
this.toBeDeletedProfileId = profileId; this.toBeDeletedProfileId = profileId;
...@@ -89,25 +100,6 @@ export default { ...@@ -89,25 +100,6 @@ export default {
this.toBeDeletedProfileId = null; this.toBeDeletedProfileId = null;
}, },
}, },
tableFields: [
{
key: 'profileName',
class: 'gl-word-break-all',
},
{
key: 'targetUrl',
class: 'gl-word-break-all',
},
{
key: 'validationStatus',
// NOTE: hidden for now, since the site validation is still WIP and will be finished in an upcoming iteration
// roadmap: https://gitlab.com/groups/gitlab-org/-/epics/2912#ui-configuration
class: 'gl-display-none!',
},
{
key: 'actions',
},
],
}; };
</script> </script>
<template> <template>
...@@ -116,13 +108,13 @@ export default { ...@@ -116,13 +108,13 @@ export default {
<gl-table <gl-table
:aria-label="s__('DastProfiles|Site Profiles')" :aria-label="s__('DastProfiles|Site Profiles')"
:busy="isLoadingInitialProfiles" :busy="isLoadingInitialProfiles"
:fields="$options.tableFields" :fields="tableFields"
:items="profiles" :items="profiles"
stacked="md" stacked="md"
thead-class="gl-display-none" thead-class="gl-display-none"
> >
<template v-if="hasError" #top-row> <template v-if="hasError" #top-row>
<td :colspan="$options.tableFields.length"> <td :colspan="tableFields.length">
<gl-alert class="gl-my-4" variant="danger" :dismissible="false"> <gl-alert class="gl-my-4" variant="danger" :dismissible="false">
{{ errorMessage }} {{ errorMessage }}
<ul <ul
...@@ -161,7 +153,22 @@ export default { ...@@ -161,7 +153,22 @@ export default {
:aria-label="__('Delete')" :aria-label="__('Delete')"
@click="prepareProfileDeletion(item.id)" @click="prepareProfileDeletion(item.id)"
/> />
<gl-button :href="item.editPath">{{ __('Edit') }}</gl-button> <gl-button v-if="item.editPath" :href="item.editPath">{{ __('Edit') }}</gl-button>
<!--
NOTE: The tooltip and `disable` on the button is temporary until the edit feature has been implemented
further details: https://gitlab.com/groups/gitlab-org/-/epics/3786 (iteration outline)
-->
<span
v-else
v-gl-tooltip.hover
:title="
s__(
'DastProfiles|Edit feature will come soon. Please create a new profile if changes needed',
)
"
>
<gl-button disabled>{{ __('Edit') }}</gl-button>
</span>
</div> </div>
</template> </template>
...@@ -181,7 +188,7 @@ export default { ...@@ -181,7 +188,7 @@ export default {
<gl-button <gl-button
data-testid="loadMore" data-testid="loadMore"
:loading="isLoading && !hasError" :loading="isLoading && !hasError"
@click="$emit('loadMoreProfiles')" @click="$emit('load-more-profiles')"
> >
{{ __('Load more') }} {{ __('Load more') }}
</gl-button> </gl-button>
......
...@@ -2,16 +2,15 @@ ...@@ -2,16 +2,15 @@
* Appends paginated results to existing ones * Appends paginated results to existing ones
* - to be used with $apollo.queries.x.fetchMore * - to be used with $apollo.queries.x.fetchMore
* *
* @param previousResult * @param {*} profileType
* @param fetchMoreResult * @returns {function(*, {fetchMoreResult: *}): *}
* @returns {*}
*/ */
export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => { export const appendToPreviousResult = profileType => (previousResult, { fetchMoreResult }) => {
const newResult = { ...fetchMoreResult }; const newResult = { ...fetchMoreResult };
const previousEdges = previousResult.project.siteProfiles.edges; const previousEdges = previousResult.project[profileType].edges;
const newEdges = newResult.project.siteProfiles.edges; const newEdges = newResult.project[profileType].edges;
newResult.project.siteProfiles.edges = [...previousEdges, ...newEdges]; newResult.project[profileType].edges = [...previousEdges, ...newEdges];
return newResult; return newResult;
}; };
...@@ -19,15 +18,16 @@ export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => { ...@@ -19,15 +18,16 @@ export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => {
/** /**
* Removes profile with given id from the cache and writes the result to it * Removes profile with given id from the cache and writes the result to it
* *
* @param profileId
* @param profileType
* @param store * @param store
* @param queryBody * @param queryBody
* @param profileToBeDeletedId
*/ */
export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => { export const removeProfile = ({ profileId, profileType, store, queryBody }) => {
const data = store.readQuery(queryBody); const data = store.readQuery(queryBody);
data.project.siteProfiles.edges = data.project.siteProfiles.edges.filter(({ node }) => { data.project[profileType].edges = data.project[profileType].edges.filter(({ node }) => {
return node.id !== profileToBeDeletedId; return node.id !== profileId;
}); });
store.writeQuery({ ...queryBody, data }); store.writeQuery({ ...queryBody, data });
...@@ -36,13 +36,15 @@ export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => { ...@@ -36,13 +36,15 @@ export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => {
/** /**
* Returns an object representing a optimistic response for site-profile deletion * Returns an object representing a optimistic response for site-profile deletion
* *
* @returns {{__typename: string, dastSiteProfileDelete: {__typename: string, errors: []}}} * @param mutationName
* @param payloadTypeName
* @returns {{[p: string]: string, __typename: string}}
*/ */
export const dastSiteProfilesDeleteResponse = () => ({ export const dastProfilesDeleteResponse = ({ mutationName, payloadTypeName }) => ({
// eslint-disable-next-line @gitlab/require-i18n-strings // eslint-disable-next-line @gitlab/require-i18n-strings
__typename: 'Mutation', __typename: 'Mutation',
dastSiteProfileDelete: { [mutationName]: {
__typename: 'DastSiteProfileDeletePayload', __typename: payloadTypeName,
errors: [], errors: [],
}, },
}); });
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query DastScannerProfiles(
$fullPath: ID!
$after: String
$before: String
$first: Int
$last: Int
) {
project(fullPath: $fullPath) {
scannerProfiles: dastScannerProfiles(
after: $after
before: $before
first: $first
last: $last
) {
pageInfo {
...PageInfo
}
edges {
cursor
node {
id: globalId
profileName
spiderTimeout
targetTimeout
}
}
}
}
}
mutation dastScannerProfileDelete($projectFullPath: ID!, $profileId: DastScannerProfileID!) {
scannerProfilesDelete: dastScannerProfileDelete(
input: { fullPath: $projectFullPath, id: $profileId }
) {
errors
}
}
mutation dastSiteProfileDelete($projectFullPath: ID!, $profileId: DastSiteProfileID!) { mutation dastSiteProfileDelete($projectFullPath: ID!, $profileId: DastSiteProfileID!) {
dastSiteProfileDelete(input: { fullPath: $projectFullPath, id: $profileId }) { siteProfilesDelete: dastSiteProfileDelete(input: { fullPath: $projectFullPath, id: $profileId }) {
errors errors
} }
} }
import dastSiteProfilesQuery from 'ee/dast_profiles/graphql/dast_site_profiles.query.graphql';
import dastSiteProfilesDelete from 'ee/dast_profiles/graphql/dast_site_profiles_delete.mutation.graphql';
import dastScannerProfilesQuery from 'ee/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastScannerProfilesDelete from 'ee/dast_profiles/graphql/dast_scanner_profiles_delete.mutation.graphql';
import { dastProfilesDeleteResponse } from 'ee/dast_profiles/graphql/cache_utils';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => { const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => {
...@@ -11,18 +16,60 @@ const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => { ...@@ -11,18 +16,60 @@ const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => {
export const getProfileSettings = ({ createNewProfilePaths }, glFeatures) => { export const getProfileSettings = ({ createNewProfilePaths }, glFeatures) => {
const settings = { const settings = {
siteProfiles: { siteProfiles: {
key: 'siteProfiles', profileType: 'siteProfiles',
createNewProfilePath: createNewProfilePaths.siteProfile, createNewProfilePath: createNewProfilePaths.siteProfile,
graphQL: {
query: dastSiteProfilesQuery,
deletion: {
mutation: dastSiteProfilesDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'siteProfilesDelete',
payloadTypeName: 'DastSiteProfileDeletePayload',
}),
},
},
tableFields: ['profileName', 'targetUrl'],
i18n: { i18n: {
title: s__('DastProfiles|Site Profile'), createNewLinkText: s__('DastProfiles|Site Profile'),
tabName: s__('DastProfiles|Site Profiles'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete site profile. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete site profiles:'),
},
}, },
}, },
scannerProfiles: { scannerProfiles: {
key: 'scannerProfiles', profileType: 'scannerProfiles',
createNewProfilePath: createNewProfilePaths.scannerProfile, createNewProfilePath: createNewProfilePaths.scannerProfile,
graphQL: {
query: dastScannerProfilesQuery,
deletion: {
mutation: dastScannerProfilesDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'scannerProfilesDelete',
payloadTypeName: 'DastScannerProfileDeletePayload',
}),
},
},
featureFlag: 'securityOnDemandScansScannerProfiles', featureFlag: 'securityOnDemandScansScannerProfiles',
tableFields: ['profileName'],
i18n: { i18n: {
title: s__('DastProfiles|Scanner Profile'), createNewLinkText: s__('DastProfiles|Scanner Profile'),
tabName: s__('DastProfiles|Scanner Profiles'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later.',
),
deletionNetworkError: s__(
'DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later.',
),
deletionBackendError: s__('DastProfiles|Could not delete scanner profiles:'),
},
}, },
}, },
}; };
......
...@@ -12,6 +12,7 @@ describe('EE - DastProfilesList', () => { ...@@ -12,6 +12,7 @@ describe('EE - DastProfilesList', () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { const defaultProps = {
profiles: [], profiles: [],
fields: ['profileName', 'targetUrl', 'validationStatus'],
hasMorePages: false, hasMorePages: false,
profilesPerPage: 10, profilesPerPage: 10,
errorMessage: '', errorMessage: '',
...@@ -116,6 +117,13 @@ describe('EE - DastProfilesList', () => { ...@@ -116,6 +117,13 @@ describe('EE - DastProfilesList', () => {
editPath: '/2/edit', editPath: '/2/edit',
validationStatus: 'Pending', validationStatus: 'Pending',
}, },
{
id: 3,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '',
validationStatus: 'Pending',
},
]; ];
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)]; const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
...@@ -147,9 +155,19 @@ describe('EE - DastProfilesList', () => { ...@@ -147,9 +155,19 @@ describe('EE - DastProfilesList', () => {
expect(validationStatusCell.innerText).toContain(profile.validationStatus); expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null); expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null);
const editLink = within(actionsCell).getByRole('link', { name: /edit/i }); if (profile.editPath) {
expect(editLink).not.toBe(null); const editLink = within(actionsCell).getByRole('link', { name: /edit/i });
expect(editLink.getAttribute('href')).toBe(profile.editPath); expect(editLink).not.toBe(null);
expect(editLink.getAttribute('href')).toBe(profile.editPath);
} else {
const editButton = within(actionsCell).getByRole('button', { name: /edit/i });
const helpText = within(actionsCell).getByTitle(
/edit feature will come soon. please create a new profile if changes needed/i,
);
expect(helpText).not.toBe(null);
expect(editButton).not.toBe(null);
expect(editButton.getAttribute('disabled')).not.toBe(null);
}
}); });
}); });
...@@ -169,12 +187,12 @@ describe('EE - DastProfilesList', () => { ...@@ -169,12 +187,12 @@ describe('EE - DastProfilesList', () => {
expect(getLoadMoreButton().exists()).toBe(true); expect(getLoadMoreButton().exists()).toBe(true);
}); });
it('emits "loadMoreProfiles" when the load-more button is clicked', async () => { it('emits "load-more-profiles" when the load-more button is clicked', async () => {
expect(wrapper.emitted('loadMoreProfiles')).toBe(undefined); expect(wrapper.emitted('load-more-profiles')).toBe(undefined);
await getLoadMoreButton().trigger('click'); await getLoadMoreButton().trigger('click');
expect(wrapper.emitted('loadMoreProfiles')).toEqual(expect.any(Array)); expect(wrapper.emitted('load-more-profiles')).toEqual(expect.any(Array));
}); });
}); });
}); });
...@@ -200,7 +218,7 @@ describe('EE - DastProfilesList', () => { ...@@ -200,7 +218,7 @@ describe('EE - DastProfilesList', () => {
}); });
it(`emits "@deleteProfile" with the right payload when the modal's primary action is triggered`, async () => { it(`emits "@deleteProfile" with the right payload when the modal's primary action is triggered`, async () => {
expect(wrapper.emitted('deleteProfile')).toBe(undefined); expect(wrapper.emitted('delete-profile')).toBe(undefined);
getCurrentProfileDeleteButton().trigger('click'); getCurrentProfileDeleteButton().trigger('click');
...@@ -208,7 +226,7 @@ describe('EE - DastProfilesList', () => { ...@@ -208,7 +226,7 @@ describe('EE - DastProfilesList', () => {
getModal().vm.$emit('ok'); getModal().vm.$emit('ok');
expect(wrapper.emitted('deleteProfile')[0]).toEqual([profile.id]); expect(wrapper.emitted('delete-profile')[0]).toEqual([profile.id]);
}); });
}); });
}); });
......
...@@ -3,7 +3,6 @@ import { within } from '@testing-library/dom'; ...@@ -3,7 +3,6 @@ import { within } from '@testing-library/dom';
import { merge } from 'lodash'; import { merge } from 'lodash';
import { GlDropdown } from '@gitlab/ui'; import { GlDropdown } from '@gitlab/ui';
import DastProfiles from 'ee/dast_profiles/components/dast_profiles.vue'; import DastProfiles from 'ee/dast_profiles/components/dast_profiles.vue';
import DastProfilesList from 'ee/dast_profiles/components/dast_profiles_list.vue';
const TEST_NEW_DAST_SCANNER_PROFILE_PATH = '/-/on_demand_scans/scanner_profiles/new'; const TEST_NEW_DAST_SCANNER_PROFILE_PATH = '/-/on_demand_scans/scanner_profiles/new';
const TEST_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new'; const TEST_NEW_DAST_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
...@@ -27,8 +26,18 @@ describe('EE - DastProfiles', () => { ...@@ -27,8 +26,18 @@ describe('EE - DastProfiles', () => {
siteProfiles: { siteProfiles: {
fetchMore: jest.fn().mockResolvedValue(), fetchMore: jest.fn().mockResolvedValue(),
}, },
scannerProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
},
}, },
mutate: jest.fn().mockResolvedValue(), mutate: jest.fn().mockResolvedValue(),
addSmartQuery: jest.fn(),
},
};
const defaultProvide = {
glFeatures: {
securityOnDemandScansScannerProfiles: true,
}, },
}; };
...@@ -39,6 +48,7 @@ describe('EE - DastProfiles', () => { ...@@ -39,6 +48,7 @@ describe('EE - DastProfiles', () => {
{ {
propsData: defaultProps, propsData: defaultProps,
mocks: defaultMocks, mocks: defaultMocks,
provide: defaultProvide,
}, },
options, options,
), ),
...@@ -67,7 +77,7 @@ describe('EE - DastProfiles', () => { ...@@ -67,7 +77,7 @@ describe('EE - DastProfiles', () => {
}; };
const withinComponent = () => within(wrapper.element); const withinComponent = () => within(wrapper.element);
const getSiteProfilesComponent = () => wrapper.find(DastProfilesList); const getProfilesComponent = profileType => wrapper.find(`[data-testid="${profileType}List"]`);
const getDropdownComponent = () => wrapper.find(GlDropdown); const getDropdownComponent = () => wrapper.find(GlDropdown);
const getSiteProfilesDropdownItem = text => const getSiteProfilesDropdownItem = text =>
within(getDropdownComponent().element).queryByText(text); within(getDropdownComponent().element).queryByText(text);
...@@ -125,8 +135,9 @@ describe('EE - DastProfiles', () => { ...@@ -125,8 +135,9 @@ describe('EE - DastProfiles', () => {
}); });
it.each` it.each`
tabName | shouldBeSelectedByDefault tabName | shouldBeSelectedByDefault
${'Site Profiles'} | ${true} ${'Site Profiles'} | ${true}
${'Scanner Profiles'} | ${false}
`( `(
'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"', 'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"',
({ tabName, shouldBeSelectedByDefault }) => { ({ tabName, shouldBeSelectedByDefault }) => {
...@@ -140,65 +151,70 @@ describe('EE - DastProfiles', () => { ...@@ -140,65 +151,70 @@ describe('EE - DastProfiles', () => {
); );
}); });
describe('site profiles', () => { describe.each`
description | profileType
${'Site Profiles List'} | ${'siteProfiles'}
${'Scanner Profiles List'} | ${'scannerProfiles'}
`('$description', ({ profileType }) => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent();
}); });
it('passes down the correct default props', () => { it('passes down the correct default props', () => {
expect(getSiteProfilesComponent().props()).toEqual({ expect(getProfilesComponent(profileType).props()).toEqual({
errorMessage: '', errorMessage: '',
errorDetails: [], errorDetails: [],
hasMoreProfilesToLoad: false, hasMoreProfilesToLoad: false,
isLoading: false, isLoading: false,
profilesPerPage: expect.any(Number), profilesPerPage: expect.any(Number),
profiles: [], profiles: [],
fields: expect.any(Array),
}); });
}); });
it.each([true, false])('passes down the loading state', loading => { it.each([true, false])('passes down the loading state when loading is "%s"', loading => {
createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading } } } } }); createComponent({ mocks: { $apollo: { queries: { [profileType]: { loading } } } } });
expect(getSiteProfilesComponent().props('isLoading')).toBe(loading); expect(getProfilesComponent(profileType).props('isLoading')).toBe(loading);
}); });
it.each` it.each`
givenData | propName | expectedPropValue givenData | propName | expectedPropValue
${{ errorMessage: 'foo' }} | ${'errorMessage'} | ${'foo'} ${{ errorMessage: 'foo' }} | ${'errorMessage'} | ${'foo'}
${{ siteProfilesPageInfo: { hasNextPage: true } }} | ${'hasMoreProfilesToLoad'} | ${true} ${{ profileTypes: { [profileType]: { pageInfo: { hasNextPage: true } } } }} | ${'hasMoreProfilesToLoad'} | ${true}
${{ siteProfiles: [{ foo: 'bar' }] }} | ${'profiles'} | ${[{ foo: 'bar' }]} ${{ profileTypes: { [profileType]: { profiles: [{ foo: 'bar' }] } } }} | ${'profiles'} | ${[{ foo: 'bar' }]}
`('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => { `('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => {
wrapper.setData(givenData); wrapper.setData(givenData);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
expect(getSiteProfilesComponent().props(propName)).toEqual(expectedPropValue); expect(getProfilesComponent(profileType).props(propName)).toEqual(expectedPropValue);
}); });
it('fetches more results when "@loadMoreProfiles" is emitted', () => { it('fetches more results when "@load-more-profiles" is emitted', () => {
const { const {
$apollo: { $apollo: {
queries: { queries: {
siteProfiles: { fetchMore }, [profileType]: { fetchMore },
}, },
}, },
} = wrapper.vm; } = wrapper.vm;
expect(fetchMore).not.toHaveBeenCalled(); expect(fetchMore).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('loadMoreProfiles'); getProfilesComponent(profileType).vm.$emit('load-more-profiles');
expect(fetchMore).toHaveBeenCalledTimes(1); expect(fetchMore).toHaveBeenCalledTimes(1);
}); });
it('deletes profile when "@deleteProfile" is emitted', () => { it('deletes profile when "@delete-profile" is emitted', () => {
const { const {
$apollo: { mutate }, $apollo: { mutate },
} = wrapper.vm; } = wrapper.vm;
expect(mutate).not.toHaveBeenCalled(); expect(mutate).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('deleteProfile'); getProfilesComponent(profileType).vm.$emit('delete-profile');
expect(mutate).toHaveBeenCalledTimes(1); expect(mutate).toHaveBeenCalledTimes(1);
}); });
......
import { import {
appendToPreviousResult, appendToPreviousResult,
removeProfile, removeProfile,
dastSiteProfilesDeleteResponse, dastProfilesDeleteResponse,
} from 'ee/dast_profiles/graphql/cache_utils'; } from 'ee/dast_profiles/graphql/cache_utils';
describe('EE - DastProfiles GraphQL CacheUtils', () => { describe('EE - DastProfiles GraphQL CacheUtils', () => {
describe('appendToPreviousResult', () => { describe('appendToPreviousResult', () => {
it('appends new results to previous', () => { it.each(['siteProfiles', 'scannerProfiles'])('appends new results to previous', profileType => {
const previousResult = { project: { siteProfiles: { edges: ['foo'] } } }; const previousResult = { project: { [profileType]: { edges: ['foo'] } } };
const fetchMoreResult = { project: { siteProfiles: { edges: ['bar'] } } }; const fetchMoreResult = { project: { [profileType]: { edges: ['bar'] } } };
const expected = { project: { siteProfiles: { edges: ['foo', 'bar'] } } }; const expected = { project: { [profileType]: { edges: ['foo', 'bar'] } } };
const result = appendToPreviousResult(previousResult, { fetchMoreResult }); const result = appendToPreviousResult(profileType)(previousResult, { fetchMoreResult });
expect(result).toEqual(expected); expect(result).toEqual(expected);
}); });
}); });
describe('removeProfile', () => { describe('removeProfile', () => {
it('removes the profile with the given id from the cache', () => { it.each(['foo', 'bar'])('removes the profile with the given id from the cache', profileType => {
const mockQueryBody = { query: 'foo', variables: { foo: 'bar' } }; const mockQueryBody = { query: 'foo', variables: { foo: 'bar' } };
const mockProfiles = [{ id: 0 }, { id: 1 }]; const mockProfiles = [{ id: 0 }, { id: 1 }];
const mockData = { const mockData = {
project: { project: {
siteProfiles: { [profileType]: {
edges: [{ node: mockProfiles[0] }, { node: mockProfiles[1] }], edges: [{ node: mockProfiles[0] }, { node: mockProfiles[1] }],
}, },
}, },
...@@ -36,14 +36,15 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => { ...@@ -36,14 +36,15 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
removeProfile({ removeProfile({
store: mockStore, store: mockStore,
queryBody: mockQueryBody, queryBody: mockQueryBody,
profileToBeDeletedId: mockProfiles[0].id, profileId: mockProfiles[0].id,
profileType,
}); });
expect(mockStore.writeQuery).toHaveBeenCalledWith({ expect(mockStore.writeQuery).toHaveBeenCalledWith({
...mockQueryBody, ...mockQueryBody,
data: { data: {
project: { project: {
siteProfiles: { [profileType]: {
edges: [{ node: mockProfiles[1] }], edges: [{ node: mockProfiles[1] }],
}, },
}, },
...@@ -52,12 +53,20 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => { ...@@ -52,12 +53,20 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
}); });
}); });
describe('dastSiteProfilesDeleteResponse', () => { describe('dastProfilesDeleteResponse', () => {
it('returns a mutation response with the correct shape', () => { it('returns a mutation response with the correct shape', () => {
expect(dastSiteProfilesDeleteResponse()).toEqual({ const mockMutationName = 'mutationName';
const mockPayloadTypeName = 'payloadTypeName';
expect(
dastProfilesDeleteResponse({
mutationName: mockMutationName,
payloadTypeName: mockPayloadTypeName,
}),
).toEqual({
__typename: 'Mutation', __typename: 'Mutation',
dastSiteProfileDelete: { [mockMutationName]: {
__typename: 'DastSiteProfileDeletePayload', __typename: mockPayloadTypeName,
errors: [], errors: [],
}, },
}); });
......
...@@ -7734,12 +7734,21 @@ msgstr "" ...@@ -7734,12 +7734,21 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again." msgid "DastProfiles|Could not create the site profile. Please try again."
msgstr "" msgstr ""
msgid "DastProfiles|Could not delete scanner profile. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not delete scanner profiles:"
msgstr ""
msgid "DastProfiles|Could not delete site profile. Please refresh the page, or try again later." msgid "DastProfiles|Could not delete site profile. Please refresh the page, or try again later."
msgstr "" msgstr ""
msgid "DastProfiles|Could not delete site profiles:" msgid "DastProfiles|Could not delete site profiles:"
msgstr "" msgstr ""
msgid "DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later."
msgstr ""
msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later." msgid "DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later."
msgstr "" msgstr ""
...@@ -7752,6 +7761,9 @@ msgstr "" ...@@ -7752,6 +7761,9 @@ msgstr ""
msgid "DastProfiles|Do you want to discard your changes?" msgid "DastProfiles|Do you want to discard your changes?"
msgstr "" msgstr ""
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
msgstr ""
msgid "DastProfiles|Edit site profile" msgid "DastProfiles|Edit site profile"
msgstr "" msgstr ""
...@@ -7794,6 +7806,9 @@ msgstr "" ...@@ -7794,6 +7806,9 @@ msgstr ""
msgid "DastProfiles|Scanner Profile" msgid "DastProfiles|Scanner Profile"
msgstr "" msgstr ""
msgid "DastProfiles|Scanner Profiles"
msgstr ""
msgid "DastProfiles|Site Profile" msgid "DastProfiles|Site Profile"
msgstr "" msgstr ""
......
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