Commit b9bce284 authored by Fernando Arias's avatar Fernando Arias Committed by Frédéric Caplette

Corpus management validation logic

* Clientside and server side validations
parent 87734e88
...@@ -10,6 +10,7 @@ export default { ...@@ -10,6 +10,7 @@ export default {
directives: { directives: {
GlModalDirective, GlModalDirective,
}, },
inject: ['canReadCorpus', 'canDestroyCorpus'],
props: { props: {
corpus: { corpus: {
type: Object, type: Object,
...@@ -43,12 +44,22 @@ export default { ...@@ -43,12 +44,22 @@ export default {
</script> </script>
<template> <template>
<span> <span>
<gl-button class="gl-mr-2" icon="download" :href="downloadPath" />
<gl-button <gl-button
v-if="canReadCorpus"
class="gl-mr-2"
icon="download"
:href="downloadPath"
:aria-label="__('Download')"
data-testid="download-corpus"
/>
<gl-button
v-if="canDestroyCorpus"
v-gl-modal-directive="directiveName" v-gl-modal-directive="directiveName"
icon="remove" icon="remove"
category="secondary" category="secondary"
variant="danger" variant="danger"
:aria-label="__('Delete')"
data-testid="destroy-corpus"
/> />
<gl-modal <gl-modal
......
...@@ -27,7 +27,7 @@ export default { ...@@ -27,7 +27,7 @@ export default {
return this.corpus.package.pipelines.nodes[0]?.ref; return this.corpus.package.pipelines.nodes[0]?.ref;
}, },
latestJob() { latestJob() {
return `${this.jobURL} (${this.ref})`; return `${this.jobUrl} (${this.ref})`;
}, },
name() { name() {
return this.corpus.package.name; return this.corpus.package.name;
......
<script> <script>
import { GlLoadingIcon, GlLink, GlKeysetPagination } from '@gitlab/ui'; import { GlLoadingIcon, GlLink, GlKeysetPagination } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
import CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue'; import CorpusTable from 'ee/security_configuration/corpus_management/components/corpus_table.vue';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue'; import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import { s__, __ } from '~/locale'; import { s__, __ } from '~/locale';
...@@ -78,7 +79,8 @@ export default { ...@@ -78,7 +79,8 @@ export default {
beforeCursor: null, beforeCursor: null,
firstPageSize: this.$options.pageSize, firstPageSize: this.$options.pageSize,
}; };
this.$apollo.queries.states.refetch(); this.$apollo.queries.states.setOptions({ fetchPolicy: fetchPolicies.NETWORK_ONLY });
this.$apollo.queries.states.setOptions({ fetchPolicy: fetchPolicies.CACHE_FIRST });
}, },
onDelete(id) { onDelete(id) {
return this.$apollo return this.$apollo
......
...@@ -6,6 +6,8 @@ import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql' ...@@ -6,6 +6,8 @@ import addCorpusMutation from '../graphql/mutations/add_corpus.mutation.graphql'
import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql'; import resetCorpus from '../graphql/mutations/reset_corpus.mutation.graphql';
import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql'; import uploadCorpus from '../graphql/mutations/upload_corpus.mutation.graphql';
import getUploadState from '../graphql/queries/get_upload_state.query.graphql'; import getUploadState from '../graphql/queries/get_upload_state.query.graphql';
import uploadError from '../graphql/mutations/upload_error.mutation.graphql';
import { I18N, MAX_FILE_SIZE } from '../constants';
import CorpusUploadForm from './corpus_upload_form.vue'; import CorpusUploadForm from './corpus_upload_form.vue';
export default { export default {
...@@ -23,7 +25,7 @@ export default { ...@@ -23,7 +25,7 @@ export default {
newUpload: s__('CorpusManagement|New upload'), newUpload: s__('CorpusManagement|New upload'),
newCorpus: s__('CorpusMnagement|New corpus'), newCorpus: s__('CorpusMnagement|New corpus'),
}, },
inject: ['projectFullPath'], inject: ['projectFullPath', 'canUploadCorpus'],
apollo: { apollo: {
states: { states: {
query: getUploadState, query: getUploadState,
...@@ -93,10 +95,17 @@ export default { ...@@ -93,10 +95,17 @@ export default {
}); });
}, },
beginFileUpload({ name, files }) { beginFileUpload({ name, files }) {
this.$apollo.mutate({ if (files[0].size >= MAX_FILE_SIZE) {
mutation: uploadCorpus, this.$apollo.mutate({
variables: { name, projectPath: this.projectFullPath, files }, mutation: uploadError,
}); variables: { projectPath: this.projectFullPath, error: I18N.fileTooLarge },
});
} else {
this.$apollo.mutate({
mutation: uploadCorpus,
variables: { name, projectPath: this.projectFullPath, files },
});
}
}, },
}, },
}; };
...@@ -114,7 +123,9 @@ export default { ...@@ -114,7 +123,9 @@ export default {
</div> </div>
<gl-button <gl-button
v-if="canUploadCorpus"
v-gl-modal-directive="$options.modal.modalId" v-gl-modal-directive="$options.modal.modalId"
data-testid="new-corpus"
class="gl-mr-5 gl-ml-auto" class="gl-mr-5 gl-ml-auto"
variant="confirm" variant="confirm"
> >
......
...@@ -28,10 +28,9 @@ export default { ...@@ -28,10 +28,9 @@ export default {
}, },
i18n: { i18n: {
corpusName: s__('CorpusManagement|Corpus name'), corpusName: s__('CorpusManagement|Corpus name'),
corpusFile: s__('CorpusManagement|Corpus file'),
uploadButtonText: __('Choose File...'), uploadButtonText: __('Choose File...'),
uploadMessage: s__( uploadMessage: s__('CorpusManagement|Corpus files must be in *.zip format. Maximum 5 GB'),
'CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB',
),
}, },
data() { data() {
return { return {
...@@ -44,6 +43,15 @@ export default { ...@@ -44,6 +43,15 @@ export default {
hasAttachment() { hasAttachment() {
return Boolean(this.attachmentName); return Boolean(this.attachmentName);
}, },
hasValidName() {
return !this.nameError;
},
hasValidFile() {
return !this.fileError;
},
isShowingUploadText() {
return this.hasValidFile && !this.isUploaded;
},
isShowingAttachmentName() { isShowingAttachmentName() {
return this.hasAttachment && !this.isLoading; return this.hasAttachment && !this.isLoading;
}, },
...@@ -51,11 +59,14 @@ export default { ...@@ -51,11 +59,14 @@ export default {
return !this.isUploaded && !this.isUploading; return !this.isUploaded && !this.isUploading;
}, },
isUploading() { isUploading() {
return this.states?.uploadState.isUploading; return this.states?.uploadState?.isUploading;
}, },
isUploaded() { isUploaded() {
return this.progress === 100; return this.progress === 100;
}, },
isUploadButtonEnabled() {
return !this.corpusName;
},
showUploadButton() { showUploadButton() {
return this.hasAttachment && !this.isUploading && !this.isUploaded; return this.hasAttachment && !this.isUploading && !this.isUploaded;
}, },
...@@ -63,11 +74,17 @@ export default { ...@@ -63,11 +74,17 @@ export default {
return !this.isUploaded; return !this.isUploaded;
}, },
progress() { progress() {
return this.states?.uploadState.progress; return this.states?.uploadState?.progress;
}, },
progressText() { progressText() {
return sprintf(__('Attaching File - %{progress}'), { progress: `${this.progress}%` }); return sprintf(__('Attaching File - %{progress}'), { progress: `${this.progress}%` });
}, },
nameError() {
return this.states?.uploadState?.errors.name;
},
fileError() {
return this.states?.uploadState?.errors.file;
},
}, },
beforeDestroy() { beforeDestroy() {
this.resetAttachment(); this.resetAttachment();
...@@ -81,6 +98,7 @@ export default { ...@@ -81,6 +98,7 @@ export default {
this.$refs.fileUpload.value = null; this.$refs.fileUpload.value = null;
this.attachmentName = ''; this.attachmentName = '';
this.files = []; this.files = [];
this.$emit('resetCorpus');
}, },
cancelUpload() { cancelUpload() {
this.$emit('resetCorpus'); this.$emit('resetCorpus');
...@@ -94,6 +112,7 @@ export default { ...@@ -94,6 +112,7 @@ export default {
onFileUploadChange(e) { onFileUploadChange(e) {
this.attachmentName = e.target.files[0].name; this.attachmentName = e.target.files[0].name;
this.files = e.target.files; this.files = e.target.files;
this.$emit('resetCorpus');
}, },
}, },
VALID_CORPUS_MIMETYPE, VALID_CORPUS_MIMETYPE,
...@@ -101,13 +120,21 @@ export default { ...@@ -101,13 +120,21 @@ export default {
</script> </script>
<template> <template>
<gl-form> <gl-form>
<gl-form-group :label="$options.i18n.corpusName" label-size="sm" label-for="corpus-name"> <gl-form-group
:label="$options.i18n.corpusName"
label-size="sm"
label-for="corpus-name"
data-testid="corpus-name-group"
:invalid-feedback="nameError"
:state="hasValidName"
>
<gl-form-input-group> <gl-form-input-group>
<gl-form-input <gl-form-input
id="corpus-name" id="corpus-name"
ref="input" ref="input"
v-model="corpusName" v-model="corpusName"
data-testid="corpus-name" data-testid="corpus-name"
:state="hasValidName"
/> />
<gl-button <gl-button
...@@ -124,7 +151,14 @@ export default { ...@@ -124,7 +151,14 @@ export default {
</gl-form-input-group> </gl-form-input-group>
</gl-form-group> </gl-form-group>
<gl-form-group :label="$options.i18n.corpusName" label-size="sm" label-for="corpus-file"> <gl-form-group
:label="$options.i18n.corpusFile"
label-size="sm"
label-for="corpus-file"
data-testid="corpus-file-group"
:invalid-feedback="fileError"
:state="hasValidFile"
>
<gl-button <gl-button
v-if="showFilePickerButton" v-if="showFilePickerButton"
data-testid="upload-attachment-button" data-testid="upload-attachment-button"
...@@ -155,17 +189,23 @@ export default { ...@@ -155,17 +189,23 @@ export default {
/> />
</gl-form-group> </gl-form-group>
<span>{{ $options.i18n.uploadMessage }}</span> <span v-if="isShowingUploadText" class="gl-text-gray-500">{{
$options.i18n.uploadMessage
}}</span>
<gl-button <gl-form-group>
v-if="showUploadButton" <gl-button
data-testid="upload-corpus" v-if="showUploadButton"
class="gl-mt-2" data-testid="upload-corpus"
variant="success" class="gl-mt-2"
@click="beginFileUpload" :disabled="isUploadButtonEnabled"
> category="primary"
{{ __('Upload file') }} variant="confirm"
</gl-button> @click="beginFileUpload"
>
{{ __('Upload file') }}
</gl-button>
</gl-form-group>
<div v-if="isUploading" data-testid="upload-status" class="gl-mt-2"> <div v-if="isUploading" data-testid="upload-status" class="gl-mt-2">
<gl-loading-icon inline size="sm" /> <gl-loading-icon inline size="sm" />
......
import { s__ } from '~/locale';
export const MAX_LIST_COUNT = 25; export const MAX_LIST_COUNT = 25;
export const MAX_FILE_SIZE = 5e9;
export const VALID_CORPUS_MIMETYPE = { export const VALID_CORPUS_MIMETYPE = {
mimetype: 'application/zip', mimetype: 'application/zip',
}; };
export const I18N = {
fileTooLarge: s__('CorpusManagement|File too large, Maximum 5 GB'),
invalidName: s__(
'CorpusManagement|Filename can contain only lowercase letters (a-z), uppercase letter (A-Z), numbers (0-9), dots (.), hyphens (-), or underscores (_).',
),
};
export const ERROR_RESPONSE = {
packageNameInvalid: 'package_name is invalid',
/* eslint-disable-next-line @gitlab/require-i18n-strings */
notFound: '404 Not Found',
};
...@@ -23,7 +23,7 @@ export default () => { ...@@ -23,7 +23,7 @@ export default () => {
}); });
const { const {
dataset: { projectFullPath }, dataset: { projectFullPath, canUploadCorpus, canReadCorpus, canDestroyCorpus },
} = el; } = el;
let { let {
...@@ -36,6 +36,9 @@ export default () => { ...@@ -36,6 +36,9 @@ export default () => {
const provide = { const provide = {
projectFullPath, projectFullPath,
corpusHelpPath, corpusHelpPath,
canUploadCorpus: Boolean(canUploadCorpus),
canReadCorpus: Boolean(canReadCorpus),
canDestroyCorpus: Boolean(canDestroyCorpus),
}; };
return new Vue({ return new Vue({
......
...@@ -3,4 +3,8 @@ fragment UploadState on UploadState { ...@@ -3,4 +3,8 @@ fragment UploadState on UploadState {
progress progress
cancelSource cancelSource
uploadedPackageId uploadedPackageId
errors {
name
file
}
} }
mutation uploadError($projectPath: ID!, $error: String!) {
uploadError(projectPath: $projectPath, error: $error) @client
}
...@@ -34,6 +34,8 @@ query getCorpuses( ...@@ -34,6 +34,8 @@ query getCorpuses(
id id
createdAt createdAt
ref ref
path
updatedAt
} }
} }
} }
......
...@@ -6,7 +6,9 @@ import { TYPE_PACKAGES_PACKAGE } from '~/graphql_shared/constants'; ...@@ -6,7 +6,9 @@ import { TYPE_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
import getUploadState from '../queries/get_upload_state.query.graphql'; import getUploadState from '../queries/get_upload_state.query.graphql';
import updateProgress from '../mutations/update_progress.mutation.graphql'; import updateProgress from '../mutations/update_progress.mutation.graphql';
import uploadComplete from '../mutations/upload_complete.mutation.graphql'; import uploadComplete from '../mutations/upload_complete.mutation.graphql';
import uploadError from '../mutations/upload_error.mutation.graphql';
import corpusCreate from '../mutations/corpus_create.mutation.graphql'; import corpusCreate from '../mutations/corpus_create.mutation.graphql';
import { parseNameError, parseFileError } from './utils';
export default { export default {
Query: { Query: {
...@@ -16,6 +18,12 @@ export default { ...@@ -16,6 +18,12 @@ export default {
progress: 0, progress: 0,
cancelSource: null, cancelSource: null,
uploadedPackageId: null, uploadedPackageId: null,
errors: {
name: '',
file: '',
/* eslint-disable-next-line @gitlab/require-i18n-strings */
__typename: 'Errors',
},
__typename: 'UploadState', __typename: 'UploadState',
}; };
}, },
...@@ -64,12 +72,14 @@ export default { ...@@ -64,12 +72,14 @@ export default {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = true; uploadState.isUploading = true;
uploadState.cancelSource = source; uploadState.cancelSource = source;
uploadState.errors.name = '';
uploadState.errors.file = '';
}); });
cache.writeQuery({ query: getUploadState, data: targetData, variables: { projectPath } }); cache.writeQuery({ query: getUploadState, data: targetData, variables: { projectPath } });
publishPackage( publishPackage(
{ projectPath, name, version: 0, fileName: `${name}.zip`, files }, { projectPath, name, version: '1.0.0', fileName: `artifacts.zip`, files },
{ status: 'hidden', select: 'package_file' }, { status: 'hidden', select: 'package_file' },
{ onUploadProgress, cancelToken: source.token }, { onUploadProgress, cancelToken: source.token },
) )
...@@ -79,10 +89,31 @@ export default { ...@@ -79,10 +89,31 @@ export default {
variables: { projectPath, packageId: data.package_id }, variables: { projectPath, packageId: data.package_id },
}); });
}) })
.catch(() => { .catch((e) => {
/* TODO: Error handling */ const { error } = e.response?.data;
client.mutate({
mutation: uploadError,
variables: { projectPath, error },
});
}); });
}, },
uploadError: (_, { projectPath, error }, { cache }) => {
const sourceData = cache.readQuery({
query: getUploadState,
variables: { projectPath },
});
const data = produce(sourceData, (draftState) => {
const { uploadState } = draftState;
uploadState.isUploading = false;
uploadState.progress = 0;
uploadState.cancelSource = null;
uploadState.errors.name = parseNameError(error);
uploadState.errors.file = parseFileError(error);
});
cache.writeQuery({ query: getUploadState, data, variables: { projectPath } });
},
uploadComplete: (_, { projectPath, packageId }, { cache }) => { uploadComplete: (_, { projectPath, packageId }, { cache }) => {
const sourceData = cache.readQuery({ const sourceData = cache.readQuery({
query: getUploadState, query: getUploadState,
...@@ -94,6 +125,8 @@ export default { ...@@ -94,6 +125,8 @@ export default {
uploadState.isUploading = false; uploadState.isUploading = false;
uploadState.cancelSource = null; uploadState.cancelSource = null;
uploadState.uploadedPackageId = packageId; uploadState.uploadedPackageId = packageId;
uploadState.errors.name = '';
uploadState.errors.file = '';
}); });
cache.writeQuery({ query: getUploadState, data, variables: { projectPath } }); cache.writeQuery({ query: getUploadState, data, variables: { projectPath } });
...@@ -108,6 +141,8 @@ export default { ...@@ -108,6 +141,8 @@ export default {
const { uploadState } = draftState; const { uploadState } = draftState;
uploadState.isUploading = true; uploadState.isUploading = true;
uploadState.progress = progress; uploadState.progress = progress;
uploadState.errors.name = '';
uploadState.errors.file = '';
}); });
cache.writeQuery({ query: getUploadState, data, variables: { projectPath } }); cache.writeQuery({ query: getUploadState, data, variables: { projectPath } });
...@@ -125,6 +160,8 @@ export default { ...@@ -125,6 +160,8 @@ export default {
uploadState.isUploading = false; uploadState.isUploading = false;
uploadState.progress = 0; uploadState.progress = 0;
uploadState.cancelToken = null; uploadState.cancelToken = null;
uploadState.errors.name = '';
uploadState.errors.file = '';
}); });
cache.writeQuery({ query: getUploadState, data, variables: { projectPath } }); cache.writeQuery({ query: getUploadState, data, variables: { projectPath } });
......
import { I18N, ERROR_RESPONSE } from '../../constants';
export const parseNameError = (error) => {
if (error === ERROR_RESPONSE.packageNameInvalid || error === ERROR_RESPONSE.notFound) {
return I18N.invalidName;
}
return '';
};
export const parseFileError = (error) => {
if (error === I18N.fileTooLarge) {
return I18N.fileTooLarge;
}
return '';
};
...@@ -2,4 +2,7 @@ ...@@ -2,4 +2,7 @@
- breadcrumb_title s_('CorpusManagement|Fuzz testing corpus management') - breadcrumb_title s_('CorpusManagement|Fuzz testing corpus management')
- page_title s_('CorpusManagement|Fuzz testing corpus management') - page_title s_('CorpusManagement|Fuzz testing corpus management')
.js-corpus-management{ data: { project_full_path: @project.full_path } } .js-corpus-management{ data: {project_full_path: @project.full_path,
can_upload_corpus: can?(current_user, :create_package, @project).to_s,
can_read_corpus: can?(current_user, :read_package, @project).to_s,
can_destroy_corpus: can?(current_user, :destroy_package, @project).to_s } }
...@@ -16,6 +16,7 @@ exports[`Corpus Upload component renders header 1`] = ` ...@@ -16,6 +16,7 @@ exports[`Corpus Upload component renders header 1`] = `
buttontextclasses="" buttontextclasses=""
category="primary" category="primary"
class="gl-mr-5 gl-ml-auto" class="gl-mr-5 gl-ml-auto"
data-testid="new-corpus"
icon="" icon=""
role="button" role="button"
size="medium" size="medium"
......
...@@ -7,12 +7,19 @@ import { corpuses } from '../../mock_data'; ...@@ -7,12 +7,19 @@ import { corpuses } from '../../mock_data';
describe('Action buttons', () => { describe('Action buttons', () => {
let wrapper; let wrapper;
const findCorpusDownloadButton = () => wrapper.find('[data-testid="download-corpus"]');
const findCorpusDestroyButton = () => wrapper.find('[data-testid="destroy-corpus"]');
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { const defaultProps = {
corpus: corpuses[0], corpus: corpuses[0],
}; };
wrapper = mountFn(Actions, { wrapper = mountFn(Actions, {
propsData: defaultProps, propsData: defaultProps,
provide: {
canReadCorpus: true,
canDestroyCorpus: true,
},
...options, ...options,
}); });
}; };
...@@ -23,7 +30,7 @@ describe('Action buttons', () => { ...@@ -23,7 +30,7 @@ describe('Action buttons', () => {
wrapper.destroy(); wrapper.destroy();
}); });
describe('corpus management', () => { describe('corpus management with read and destroy enabled', () => {
it('renders the action buttons', () => { it('renders the action buttons', () => {
createComponent(); createComponent();
expect(wrapper.findAllComponents(GlButton)).toHaveLength(2); expect(wrapper.findAllComponents(GlButton)).toHaveLength(2);
...@@ -42,4 +49,45 @@ describe('Action buttons', () => { ...@@ -42,4 +49,45 @@ describe('Action buttons', () => {
}); });
}); });
}); });
describe('corpus management with read disabled', () => {
it('renders the destroy button only', () => {
createComponent({
provide: {
canReadCorpus: false,
canDestroyCorpus: true,
},
});
expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
expect(findCorpusDownloadButton().exists()).toBe(false);
expect(findCorpusDestroyButton().exists()).toBe(true);
});
describe('delete confirmation modal', () => {
beforeEach(() => {
createComponent({ stubs: { GlModal } });
});
it('calls the deleteCorpus method', async () => {
wrapper.findComponent(GlModal).vm.$emit('primary');
await nextTick();
expect(wrapper.emitted().delete).toBeTruthy();
});
});
});
describe('corpus management with destroy disabled', () => {
it('renders the download button only', () => {
createComponent({
provide: {
canReadCorpus: true,
canDestroyCorpus: false,
},
});
expect(wrapper.findAllComponents(GlButton)).toHaveLength(1);
expect(findCorpusDownloadButton().exists()).toBe(true);
expect(findCorpusDestroyButton().exists()).toBe(false);
});
});
}); });
import { mount } from '@vue/test-utils'; import { mountExtended } from 'helpers/vue_test_utils_helper';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue'; import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
import { I18N } from 'ee/security_configuration/corpus_management/constants';
const TEST_PROJECT_FULL_PATH = '/namespace/project'; const TEST_PROJECT_FULL_PATH = '/namespace/project';
describe('Corpus upload modal', () => { describe('Corpus upload modal', () => {
let wrapper; let wrapper;
const findCorpusName = () => wrapper.find('[data-testid="corpus-name"]'); const findCorpusName = () => wrapper.findByTestId('corpus-name');
const findUploadAttachment = () => wrapper.find('[data-testid="upload-attachment-button"]'); const findUploadAttachment = () => wrapper.findByTestId('upload-attachment-button');
const findUploadCorpus = () => wrapper.find('[data-testid="upload-corpus"]'); const findUploadCorpus = () => wrapper.findByTestId('upload-corpus');
const findUploadStatus = () => wrapper.find('[data-testid="upload-status"]'); const findUploadStatus = () => wrapper.findByTestId('upload-status');
const findFileInput = () => wrapper.findComponent({ ref: 'fileUpload' }); const findFileInput = () => wrapper.findComponent({ ref: 'fileUpload' });
const findCancelButton = () => wrapper.find('[data-testid="cancel-upload"]'); const findCancelButton = () => wrapper.findByTestId('cancel-upload');
const findNameErrorMsg = () => wrapper.findByText(I18N.invalidName);
const findFileErrorMsg = () => wrapper.findByText(I18N.fileTooLarge);
const createComponent = (propsData, options = {}) => { const createComponent = (propsData, options = {}) => {
wrapper = mount(CorpusUploadForm, { wrapper = mountExtended(CorpusUploadForm, {
propsData, propsData,
provide: { provide: {
projectFullPath: TEST_PROJECT_FULL_PATH, projectFullPath: TEST_PROJECT_FULL_PATH,
...@@ -44,6 +47,10 @@ describe('Corpus upload modal', () => { ...@@ -44,6 +47,10 @@ describe('Corpus upload modal', () => {
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
errors: {
name: '',
file: '',
},
}, },
}, },
}; };
...@@ -95,6 +102,10 @@ describe('Corpus upload modal', () => { ...@@ -95,6 +102,10 @@ describe('Corpus upload modal', () => {
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 0, progress: 0,
errors: {
name: '',
file: '',
},
}, },
}, },
}; };
...@@ -146,6 +157,10 @@ describe('Corpus upload modal', () => { ...@@ -146,6 +157,10 @@ describe('Corpus upload modal', () => {
uploadState: { uploadState: {
isUploading: true, isUploading: true,
progress: 25, progress: 25,
errors: {
name: '',
file: '',
},
}, },
}, },
}; };
...@@ -197,6 +212,10 @@ describe('Corpus upload modal', () => { ...@@ -197,6 +212,10 @@ describe('Corpus upload modal', () => {
uploadState: { uploadState: {
isUploading: false, isUploading: false,
progress: 100, progress: 100,
errors: {
name: '',
file: '',
},
}, },
}, },
}; };
...@@ -220,5 +239,159 @@ describe('Corpus upload modal', () => { ...@@ -220,5 +239,159 @@ describe('Corpus upload modal', () => {
expect(findUploadStatus().exists()).toBe(false); expect(findUploadStatus().exists()).toBe(false);
}); });
}); });
describe('error states', () => {
describe('invalid corpus name', () => {
const attachmentName = 'corpus.zip';
const corpusName = 'User entered name';
beforeEach(() => {
const data = () => {
return {
attachmentName,
corpusName,
files: [attachmentName],
};
};
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: I18N.invalidName,
file: '',
},
},
},
};
createComponent(props, { data });
});
it('shows name field', () => {
expect(findCorpusName().element.value).toBe(corpusName);
});
it('shows the choose file button', () => {
expect(findUploadAttachment().exists()).toBe(true);
});
it('shows the upload corpus button', () => {
expect(findUploadCorpus().exists()).toBe(true);
});
it('does not show the upload progress', () => {
expect(findUploadStatus().exists()).toBe(false);
});
it('shows corpus name invalid', () => {
expect(findNameErrorMsg().exists()).toBe(true);
});
});
describe('file too large', () => {
const attachmentName = 'corpus.zip';
const corpusName = 'User entered name';
beforeEach(() => {
const data = () => {
return {
attachmentName,
corpusName,
files: [attachmentName],
};
};
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: I18N.fileTooLarge,
},
},
},
};
createComponent(props, { data });
});
it('shows name field', () => {
expect(findCorpusName().element.value).toBe(corpusName);
});
it('shows the choose file button', () => {
expect(findUploadAttachment().exists()).toBe(true);
});
it('shows the upload corpus button', () => {
expect(findUploadCorpus().exists()).toBe(true);
});
it('does not show the upload progress', () => {
expect(findUploadStatus().exists()).toBe(false);
});
it('shows corpus size too large', () => {
expect(findFileErrorMsg().exists()).toBe(true);
});
});
describe('blank corpus name', () => {
const attachmentName = 'corpus.zip';
const corpusName = '';
beforeEach(() => {
const data = () => {
return {
attachmentName,
corpusName,
files: [attachmentName],
};
};
const props = {
states: {
uploadState: {
isUploading: false,
progress: 0,
errors: {
name: '',
file: '',
},
},
},
};
createComponent(props, { data });
});
it('shows name field', () => {
expect(findCorpusName().element.value).toBe(corpusName);
});
it('shows the choose file button', () => {
expect(findUploadAttachment().exists()).toBe(true);
});
it('shows the upload corpus button as disabled', () => {
expect(findUploadCorpus().exists()).toBe(true);
expect(findUploadCorpus().attributes('disabled')).toBe('disabled');
});
it('does not show the upload progress', () => {
expect(findUploadStatus().exists()).toBe(false);
});
it('does not show name format and file error messages', () => {
expect(findFileErrorMsg().exists()).toBe(false);
expect(findNameErrorMsg().exists()).toBe(false);
});
});
});
}); });
}); });
import { GlButton, GlModal } from '@gitlab/ui'; import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue'; import CorpusUpload from 'ee/security_configuration/corpus_management/components/corpus_upload.vue';
import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue'; import CorpusUploadForm from 'ee/security_configuration/corpus_management/components/corpus_upload_form.vue';
...@@ -10,6 +10,7 @@ describe('Corpus Upload', () => { ...@@ -10,6 +10,7 @@ describe('Corpus Upload', () => {
const findModal = () => wrapper.findComponent(GlModal); const findModal = () => wrapper.findComponent(GlModal);
const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm); const findCorpusUploadForm = () => wrapper.findComponent(CorpusUploadForm);
const findNewCorpusButton = () => wrapper.find('[data-testid="new-corpus"]');
const createComponentFactory = (mountFn = shallowMount) => (options = {}) => { const createComponentFactory = (mountFn = shallowMount) => (options = {}) => {
const defaultProps = { totalSize: 4e8 }; const defaultProps = { totalSize: 4e8 };
...@@ -24,6 +25,7 @@ describe('Corpus Upload', () => { ...@@ -24,6 +25,7 @@ describe('Corpus Upload', () => {
}, },
provide: { provide: {
projectFullPath: TEST_PROJECT_FULL_PATH, projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: true,
}, },
...options, ...options,
}); });
...@@ -38,7 +40,7 @@ describe('Corpus Upload', () => { ...@@ -38,7 +40,7 @@ describe('Corpus Upload', () => {
describe('component', () => { describe('component', () => {
it('renders header', () => { it('renders header', () => {
createComponent(); createComponent();
expect(wrapper.findComponent(GlButton).exists()).toBe(true); expect(findNewCorpusButton().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
...@@ -79,5 +81,18 @@ describe('Corpus Upload', () => { ...@@ -79,5 +81,18 @@ describe('Corpus Upload', () => {
expect(wrapper.vm.beginFileUpload).toHaveBeenCalled(); expect(wrapper.vm.beginFileUpload).toHaveBeenCalled();
}); });
}); });
describe('with new uploading disabled', () => {
it('does not render the upload button', () => {
createComponent({
provide: {
projectFullPath: TEST_PROJECT_FULL_PATH,
canUploadCorpus: false,
},
});
expect(findNewCorpusButton().exists()).toBe(false);
});
});
}); });
}); });
...@@ -40,6 +40,11 @@ describe('EE - CorpusManagement', () => { ...@@ -40,6 +40,11 @@ describe('EE - CorpusManagement', () => {
progress: 0, progress: 0,
cancelSource: null, cancelSource: null,
uploadedPackageId: null, uploadedPackageId: null,
errors: {
name: '',
file: '',
__typename: 'Errors',
},
__typename: 'UploadState', __typename: 'UploadState',
}; };
}, },
......
...@@ -24,6 +24,8 @@ describe('Corpus table', () => { ...@@ -24,6 +24,8 @@ describe('Corpus table', () => {
propsData: defaultProps, propsData: defaultProps,
provide: { provide: {
projectFullPath: TEST_PROJECT_FULL_PATH, projectFullPath: TEST_PROJECT_FULL_PATH,
canReadCorpus: true,
canDestroyCorpus: true,
}, },
...options, ...options,
}); });
......
...@@ -5,6 +5,7 @@ const pipelines = { ...@@ -5,6 +5,7 @@ const pipelines = {
ref: 'farias-gl/go-fuzzing-example', ref: 'farias-gl/go-fuzzing-example',
path: 'gitlab-examples/security/security-reports/-/jobs/1107103952', path: 'gitlab-examples/security/security-reports/-/jobs/1107103952',
createdAt: new Date(2020, 4, 3).toString(), createdAt: new Date(2020, 4, 3).toString(),
updatedAt: new Date(2020, 4, 5).toString(),
}, },
], ],
}; };
......
...@@ -9811,12 +9811,24 @@ msgstr "" ...@@ -9811,12 +9811,24 @@ msgstr ""
msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing." msgid "CorpusManagement|Corpus are used in fuzz testing as mutation source to Improve future testing."
msgstr "" msgstr ""
msgid "CorpusManagement|Corpus file"
msgstr ""
msgid "CorpusManagement|Corpus files must be in *.zip format. Maximum 5 GB"
msgstr ""
msgid "CorpusManagement|Corpus name" msgid "CorpusManagement|Corpus name"
msgstr "" msgstr ""
msgid "CorpusManagement|Currently, there are no uploaded or generated corpuses." msgid "CorpusManagement|Currently, there are no uploaded or generated corpuses."
msgstr "" msgstr ""
msgid "CorpusManagement|File too large, Maximum 5 GB"
msgstr ""
msgid "CorpusManagement|Filename can contain only lowercase letters (a-z), uppercase letter (A-Z), numbers (0-9), dots (.), hyphens (-), or underscores (_)."
msgstr ""
msgid "CorpusManagement|Fuzz testing corpus management" msgid "CorpusManagement|Fuzz testing corpus management"
msgstr "" msgstr ""
...@@ -9829,9 +9841,6 @@ msgstr "" ...@@ -9829,9 +9841,6 @@ msgstr ""
msgid "CorpusManagement|Latest Job:" msgid "CorpusManagement|Latest Job:"
msgstr "" msgstr ""
msgid "CorpusManagement|New corpus needs to be a upload in *.zip format. Maximum 5GB"
msgstr ""
msgid "CorpusManagement|New upload" msgid "CorpusManagement|New upload"
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