summaryrefslogtreecommitdiff
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-14 06:08:49 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-14 06:08:49 +0000
commitca1dcb848f19e854d2022587436fa5bc5f8ef933 (patch)
treedc59a85ef03ff80b78572f797ece8e3e571ab116 /spec
parent962711501ff8e5a004c700b97a367930ed5a1f20 (diff)
downloadgitlab-ce-ca1dcb848f19e854d2022587436fa5bc5f8ef933.tar.gz
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/features/projects/files/dockerfile_dropdown_spec.rb2
-rw-r--r--spec/features/projects/files/gitignore_dropdown_spec.rb4
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb12
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb8
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap4
-rw-r--r--spec/frontend/design_management/pages/index_spec.js78
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js173
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js2
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js5
-rw-r--r--spec/frontend/invite_members/mock_data/member_modal.js10
-rw-r--r--spec/frontend/invite_members/utils/member_utils_spec.js12
-rw-r--r--spec/frontend/invite_members/utils/response_message_parser_spec.js33
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js137
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js10
-rw-r--r--spec/frontend/work_items/pages/work_item_detail_spec.js36
-rw-r--r--spec/frontend/work_items/pages/work_item_root_spec.js1
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb261
-rw-r--r--spec/lib/gitlab/x509/commit_spec.rb41
-rw-r--r--spec/support/helpers/features/invite_members_modal_helper.rb44
-rw-r--r--spec/support/helpers/features/source_editor_spec_helpers.rb7
-rw-r--r--spec/support/helpers/features/web_ide_spec_helpers.rb4
-rw-r--r--spec/support/shared_examples/features/inviting_members_shared_examples.rb63
22 files changed, 546 insertions, 401 deletions
diff --git a/spec/features/projects/files/dockerfile_dropdown_spec.rb b/spec/features/projects/files/dockerfile_dropdown_spec.rb
index 3a0cc61d9c6..dd1635c900e 100644
--- a/spec/features/projects/files/dockerfile_dropdown_spec.rb
+++ b/spec/features/projects/files/dockerfile_dropdown_spec.rb
@@ -26,6 +26,6 @@ RSpec.describe 'Projects > Files > User wants to add a Dockerfile file', :js do
wait_for_requests
expect(page).to have_css('.dockerfile-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(editor_get_value).to have_content('COPY ./ /usr/local/apache2/htdocs/')
+ expect(find('.monaco-editor')).to have_content('COPY ./ /usr/local/apache2/htdocs/')
end
end
diff --git a/spec/features/projects/files/gitignore_dropdown_spec.rb b/spec/features/projects/files/gitignore_dropdown_spec.rb
index 4a92216f46c..a86adf951d8 100644
--- a/spec/features/projects/files/gitignore_dropdown_spec.rb
+++ b/spec/features/projects/files/gitignore_dropdown_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitignore file', :js do
wait_for_requests
expect(page).to have_css('.gitignore-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(editor_get_value).to have_content('/.bundle')
- expect(editor_get_value).to have_content('config/initializers/secret_token.rb')
+ expect(find('.monaco-editor')).to have_content('/.bundle')
+ expect(find('.monaco-editor')).to have_content('config/initializers/secret_token.rb')
end
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
index cdf6c219ea5..46ac0dee7eb 100644
--- a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -30,8 +30,8 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js
wait_for_requests
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(editor_get_value).to have_content('This file is a template, and might need editing before it works on your project')
- expect(editor_get_value).to have_content('jekyll build -d test')
+ expect(find('.monaco-editor')).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(find('.monaco-editor')).to have_content('jekyll build -d test')
end
context 'when template param is provided' do
@@ -41,8 +41,8 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js
wait_for_requests
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(editor_get_value).to have_content('This file is a template, and might need editing before it works on your project')
- expect(editor_get_value).to have_content('jekyll build -d test')
+ expect(find('.monaco-editor')).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(find('.monaco-editor')).to have_content('jekyll build -d test')
end
end
@@ -53,7 +53,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js
wait_for_requests
expect(page).to have_css('.gitlab-ci-yml-selector .dropdown-toggle-text', text: 'Apply a template')
- expect(editor_get_value).to have_content('')
+ expect(find('.monaco-editor')).to have_content('')
end
end
@@ -64,7 +64,7 @@ RSpec.describe 'Projects > Files > User wants to add a .gitlab-ci.yml file', :js
it 'leaves the editor empty' do
wait_for_requests
- expect(editor_get_value).to have_content('')
+ expect(find('.monaco-editor')).to have_content('')
end
end
end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 0e87622d3c2..6b1e60db5b1 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -22,8 +22,10 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license
select_template('MIT License')
- expect(ide_editor_value).to have_content('MIT License')
- expect(ide_editor_value).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}")
+ file_content = "Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}"
+
+ expect(find('.monaco-editor')).to have_content('MIT License')
+ expect(find('.monaco-editor')).to have_content(file_content)
ide_commit
@@ -33,7 +35,7 @@ RSpec.describe 'Projects > Files > Project owner sees a link to create a license
license_file = project.repository.blob_at('master', 'LICENSE').data
expect(license_file).to have_content('MIT License')
- expect(license_file).to have_content("Copyright (c) #{Time.zone.now.year} #{project.namespace.human_name}")
+ expect(license_file).to have_content(file_content)
end
def select_template(template)
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index be736184e60..9997f02cd01 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -7,6 +7,8 @@ exports[`Design management index page designs renders error 1`] = `
>
<!---->
+ <!---->
+
<div
class="gl-mt-6"
>
@@ -39,6 +41,8 @@ exports[`Design management index page designs renders loading icon 1`] = `
>
<!---->
+ <!---->
+
<div
class="gl-mt-6"
>
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 087655d10f7..21be7bd148b 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,5 +1,4 @@
import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo, { ApolloMutation } from 'vue-apollo';
@@ -9,6 +8,7 @@ import VueDraggable from 'vuedraggable';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import permissionsQuery from 'shared_queries/design_management/design_permissions.query.graphql';
import getDesignListQuery from 'shared_queries/design_management/get_design_list.query.graphql';
import DeleteButton from '~/design_management/components/delete_button.vue';
@@ -23,6 +23,7 @@ import * as utils from '~/design_management/utils/design_management_utils';
import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
+ UPLOAD_DESIGN_ERROR,
} from '~/design_management/utils/error_messages';
import {
DESIGN_TRACKING_PAGE_NAME,
@@ -101,20 +102,20 @@ describe('Design management index page', () => {
let moveDesignHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
- const findSelectAllButton = () => wrapper.find('[data-testid="select-all-designs-button"');
- const findToolbar = () => wrapper.find('.qa-selector-toolbar');
- const findDesignCollectionIsCopying = () =>
- wrapper.find('[data-testid="design-collection-is-copying"');
- const findDeleteButton = () => wrapper.find(DeleteButton);
- const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
+ const findSelectAllButton = () => wrapper.findByTestId('select-all-designs-button');
+ const findToolbar = () => wrapper.findByTestId('design-selector-toolbar');
+ const findDesignCollectionIsCopying = () => wrapper.findByTestId('design-collection-is-copying');
+ const findDeleteButton = () => wrapper.findComponent(DeleteButton);
+ const findDropzone = () => wrapper.findAllComponents(DesignDropzone).at(0);
const dropzoneClasses = () => findDropzone().classes();
- const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
- const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
- const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
+ const findDropzoneWrapper = () => wrapper.findByTestId('design-dropzone-wrapper');
+ const findFirstDropzoneWithDesign = () => wrapper.findAllComponents(DesignDropzone).at(1);
+ const findDesignsWrapper = () => wrapper.findByTestId('designs-root');
const findDesigns = () => wrapper.findAll(Design);
const draggableAttributes = () => wrapper.find(VueDraggable).vm.$attrs;
- const findDesignUploadButton = () => wrapper.find('[data-testid="design-upload-button"]');
- const findDesignToolbarWrapper = () => wrapper.find('[data-testid="design-toolbar-wrapper"]');
+ const findDesignUploadButton = () => wrapper.findByTestId('design-upload-button');
+ const findDesignToolbarWrapper = () => wrapper.findByTestId('design-toolbar-wrapper');
+ const findDesignUpdateAlert = () => wrapper.findByTestId('design-update-alert');
async function moveDesigns(localWrapper) {
await waitForPromises();
@@ -149,7 +150,7 @@ describe('Design management index page', () => {
mutate,
};
- wrapper = shallowMount(Index, {
+ wrapper = shallowMountExtended(Index, {
data() {
return {
allVersions,
@@ -185,7 +186,7 @@ describe('Design management index page', () => {
];
fakeApollo = createMockApollo(requestHandlers, {}, { addTypename: true });
- wrapper = shallowMount(Index, {
+ wrapper = shallowMountExtended(Index, {
apolloProvider: fakeApollo,
router,
stubs: { VueDraggable },
@@ -412,7 +413,8 @@ describe('Design management index page', () => {
await nextTick();
expect(wrapper.vm.filesToBeSaved).toEqual([]);
expect(wrapper.vm.isSaving).toBeFalsy();
- expect(createFlash).toHaveBeenCalled();
+ expect(findDesignUpdateAlert().exists()).toBe(true);
+ expect(findDesignUpdateAlert().text()).toBe(UPLOAD_DESIGN_ERROR);
});
it('does not call mutation if createDesign is false', () => {
@@ -431,19 +433,23 @@ describe('Design management index page', () => {
wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT).fill(mockDesigns[0]));
- expect(createFlash).not.toHaveBeenCalled();
+ expect(findDesignUpdateAlert().exists()).toBe(false);
});
- it('warns when too many files are uploaded', () => {
+ it('warns when too many files are uploaded', async () => {
createComponent();
wrapper.vm.onUploadDesign(new Array(MAXIMUM_FILE_UPLOAD_LIMIT + 1).fill(mockDesigns[0]));
+ await nextTick();
- expect(createFlash).toHaveBeenCalled();
+ expect(findDesignUpdateAlert().exists()).toBe(true);
+ expect(findDesignUpdateAlert().text()).toBe(
+ 'The maximum number of designs allowed to be uploaded is 10. Please try again.',
+ );
});
});
- it('flashes warning if designs are skipped', async () => {
+ it('displays warning if designs are skipped', async () => {
createComponent({
mockMutate: () =>
Promise.resolve({
@@ -458,11 +464,8 @@ describe('Design management index page', () => {
]);
await uploadDesign;
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Upload skipped. test.jpg did not change.',
- types: 'warning',
- });
+ expect(findDesignUpdateAlert().exists()).toBe(true);
+ expect(findDesignUpdateAlert().text()).toBe('Upload skipped. test.jpg did not change.');
});
describe('dragging onto an existing design', () => {
@@ -495,13 +498,17 @@ describe('Design management index page', () => {
description | eventPayload | message
${'> 1 file'} | ${[{ name: 'test' }, { name: 'test-2' }]} | ${EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE}
${'different filename'} | ${[{ name: 'wrong-name' }]} | ${EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE}
- `('calls createFlash when upload has $description', ({ eventPayload, message }) => {
- const designDropzone = findFirstDropzoneWithDesign();
- designDropzone.vm.$emit('change', eventPayload);
-
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith({ message });
- });
+ `(
+ 'displays GlAlert component when upload has $description',
+ async ({ eventPayload, message }) => {
+ expect(findDesignUpdateAlert().exists()).toBe(false);
+ const designDropzone = findFirstDropzoneWithDesign();
+ await designDropzone.vm.$emit('change', eventPayload);
+
+ expect(findDesignUpdateAlert().exists()).toBe(true);
+ expect(findDesignUpdateAlert().text()).toBe(message);
+ },
+ );
});
describe('tracking', () => {
@@ -804,7 +811,7 @@ describe('Design management index page', () => {
expect(createFlash).toHaveBeenCalledWith({ message: 'Houston, we have a problem' });
});
- it('displays flash if mutation had a non-recoverable error', async () => {
+ it('displays alert if mutation had a non-recoverable error', async () => {
createComponentWithApollo({
moveHandler: jest.fn().mockRejectedValue('Error'),
});
@@ -812,9 +819,10 @@ describe('Design management index page', () => {
await moveDesigns(wrapper);
await waitForPromises();
- expect(createFlash).toHaveBeenCalledWith({
- message: 'Something went wrong when reordering designs. Please try again',
- });
+ expect(findDesignUpdateAlert().exists()).toBe(true);
+ expect(findDesignUpdateAlert().text()).toBe(
+ 'Something went wrong when reordering designs. Please try again',
+ );
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 13985ce7d74..045a454e63a 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -35,6 +35,7 @@ import {
user2,
user3,
user4,
+ user5,
GlEmoji,
} from '../mock_data/member_modal';
@@ -93,6 +94,11 @@ describe('InviteMembersModal', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findBase = () => wrapper.findComponent(InviteModalBase);
const findIntroText = () => wrapper.findByTestId('modal-base-intro-text').text();
+ const findMemberErrorAlert = () => wrapper.findByTestId('alert-member-error');
+ const findMemberErrorMessage = (element) =>
+ `${Object.keys(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]}: ${
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[element]
+ }`;
const emitEventFromModal = (eventName) => () =>
findModal().vm.$emit(eventName, { preventDefault: jest.fn() });
const clickInviteButton = emitEventFromModal('primary');
@@ -123,6 +129,10 @@ describe('InviteMembersModal', () => {
findBase().vm.$emit('access-level', val);
await nextTick();
};
+ const removeMembersToken = async (val) => {
+ findMembersSelect().vm.$emit('token-remove', val);
+ await nextTick();
+ };
describe('rendering the tasks to be done', () => {
const setupComponent = async (props = {}, urlParameter = ['invite_members_for_task']) => {
@@ -431,17 +441,20 @@ describe('InviteMembersModal', () => {
});
it('clears the error when the list of members to invite is cleared', async () => {
- expect(membersFormGroupInvalidFeedback()).toBe(
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(
Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
findMembersSelect().vm.$emit('clear');
await nextTick();
+ expect(findMemberErrorAlert().exists()).toBe(false);
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('validationState')).not.toBe(false);
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
it('clears the error when the cancel button is clicked', async () => {
@@ -450,7 +463,7 @@ describe('InviteMembersModal', () => {
await nextTick();
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('validationState')).not.toBe(false);
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
it('clears the error when the modal is hidden', async () => {
@@ -458,33 +471,12 @@ describe('InviteMembersModal', () => {
await nextTick();
+ expect(findMemberErrorAlert().exists()).toBe(false);
expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('validationState')).not.toBe(false);
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
});
- it('clears the invalid state and message once the list of members to invite is cleared', async () => {
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_TAKEN);
-
- clickInviteButton();
-
- await waitForPromises();
-
- expect(membersFormGroupInvalidFeedback()).toBe(
- Object.values(invitationsApiResponse.EMAIL_TAKEN.message)[0],
- );
- expect(findMembersSelect().props('validationState')).toBe(false);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
-
- findMembersSelect().vm.$emit('clear');
-
- await waitForPromises();
-
- expect(membersFormGroupInvalidFeedback()).toBe('');
- expect(findMembersSelect().props('validationState')).toBe(null);
- expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
- });
-
it('displays the generic error for http server error', async () => {
mockInvitationsApi(
httpStatus.INTERNAL_SERVER_ERROR,
@@ -496,6 +488,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
});
it('displays the restricted user api message for response with bad request', async () => {
@@ -505,20 +498,31 @@ describe('InviteMembersModal', () => {
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toBe(expectedEmailRestrictedError);
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
- it('displays the first part of the error when multiple existing users are restricted by email', async () => {
+ it('displays all errors when there are multiple existing users that are restricted by email', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toBe(
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ );
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
});
});
@@ -573,10 +577,30 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
+ it('clears the error when the modal is hidden', async () => {
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
+
+ findModal().vm.$emit('hidden');
+
+ await nextTick();
+
+ expect(findMemberErrorAlert().exists()).toBe(false);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
+ });
+
it('displays the restricted email error when restricted email is invited', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
@@ -584,20 +608,32 @@ describe('InviteMembersModal', () => {
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
expect(findModal().props('actionPrimary').attributes.loading).toBe(false);
});
- it('displays the first error message when multiple emails return a restricted error message', async () => {
+ it('displays all errors when there are multiple emails that return a restricted error message', async () => {
mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
clickInviteButton();
await waitForPromises();
- expect(membersFormGroupInvalidFeedback()).toContain(expectedEmailRestrictedError);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ );
+ expect(membersFormGroupInvalidFeedback()).toBe('');
+ expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
it('displays the invalid syntax error for bad request', async () => {
@@ -608,7 +644,7 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
});
});
@@ -617,14 +653,51 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user3, user4]);
- mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.ERROR_EMAIL_INVALID);
+ mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
clickInviteButton();
await waitForPromises();
expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- expect(findMembersSelect().props('validationState')).toBe(false);
+ expect(findMembersSelect().props('exceptionState')).toBe(false);
+ });
+
+ it('displays errors for multiple and allows clearing', async () => {
+ createInviteMembersToGroupWrapper();
+
+ await triggerMembersTokenSelect([user3, user4, user5]);
+ mockInvitationsApi(httpStatus.CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+
+ clickInviteButton();
+
+ await waitForPromises();
+
+ expect(findMemberErrorAlert().exists()).toBe(true);
+ expect(findMemberErrorAlert().props('title')).toContain(
+ "The following 3 members couldn't be invited",
+ );
+ expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0));
+ expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1));
+ expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2));
+
+ await removeMembersToken(user3);
+
+ expect(findMemberErrorAlert().props('title')).toContain(
+ "The following 2 members couldn't be invited",
+ );
+ expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0));
+
+ await removeMembersToken(user4);
+
+ expect(findMemberErrorAlert().props('title')).toContain(
+ "The following member couldn't be invited",
+ );
+ expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(1));
+
+ await removeMembersToken(user5);
+
+ expect(findMemberErrorAlert().exists()).toBe(false);
});
});
});
@@ -675,24 +748,6 @@ describe('InviteMembersModal', () => {
});
});
});
-
- describe('when any invite failed for any reason', () => {
- beforeEach(async () => {
- createInviteMembersToGroupWrapper();
-
- await triggerMembersTokenSelect([user1, user3]);
-
- mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
-
- clickInviteButton();
- });
-
- it('displays the first error message', async () => {
- await waitForPromises();
-
- expect(membersFormGroupInvalidFeedback()).toBe(expectedSyntaxError);
- });
- });
});
describe('tracking', () => {
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index cc19e90a5fa..b55eeb72471 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -254,7 +254,7 @@ describe('InviteModalBase', () => {
expect(wrapper.findComponent(GlModal).props('actionPrimary').attributes.loading).toBe(true);
});
- it('with invalidFeedbackMessage, set members form group validation state', () => {
+ it('with invalidFeedbackMessage, set members form group exception state', () => {
createComponent({
invalidFeedbackMessage: 'invalid message!',
});
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index bf5564e4d63..6375d0f7e2e 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -16,6 +16,7 @@ const createComponent = (props) => {
return shallowMount(MembersTokenSelect, {
propsData: {
ariaLabelledby: label,
+ invalidMembers: {},
placeholder,
...props,
},
@@ -124,12 +125,14 @@ describe('MembersTokenSelect', () => {
findTokenSelector().vm.$emit('token-remove', [user1]);
expect(wrapper.emitted('clear')).toEqual([[]]);
+ expect(wrapper.emitted('token-remove')).toBeUndefined();
});
- it('does not emit `clear` event when there are still tokens selected', () => {
+ it('emits `token-remove` event with the token when there are still tokens selected', () => {
findTokenSelector().vm.$emit('input', [user1, user2]);
findTokenSelector().vm.$emit('token-remove', [user1]);
+ expect(wrapper.emitted('token-remove')).toEqual([[[user1]]]);
expect(wrapper.emitted('clear')).toBeUndefined();
});
});
diff --git a/spec/frontend/invite_members/mock_data/member_modal.js b/spec/frontend/invite_members/mock_data/member_modal.js
index 474234cfacb..7d675b6206c 100644
--- a/spec/frontend/invite_members/mock_data/member_modal.js
+++ b/spec/frontend/invite_members/mock_data/member_modal.js
@@ -26,13 +26,17 @@ export const user2 = { id: 2, name: 'Name Two', username: 'one_2', avatar_url: '
export const user3 = {
id: 'user-defined-token',
name: 'email@example.com',
- username: 'one_2',
avatar_url: '',
};
export const user4 = {
- id: 'user-defined-token',
+ id: 'user-defined-token2',
name: 'email4@example.com',
- username: 'one_4',
+ avatar_url: '',
+};
+export const user5 = {
+ id: '3',
+ username: 'root',
+ name: 'root',
avatar_url: '',
};
diff --git a/spec/frontend/invite_members/utils/member_utils_spec.js b/spec/frontend/invite_members/utils/member_utils_spec.js
new file mode 100644
index 00000000000..eb76c9845d4
--- /dev/null
+++ b/spec/frontend/invite_members/utils/member_utils_spec.js
@@ -0,0 +1,12 @@
+import { memberName } from '~/invite_members/utils/member_utils';
+
+describe('Member Name', () => {
+ it.each([
+ [{ username: '_username_', name: '_name_' }, '_username_'],
+ [{ username: '_username_' }, '_username_'],
+ [{ name: '_name_' }, '_name_'],
+ [{}, undefined],
+ ])(`returns name from supplied member token: %j`, (member, result) => {
+ expect(memberName(member)).toBe(result);
+ });
+});
diff --git a/spec/frontend/invite_members/utils/response_message_parser_spec.js b/spec/frontend/invite_members/utils/response_message_parser_spec.js
index 8b2064df374..92f38c54c99 100644
--- a/spec/frontend/invite_members/utils/response_message_parser_spec.js
+++ b/spec/frontend/invite_members/utils/response_message_parser_spec.js
@@ -1,5 +1,5 @@
import {
- responseMessageFromSuccess,
+ responseFromSuccess,
responseMessageFromError,
} from '~/invite_members/utils/response_message_parser';
import { invitationsApiResponse } from '../mock_data/api_responses';
@@ -11,12 +11,12 @@ describe('Response message parser', () => {
const exampleKeyedMsg = { 'email@example.com': expectedMessage };
it.each([
- [{ data: { message: expectedMessage } }],
- [{ data: { error: expectedMessage } }],
- [{ data: { message: [expectedMessage] } }],
- [{ data: { message: exampleKeyedMsg } }],
- ])(`returns "${expectedMessage}" from success response: %j`, (successResponse) => {
- expect(responseMessageFromSuccess(successResponse)).toBe(expectedMessage);
+ [{ data: { message: expectedMessage } }, { error: true, message: expectedMessage }],
+ [{ data: { error: expectedMessage } }, { error: true, message: expectedMessage }],
+ [{ data: { message: [expectedMessage] } }, { error: true, message: expectedMessage }],
+ [{ data: { message: exampleKeyedMsg } }, { error: true, message: { ...exampleKeyedMsg } }],
+ ])(`returns "${expectedMessage}" from success response: %j`, (successResponse, result) => {
+ expect(responseFromSuccess(successResponse)).toStrictEqual(result);
});
});
@@ -30,15 +30,18 @@ describe('Response message parser', () => {
});
});
- describe('displaying only the first error when a response has messages for multiple users', () => {
- const expected =
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
-
+ describe('displaying all errors when a response has messages for multiple users', () => {
it.each([
- [{ data: invitationsApiResponse.MULTIPLE_RESTRICTED }],
- [{ data: invitationsApiResponse.EMAIL_RESTRICTED }],
- ])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse) => {
- expect(responseMessageFromSuccess(restrictedResponse)).toBe(expected);
+ [
+ { data: invitationsApiResponse.MULTIPLE_RESTRICTED },
+ { error: true, message: { ...invitationsApiResponse.MULTIPLE_RESTRICTED.message } },
+ ],
+ [
+ { data: invitationsApiResponse.EMAIL_RESTRICTED },
+ { error: true, message: { ...invitationsApiResponse.EMAIL_RESTRICTED.message } },
+ ],
+ ])(`returns "${expectedMessage}" from success response: %j`, (restrictedResponse, result) => {
+ expect(responseFromSuccess(restrictedResponse)).toStrictEqual(result);
});
});
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index b9ba0833c4f..6692a3b9347 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -1,44 +1,59 @@
-import Vue from 'vue';
+import { GlSprintf, GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
-import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
-import { redirectTo } from '~/lib/utils/url_utility';
-import deleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
+import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
import eventHub from '~/milestones/event_hub';
+import { redirectTo } from '~/lib/utils/url_utility';
+import { createAlert } from '~/flash';
-jest.mock('~/lib/utils/url_utility', () => ({
- ...jest.requireActual('~/lib/utils/url_utility'),
- redirectTo: jest.fn(),
-}));
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/flash');
-describe('delete_milestone_modal.vue', () => {
- const Component = Vue.extend(deleteMilestoneModal);
- const props = {
+describe('Delete milestone modal', () => {
+ let wrapper;
+ const mockProps = {
issueCount: 1,
mergeRequestCount: 2,
milestoneId: 3,
milestoneTitle: 'my milestone title',
milestoneUrl: `${TEST_HOST}/delete_milestone_modal.vue/milestone`,
};
- let vm;
+
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(DeleteMilestoneModal, {
+ propsData: {
+ ...mockProps,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('onSubmit', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('deletes milestone and redirects to overview page', async () => {
const responseURL = `${TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
jest.spyOn(axios, 'delete').mockImplementation((url) => {
- expect(url).toBe(props.milestoneUrl);
+ expect(url).toBe(mockProps.milestoneUrl);
expect(eventHub.$emit).toHaveBeenCalledWith(
'deleteMilestoneModal.requestStarted',
- props.milestoneUrl,
+ mockProps.milestoneUrl,
);
eventHub.$emit.mockReset();
return Promise.resolve({
@@ -47,55 +62,71 @@ describe('delete_milestone_modal.vue', () => {
},
});
});
-
- await vm.onSubmit();
+ await findModal().vm.$emit('primary');
expect(redirectTo).toHaveBeenCalledWith(responseURL);
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
+ milestoneUrl: mockProps.milestoneUrl,
successful: true,
});
});
- it('displays error if deleting milestone failed', async () => {
- const dummyError = new Error('deleting milestone failed');
- dummyError.response = { status: 418 };
- jest.spyOn(axios, 'delete').mockImplementation((url) => {
- expect(url).toBe(props.milestoneUrl);
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'deleteMilestoneModal.requestStarted',
- props.milestoneUrl,
- );
- eventHub.$emit.mockReset();
- return Promise.reject(dummyError);
- });
+ it.each`
+ statusCode | alertMessage
+ ${418} | ${`Failed to delete milestone ${mockProps.milestoneTitle}`}
+ ${404} | ${`Milestone ${mockProps.milestoneTitle} was not found`}
+ `(
+ 'displays error if deleting milestone failed with code $statusCode',
+ async ({ statusCode, alertMessage }) => {
+ const dummyError = new Error('deleting milestone failed');
+ dummyError.response = { status: statusCode };
+ jest.spyOn(axios, 'delete').mockImplementation((url) => {
+ expect(url).toBe(mockProps.milestoneUrl);
+ expect(eventHub.$emit).toHaveBeenCalledWith(
+ 'deleteMilestoneModal.requestStarted',
+ mockProps.milestoneUrl,
+ );
+ eventHub.$emit.mockReset();
+ return Promise.reject(dummyError);
+ });
- await expect(vm.onSubmit()).rejects.toEqual(dummyError);
- expect(redirectTo).not.toHaveBeenCalled();
- expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
- milestoneUrl: props.milestoneUrl,
- successful: false,
- });
- });
+ await expect(wrapper.vm.onSubmit()).rejects.toEqual(dummyError);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: alertMessage,
+ });
+ expect(redirectTo).not.toHaveBeenCalled();
+ expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
+ milestoneUrl: mockProps.milestoneUrl,
+ successful: false,
+ });
+ },
+ );
});
- describe('text', () => {
- it('contains the issue and milestone count', () => {
- vm = mountComponent(Component, props);
- const value = vm.text;
+ describe('Modal title and description', () => {
+ const emptyDescription = `You’re about to permanently delete the milestone ${mockProps.milestoneTitle}. This milestone is not currently used in any issues or merge requests.`;
+ const description = `You’re about to permanently delete the milestone ${mockProps.milestoneTitle} and remove it from 1 issue and 2 merge requests. Once deleted, it cannot be undone or recovered.`;
+ const title = `Delete milestone ${mockProps.milestoneTitle}?`;
- expect(value).toContain('remove it from 1 issue and 2 merge requests');
+ it('renders proper title', () => {
+ const value = findModal().props('title');
+ expect(value).toBe(title);
});
- it('contains neither issue nor milestone count', () => {
- vm = mountComponent(Component, {
- ...props,
- issueCount: 0,
- mergeRequestCount: 0,
- });
-
- const value = vm.text;
+ it.each`
+ statement | descriptionText | issueCount | mergeRequestCount
+ ${'1 issue and 2 merge requests'} | ${description} | ${1} | ${2}
+ ${'no issues and merge requests'} | ${emptyDescription} | ${0} | ${0}
+ `(
+ 'renders proper description when the milestone contains $statement',
+ ({ issueCount, mergeRequestCount, descriptionText }) => {
+ createComponent({
+ issueCount,
+ mergeRequestCount,
+ });
- expect(value).toContain('is not currently used');
- });
+ const value = findModal().text();
+ expect(value).toBe(descriptionText);
+ },
+ );
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index d55ba318e46..70b1261bdb7 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -66,6 +66,7 @@ describe('WorkItemDetailModal component', () => {
createComponent();
expect(findWorkItemDetail().props()).toEqual({
+ isModal: true,
workItemId: '1',
workItemParentId: '2',
});
@@ -98,6 +99,15 @@ describe('WorkItemDetailModal component', () => {
expect(wrapper.emitted('close')).toBeTruthy();
});
+ it('hides the modal when WorkItemDetail emits `close` event', () => {
+ createComponent();
+ const closeSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+
+ findWorkItemDetail().vm.$emit('close');
+
+ expect(closeSpy).toHaveBeenCalled();
+ });
+
describe('delete work item', () => {
it('emits workItemDeleted and closes modal', async () => {
createComponent();
diff --git a/spec/frontend/work_items/pages/work_item_detail_spec.js b/spec/frontend/work_items/pages/work_item_detail_spec.js
index 95a894f0fac..6fb7bb5226e 100644
--- a/spec/frontend/work_items/pages/work_item_detail_spec.js
+++ b/spec/frontend/work_items/pages/work_item_detail_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -26,6 +26,7 @@ describe('WorkItemDetail component', () => {
const initialSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCloseButton = () => wrapper.findComponent(GlButton);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
@@ -34,6 +35,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemWeight = () => wrapper.findComponent(WorkItemWeight);
const createComponent = ({
+ isModal = false,
workItemId = workItemQueryResponse.data.workItem.id,
handler = successHandler,
subscriptionHandler = initialSubscriptionHandler,
@@ -51,7 +53,7 @@ describe('WorkItemDetail component', () => {
typePolicies: includeWidgets ? temporaryConfig.cacheConfig.typePolicies : {},
},
),
- propsData: { workItemId },
+ propsData: { isModal, workItemId },
provide: {
glFeatures: {
workItemsMvc2: workItemsMvc2Enabled,
@@ -99,6 +101,36 @@ describe('WorkItemDetail component', () => {
});
});
+ describe('close button', () => {
+ describe('when isModal prop is false', () => {
+ it('does not render', async () => {
+ createComponent({ isModal: false });
+ await waitForPromises();
+
+ expect(findCloseButton().exists()).toBe(false);
+ });
+ });
+
+ describe('when isModal prop is true', () => {
+ it('renders', async () => {
+ createComponent({ isModal: true });
+ await waitForPromises();
+
+ expect(findCloseButton().props('icon')).toBe('close');
+ expect(findCloseButton().attributes('aria-label')).toBe('Close');
+ });
+
+ it('emits `close` event when clicked', async () => {
+ createComponent({ isModal: true });
+ await waitForPromises();
+
+ findCloseButton().vm.$emit('click');
+
+ expect(wrapper.emitted('close')).toEqual([[]]);
+ });
+ });
+ });
+
describe('description', () => {
it('does not show description widget if loading description fails', () => {
createComponent();
diff --git a/spec/frontend/work_items/pages/work_item_root_spec.js b/spec/frontend/work_items/pages/work_item_root_spec.js
index 3c5da94114e..d9372f2bcf0 100644
--- a/spec/frontend/work_items/pages/work_item_root_spec.js
+++ b/spec/frontend/work_items/pages/work_item_root_spec.js
@@ -52,6 +52,7 @@ describe('Work items root component', () => {
createComponent();
expect(findWorkItemDetail().props()).toEqual({
+ isModal: false,
workItemId: 'gid://gitlab/WorkItem/1',
workItemParentId: null,
});
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 9c399e78d80..919335bc9fa 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -3,6 +3,34 @@
require 'spec_helper'
RSpec.describe Gitlab::Gpg::Commit do
+ let_it_be(:project) { create(:project, :repository, path: 'sample-project') }
+
+ let(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
+ let(:committer_email) { GpgHelpers::User1.emails.first }
+ let(:user_email) { committer_email }
+ let(:public_key) { GpgHelpers::User1.public_key }
+ let(:user) { create(:user, email: user_email) }
+ let(:commit) { create(:commit, project: project, sha: commit_sha, committer_email: committer_email) }
+ let(:crypto) { instance_double(GPGME::Crypto) }
+ let(:mock_signature_data?) { true }
+ # gpg_keys must be pre-loaded so that they can be found during signature verification.
+ let!(:gpg_key) { create(:gpg_key, key: public_key, user: user) }
+
+ let(:signature_data) do
+ [
+ GpgHelpers::User1.signed_commit_signature,
+ GpgHelpers::User1.signed_commit_base_data
+ ]
+ end
+
+ before do
+ if mock_signature_data?
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
+ .with(Gitlab::Git::Repository, commit_sha)
+ .and_return(signature_data)
+ end
+ end
+
describe '#signature' do
shared_examples 'returns the cached signature on second call' do
it 'returns the cached signature on second call' do
@@ -17,11 +45,8 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
- let!(:project) { create :project, :repository, path: 'sample-project' }
- let!(:commit_sha) { '0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33' }
-
context 'unsigned commit' do
- let!(:commit) { create :commit, project: project, sha: commit_sha }
+ let(:signature_data) { nil }
it 'returns nil' do
expect(described_class.new(commit).signature).to be_nil
@@ -29,20 +54,12 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'invalid signature' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- # Corrupt the key
- GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
+ let(:signature_data) do
+ [
+ # Corrupt the key
+ GpgHelpers::User1.signed_commit_signature.tr('=', 'a'),
+ GpgHelpers::User1.signed_commit_base_data
+ ]
end
it 'returns nil' do
@@ -53,25 +70,6 @@ RSpec.describe Gitlab::Gpg::Commit do
context 'known key' do
context 'user matches the key uid' do
context 'user email matches the email committer' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
- end
-
it 'returns a valid signature' do
signature = described_class.new(commit).signature
@@ -112,32 +110,13 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'valid key signed using recent version of Gnupg' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- let!(:crypto) { instance_double(GPGME::Crypto) }
-
before do
- fake_signature = [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
-
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(fake_signature)
- end
-
- it 'returns a valid signature' do
verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
allow(crypto).to receive(:verify).and_yield(verified_signature)
+ end
+ it 'returns a valid signature' do
signature = described_class.new(commit).signature
expect(signature).to have_attributes(
@@ -153,33 +132,14 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'valid key signed using older version of Gnupg' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- let!(:crypto) { instance_double(GPGME::Crypto) }
-
before do
- fake_signature = [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
-
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(fake_signature)
- end
-
- it 'returns a valid signature' do
keyid = GpgHelpers::User1.fingerprint.last(16)
verified_signature = double('verified-signature', fingerprint: keyid, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
allow(crypto).to receive(:verify).and_yield(verified_signature)
+ end
+ it 'returns a valid signature' do
signature = described_class.new(commit).signature
expect(signature).to have_attributes(
@@ -195,32 +155,13 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'commit with multiple signatures' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User1.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- let!(:crypto) { instance_double(GPGME::Crypto) }
-
before do
- fake_signature = [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
-
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(fake_signature)
- end
-
- it 'returns an invalid signatures error' do
verified_signature = double('verified-signature', fingerprint: GpgHelpers::User1.fingerprint, valid?: true)
allow(GPGME::Crypto).to receive(:new).and_return(crypto)
allow(crypto).to receive(:verify).and_yield(verified_signature).and_yield(verified_signature)
+ end
+ it 'returns an invalid signatures error' do
signature = described_class.new(commit).signature
expect(signature).to have_attributes(
@@ -236,27 +177,18 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'commit signed with a subkey' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User3.emails.first }
-
- let!(:user) { create(:user, email: GpgHelpers::User3.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User3.public_key, user: user
- end
+ let(:committer_email) { GpgHelpers::User3.emails.first }
+ let(:public_key) { GpgHelpers::User3.public_key }
let(:gpg_key_subkey) do
gpg_key.subkeys.find_by(fingerprint: GpgHelpers::User3.subkey_fingerprints.last)
end
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User3.signed_commit_signature,
- GpgHelpers::User3.signed_commit_base_data
- ]
- )
+ let(:signature_data) do
+ [
+ GpgHelpers::User3.signed_commit_signature,
+ GpgHelpers::User3.signed_commit_base_data
+ ]
end
it 'returns a valid signature' do
@@ -275,7 +207,7 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'user email does not match the committer email, but is the same user' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
+ let(:committer_email) { GpgHelpers::User2.emails.first }
let(:user) do
create(:user, email: GpgHelpers::User1.emails.first).tap do |user|
@@ -283,21 +215,6 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
- end
-
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
commit_sha: commit_sha,
@@ -314,24 +231,8 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'user email does not match the committer email' do
- let!(:commit) { create :commit, project: project, sha: commit_sha, committer_email: GpgHelpers::User2.emails.first }
-
- let(:user) { create(:user, email: GpgHelpers::User1.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
- end
+ let(:committer_email) { GpgHelpers::User2.emails.first }
+ let(:user_email) { GpgHelpers::User1.emails.first }
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
@@ -350,24 +251,8 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'user does not match the key uid' do
- let!(:commit) { create :commit, project: project, sha: commit_sha }
-
- let(:user) { create(:user, email: GpgHelpers::User2.emails.first) }
-
- let!(:gpg_key) do
- create :gpg_key, key: GpgHelpers::User1.public_key, user: user
- end
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
- end
+ let(:user_email) { GpgHelpers::User2.emails.first }
+ let(:public_key) { GpgHelpers::User1.public_key }
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
@@ -386,18 +271,7 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'unknown key' do
- let!(:commit) { create :commit, project: project, sha: commit_sha }
-
- before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
- .with(Gitlab::Git::Repository, commit_sha)
- .and_return(
- [
- GpgHelpers::User1.signed_commit_signature,
- GpgHelpers::User1.signed_commit_base_data
- ]
- )
- end
+ let(:gpg_key) { nil }
it 'returns an invalid signature' do
expect(described_class.new(commit).signature).to have_attributes(
@@ -415,15 +289,15 @@ RSpec.describe Gitlab::Gpg::Commit do
end
context 'multiple commits with signatures' do
- let(:first_signature) { create(:gpg_signature) }
-
- let(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
- let(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) }
+ let(:mock_signature_data?) { false }
+ let!(:first_signature) { create(:gpg_signature) }
+ let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User2.public_key) }
+ let!(:second_signature) { create(:gpg_signature, gpg_key: gpg_key) }
let!(:first_commit) { create(:commit, project: project, sha: first_signature.commit_sha) }
let!(:second_commit) { create(:commit, project: project, sha: second_signature.commit_sha) }
- let(:commits) do
+ let!(:commits) do
[first_commit, second_commit].map do |commit|
gpg_commit = described_class.new(commit)
@@ -442,4 +316,21 @@ RSpec.describe Gitlab::Gpg::Commit do
end
end
end
+
+ describe '#update_signature!' do
+ let!(:gpg_key) { nil }
+
+ let(:signature) { described_class.new(commit).signature }
+
+ it 'updates signature record' do
+ signature
+
+ create(:gpg_key, key: public_key, user: user)
+
+ stored_signature = CommitSignatures::GpgSignature.find_by_commit_sha(commit_sha)
+ expect { described_class.new(commit).update_signature!(stored_signature) }.to(
+ change { signature.reload.verification_status }.from('unknown_key').to('verified')
+ )
+ end
+ end
end
diff --git a/spec/lib/gitlab/x509/commit_spec.rb b/spec/lib/gitlab/x509/commit_spec.rb
index a81955b995e..c7d56e49fab 100644
--- a/spec/lib/gitlab/x509/commit_spec.rb
+++ b/spec/lib/gitlab/x509/commit_spec.rb
@@ -2,14 +2,21 @@
require 'spec_helper'
RSpec.describe Gitlab::X509::Commit do
- describe '#signature' do
- let(:signature) { described_class.new(commit).signature }
+ let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
+ let(:user) { create(:user, email: X509Helpers::User1.certificate_email) }
+ let(:project) { create(:project, :repository, path: X509Helpers::User1.path, creator: user) }
+ let(:commit) { project.commit_by(oid: commit_sha ) }
+ let(:signature) { Gitlab::X509::Commit.new(commit).signature }
+ let(:store) { OpenSSL::X509::Store.new }
+ let(:certificate) { OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert) }
- context 'returns the cached signature' do
- let(:commit_sha) { '189a6c924013fc3fe40d6f1ec1dc20214183bc97' }
- let(:project) { create(:project, :public, :repository) }
- let(:commit) { create(:commit, project: project, sha: commit_sha) }
+ before do
+ store.add_cert(certificate) if certificate
+ allow(OpenSSL::X509::Store).to receive(:new).and_return(store)
+ end
+ describe '#signature' do
+ context 'returns the cached signature' do
it 'on second call' do
allow_any_instance_of(described_class).to receive(:new).and_call_original
expect_any_instance_of(described_class).to receive(:create_cached_signature!).and_call_original
@@ -23,13 +30,29 @@ RSpec.describe Gitlab::X509::Commit do
end
context 'unsigned commit' do
- let!(:project) { create :project, :repository, path: X509Helpers::User1.path }
- let!(:commit_sha) { X509Helpers::User1.commit }
- let!(:commit) { create :commit, project: project, sha: commit_sha }
+ let(:project) { create :project, :repository, path: X509Helpers::User1.path }
+ let(:commit_sha) { X509Helpers::User1.commit }
+ let(:commit) { create :commit, project: project, sha: commit_sha }
it 'returns nil' do
expect(signature).to be_nil
end
end
end
+
+ describe '#update_signature!' do
+ let(:certificate) { nil }
+
+ it 'updates verification status' do
+ signature
+
+ cert = OpenSSL::X509::Certificate.new(X509Helpers::User1.trust_cert)
+ store.add_cert(cert)
+
+ stored_signature = CommitSignatures::X509CommitSignature.find_by_commit_sha(commit_sha)
+ expect { described_class.new(commit).update_signature!(stored_signature) }.to(
+ change { signature.reload.verification_status }.from('unverified').to('verified')
+ )
+ end
+ end
end
diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb
index 7ed64615020..b56ac5b32c6 100644
--- a/spec/support/helpers/features/invite_members_modal_helper.rb
+++ b/spec/support/helpers/features/invite_members_modal_helper.rb
@@ -9,18 +9,28 @@ module Spec
click_on 'Invite members'
page.within invite_modal_selector do
- Array.wrap(names).each do |name|
- find(member_dropdown_selector).set(name)
+ select_members(names)
+ choose_options(role, expires_at)
+ click_button 'Invite'
+ end
- wait_for_requests
- click_button name
- end
+ page.refresh if refresh
+ end
- choose_options(role, expires_at)
+ def input_invites(names)
+ click_on 'Invite members'
- click_button 'Invite'
+ page.within invite_modal_selector do
+ select_members(names)
+ end
+ end
+
+ def select_members(names)
+ Array.wrap(names).each do |name|
+ find(member_dropdown_selector).set(name)
- page.refresh if refresh
+ wait_for_requests
+ click_button name
end
end
@@ -64,6 +74,24 @@ module Spec
'[data-testid="invite-modal"]'
end
+ def member_token_error_selector(id)
+ "[data-testid='error-icon-#{id}']"
+ end
+
+ def member_token_avatar_selector
+ "[data-testid='token-avatar']"
+ end
+
+ def member_token_selector(id)
+ "[data-token-id='#{id}']"
+ end
+
+ def remove_token(id)
+ page.within member_token_selector(id) do
+ find('[data-testid="close-icon"]').click
+ end
+ end
+
def expect_to_have_group(group)
expect(page).to have_selector("[entity-id='#{group.id}']")
end
diff --git a/spec/support/helpers/features/source_editor_spec_helpers.rb b/spec/support/helpers/features/source_editor_spec_helpers.rb
index 57057b47fbb..cdc59f9cbe1 100644
--- a/spec/support/helpers/features/source_editor_spec_helpers.rb
+++ b/spec/support/helpers/features/source_editor_spec_helpers.rb
@@ -15,13 +15,6 @@ module Spec
execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')")
end
-
- def editor_get_value
- editor = find('.monaco-editor')
- uri = editor['data-uri']
-
- evaluate_script("monaco.editor.getModel('#{uri}').getValue()")
- end
end
end
end
diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb
index d5d9fed0f79..70dedc3ac50 100644
--- a/spec/support/helpers/features/web_ide_spec_helpers.rb
+++ b/spec/support/helpers/features/web_ide_spec_helpers.rb
@@ -100,10 +100,6 @@ module WebIdeSpecHelpers
editor_set_value(value)
end
- def ide_editor_value
- editor_get_value
- end
-
def ide_commit_tab_selector
ide_tab_selector('commit')
end
diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
index 5d643658ab4..bca0e02fcdd 100644
--- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb
+++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb
@@ -23,6 +23,22 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
)
end
+ it 'displays the user\'s avatar in the member input token', :js do
+ visit members_page_path
+
+ input_invites(user2.name)
+
+ expect(page).to have_selector(member_token_avatar_selector)
+ end
+
+ it 'does not display an avatar in the member input token for an email address', :js do
+ visit members_page_path
+
+ input_invites('test@example.com')
+
+ expect(page).not_to have_selector(member_token_avatar_selector)
+ end
+
it 'invites user by email', :js, :snowplow, :aggregate_failures do
visit members_page_path
@@ -79,11 +95,13 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
context 'when member is already a member by email' do
it 'updates the member for that email', :js do
+ email = 'test@example.com'
+
visit members_page_path
- invite_member('test@example.com', role: 'Developer')
+ invite_member(email, role: 'Developer')
- invite_member('test@example.com', role: 'Reporter', refresh: false)
+ invite_member(email, role: 'Reporter', refresh: false)
expect(page).not_to have_selector(invite_modal_selector)
@@ -91,7 +109,7 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
click_link 'Invited'
- page.within find_invited_member_row('test@example.com') do
+ page.within find_invited_member_row(email) do
expect(page).to have_button('Reporter')
end
end
@@ -130,8 +148,8 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
invite_member(user2.name, role: role, refresh: false)
expect(page).to have_selector(invite_modal_selector)
- expect(page).to have_content "Access level should be greater than or equal to Developer inherited membership " \
- "from group #{group.name}"
+ expect(page).to have_content "#{user2.name}: Access level should be greater than or equal to Developer " \
+ "inherited membership from group #{group.name}"
page.refresh
@@ -148,13 +166,31 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
group.add_maintainer(user3)
end
- it 'only shows the first user error', :js do
+ it 'shows the user errors and then removes them from the form', :js do
visit subentity_members_page_path
invite_member([user2.name, user3.name], role: role, refresh: false)
expect(page).to have_selector(invite_modal_selector)
- expect(page).to have_text("Access level should be greater than or equal to", count: 1)
+ expect(page).to have_selector(member_token_error_selector(user2.id))
+ expect(page).to have_selector(member_token_error_selector(user3.id))
+ expect(page).to have_text("The following 2 members couldn't be invited")
+ expect(page).to have_text("#{user2.name}: Access level should be greater than or equal to")
+ expect(page).to have_text("#{user3.name}: Access level should be greater than or equal to")
+
+ remove_token(user2.id)
+
+ expect(page).not_to have_selector(member_token_error_selector(user2.id))
+ expect(page).to have_selector(member_token_error_selector(user3.id))
+ expect(page).to have_text("The following member couldn't be invited")
+ expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to")
+
+ remove_token(user3.id)
+
+ expect(page).not_to have_selector(member_token_error_selector(user3.id))
+ expect(page).not_to have_text("The following member couldn't be invited")
+ expect(page).not_to have_text("Review the invite errors and try again")
+ expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to")
page.refresh
@@ -168,6 +204,19 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label|
expect(page).not_to have_button('Maintainer')
end
end
+
+ it 'only shows the error for an invalid formatted email and does not display other member errors', :js do
+ visit subentity_members_page_path
+
+ invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false)
+
+ expect(page).to have_selector(invite_modal_selector)
+ expect(page).to have_text('email contains an invalid email address')
+ expect(page).not_to have_text("The following 2 members couldn't be invited")
+ expect(page).not_to have_text("Review the invite errors and try again")
+ expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to")
+ expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to")
+ end
end
end
end