Commit aa965b0c authored by David Pisek's avatar David Pisek Committed by Rémy Coutable

Update profile settings

* Adds graphql queries / mutations
* Adds helper to graphql-utils
parent 7240c379
......@@ -4,8 +4,6 @@ import { GlDropdown, GlDropdownItem, GlTab, GlTabs } from '@gitlab/ui';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
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 { getProfileSettings } from '../settings/profiles';
......@@ -32,39 +30,11 @@ export default {
},
data() {
return {
siteProfiles: [],
siteProfilesPageInfo: {},
profileTypes: {},
errorMessage: '',
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: {
profileSettings() {
const { glFeatures, createNewProfilePaths } = this;
......@@ -76,14 +46,66 @@ export default {
glFeatures,
);
},
hasMoreSiteProfiles() {
return this.siteProfilesPageInfo.hasNextPage;
},
isLoadingSiteProfiles() {
return this.$apollo.queries.siteProfiles.loading;
},
},
created() {
this.addSmartQueriesForEnabledProfileTypes();
},
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 = [] }) {
Sentry.captureException(exception);
this.errorMessage = message;
......@@ -93,32 +115,37 @@ export default {
this.errorMessage = '';
this.errorDetails = [];
},
fetchMoreProfiles() {
fetchMoreProfiles(profileType) {
const {
$apollo,
siteProfilesPageInfo,
$options: { i18n },
} = this;
const { pageInfo } = this.profileTypes[profileType];
this.resetErrors();
$apollo.queries.siteProfiles
$apollo.queries[profileType]
.fetchMore({
variables: { after: siteProfilesPageInfo.endCursor },
updateQuery: cacheUtils.appendToPreviousResult,
variables: { after: pageInfo.endCursor },
updateQuery: cacheUtils.appendToPreviousResult(profileType),
})
.catch(error => {
this.handleError({ exception: error, message: i18n.errorMessages.fetchNetworkError });
});
},
deleteSiteProfile(profileToBeDeletedId) {
deleteProfile(profileType, profileId) {
const {
projectFullPath,
handleError,
$options: { i18n },
profileSettings: {
[profileType]: {
i18n,
graphQL: { deletion },
},
},
$apollo: {
queries: {
siteProfiles: { options: siteProfilesQueryOptions },
[profileType]: { options: queryOptions },
},
},
} = this;
......@@ -127,27 +154,23 @@ export default {
this.$apollo
.mutate({
mutation: dastSiteProfilesDelete,
mutation: deletion.mutation,
variables: {
projectFullPath,
profileId: profileToBeDeletedId,
profileId,
},
update(
store,
{
data: {
dastSiteProfileDelete: { errors = [] },
},
},
) {
update(store, { data = {} }) {
const errors = data[`${profileType}Delete`]?.errors ?? [];
if (errors.length === 0) {
cacheUtils.removeProfile({
profileId,
profileType,
store,
queryBody: {
query: siteProfilesQueryOptions.query,
variables: siteProfilesQueryOptions.variables,
query: queryOptions.query,
variables: queryOptions.variables,
},
profileToBeDeletedId,
});
} else {
handleError({
......@@ -156,7 +179,7 @@ export default {
});
}
},
optimisticResponse: cacheUtils.dastSiteProfilesDeleteResponse(),
optimisticResponse: deletion.optimisticResponse,
})
.catch(error => {
this.handleError({
......@@ -173,15 +196,6 @@ export default {
subHeading: s__(
'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>
......@@ -200,11 +214,11 @@ export default {
class="gl-ml-auto"
>
<gl-dropdown-item
v-for="{ i18n, createNewProfilePath, key } in profileSettings"
:key="key"
v-for="{ i18n, createNewProfilePath, profileType } in profileSettings"
:key="profileType"
:href="createNewProfilePath"
>
{{ i18n.title }}
{{ i18n.createNewLinkText }}
</gl-dropdown-item>
</gl-dropdown>
</div>
......@@ -214,20 +228,22 @@ export default {
</header>
<gl-tabs>
<gl-tab>
<gl-tab v-for="(data, profileType) in profileSettings" :key="profileType">
<template #title>
<span>{{ s__('DastProfiles|Site Profiles') }}</span>
<span>{{ profileSettings[profileType].i18n.tabName }}</span>
</template>
<profiles-list
:data-testid="`${profileType}List`"
:error-message="errorMessage"
:error-details="errorDetails"
:has-more-profiles-to-load="hasMoreSiteProfiles"
:is-loading="isLoadingSiteProfiles"
:has-more-profiles-to-load="hasMoreProfiles(profileType)"
:is-loading="isLoadingProfiles(profileType)"
:profiles-per-page="$options.profilesPerPage"
:profiles="siteProfiles"
@loadMoreProfiles="fetchMoreProfiles"
@deleteProfile="deleteSiteProfile"
:profiles="profileTypes[profileType].profiles"
:fields="profileSettings[profileType].tableFields"
@load-more-profiles="fetchMoreProfiles(profileType)"
@delete-profile="deleteProfile(profileType, $event)"
/>
</gl-tab>
</gl-tabs>
......
......@@ -27,6 +27,10 @@ export default {
type: Array,
required: true,
},
fields: {
type: Array,
required: true,
},
errorMessage: {
type: String,
required: false,
......@@ -76,10 +80,17 @@ export default {
modalId() {
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: {
handleDelete() {
this.$emit('deleteProfile', this.toBeDeletedProfileId);
this.$emit('delete-profile', this.toBeDeletedProfileId);
},
prepareProfileDeletion(profileId) {
this.toBeDeletedProfileId = profileId;
......@@ -89,25 +100,6 @@ export default {
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>
<template>
......@@ -116,13 +108,13 @@ export default {
<gl-table
:aria-label="s__('DastProfiles|Site Profiles')"
:busy="isLoadingInitialProfiles"
:fields="$options.tableFields"
:fields="tableFields"
:items="profiles"
stacked="md"
thead-class="gl-display-none"
>
<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">
{{ errorMessage }}
<ul
......@@ -161,7 +153,22 @@ export default {
:aria-label="__('Delete')"
@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>
</template>
......@@ -181,7 +188,7 @@ export default {
<gl-button
data-testid="loadMore"
:loading="isLoading && !hasError"
@click="$emit('loadMoreProfiles')"
@click="$emit('load-more-profiles')"
>
{{ __('Load more') }}
</gl-button>
......
......@@ -2,16 +2,15 @@
* Appends paginated results to existing ones
* - to be used with $apollo.queries.x.fetchMore
*
* @param previousResult
* @param fetchMoreResult
* @returns {*}
* @param {*} profileType
* @returns {function(*, {fetchMoreResult: *}): *}
*/
export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => {
export const appendToPreviousResult = profileType => (previousResult, { fetchMoreResult }) => {
const newResult = { ...fetchMoreResult };
const previousEdges = previousResult.project.siteProfiles.edges;
const newEdges = newResult.project.siteProfiles.edges;
const previousEdges = previousResult.project[profileType].edges;
const newEdges = newResult.project[profileType].edges;
newResult.project.siteProfiles.edges = [...previousEdges, ...newEdges];
newResult.project[profileType].edges = [...previousEdges, ...newEdges];
return newResult;
};
......@@ -19,15 +18,16 @@ export const appendToPreviousResult = (previousResult, { fetchMoreResult }) => {
/**
* Removes profile with given id from the cache and writes the result to it
*
* @param profileId
* @param profileType
* @param store
* @param queryBody
* @param profileToBeDeletedId
*/
export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => {
export const removeProfile = ({ profileId, profileType, store, queryBody }) => {
const data = store.readQuery(queryBody);
data.project.siteProfiles.edges = data.project.siteProfiles.edges.filter(({ node }) => {
return node.id !== profileToBeDeletedId;
data.project[profileType].edges = data.project[profileType].edges.filter(({ node }) => {
return node.id !== profileId;
});
store.writeQuery({ ...queryBody, data });
......@@ -36,13 +36,15 @@ export const removeProfile = ({ store, queryBody, profileToBeDeletedId }) => {
/**
* 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
__typename: 'Mutation',
dastSiteProfileDelete: {
__typename: 'DastSiteProfileDeletePayload',
[mutationName]: {
__typename: payloadTypeName,
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!) {
dastSiteProfileDelete(input: { fullPath: $projectFullPath, id: $profileId }) {
siteProfilesDelete: dastSiteProfileDelete(input: { fullPath: $projectFullPath, id: $profileId }) {
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';
const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => {
......@@ -11,18 +16,60 @@ const hasNoFeatureFlagOrIsEnabled = glFeatures => ([, { featureFlag }]) => {
export const getProfileSettings = ({ createNewProfilePaths }, glFeatures) => {
const settings = {
siteProfiles: {
key: 'siteProfiles',
profileType: 'siteProfiles',
createNewProfilePath: createNewProfilePaths.siteProfile,
graphQL: {
query: dastSiteProfilesQuery,
deletion: {
mutation: dastSiteProfilesDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'siteProfilesDelete',
payloadTypeName: 'DastSiteProfileDeletePayload',
}),
},
},
tableFields: ['profileName', 'targetUrl'],
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: {
key: 'scannerProfiles',
profileType: 'scannerProfiles',
createNewProfilePath: createNewProfilePaths.scannerProfile,
graphQL: {
query: dastScannerProfilesQuery,
deletion: {
mutation: dastScannerProfilesDelete,
optimisticResponse: dastProfilesDeleteResponse({
mutationName: 'scannerProfilesDelete',
payloadTypeName: 'DastScannerProfileDeletePayload',
}),
},
},
featureFlag: 'securityOnDemandScansScannerProfiles',
tableFields: ['profileName'],
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', () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = {
profiles: [],
fields: ['profileName', 'targetUrl', 'validationStatus'],
hasMorePages: false,
profilesPerPage: 10,
errorMessage: '',
......@@ -116,6 +117,13 @@ describe('EE - DastProfilesList', () => {
editPath: '/2/edit',
validationStatus: 'Pending',
},
{
id: 3,
profileName: 'Profile 2',
targetUrl: 'http://example-2.com',
editPath: '',
validationStatus: 'Pending',
},
];
const getTableRowForProfile = profile => getAllTableRows()[profiles.indexOf(profile)];
......@@ -147,9 +155,19 @@ describe('EE - DastProfilesList', () => {
expect(validationStatusCell.innerText).toContain(profile.validationStatus);
expect(within(actionsCell).getByRole('button', { name: /delete/i })).not.toBe(null);
const editLink = within(actionsCell).getByRole('link', { name: /edit/i });
expect(editLink).not.toBe(null);
expect(editLink.getAttribute('href')).toBe(profile.editPath);
if (profile.editPath) {
const editLink = within(actionsCell).getByRole('link', { name: /edit/i });
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', () => {
expect(getLoadMoreButton().exists()).toBe(true);
});
it('emits "loadMoreProfiles" when the load-more button is clicked', async () => {
expect(wrapper.emitted('loadMoreProfiles')).toBe(undefined);
it('emits "load-more-profiles" when the load-more button is clicked', async () => {
expect(wrapper.emitted('load-more-profiles')).toBe(undefined);
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', () => {
});
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');
......@@ -208,7 +226,7 @@ describe('EE - DastProfilesList', () => {
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';
import { merge } from 'lodash';
import { GlDropdown } from '@gitlab/ui';
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_SITE_PROFILE_PATH = '/-/on_demand_scans/site_profiles/new';
......@@ -27,8 +26,18 @@ describe('EE - DastProfiles', () => {
siteProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
},
scannerProfiles: {
fetchMore: jest.fn().mockResolvedValue(),
},
},
mutate: jest.fn().mockResolvedValue(),
addSmartQuery: jest.fn(),
},
};
const defaultProvide = {
glFeatures: {
securityOnDemandScansScannerProfiles: true,
},
};
......@@ -39,6 +48,7 @@ describe('EE - DastProfiles', () => {
{
propsData: defaultProps,
mocks: defaultMocks,
provide: defaultProvide,
},
options,
),
......@@ -67,7 +77,7 @@ describe('EE - DastProfiles', () => {
};
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 getSiteProfilesDropdownItem = text =>
within(getDropdownComponent().element).queryByText(text);
......@@ -125,8 +135,9 @@ describe('EE - DastProfiles', () => {
});
it.each`
tabName | shouldBeSelectedByDefault
${'Site Profiles'} | ${true}
tabName | shouldBeSelectedByDefault
${'Site Profiles'} | ${true}
${'Scanner Profiles'} | ${false}
`(
'shows a "$tabName" tab which has "selected" set to "$shouldBeSelectedByDefault"',
({ tabName, shouldBeSelectedByDefault }) => {
......@@ -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(() => {
createComponent();
});
it('passes down the correct default props', () => {
expect(getSiteProfilesComponent().props()).toEqual({
expect(getProfilesComponent(profileType).props()).toEqual({
errorMessage: '',
errorDetails: [],
hasMoreProfilesToLoad: false,
isLoading: false,
profilesPerPage: expect.any(Number),
profiles: [],
fields: expect.any(Array),
});
});
it.each([true, false])('passes down the loading state', loading => {
createComponent({ mocks: { $apollo: { queries: { siteProfiles: { loading } } } } });
it.each([true, false])('passes down the loading state when loading is "%s"', loading => {
createComponent({ mocks: { $apollo: { queries: { [profileType]: { loading } } } } });
expect(getSiteProfilesComponent().props('isLoading')).toBe(loading);
expect(getProfilesComponent(profileType).props('isLoading')).toBe(loading);
});
it.each`
givenData | propName | expectedPropValue
${{ errorMessage: 'foo' }} | ${'errorMessage'} | ${'foo'}
${{ siteProfilesPageInfo: { hasNextPage: true } }} | ${'hasMoreProfilesToLoad'} | ${true}
${{ siteProfiles: [{ foo: 'bar' }] }} | ${'profiles'} | ${[{ foo: 'bar' }]}
givenData | propName | expectedPropValue
${{ errorMessage: 'foo' }} | ${'errorMessage'} | ${'foo'}
${{ profileTypes: { [profileType]: { pageInfo: { hasNextPage: true } } } }} | ${'hasMoreProfilesToLoad'} | ${true}
${{ profileTypes: { [profileType]: { profiles: [{ foo: 'bar' }] } } }} | ${'profiles'} | ${[{ foo: 'bar' }]}
`('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => {
wrapper.setData(givenData);
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 {
$apollo: {
queries: {
siteProfiles: { fetchMore },
[profileType]: { fetchMore },
},
},
} = wrapper.vm;
expect(fetchMore).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('loadMoreProfiles');
getProfilesComponent(profileType).vm.$emit('load-more-profiles');
expect(fetchMore).toHaveBeenCalledTimes(1);
});
it('deletes profile when "@deleteProfile" is emitted', () => {
it('deletes profile when "@delete-profile" is emitted', () => {
const {
$apollo: { mutate },
} = wrapper.vm;
expect(mutate).not.toHaveBeenCalled();
getSiteProfilesComponent().vm.$emit('deleteProfile');
getProfilesComponent(profileType).vm.$emit('delete-profile');
expect(mutate).toHaveBeenCalledTimes(1);
});
......
import {
appendToPreviousResult,
removeProfile,
dastSiteProfilesDeleteResponse,
dastProfilesDeleteResponse,
} from 'ee/dast_profiles/graphql/cache_utils';
describe('EE - DastProfiles GraphQL CacheUtils', () => {
describe('appendToPreviousResult', () => {
it('appends new results to previous', () => {
const previousResult = { project: { siteProfiles: { edges: ['foo'] } } };
const fetchMoreResult = { project: { siteProfiles: { edges: ['bar'] } } };
it.each(['siteProfiles', 'scannerProfiles'])('appends new results to previous', profileType => {
const previousResult = { project: { [profileType]: { edges: ['foo'] } } };
const fetchMoreResult = { project: { [profileType]: { edges: ['bar'] } } };
const expected = { project: { siteProfiles: { edges: ['foo', 'bar'] } } };
const result = appendToPreviousResult(previousResult, { fetchMoreResult });
const expected = { project: { [profileType]: { edges: ['foo', 'bar'] } } };
const result = appendToPreviousResult(profileType)(previousResult, { fetchMoreResult });
expect(result).toEqual(expected);
});
});
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 mockProfiles = [{ id: 0 }, { id: 1 }];
const mockData = {
project: {
siteProfiles: {
[profileType]: {
edges: [{ node: mockProfiles[0] }, { node: mockProfiles[1] }],
},
},
......@@ -36,14 +36,15 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
removeProfile({
store: mockStore,
queryBody: mockQueryBody,
profileToBeDeletedId: mockProfiles[0].id,
profileId: mockProfiles[0].id,
profileType,
});
expect(mockStore.writeQuery).toHaveBeenCalledWith({
...mockQueryBody,
data: {
project: {
siteProfiles: {
[profileType]: {
edges: [{ node: mockProfiles[1] }],
},
},
......@@ -52,12 +53,20 @@ describe('EE - DastProfiles GraphQL CacheUtils', () => {
});
});
describe('dastSiteProfilesDeleteResponse', () => {
describe('dastProfilesDeleteResponse', () => {
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',
dastSiteProfileDelete: {
__typename: 'DastSiteProfileDeletePayload',
[mockMutationName]: {
__typename: mockPayloadTypeName,
errors: [],
},
});
......
......@@ -7728,12 +7728,21 @@ msgstr ""
msgid "DastProfiles|Could not create the site profile. Please try again."
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."
msgstr ""
msgid "DastProfiles|Could not delete site profiles:"
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."
msgstr ""
......@@ -7746,6 +7755,9 @@ msgstr ""
msgid "DastProfiles|Do you want to discard your changes?"
msgstr ""
msgid "DastProfiles|Edit feature will come soon. Please create a new profile if changes needed"
msgstr ""
msgid "DastProfiles|Edit site profile"
msgstr ""
......@@ -7788,6 +7800,9 @@ msgstr ""
msgid "DastProfiles|Scanner Profile"
msgstr ""
msgid "DastProfiles|Scanner Profiles"
msgstr ""
msgid "DastProfiles|Site Profile"
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