Commit 993a83a6 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'djadmin-dast-profiles-refactor' into 'master'

Refactor DAST Profiles List components

See merge request gitlab-org/gitlab!48670
parents ed5ba395 d926063f
......@@ -4,7 +4,6 @@ import { camelCase, kebabCase } from 'lodash';
import * as Sentry from '~/sentry/wrapper';
import { s__ } from '~/locale';
import { getLocationHash } from '~/lib/utils/url_utility';
import ProfilesList from './dast_profiles_list.vue';
import * as cacheUtils from '../graphql/cache_utils';
import { getProfileSettings } from '../settings/profiles';
......@@ -14,7 +13,6 @@ export default {
GlDropdownItem,
GlTab,
GlTabs,
ProfilesList,
},
props: {
createNewProfilePaths: {
......@@ -249,10 +247,11 @@ export default {
<gl-tabs v-model="tabIndex">
<gl-tab v-for="(settings, profileType) in profileSettings" :key="profileType">
<template #title>
<span>{{ settings.i18n.tabName }}</span>
<span>{{ settings.i18n.name }}</span>
</template>
<profiles-list
<component
:is="profileSettings[profileType].component"
:data-testid="`${profileType}List`"
:error-message="profileTypes[profileType].errorMessage"
:error-details="profileTypes[profileType].errorDetails"
......@@ -260,6 +259,7 @@ export default {
:is-loading="isLoadingProfiles(profileType)"
:profiles-per-page="$options.profilesPerPage"
:profiles="profileTypes[profileType].profiles"
:table-label="settings.i18n.name"
:fields="settings.tableFields"
:full-path="projectFullPath"
@load-more-profiles="fetchMoreProfiles(profileType)"
......
......@@ -3,40 +3,32 @@ import { uniqueId } from 'lodash';
import {
GlAlert,
GlButton,
GlIcon,
GlModal,
GlSkeletonLoader,
GlTable,
GlTooltipDirective,
} from '@gitlab/ui';
import DastSiteValidationModal from 'ee/security_configuration/dast_site_validation/components/dast_site_validation_modal.vue';
import {
DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_STATUS_PROPS,
} from 'ee/security_configuration/dast_site_validation/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const { PENDING, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default {
components: {
GlAlert,
GlButton,
GlIcon,
GlModal,
GlSkeletonLoader,
GlTable,
DastSiteValidationModal,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
profiles: {
type: Array,
required: true,
},
tableLabel: {
type: String,
required: true,
},
fields: {
type: Array,
required: true,
......@@ -73,10 +65,8 @@ export default {
data() {
return {
toBeDeletedProfileId: null,
validatingProfile: null,
};
},
statuses: DAST_SITE_VALIDATION_STATUS_PROPS,
computed: {
hasError() {
return this.errorMessage !== '';
......@@ -115,24 +105,6 @@ export default {
handleCancel() {
this.toBeDeletedProfileId = null;
},
shouldShowValidationBtn(status) {
return (
this.glFeatures.securityOnDemandScansSiteValidation &&
(status === PENDING || status === FAILED)
);
},
shouldShowValidationStatus(status) {
return this.glFeatures.securityOnDemandScansSiteValidation && status !== PENDING;
},
showValidationModal() {
this.$refs['dast-site-validation-modal'].show();
},
setValidatingProfile(profile) {
this.validatingProfile = profile;
this.$nextTick(() => {
this.showValidationModal();
});
},
},
};
</script>
......@@ -140,7 +112,7 @@ export default {
<section>
<div v-if="shouldShowTable">
<gl-table
:aria-label="s__('DastProfiles|Site Profiles')"
:aria-label="tableLabel"
:busy="isLoadingInitialProfiles"
:fields="tableFields"
:items="profiles"
......@@ -166,30 +138,13 @@ export default {
<strong>{{ value }}</strong>
</template>
<template #cell(validationStatus)="{ value }">
<template v-if="shouldShowValidationStatus(value)">
<span :class="$options.statuses[value].cssClass">
{{ $options.statuses[value].label }}
</span>
<gl-icon
v-gl-tooltip
name="question-o"
class="gl-vertical-align-text-bottom gl-text-gray-300 gl-ml-2"
:title="$options.statuses[value].tooltipText"
/>
</template>
<template v-for="slotName in Object.keys($scopedSlots)" #[slotName]="slotScope">
<slot :name="slotName" v-bind="slotScope"></slot>
</template>
<template #cell(actions)="{ item }">
<div class="gl-text-right">
<gl-button
v-if="shouldShowValidationBtn(item.validationStatus)"
variant="info"
category="secondary"
size="small"
@click="setValidatingProfile(item)"
>{{ s__('DastSiteValidation|Validate target site') }}</gl-button
>
<slot name="actions" :profile="item"></slot>
<gl-button v-if="item.editPath" :href="item.editPath" class="gl-mx-5" size="small">{{
__('Edit')
......@@ -248,11 +203,6 @@ export default {
@cancel="handleCancel"
/>
<dast-site-validation-modal
v-if="validatingProfile"
ref="dast-site-validation-modal"
:full-path="fullPath"
:target-url="validatingProfile.targetUrl"
/>
<slot></slot>
</section>
</template>
<script>
import ProfilesList from './dast_profiles_list.vue';
export default {
components: {
ProfilesList,
},
};
</script>
<template>
<profiles-list v-bind="$attrs" v-on="$listeners" />
</template>
<script>
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import {
DAST_SITE_VALIDATION_STATUS,
DAST_SITE_VALIDATION_STATUS_PROPS,
} from 'ee/security_configuration/dast_site_validation/constants';
import DastSiteValidationModal from 'ee/security_configuration/dast_site_validation/components/dast_site_validation_modal.vue';
import ProfilesList from './dast_profiles_list.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const { PENDING, FAILED } = DAST_SITE_VALIDATION_STATUS;
export default {
components: {
GlButton,
GlIcon,
DastSiteValidationModal,
ProfilesList,
},
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
fullPath: {
type: String,
required: true,
},
},
data() {
return {
validatingProfile: null,
};
},
statuses: DAST_SITE_VALIDATION_STATUS_PROPS,
methods: {
shouldShowValidationBtn(status) {
return (
this.glFeatures.securityOnDemandScansSiteValidation &&
(status === PENDING || status === FAILED)
);
},
shouldShowValidationStatus(status) {
return this.glFeatures.securityOnDemandScansSiteValidation && status !== PENDING;
},
showValidationModal() {
this.$refs['dast-site-validation-modal'].show();
},
setValidatingProfile(profile) {
this.validatingProfile = profile;
this.$nextTick(() => {
this.showValidationModal();
});
},
},
};
</script>
<template>
<profiles-list :full-path="fullPath" v-bind="$attrs" v-on="$listeners">
<template #cell(validationStatus)="{ value }">
<template v-if="shouldShowValidationStatus(value)">
<span :class="$options.statuses[value].cssClass">
{{ $options.statuses[value].label }}
</span>
<gl-icon
v-gl-tooltip
name="question-o"
class="gl-vertical-align-text-bottom gl-text-gray-300 gl-ml-2"
:title="$options.statuses[value].tooltipText"
/>
</template>
</template>
<template #actions="{ profile }">
<gl-button
v-if="shouldShowValidationBtn(profile.validationStatus)"
variant="info"
category="secondary"
size="small"
@click="setValidatingProfile(profile)"
>{{ s__('DastSiteValidation|Validate target site') }}</gl-button
>
</template>
<dast-site-validation-modal
v-if="validatingProfile"
ref="dast-site-validation-modal"
:full-path="fullPath"
:target-url="validatingProfile.targetUrl"
/>
</profiles-list>
</template>
......@@ -3,6 +3,8 @@ import dastSiteProfilesDelete from 'ee/security_configuration/dast_profiles/grap
import dastScannerProfilesQuery from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles.query.graphql';
import dastScannerProfilesDelete from 'ee/security_configuration/dast_profiles/graphql/dast_scanner_profiles_delete.mutation.graphql';
import { dastProfilesDeleteResponse } from 'ee/security_configuration/dast_profiles/graphql/cache_utils';
import DastSiteProfileList from 'ee/security_configuration/dast_profiles/components/dast_site_profiles_list.vue';
import DastScannerProfileList from 'ee/security_configuration/dast_profiles/components/dast_scanner_profiles_list.vue';
import { s__ } from '~/locale';
export const getProfileSettings = ({ createNewProfilePaths }) => ({
......@@ -19,10 +21,11 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
}),
},
},
component: DastSiteProfileList,
tableFields: ['profileName', 'targetUrl', 'validationStatus'],
i18n: {
createNewLinkText: s__('DastProfiles|Site Profile'),
tabName: s__('DastProfiles|Site Profiles'),
name: s__('DastProfiles|Site Profiles'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch site profiles. Please refresh the page, or try again later.',
......@@ -47,10 +50,11 @@ export const getProfileSettings = ({ createNewProfilePaths }) => ({
}),
},
},
component: DastScannerProfileList,
tableFields: ['profileName'],
i18n: {
createNewLinkText: s__('DastProfiles|Scanner Profile'),
tabName: s__('DastProfiles|Scanner Profiles'),
name: s__('DastProfiles|Scanner Profiles'),
errorMessages: {
fetchNetworkError: s__(
'DastProfiles|Could not fetch scanner profiles. Please refresh the page, or try again later.',
......
......@@ -14,6 +14,7 @@ describe('EE - DastProfilesList', () => {
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = {
profiles: [],
tableLabel: 'Profiles Table',
fields: ['profileName', 'targetUrl', 'validationStatus'],
hasMorePages: false,
profilesPerPage: 10,
......@@ -27,9 +28,6 @@ describe('EE - DastProfilesList', () => {
merge(
{},
{
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
propsData: defaultProps,
},
options,
......@@ -46,7 +44,7 @@ describe('EE - DastProfilesList', () => {
const createFullComponent = createComponentFactory(mount);
const withinComponent = () => within(wrapper.element);
const getTable = () => withinComponent().getByRole('table', { name: /site profiles/i });
const getTable = () => withinComponent().getByRole('table', { name: /profiles table/i });
const getAllRowGroups = () => within(getTable()).getAllByRole('rowgroup');
const getTableBody = () => {
// first item is the table head
......@@ -140,53 +138,23 @@ describe('EE - DastProfilesList', () => {
expect(editLink).not.toBe(null);
expect(editLink.getAttribute('href')).toBe(profile.editPath);
});
describe('with site validation enabled', () => {
describe.each`
status | statusEnum | label | hasValidateButton
${'pending'} | ${'PENDING_VALIDATION'} | ${''} | ${true}
${'in-progress'} | ${'INPROGRESS_VALIDATION'} | ${'Validating...'} | ${false}
${'passed'} | ${'PASSED_VALIDATION'} | ${'Validated'} | ${false}
${'failed'} | ${'FAILED_VALIDATION'} | ${'Validation failed'} | ${true}
`('profile with validation $status', ({ statusEnum, label, hasValidateButton }) => {
const profile = profiles.find(({ validationStatus }) => validationStatus === statusEnum);
it(`should show correct label`, () => {
const validationStatusCell = getTableRowForProfile(profile).cells[2];
expect(validationStatusCell.innerText).toContain(label);
});
it(`should ${hasValidateButton ? '' : 'not '}render validate button`, () => {
const actionsCell = getTableRowForProfile(profile).cells[3];
const validateButton = within(actionsCell).queryByRole('button', {
name: /validate/i,
});
if (hasValidateButton) {
expect(validateButton).not.toBeNull();
} else {
expect(validateButton).toBeNull();
}
});
});
});
describe('without site validation enabled', () => {
describe('profile list with scoped slots', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: { profiles },
scopedSlots: {
'cell(profileName)': '<b>{{props.item.profileName}}</b>',
actions: '<button>hello</button>',
},
});
});
it.each(profiles)('renders list item %# correctly', profile => {
const [profileCell, , , actionsCell] = getTableRowForProfile(profile).cells;
it.each(profiles)('profile %# should not have validate button and status', profile => {
const [, , validationStatusCell, actionsCell] = getTableRowForProfile(profile).cells;
expect(within(actionsCell).queryByRole('button', { name: /validate/i })).toBe(null);
expect(validationStatusCell.innerText).toBe('');
});
expect(profileCell.innerHTML).toContain(`<b>${profile.profileName}</b>`);
expect(within(actionsCell).getByRole('button', { name: /hello/i })).not.toBe(null);
});
});
......
......@@ -171,37 +171,23 @@ describe('EE - DastProfiles', () => {
createComponent();
});
it('passes down the correct default props', () => {
expect(getProfilesComponent(profileType).props()).toEqual({
errorMessage: '',
errorDetails: [],
hasMoreProfilesToLoad: false,
isLoading: false,
profilesPerPage: expect.any(Number),
profiles: [],
fields: expect.any(Array),
fullPath: '/namespace/project',
});
});
it.each([true, false])('passes down the loading state when loading is "%s"', loading => {
createComponent({ mocks: { $apollo: { queries: { [profileType]: { loading } } } } });
it('passes down the loading state when loading is true', () => {
createComponent({ mocks: { $apollo: { queries: { [profileType]: { loading: true } } } } });
expect(getProfilesComponent(profileType).props('isLoading')).toBe(loading);
expect(getProfilesComponent(profileType).attributes('is-loading')).toBe('true');
});
it.each`
givenData | propName | expectedPropValue
${{ profileTypes: { [profileType]: { errorMessage: 'foo' } } }} | ${'errorMessage'} | ${'foo'}
${{ profileTypes: { [profileType]: { errorDetails: ['foo'] } } }} | ${'errorDetails'} | ${['foo']}
${{ profileTypes: { [profileType]: { pageInfo: { hasNextPage: true } } } }} | ${'hasMoreProfilesToLoad'} | ${true}
${{ profileTypes: { [profileType]: { profiles: [{ foo: 'bar' }] } } }} | ${'profiles'} | ${[{ foo: 'bar' }]}
${{ profileTypes: { [profileType]: { errorMessage: 'foo' } } }} | ${'error-message'} | ${'foo'}
${{ profileTypes: { [profileType]: { errorDetails: ['foo'] } } }} | ${'error-details'} | ${'foo'}
${{ profileTypes: { [profileType]: { pageInfo: { hasNextPage: true } } } }} | ${'has-more-profiles-to-load'} | ${'true'}
`('passes down $propName correctly', async ({ givenData, propName, expectedPropValue }) => {
wrapper.setData(givenData);
await wrapper.vm.$nextTick();
expect(getProfilesComponent(profileType).props(propName)).toEqual(expectedPropValue);
expect(getProfilesComponent(profileType).attributes(propName)).toEqual(expectedPropValue);
});
it('fetches more results when "@load-more-profiles" is emitted', () => {
......
import { mount, shallowMount } from '@vue/test-utils';
import { merge } from 'lodash';
import Component from 'ee/security_configuration/dast_profiles/components/dast_scanner_profiles_list.vue';
import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { scannerProfiles } from './mock_data';
describe('EE - DastScannerProfileList', () => {
let wrapper;
const defaultProps = {
profiles: [],
tableLabel: 'Scanner profiles',
fields: ['profileName'],
profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
fullPath: '/namespace/project',
hasMoreProfilesToLoad: false,
isLoading: false,
};
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(
Component,
merge(
{
propsData: defaultProps,
},
options,
),
);
};
const createComponent = wrapperFactory();
const createFullComponent = wrapperFactory(mount);
const findProfileList = () => wrapper.find(ProfilesList);
afterEach(() => {
wrapper.destroy();
});
it('renders profile list properly', () => {
createComponent({
propsData: { profiles: scannerProfiles },
});
expect(findProfileList()).toExist();
});
it('passes down the props properly', () => {
createFullComponent();
expect(findProfileList().props()).toEqual(defaultProps);
});
it('sets listeners on profile list component', () => {
const inputHandler = jest.fn();
createComponent({
listeners: {
input: inputHandler,
},
});
findProfileList().vm.$emit('input');
expect(inputHandler).toHaveBeenCalled();
});
});
import { mount, shallowMount } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { merge } from 'lodash';
import Component from 'ee/security_configuration/dast_profiles/components/dast_site_profiles_list.vue';
import ProfilesList from 'ee/security_configuration/dast_profiles/components/dast_profiles_list.vue';
import { siteProfiles } from './mock_data';
describe('EE - DastSiteProfileList', () => {
let wrapper;
const defaultProps = {
profiles: [],
tableLabel: 'Site profiles',
fields: ['profileName', 'targetUrl', 'validationStatus'],
profilesPerPage: 10,
errorMessage: '',
errorDetails: [],
fullPath: '/namespace/project',
hasMoreProfilesToLoad: false,
isLoading: false,
};
const wrapperFactory = (mountFn = shallowMount) => (options = {}) => {
wrapper = mountFn(
Component,
merge(
{
propsData: defaultProps,
provide: {
glFeatures: { securityOnDemandScansSiteValidation: true },
},
},
options,
),
);
};
const createComponent = wrapperFactory();
const createFullComponent = wrapperFactory(mount);
const withinComponent = () => within(wrapper.element);
const getTable = () => withinComponent().getByRole('table', { name: /profiles/i });
const getAllRowGroups = () => within(getTable()).getAllByRole('rowgroup');
const getTableBody = () => {
// first item is the table head
const [, tableBody] = getAllRowGroups();
return tableBody;
};
const getAllTableRows = () => within(getTableBody()).getAllByRole('row');
const getTableRowForProfile = profile => getAllTableRows()[siteProfiles.indexOf(profile)];
const findProfileList = () => wrapper.find(ProfilesList);
afterEach(() => {
wrapper.destroy();
});
it('renders profile list properly', () => {
createComponent({
propsData: { profiles: siteProfiles },
});
expect(findProfileList()).toExist();
});
it('passes down the props properly', () => {
createFullComponent();
expect(findProfileList().props()).toEqual(defaultProps);
});
it('sets listeners on profile list component', () => {
const inputHandler = jest.fn();
createComponent({
listeners: {
input: inputHandler,
},
});
findProfileList().vm.$emit('input');
expect(inputHandler).toHaveBeenCalled();
});
describe('with site validation enabled', () => {
beforeEach(() => {
createFullComponent({ propsData: { siteProfiles } });
});
describe.each`
status | statusEnum | label | hasValidateButton
${'pending'} | ${'PENDING_VALIDATION'} | ${''} | ${true}
${'in-progress'} | ${'INPROGRESS_VALIDATION'} | ${'Validating...'} | ${false}
${'passed'} | ${'PASSED_VALIDATION'} | ${'Validated'} | ${false}
${'failed'} | ${'FAILED_VALIDATION'} | ${'Validation failed'} | ${true}
`('profile with validation $status', ({ statusEnum, label, hasValidateButton }) => {
const profile = siteProfiles.find(({ validationStatus }) => validationStatus === statusEnum);
it(`should show correct label`, () => {
const validationStatusCell = getTableRowForProfile(profile).cells[2];
expect(validationStatusCell.innerText).toContain(label);
});
it(`should ${hasValidateButton ? '' : 'not '}render validate button`, () => {
const actionsCell = getTableRowForProfile(profile).cells[3];
const validateButton = within(actionsCell).queryByRole('button', {
name: /validate/i,
});
if (hasValidateButton) {
expect(validateButton).not.toBeNull();
} else {
expect(validateButton).toBeNull();
}
});
});
});
describe('without site validation enabled', () => {
beforeEach(() => {
createFullComponent({
provide: {
glFeatures: { securityOnDemandScansSiteValidation: false },
},
propsData: { siteProfiles },
});
});
it.each(siteProfiles)('profile %# should not have validate button and status', profile => {
const [, , validationStatusCell, actionsCell] = getTableRowForProfile(profile).cells;
expect(within(actionsCell).queryByRole('button', { name: /validate/i })).toBe(null);
expect(validationStatusCell.innerText).toBe('');
});
});
});
......@@ -28,3 +28,24 @@ export const siteProfiles = [
validationStatus: 'FAILED_VALIDATION',
},
];
export const scannerProfiles = [
{
id: 'gid://gitlab/DastScannerProfile/1',
profileName: 'Scanner profile #1',
spiderTimeout: 5,
targetTimeout: 10,
scanType: 'PASSIVE',
useAjaxSpider: false,
showDebugMessages: false,
},
{
id: 'gid://gitlab/DastScannerProfile/2',
profileName: 'Scanner profile #2',
spiderTimeout: 20,
targetTimeout: 150,
scanType: 'ACTIVE',
useAjaxSpider: true,
showDebugMessages: true,
},
];
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