diff options
Diffstat (limited to 'spec')
| -rw-r--r-- | spec/features/boards/sidebar_spec.rb | 36 | ||||
| -rw-r--r-- | spec/features/profiles/user_edit_profile_spec.rb | 32 | ||||
| -rw-r--r-- | spec/frontend/boards/components/board_assignee_dropdown_spec.js | 377 | ||||
| -rw-r--r-- | spec/frontend/boards/stores/actions_spec.js | 42 | ||||
| -rw-r--r-- | spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js | 120 | ||||
| -rw-r--r-- | spec/services/merge_requests/post_merge_service_spec.rb | 142 | ||||
| -rw-r--r-- | spec/services/merge_requests/refresh_service_spec.rb | 41 | ||||
| -rw-r--r-- | spec/services/merge_requests/update_service_spec.rb | 27 | ||||
| -rw-r--r-- | spec/services/system_note_service_spec.rb | 11 | ||||
| -rw-r--r-- | spec/services/system_notes/merge_requests_service_spec.rb | 28 |
10 files changed, 396 insertions, 460 deletions
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb index cf7554b3646..08bc70d7116 100644 --- a/spec/features/boards/sidebar_spec.rb +++ b/spec/features/boards/sidebar_spec.rb @@ -107,17 +107,20 @@ RSpec.describe 'Issue Boards', :js do click_card(card) page.within('.assignee') do - click_link 'Edit' + click_button('Edit') wait_for_requests - page.within('.dropdown-menu-user') do - click_link user.name + assignee = first('.gl-avatar-labeled').find('.gl-avatar-labeled-label').text - wait_for_requests + page.within('.dropdown-menu-user') do + first('.gl-avatar-labeled').click end - expect(page).to have_content(user.name) + click_button('Edit') + wait_for_requests + + expect(page).to have_content(assignee) end expect(card).to have_selector('.avatar') @@ -128,15 +131,15 @@ RSpec.describe 'Issue Boards', :js do click_card(card_two) page.within('.assignee') do - click_link 'Edit' + click_button('Edit') wait_for_requests page.within('.dropdown-menu-user') do - click_link 'Unassigned' + find('[data-testid="unassign"]').click end - close_dropdown_menu_if_visible + click_button('Edit') wait_for_requests expect(page).to have_content('None') @@ -165,17 +168,20 @@ RSpec.describe 'Issue Boards', :js do click_card(card) page.within('.assignee') do - click_link 'Edit' + click_button('Edit') wait_for_requests - page.within('.dropdown-menu-user') do - click_link user.name + assignee = first('.gl-avatar-labeled').find('.gl-avatar-labeled-label').text - wait_for_requests + page.within('.dropdown-menu-user') do + first('.gl-avatar-labeled').click end - expect(page).to have_content(user.name) + click_button('Edit') + wait_for_requests + + expect(page).to have_content(assignee) end page.within(find('.board:nth-child(2)')) do @@ -183,9 +189,9 @@ RSpec.describe 'Issue Boards', :js do end page.within('.assignee') do - click_link 'Edit' + click_button('Edit') - expect(find('.dropdown-menu')).to have_selector('.is-active') + expect(find('.dropdown-menu')).to have_selector('.gl-new-dropdown-item-check-icon') end end end diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb index 239bc04a9cb..bd4917824d1 100644 --- a/spec/features/profiles/user_edit_profile_spec.rb +++ b/spec/features/profiles/user_edit_profile_spec.rb @@ -199,6 +199,38 @@ RSpec.describe 'User edit profile' do expect(busy_status.checked?).to eq(true) end + context 'with user status set to busy' do + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, author: user) } + + before do + toggle_busy_status + submit_settings + + project.add_developer(user) + visit project_issue_path(project, issue) + end + + it 'shows author as busy in the assignee dropdown' do + find('.block.assignee .edit-link').click + wait_for_requests + + page.within '.dropdown-menu-user' do + expect(page).to have_content("#{user.name} (Busy)") + end + end + + it 'displays the assignee busy status' do + click_button 'assign yourself' + wait_for_requests + + visit project_issue_path(project, issue) + wait_for_requests + + expect(page.find('[data-testid="expanded-assignee"]')).to have_text("#{user.name} (Busy)") + end + end + context 'with set_user_availability_status feature flag disabled' do before do stub_feature_flags(set_user_availability_status: false) diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js deleted file mode 100644 index 629c2b3be83..00000000000 --- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js +++ /dev/null @@ -1,377 +0,0 @@ -import { - GlDropdownItem, - GlAvatarLink, - GlAvatarLabeled, - GlSearchBoxByType, - GlLoadingIcon, -} from '@gitlab/ui'; -import { mount, createLocalVue } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue'; -import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue'; -import searchUsers from '~/boards/graphql/users_search.query.graphql'; -import store from '~/boards/stores'; -import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue'; -import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue'; -import getIssueParticipants from '~/vue_shared/components/sidebar/queries/getIssueParticipants.query.graphql'; -import { participants } from '../mock_data'; - -const localVue = createLocalVue(); - -localVue.use(VueApollo); - -describe('BoardCardAssigneeDropdown', () => { - let wrapper; - let fakeApollo; - let getIssueParticipantsSpy; - let getSearchUsersSpy; - let dispatchSpy; - - const iid = '111'; - const activeIssueName = 'test'; - const anotherIssueName = 'hello'; - - const createComponent = (search = '', loading = false) => { - wrapper = mount(BoardAssigneeDropdown, { - data() { - return { - search, - selected: [], - issueParticipants: participants, - }; - }, - store, - provide: { - canUpdate: true, - rootPath: '', - }, - mocks: { - $apollo: { - queries: { - searchUsers: { - loading, - }, - }, - }, - }, - }); - }; - - const createComponentWithApollo = (search = '') => { - fakeApollo = createMockApollo([ - [getIssueParticipants, getIssueParticipantsSpy], - [searchUsers, getSearchUsersSpy], - ]); - wrapper = mount(BoardAssigneeDropdown, { - localVue, - apolloProvider: fakeApollo, - data() { - return { - search, - selected: [], - }; - }, - store, - provide: { - canUpdate: true, - rootPath: '', - }, - }); - }; - - const unassign = async () => { - wrapper.find('[data-testid="unassign"]').trigger('click'); - - await wrapper.vm.$nextTick(); - }; - - const openDropdown = async () => { - wrapper.find('[data-testid="edit-button"]').trigger('click'); - - await wrapper.vm.$nextTick(); - }; - - const findByText = (text) => { - return wrapper.findAll(GlDropdownItem).wrappers.find((node) => node.text().indexOf(text) === 0); - }; - - const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - - beforeEach(() => { - store.state.activeId = '1'; - store.state.issues = { - 1: { - iid, - assignees: [{ username: activeIssueName, name: activeIssueName, id: activeIssueName }], - }, - }; - - dispatchSpy = jest.spyOn(store, 'dispatch').mockResolvedValue(); - }); - - afterEach(() => { - window.gon = {}; - jest.restoreAllMocks(); - }); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('when mounted', () => { - beforeEach(() => { - createComponent(); - }); - - it.each` - text - ${anotherIssueName} - ${activeIssueName} - `('finds item with $text', ({ text }) => { - const item = findByText(text); - - expect(item.exists()).toBe(true); - }); - - it('renders gl-avatar-link in gl-dropdown-item', () => { - const item = findByText('hello'); - - expect(item.find(GlAvatarLink).exists()).toBe(true); - }); - - it('renders gl-avatar-labeled in gl-avatar-link', () => { - const item = findByText('hello'); - - expect(item.find(GlAvatarLink).find(GlAvatarLabeled).exists()).toBe(true); - }); - }); - - describe('when selected users are present', () => { - it('renders a divider', () => { - createComponent(); - - expect(wrapper.find('[data-testid="selected-user-divider"]').exists()).toBe(true); - }); - }); - - describe('when collapsed', () => { - it('renders IssuableAssignees', () => { - createComponent(); - - expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); - expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(false); - }); - }); - - describe('when dropdown is open', () => { - beforeEach(async () => { - createComponent(); - - await openDropdown(); - }); - - it('shows assignees dropdown', async () => { - expect(wrapper.find(IssuableAssignees).isVisible()).toBe(false); - expect(wrapper.find(MultiSelectDropdown).isVisible()).toBe(true); - }); - - it('shows the issue returned as the activeIssue', async () => { - expect(findByText(activeIssueName).props('isChecked')).toBe(true); - }); - - describe('when "Unassign" is clicked', () => { - it('unassigns assignees', async () => { - await unassign(); - - expect(findByText('Unassign').props('isChecked')).toBe(true); - }); - }); - - describe('when an unselected item is clicked', () => { - beforeEach(async () => { - await unassign(); - }); - - it('assigns assignee in the dropdown', async () => { - wrapper.find('[data-testid="item_test"]').trigger('click'); - - await wrapper.vm.$nextTick(); - - expect(findByText(activeIssueName).props('isChecked')).toBe(true); - }); - - it('calls setAssignees with username list', async () => { - wrapper.find('[data-testid="item_test"]').trigger('click'); - - await wrapper.vm.$nextTick(); - - document.body.click(); - - await wrapper.vm.$nextTick(); - - expect(store.dispatch).toHaveBeenCalledWith('setAssignees', [activeIssueName]); - }); - }); - - describe('when the user off clicks', () => { - beforeEach(async () => { - await unassign(); - - document.body.click(); - - await wrapper.vm.$nextTick(); - }); - - it('calls setAssignees with username list', async () => { - expect(store.dispatch).toHaveBeenCalledWith('setAssignees', []); - }); - - it('closes the dropdown', async () => { - expect(wrapper.find(IssuableAssignees).isVisible()).toBe(true); - }); - }); - }); - - it('renders divider after unassign', () => { - createComponent(); - - expect(wrapper.find('[data-testid="unassign-divider"]').exists()).toBe(true); - }); - - it.each` - assignees | expected - ${[{ id: 5, username: '', name: '' }]} | ${'Assignee'} - ${[{ id: 6, username: '', name: '' }, { id: 7, username: '', name: '' }]} | ${'2 Assignees'} - `( - 'when assignees have a length of $assignees.length, it renders $expected', - ({ assignees, expected }) => { - store.state.issues['1'].assignees = assignees; - - createComponent(); - - expect(wrapper.find(BoardEditableItem).props('title')).toBe(expected); - }, - ); - - describe('when searching users is loading', () => { - it('finds a loading icon in the dropdown', () => { - createComponent('test', true); - - expect(findLoadingIcon().exists()).toBe(true); - }); - }); - - describe('when participants loading is false', () => { - beforeEach(() => { - createComponent(); - }); - - it('does not find GlLoading icon in the dropdown', () => { - expect(findLoadingIcon().exists()).toBe(false); - }); - - it('finds at least 1 GlDropdownItem', () => { - expect(wrapper.findAll(GlDropdownItem).length).toBeGreaterThan(0); - }); - }); - - describe('Apollo', () => { - beforeEach(() => { - getIssueParticipantsSpy = jest.fn().mockResolvedValue({ - data: { - issue: { - participants: { - nodes: [ - { - username: 'participant', - name: 'participant', - webUrl: '', - avatarUrl: '', - id: '', - }, - ], - }, - }, - }, - }); - getSearchUsersSpy = jest.fn().mockResolvedValue({ - data: { - users: { - nodes: [{ username: 'root', name: 'root', webUrl: '', avatarUrl: '', id: '' }], - }, - }, - }); - }); - - describe('when search is empty', () => { - beforeEach(() => { - createComponentWithApollo(); - }); - - it('calls getIssueParticipants', async () => { - jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(getIssueParticipantsSpy).toHaveBeenCalledWith({ id: 'gid://gitlab/Issue/111' }); - }); - }); - - describe('when search is not empty', () => { - beforeEach(() => { - createComponentWithApollo('search term'); - }); - - it('calls searchUsers', async () => { - jest.runOnlyPendingTimers(); - await wrapper.vm.$nextTick(); - - expect(getSearchUsersSpy).toHaveBeenCalledWith({ search: 'search term' }); - }); - }); - }); - - it('finds GlSearchBoxByType', async () => { - createComponent(); - - await openDropdown(); - - expect(wrapper.find(GlSearchBoxByType).exists()).toBe(true); - }); - - describe('when assign-self is emitted from IssuableAssignees', () => { - const currentUser = { username: 'self', name: '', id: '' }; - - beforeEach(() => { - window.gon = { current_username: currentUser.username }; - - dispatchSpy.mockResolvedValue([currentUser]); - createComponent(); - - wrapper.find(IssuableAssignees).vm.$emit('assign-self'); - }); - - it('calls setAssignees with currentUser', () => { - expect(store.dispatch).toHaveBeenCalledWith('setAssignees', currentUser.username); - }); - - it('adds the user to the selected list', async () => { - expect(findByText(currentUser.username).exists()).toBe(true); - }); - }); - - describe('when setting an assignee', () => { - beforeEach(() => { - createComponent(); - }); - - it('passes loading state from Vuex to BoardEditableItem', async () => { - store.state.isSettingAssignees = true; - - await wrapper.vm.$nextTick(); - - expect(wrapper.find(BoardEditableItem).props('loading')).toBe(true); - }); - }); -}); diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js index 1649bab3baf..32d0e7ae886 100644 --- a/spec/frontend/boards/stores/actions_spec.js +++ b/spec/frontend/boards/stores/actions_spec.js @@ -11,8 +11,6 @@ import issueCreateMutation from '~/boards/graphql/issue_create.mutation.graphql' import issueMoveListMutation from '~/boards/graphql/issue_move_list.mutation.graphql'; import actions, { gqlClient } from '~/boards/stores/actions'; import * as types from '~/boards/stores/mutation_types'; -import createFlash from '~/flash'; -import updateAssignees from '~/vue_shared/components/sidebar/queries/updateAssignees.mutation.graphql'; import { mockLists, mockListsById, @@ -726,65 +724,27 @@ describe('moveIssue', () => { describe('setAssignees', () => { const node = { username: 'name' }; - const name = 'username'; const projectPath = 'h/h'; const refPath = `${projectPath}#3`; const iid = '1'; describe('when succeeds', () => { - beforeEach(() => { - jest.spyOn(gqlClient, 'mutate').mockResolvedValue({ - data: { issueSetAssignees: { issue: { assignees: { nodes: [{ ...node }] } } } }, - }); - }); - - it('calls mutate with the correct values', async () => { - await actions.setAssignees( - { commit: () => {}, getters: { activeIssue: { iid, referencePath: refPath } } }, - [name], - ); - - expect(gqlClient.mutate).toHaveBeenCalledWith({ - mutation: updateAssignees, - variables: { iid, assigneeUsernames: [name], projectPath }, - }); - }); - it('calls the correct mutation with the correct values', (done) => { testAction( actions.setAssignees, - {}, + [node], { activeIssue: { iid, referencePath: refPath }, commit: () => {} }, [ - { type: types.SET_ASSIGNEE_LOADING, payload: true }, { type: 'UPDATE_ISSUE_BY_ID', payload: { prop: 'assignees', issueId: undefined, value: [node] }, }, - { type: types.SET_ASSIGNEE_LOADING, payload: false }, ], [], done, ); }); }); - - describe('when fails', () => { - beforeEach(() => { - jest.spyOn(gqlClient, 'mutate').mockRejectedValue(); - }); - - it('calls createFlash', async () => { - await actions.setAssignees({ - commit: () => {}, - getters: { activeIssue: { iid, referencePath: refPath } }, - }); - - expect(createFlash).toHaveBeenCalledWith({ - message: 'An error occurred while updating assignees.', - }); - }); - }); }); describe('createNewIssue', () => { diff --git a/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js new file mode 100644 index 00000000000..4ee12838491 --- /dev/null +++ b/spec/frontend/sidebar/components/assignees/sidebar_editable_item_spec.js @@ -0,0 +1,120 @@ +import { GlLoadingIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue'; + +describe('boards sidebar remove issue', () => { + let wrapper; + + const findLoader = () => wrapper.find(GlLoadingIcon); + const findEditButton = () => wrapper.find('[data-testid="edit-button"]'); + const findTitle = () => wrapper.find('[data-testid="title"]'); + const findCollapsed = () => wrapper.find('[data-testid="collapsed-content"]'); + const findExpanded = () => wrapper.find('[data-testid="expanded-content"]'); + + const createComponent = ({ props = {}, slots = {}, canUpdate = false } = {}) => { + wrapper = shallowMount(SidebarEditableItem, { + attachTo: document.body, + provide: { canUpdate }, + propsData: props, + slots, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('template', () => { + it('renders title', () => { + const title = 'Sidebar item title'; + createComponent({ props: { title } }); + + expect(findTitle().text()).toBe(title); + }); + + it('hides edit button, loader and expanded content by default', () => { + createComponent(); + + expect(findEditButton().exists()).toBe(false); + expect(findLoader().exists()).toBe(false); + expect(findExpanded().isVisible()).toBe(false); + }); + + it('shows "None" if empty collapsed slot', () => { + createComponent(); + + expect(findCollapsed().text()).toBe('None'); + }); + + it('renders collapsed content by default', () => { + const slots = { collapsed: '<div>Collapsed content</div>' }; + createComponent({ slots }); + + expect(findCollapsed().text()).toBe('Collapsed content'); + }); + + it('shows edit button if can update', () => { + createComponent({ canUpdate: true }); + + expect(findEditButton().exists()).toBe(true); + }); + + it('shows loading icon if loading', () => { + createComponent({ props: { loading: true } }); + + expect(findLoader().exists()).toBe(true); + }); + + it('shows expanded content and hides collapsed content when clicking edit button', async () => { + const slots = { default: '<div>Select item</div>' }; + createComponent({ canUpdate: true, slots }); + findEditButton().vm.$emit('click'); + + await wrapper.vm.$nextTick; + + expect(findCollapsed().isVisible()).toBe(false); + expect(findExpanded().isVisible()).toBe(true); + }); + }); + + describe('collapsing an item by offclicking', () => { + beforeEach(async () => { + createComponent({ canUpdate: true }); + findEditButton().vm.$emit('click'); + await wrapper.vm.$nextTick(); + }); + + it('hides expanded section and displays collapsed section', async () => { + expect(findExpanded().isVisible()).toBe(true); + document.body.click(); + + await wrapper.vm.$nextTick(); + + expect(findCollapsed().isVisible()).toBe(true); + expect(findExpanded().isVisible()).toBe(false); + }); + }); + + it('emits open when edit button is clicked and edit is initailized to false', async () => { + createComponent({ canUpdate: true }); + + findEditButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + expect(wrapper.emitted().open.length).toBe(1); + }); + + it('does not emits events when collapsing with false `emitEvent`', async () => { + createComponent({ canUpdate: true }); + + findEditButton().vm.$emit('click'); + + await wrapper.vm.$nextTick(); + + wrapper.vm.collapse({ emitEvent: false }); + + expect(wrapper.emitted().close).toBeUndefined(); + }); +}); diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb index 6523b5a158c..71329905558 100644 --- a/spec/services/merge_requests/post_merge_service_spec.rb +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' RSpec.describe MergeRequests::PostMergeService do - let(:user) { create(:user) } - let(:merge_request) { create(:merge_request, assignees: [user]) } - let(:project) { merge_request.project } + include ProjectForksHelper + + let_it_be(:user) { create(:user) } + let_it_be(:merge_request, reload: true) { create(:merge_request, assignees: [user]) } + let_it_be(:project) { merge_request.project } subject { described_class.new(project, user).execute(merge_request) } @@ -128,5 +130,139 @@ RSpec.describe MergeRequests::PostMergeService do expect(deploy_job.reload.canceled?).to be false end end + + context 'for a merge request chain' do + before do + ::MergeRequests::UpdateService + .new(project, user, force_remove_source_branch: '1') + .execute(merge_request) + end + + context 'when there is another MR' do + let!(:another_merge_request) do + create(:merge_request, + source_project: source_project, + source_branch: 'my-awesome-feature', + target_project: merge_request.source_project, + target_branch: merge_request.source_branch + ) + end + + shared_examples 'does not retarget merge request' do + it 'another merge request is unchanged' do + expect { subject }.not_to change { another_merge_request.reload.target_branch } + .from(merge_request.source_branch) + end + end + + shared_examples 'retargets merge request' do + it 'another merge request is retargeted' do + expect(SystemNoteService) + .to receive(:change_branch).once + .with(another_merge_request, another_merge_request.project, user, + 'target', 'delete', + merge_request.source_branch, merge_request.target_branch) + + expect { subject }.to change { another_merge_request.reload.target_branch } + .from(merge_request.source_branch) + .to(merge_request.target_branch) + end + + context 'when FF retarget_merge_requests is disabled' do + before do + stub_feature_flags(retarget_merge_requests: false) + end + + include_examples 'does not retarget merge request' + end + + context 'when source branch is to be kept' do + before do + ::MergeRequests::UpdateService + .new(project, user, force_remove_source_branch: false) + .execute(merge_request) + end + + include_examples 'does not retarget merge request' + end + end + + context 'in the same project' do + let(:source_project) { project } + + it_behaves_like 'retargets merge request' + + context 'and is closed' do + before do + another_merge_request.close + end + + it_behaves_like 'does not retarget merge request' + end + + context 'and is merged' do + before do + another_merge_request.mark_as_merged + end + + it_behaves_like 'does not retarget merge request' + end + end + + context 'in forked project' do + let!(:source_project) { fork_project(project) } + + context 'when user has access to source project' do + before do + source_project.add_developer(user) + end + + it_behaves_like 'retargets merge request' + end + + context 'when user does not have access to source project' do + it_behaves_like 'does not retarget merge request' + end + end + + context 'and current and another MR is from a fork' do + let(:project) { create(:project) } + let(:source_project) { fork_project(project) } + + let(:merge_request) do + create(:merge_request, + source_project: source_project, + target_project: project + ) + end + + before do + source_project.add_developer(user) + end + + it_behaves_like 'does not retarget merge request' + end + end + + context 'when many merge requests are to be retargeted' do + let!(:many_merge_requests) do + create_list(:merge_request, 10, :unique_branches, + source_project: merge_request.source_project, + target_project: merge_request.source_project, + target_branch: merge_request.source_branch + ) + end + + it 'retargets only 4 of them' do + subject + + expect(many_merge_requests.each(&:reload).pluck(:target_branch).tally) + .to eq( + merge_request.source_branch => 6, + merge_request.target_branch => 4 + ) + end + end + end end end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 3ccf02fcdfb..747ecbf4fa4 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -633,31 +633,37 @@ RSpec.describe MergeRequests::RefreshService do end context 'merge request metrics' do - let(:issue) { create :issue, project: @project } - let(:commit_author) { create :user } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:issue) { create(:issue, project: project) } let(:commit) { project.commit } before do - project.add_developer(commit_author) project.add_developer(user) allow(commit).to receive_messages( safe_message: "Closes #{issue.to_reference}", references: [issue], - author_name: commit_author.name, - author_email: commit_author.email, + author_name: user.name, + author_email: user.email, committed_date: Time.current ) - - allow_any_instance_of(MergeRequest).to receive(:commits).and_return(CommitCollection.new(@project, [commit], 'feature')) end context 'when the merge request is sourced from the same project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - merge_request = create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: @project) - refresh_service = service.new(@project, @user) + allow_any_instance_of(MergeRequest).to receive(:commits).and_return( + CommitCollection.new(project, [commit], 'close-by-commit') + ) + + merge_request = create(:merge_request, + target_branch: 'master', + source_branch: 'close-by-commit', + source_project: project) + + refresh_service = service.new(project, user) allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature') + refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit') issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) expect(issue_ids).to eq([issue.id]) @@ -666,16 +672,21 @@ RSpec.describe MergeRequests::RefreshService do context 'when the merge request is sourced from a different project' do it 'creates a `MergeRequestsClosingIssues` record for each issue closed by a commit' do - forked_project = fork_project(@project, @user, repository: true) + forked_project = fork_project(project, user, repository: true) + + allow_any_instance_of(MergeRequest).to receive(:commits).and_return( + CommitCollection.new(forked_project, [commit], 'close-by-commit') + ) merge_request = create(:merge_request, target_branch: 'master', - source_branch: 'feature', - target_project: @project, + target_project: project, + source_branch: 'close-by-commit', source_project: forked_project) - refresh_service = service.new(@project, @user) + + refresh_service = service.new(forked_project, user) allow(refresh_service).to receive(:execute_hooks) - refresh_service.execute(@oldrev, @newrev, 'refs/heads/feature') + refresh_service.execute(@oldrev, @newrev, 'refs/heads/close-by-commit') issue_ids = MergeRequestsClosingIssues.where(merge_request: merge_request).pluck(:issue_id) expect(issue_ids).to eq([issue.id]) diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index dff0d3297b3..edb95840604 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -913,6 +913,33 @@ RSpec.describe MergeRequests::UpdateService, :mailer do end end + context 'updating `target_branch`' do + let(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'mr-b', + target_branch: 'mr-a') + end + + it 'updates to master' do + expect(SystemNoteService).to receive(:change_branch).with( + merge_request, project, user, 'target', 'update', 'mr-a', 'master' + ) + + expect { update_merge_request(target_branch: 'master') } + .to change { merge_request.reload.target_branch }.from('mr-a').to('master') + end + + it 'updates to master because of branch deletion' do + expect(SystemNoteService).to receive(:change_branch).with( + merge_request, project, user, 'target', 'delete', 'mr-a', 'master' + ) + + expect { update_merge_request(target_branch: 'master', target_branch_was_deleted: true) } + .to change { merge_request.reload.target_branch }.from('mr-a').to('master') + end + end + it_behaves_like 'issuable record that supports quick actions' do let(:existing_merge_request) { create(:merge_request, source_project: project) } let(:issuable) { described_class.new(project, user, params).execute(existing_merge_request) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 9c35f9e3817..df4880dfa13 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -213,15 +213,16 @@ RSpec.describe SystemNoteService do describe '.change_branch' do it 'calls MergeRequestsService' do - old_branch = double - new_branch = double - branch_type = double + old_branch = double('old_branch') + new_branch = double('new_branch') + branch_type = double('branch_type') + event_type = double('event_type') expect_next_instance_of(::SystemNotes::MergeRequestsService) do |service| - expect(service).to receive(:change_branch).with(branch_type, old_branch, new_branch) + expect(service).to receive(:change_branch).with(branch_type, event_type, old_branch, new_branch) end - described_class.change_branch(noteable, project, author, branch_type, old_branch, new_branch) + described_class.change_branch(noteable, project, author, branch_type, event_type, old_branch, new_branch) end end diff --git a/spec/services/system_notes/merge_requests_service_spec.rb b/spec/services/system_notes/merge_requests_service_spec.rb index 50d16231e8f..2131f3d3bdf 100644 --- a/spec/services/system_notes/merge_requests_service_spec.rb +++ b/spec/services/system_notes/merge_requests_service_spec.rb @@ -167,18 +167,38 @@ RSpec.describe ::SystemNotes::MergeRequestsService do end describe '.change_branch' do - subject { service.change_branch('target', old_branch, new_branch) } - let(:old_branch) { 'old_branch'} let(:new_branch) { 'new_branch'} it_behaves_like 'a system note' do let(:action) { 'branch' } + + subject { service.change_branch('target', 'update', old_branch, new_branch) } end context 'when target branch name changed' do - it 'sets the note text' do - expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`" + context 'on update' do + subject { service.change_branch('target', 'update', old_branch, new_branch) } + + it 'sets the note text' do + expect(subject.note).to eq "changed target branch from `#{old_branch}` to `#{new_branch}`" + end + end + + context 'on delete' do + subject { service.change_branch('target', 'delete', old_branch, new_branch) } + + it 'sets the note text' do + expect(subject.note).to eq "changed automatically target branch to `#{new_branch}` because `#{old_branch}` was deleted" + end + end + + context 'for invalid event_type' do + subject { service.change_branch('target', 'invalid', old_branch, new_branch) } + + it 'raises exception' do + expect { subject }.to raise_error /invalid value for event_type/ + end end end end |
