diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-14 15:10:58 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-05-14 15:10:58 +0300 |
commit | 793d974d7c4bd8c9cbd437a9e35087092f4e8bea (patch) | |
tree | a88b391ab97bc58f1d1eb665eec7cf64ce072716 /spec | |
parent | c19bb4adbf354562715ba019892f464080eba850 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
30 files changed, 706 insertions, 480 deletions
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb index 20ae569322c..129d03d17f3 100644 --- a/spec/features/boards/new_issue_spec.rb +++ b/spec/features/boards/new_issue_spec.rb @@ -10,6 +10,9 @@ RSpec.describe 'Issue Boards new issue', :js do let_it_be(:list) { create(:list, board: board, label: label, position: 0) } let_it_be(:user) { create(:user) } + let(:board_list_header) { first('[data-testid="board-list-header"]') } + let(:project_select_dropdown) { find('[data-testid="project-select-dropdown"]') } + context 'authorized user' do before do project.add_maintainer(user) @@ -24,18 +27,18 @@ RSpec.describe 'Issue Boards new issue', :js do end it 'displays new issue button' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in closed list' do page.within('.board:nth-child(3)') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end it 'shows form when clicking button' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' expect(page).to have_selector('.board-new-issue-form') end @@ -43,7 +46,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'hides form when clicking cancel' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' expect(page).to have_selector('.board-new-issue-form') @@ -55,7 +58,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'creates new issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -80,7 +83,7 @@ RSpec.describe 'Issue Boards new issue', :js do # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/323446 xit 'shows sidebar when creating new issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -95,7 +98,7 @@ RSpec.describe 'Issue Boards new issue', :js do it 'successfuly loads labels to be added to newly created issue' do page.within(first('.board')) do - find('.issue-count-badge-add-button').click + click_button 'New issue' end page.within(first('.board-new-issue-form')) do @@ -109,12 +112,12 @@ RSpec.describe 'Issue Boards new issue', :js do find('.board-card').click end - page.within(first('[data-testid="issue-boards-sidebar"]')) do - find('.labels [data-testid="edit-button"]').click + page.within('[data-testid="sidebar-labels"]') do + click_button 'Edit' wait_for_requests - expect(page).to have_selector('.labels-select-contents-list .dropdown-content li a') + expect(page).to have_content 'Label 1' end end end @@ -126,70 +129,94 @@ RSpec.describe 'Issue Boards new issue', :js do end it 'displays new issue button in open list' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in label list' do page.within('.board:nth-child(2)') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end context 'group boards' do let_it_be(:group) { create(:group, :public) } - let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:project) { create(:project, namespace: group, name: "root project") } + let_it_be(:subgroup) { create(:group, parent: group) } + let_it_be(:subproject1) { create(:project, group: subgroup, name: "sub project1") } + let_it_be(:subproject2) { create(:project, group: subgroup, name: "sub project2") } let_it_be(:group_board) { create(:board, group: group) } let_it_be(:project_label) { create(:label, project: project, name: 'label') } let_it_be(:list) { create(:list, board: group_board, label: project_label, position: 0) } context 'for unauthorized users' do - context 'when backlog does not exist' do - before do - sign_in(user) - visit group_board_path(group, group_board) - wait_for_requests - end + before do + visit group_board_path(group, group_board) + wait_for_requests + end + context 'when backlog does not exist' do it 'does not display new issue button in label list' do page.within('.board.is-draggable') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end context 'when backlog list already exists' do - let!(:backlog_list) { create(:backlog_list, board: group_board) } - - before do - sign_in(user) - visit group_board_path(group, group_board) - wait_for_requests - end + let_it_be(:backlog_list) { create(:backlog_list, board: group_board) } it 'displays new issue button in open list' do - expect(first('.board')).to have_selector('.issue-count-badge-add-button', count: 1) + expect(first('.board')).to have_button('New issue', count: 1) end it 'does not display new issue button in label list' do page.within('.board.is-draggable') do - expect(page).not_to have_selector('.issue-count-badge-add-button') + expect(page).not_to have_button('New issue') end end end end context 'for authorized users' do - it 'display new issue button in label list' do - project = create(:project, namespace: group) + before do project.add_reporter(user) + subproject1.add_reporter(user) sign_in(user) visit group_board_path(group, group_board) wait_for_requests + end + + context 'when backlog does not exist' do + it 'display new issue button in label list' do + expect(board_list_header).to have_button('New issue') + end + end + + context 'project select dropdown' do + let_it_be(:backlog_list) { create(:backlog_list, board: group_board) } + + before do + page.within(board_list_header) do + click_button 'New issue' + end + + project_select_dropdown.click + + wait_for_requests + end + + it 'lists a project which is a direct descendant of the top-level group' do + expect(project_select_dropdown).to have_button("root project") + end + + it 'lists a project that belongs to a subgroup' do + expect(project_select_dropdown).to have_button("sub project1") + end - page.within('.board.is-draggable') do - expect(page).to have_selector('.issue-count-badge-add-button') + it "does not list projects to which user doesn't have access" do + expect(project_select_dropdown).not_to have_button("sub project2") end end end diff --git a/spec/finders/packages/group_or_project_package_finder_spec.rb b/spec/finders/packages/group_or_project_package_finder_spec.rb new file mode 100644 index 00000000000..aaeec8e70d2 --- /dev/null +++ b/spec/finders/packages/group_or_project_package_finder_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::GroupOrProjectPackageFinder do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project) } + + let(:finder) { described_class.new(user, project) } + + describe 'execute' do + subject(:run_finder) { finder.execute } + + it { expect { run_finder }.to raise_error(NotImplementedError) } + end + + describe 'execute!' do + subject(:run_finder) { finder.execute! } + + it { expect { run_finder }.to raise_error(NotImplementedError) } + end +end diff --git a/spec/finders/packages/maven/package_finder_spec.rb b/spec/finders/packages/maven/package_finder_spec.rb index d5f521ff895..13c603f1ec4 100644 --- a/spec/finders/packages/maven/package_finder_spec.rb +++ b/spec/finders/packages/maven/package_finder_spec.rb @@ -9,10 +9,9 @@ RSpec.describe ::Packages::Maven::PackageFinder do let_it_be_with_refind(:package) { create(:maven_package, project: project) } let(:param_path) { nil } - let(:param_project) { nil } - let(:param_group) { nil } + let(:project_or_group) { nil } let(:param_order_by_package_file) { false } - let(:finder) { described_class.new(param_path, user, project: param_project, group: param_group, order_by_package_file: param_order_by_package_file) } + let(:finder) { described_class.new(user, project_or_group, path: param_path, order_by_package_file: param_order_by_package_file) } before do group.add_developer(user) @@ -49,13 +48,13 @@ RSpec.describe ::Packages::Maven::PackageFinder do end context 'within the project' do - let(:param_project) { project } + let(:project_or_group) { project } it_behaves_like 'handling valid and invalid paths' end context 'within a group' do - let(:param_group) { group } + let(:project_or_group) { group } it_behaves_like 'handling valid and invalid paths' end @@ -77,7 +76,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do let_it_be(:package2) { create(:maven_package, project: project2, name: package_name, version: nil) } let_it_be(:package3) { create(:maven_package, project: project3, name: package_name, version: nil) } - let(:param_group) { group } + let(:project_or_group) { group } let(:param_path) { package_name } before do @@ -116,7 +115,7 @@ RSpec.describe ::Packages::Maven::PackageFinder do it_behaves_like 'Packages::Maven::PackageFinder examples' it 'uses CTE in the query' do - sql = described_class.new('some_path', user, group: group).send(:packages_with_path).to_sql + sql = described_class.new(user, group, path: package.maven_metadatum.path).send(:packages).to_sql expect(sql).to include('WITH "maven_metadata_by_path" AS') end diff --git a/spec/finders/packages/pypi/package_finder_spec.rb b/spec/finders/packages/pypi/package_finder_spec.rb new file mode 100644 index 00000000000..7d9eb8a5cd1 --- /dev/null +++ b/spec/finders/packages/pypi/package_finder_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Pypi::PackageFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project2) { create(:project, group: group) } + let_it_be(:package1) { create(:pypi_package, project: project) } + let_it_be(:package2) { create(:pypi_package, project: project) } + let_it_be(:package3) { create(:pypi_package, project: project2) } + + let(:package_file) { package2.package_files.first } + let(:params) do + { + filename: package_file.file_name, + sha256: package_file.file_sha256 + } + end + + describe 'execute' do + subject { described_class.new(user, scope, params).execute } + + context 'within a project' do + let(:scope) { project } + + it { is_expected.to eq(package2) } + end + + context 'within a group' do + let(:scope) { group } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + + context 'user with access' do + before do + project.add_developer(user) + end + + it { is_expected.to eq(package2) } + end + end + end +end diff --git a/spec/finders/packages/pypi/packages_finder_spec.rb b/spec/finders/packages/pypi/packages_finder_spec.rb new file mode 100644 index 00000000000..a69c2317261 --- /dev/null +++ b/spec/finders/packages/pypi/packages_finder_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Pypi::PackagesFinder do + let_it_be(:user) { create(:user) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } + let_it_be(:project2) { create(:project, group: group) } + let_it_be(:package1) { create(:pypi_package, project: project) } + let_it_be(:package2) { create(:pypi_package, project: project) } + let_it_be(:package3) { create(:pypi_package, name: package2.name, project: project) } + let_it_be(:package4) { create(:pypi_package, name: package2.name, project: project2) } + + let(:package_name) { package2.name } + + describe 'execute!' do + subject { described_class.new(user, scope, package_name: package_name).execute! } + + shared_examples 'when no package is found' do + context 'non-existing package' do + let(:package_name) { 'none' } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end + + shared_examples 'when package_name param is a non-normalized name' do + context 'non-existing package' do + let(:package_name) { package2.name.upcase.tr('-', '.') } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + end + end + + context 'within a project' do + let(:scope) { project } + + it { is_expected.to contain_exactly(package2, package3) } + + it_behaves_like 'when no package is found' + it_behaves_like 'when package_name param is a non-normalized name' + end + + context 'within a group' do + let(:scope) { group } + + it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) } + + context 'user with access to only one project' do + before do + project2.add_developer(user) + end + + it { is_expected.to contain_exactly(package4) } + + it_behaves_like 'when no package is found' + it_behaves_like 'when package_name param is a non-normalized name' + + context ' user with access to multiple projects' do + before do + project.add_developer(user) + end + + it { is_expected.to contain_exactly(package2, package3, package4) } + end + end + end + end +end diff --git a/spec/frontend/jobs/mock_data.js b/spec/frontend/jobs/mock_data.js index 7851f633629..57f0b852ff8 100644 --- a/spec/frontend/jobs/mock_data.js +++ b/spec/frontend/jobs/mock_data.js @@ -920,6 +920,7 @@ export default { cancel_path: '/root/ci-mock/-/jobs/4757/cancel', new_issue_path: '/root/ci-mock/issues/new', playable: false, + complete: true, created_at: threeWeeksAgo.toISOString(), updated_at: threeWeeksAgo.toISOString(), finished_at: threeWeeksAgo.toISOString(), diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js index 472f2a8b211..28fe3b67e7b 100644 --- a/spec/frontend/pipelines/graph/mock_data.js +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -8,6 +8,7 @@ export const mockPipelineResponse = { __typename: 'Pipeline', id: 163, iid: '22', + complete: true, usesNeeds: true, downstream: null, upstream: null, @@ -570,6 +571,7 @@ export const wrappedPipelineReturn = { __typename: 'Pipeline', id: 'gid://gitlab/Ci::Pipeline/175', iid: '38', + complete: true, usesNeeds: true, downstream: { __typename: 'PipelineConnection', diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap new file mode 100644 index 00000000000..cef1dff3335 --- /dev/null +++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_ready_to_merge_spec.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ReadyToMerge with a mismatched SHA warns the user to refresh to review 1`] = `"<gl-sprintf-stub message=\\"New changes were added. %{linkStart}Reload the page to review them%{linkEnd}\\"></gl-sprintf-stub>"`; diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 7202f327683..85a42946325 100644 --- a/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -1,13 +1,13 @@ -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Vue from 'vue'; -import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests'; import simplePoll from '~/lib/utils/simple_poll'; import CommitEdit from '~/vue_merge_request_widget/components/states/commit_edit.vue'; import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue'; import CommitsHeader from '~/vue_merge_request_widget/components/states/commits_header.vue'; import ReadyToMerge from '~/vue_merge_request_widget/components/states/ready_to_merge.vue'; import SquashBeforeMerge from '~/vue_merge_request_widget/components/states/squash_before_merge.vue'; -import { MWPS_MERGE_STRATEGY, MTWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; +import { MWPS_MERGE_STRATEGY } from '~/vue_merge_request_widget/constants'; import eventHub from '~/vue_merge_request_widget/event_hub'; jest.mock('~/lib/utils/simple_poll', () => @@ -58,11 +58,9 @@ const createTestService = () => ({ poll: jest.fn().mockResolvedValue(), }); +let wrapper; const createComponent = (customConfig = {}) => { - const Component = Vue.extend(ReadyToMerge); - - return new Component({ - el: document.createElement('div'), + wrapper = shallowMount(ReadyToMerge, { propsData: { mr: createTestMr(customConfig), service: createTestService(), @@ -71,277 +69,207 @@ const createComponent = (customConfig = {}) => { }; describe('ReadyToMerge', () => { - let vm; - - beforeEach(() => { - vm = createComponent(); - }); - afterEach(() => { - vm.$destroy(); - }); - - describe('props', () => { - it('should have props', () => { - const { mr, service } = ReadyToMerge.props; - - expect(mr.type instanceof Object).toBeTruthy(); - expect(mr.required).toBeTruthy(); - - expect(service.type instanceof Object).toBeTruthy(); - expect(service.required).toBeTruthy(); - }); - }); - - describe('data', () => { - it('should have default data', () => { - expect(vm.mergeWhenBuildSucceeds).toBeFalsy(); - expect(vm.useCommitMessageWithDescription).toBeFalsy(); - expect(vm.showCommitMessageEditor).toBeFalsy(); - expect(vm.isMakingRequest).toBeFalsy(); - expect(vm.isMergingImmediately).toBeFalsy(); - expect(vm.commitMessage).toBe(vm.mr.commitMessage); - }); + wrapper.destroy(); }); describe('computed', () => { describe('isAutoMergeAvailable', () => { it('should return true when at least one merge strategy is available', () => { - vm.mr.availableAutoMergeStrategies = [MWPS_MERGE_STRATEGY]; + createComponent(); - expect(vm.isAutoMergeAvailable).toBe(true); + expect(wrapper.vm.isAutoMergeAvailable).toBe(true); }); it('should return false when no merge strategies are available', () => { - vm.mr.availableAutoMergeStrategies = []; + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.isAutoMergeAvailable).toBe(false); + expect(wrapper.vm.isAutoMergeAvailable).toBe(false); }); }); describe('status', () => { it('defaults to success', () => { - Vue.set(vm.mr, 'pipeline', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { pipeline: true, availableAutoMergeStrategies: [] } }); - expect(vm.status).toEqual('success'); + expect(wrapper.vm.status).toEqual('success'); }); it('returns failed when MR has CI but also has an unknown status', () => { - Vue.set(vm.mr, 'hasCI', true); + createComponent({ mr: { hasCI: true } }); - expect(vm.status).toEqual('failed'); + expect(wrapper.vm.status).toEqual('failed'); }); it('returns default when MR has no pipeline', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.status).toEqual('success'); + expect(wrapper.vm.status).toEqual('success'); }); it('returns pending when pipeline is active', () => { - Vue.set(vm.mr, 'pipeline', {}); - Vue.set(vm.mr, 'isPipelineActive', true); + createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); - expect(vm.status).toEqual('pending'); + expect(wrapper.vm.status).toEqual('pending'); }); it('returns failed when pipeline is failed', () => { - Vue.set(vm.mr, 'pipeline', {}); - Vue.set(vm.mr, 'isPipelineFailed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ + mr: { pipeline: {}, isPipelineFailed: true, availableAutoMergeStrategies: [] }, + }); - expect(vm.status).toEqual('failed'); + expect(wrapper.vm.status).toEqual('failed'); }); }); describe('mergeButtonVariant', () => { it('defaults to success class', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ + mr: { availableAutoMergeStrategies: [] }, + }); - expect(vm.mergeButtonVariant).toEqual('success'); + expect(wrapper.vm.mergeButtonVariant).toEqual('success'); }); it('returns success class for success status', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - Vue.set(vm.mr, 'pipeline', true); + createComponent({ + mr: { availableAutoMergeStrategies: [], pipeline: true }, + }); - expect(vm.mergeButtonVariant).toEqual('success'); + expect(wrapper.vm.mergeButtonVariant).toEqual('success'); }); it('returns info class for pending status', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MTWPS_MERGE_STRATEGY]); + createComponent(); - expect(vm.mergeButtonVariant).toEqual('info'); + expect(wrapper.vm.mergeButtonVariant).toEqual('info'); }); it('returns danger class for failed status', () => { - vm.mr.hasCI = true; + createComponent({ mr: { hasCI: true } }); - expect(vm.mergeButtonVariant).toEqual('danger'); + expect(wrapper.vm.mergeButtonVariant).toEqual('danger'); }); }); describe('status icon', () => { it('defaults to tick icon', () => { - expect(vm.iconClass).toEqual('success'); + createComponent(); + + expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for success status', () => { - vm.mr.pipeline = true; + createComponent({ mr: { pipeline: true } }); - expect(vm.iconClass).toEqual('success'); + expect(wrapper.vm.iconClass).toEqual('success'); }); it('shows tick for pending status', () => { - vm.mr.pipeline = {}; - vm.mr.isPipelineActive = true; + createComponent({ mr: { pipeline: {}, isPipelineActive: true } }); - expect(vm.iconClass).toEqual('success'); - }); - - it('shows warning icon for failed status', () => { - vm.mr.hasCI = true; - - expect(vm.iconClass).toEqual('warning'); - }); - - it('shows warning icon for merge not allowed', () => { - vm.mr.hasCI = true; - - expect(vm.iconClass).toEqual('warning'); + expect(wrapper.vm.iconClass).toEqual('success'); }); }); describe('mergeButtonText', () => { it('should return "Merge" when no auto merge strategies are available', () => { - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); + createComponent({ mr: { availableAutoMergeStrategies: [] } }); - expect(vm.mergeButtonText).toEqual('Merge'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge'); }); - it('should return "Merge in progress"', () => { - Vue.set(vm, 'isMergingImmediately', true); + it('should return "Merge in progress"', async () => { + createComponent(); + + wrapper.setData({ isMergingImmediately: true }); + + await Vue.nextTick(); - expect(vm.mergeButtonText).toEqual('Merge in progress'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge in progress'); }); it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => { - Vue.set(vm, 'isMergingImmediately', false); - Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY); + createComponent({ + mr: { isMergingImmediately: false, preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY }, + }); - expect(vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); + expect(wrapper.vm.mergeButtonText).toEqual('Merge when pipeline succeeds'); }); }); describe('autoMergeText', () => { it('should return Merge when pipeline succeeds', () => { - Vue.set(vm.mr, 'preferredAutoMergeStrategy', MWPS_MERGE_STRATEGY); + createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } }); - expect(vm.autoMergeText).toEqual('Merge when pipeline succeeds'); + expect(wrapper.vm.autoMergeText).toEqual('Merge when pipeline succeeds'); }); }); describe('shouldShowMergeImmediatelyDropdown', () => { it('should return false if no pipeline is active', () => { - Vue.set(vm.mr, 'isPipelineActive', false); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); + createComponent({ + mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false }, + }); - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); + expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); it('should return false if "Pipelines must succeed" is enabled for the current project', () => { - Vue.set(vm.mr, 'isPipelineActive', true); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } }); - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(false); - }); - - it('should return true if the MR\'s pipeline is active and "Pipelines must succeed" is not enabled for the current project', () => { - Vue.set(vm.mr, 'isPipelineActive', true); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', false); - - expect(vm.shouldShowMergeImmediatelyDropdown).toBe(true); + expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false); }); }); describe('isMergeButtonDisabled', () => { it('should return false with initial data', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); + createComponent({ mr: { isMergeAllowed: true } }); - expect(vm.isMergeButtonDisabled).toBe(false); + expect(wrapper.vm.isMergeButtonDisabled).toBe(false); }); it('should return true when there is no commit message', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm, 'commitMessage', ''); + createComponent({ mr: { isMergeAllowed: true, commitMessage: '' } }); - expect(vm.isMergeButtonDisabled).toBe(true); + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); it('should return true if merge is not allowed', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - Vue.set(vm.mr, 'onlyAllowMergeIfPipelineSucceeds', true); + createComponent({ + mr: { + isMergeAllowed: false, + availableAutoMergeStrategies: [], + onlyAllowMergeIfPipelineSucceeds: true, + }, + }); - expect(vm.isMergeButtonDisabled).toBe(true); + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); - it('should return true when the vm instance is making request', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm, 'isMakingRequest', true); + it('should return true when the vm instance is making request', async () => { + createComponent({ mr: { isMergeAllowed: true } }); - expect(vm.isMergeButtonDisabled).toBe(true); - }); - }); + wrapper.setData({ isMakingRequest: true }); - describe('isMergeImmediatelyDangerous', () => { - it('should always return false in CE', () => { - expect(vm.isMergeImmediatelyDangerous).toBe(false); + await Vue.nextTick(); + + expect(wrapper.vm.isMergeButtonDisabled).toBe(true); }); }); }); describe('methods', () => { - describe('shouldShowMergeControls', () => { - it('should return false when an external pipeline is running and required to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - - expect(vm.shouldShowMergeControls).toBe(false); - }); - - it('should return true when the build succeeded or build not required to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', []); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - - it('should return true when showing the MWPS button and a pipeline is running that needs to be successful', () => { - Vue.set(vm.mr, 'isMergeAllowed', false); - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - - it('should return true when showing the MWPS button but not required for the pipeline to succeed', () => { - Vue.set(vm.mr, 'isMergeAllowed', true); - Vue.set(vm.mr, 'availableAutoMergeStrategies', [MWPS_MERGE_STRATEGY]); - - expect(vm.shouldShowMergeControls).toBe(true); - }); - }); - describe('updateMergeCommitMessage', () => { it('should revert flag and change commitMessage', () => { - expect(vm.commitMessage).toEqual(commitMessage); - vm.updateMergeCommitMessage(true); + createComponent(); + + wrapper.vm.updateMergeCommitMessage(true); - expect(vm.commitMessage).toEqual(commitMessageWithDescription); - vm.updateMergeCommitMessage(false); + expect(wrapper.vm.commitMessage).toEqual(commitMessageWithDescription); + wrapper.vm.updateMergeCommitMessage(false); - expect(vm.commitMessage).toEqual(commitMessage); + expect(wrapper.vm.commitMessage).toEqual(commitMessage); }); }); @@ -356,23 +284,26 @@ describe('ReadyToMerge', () => { }); it('should handle merge when pipeline succeeds', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); jest - .spyOn(vm.service, 'merge') + .spyOn(wrapper.vm.service, 'merge') .mockReturnValue(returnPromise('merge_when_pipeline_succeeds')); - vm.removeSourceBranch = false; - vm.handleMergeButtonClick(true); + wrapper.setData({ removeSourceBranch: false }); + + wrapper.vm.handleMergeButtonClick(true); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params).toEqual( expect.objectContaining({ - sha: vm.mr.sha, - commit_message: vm.mr.commitMessage, + sha: wrapper.vm.mr.sha, + commit_message: wrapper.vm.mr.commitMessage, should_remove_source_branch: false, auto_merge_strategy: 'merge_when_pipeline_succeeds', }), @@ -382,15 +313,17 @@ describe('ReadyToMerge', () => { }); it('should handle merge failed', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('failed')); - vm.handleMergeButtonClick(false, true); + jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('failed')); + wrapper.vm.handleMergeButtonClick(false, true); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params.should_remove_source_branch).toBeTruthy(); expect(params.auto_merge_strategy).toBeUndefined(); @@ -399,15 +332,17 @@ describe('ReadyToMerge', () => { }); it('should handle merge action accepted case', (done) => { - jest.spyOn(vm.service, 'merge').mockReturnValue(returnPromise('success')); - jest.spyOn(vm, 'initiateMergePolling').mockImplementation(() => {}); - vm.handleMergeButtonClick(); + createComponent(); + + jest.spyOn(wrapper.vm.service, 'merge').mockReturnValue(returnPromise('success')); + jest.spyOn(wrapper.vm, 'initiateMergePolling').mockImplementation(() => {}); + wrapper.vm.handleMergeButtonClick(); setImmediate(() => { - expect(vm.isMakingRequest).toBeTruthy(); - expect(vm.initiateMergePolling).toHaveBeenCalled(); + expect(wrapper.vm.isMakingRequest).toBeTruthy(); + expect(wrapper.vm.initiateMergePolling).toHaveBeenCalled(); - const params = vm.service.merge.mock.calls[0][0]; + const params = wrapper.vm.service.merge.mock.calls[0][0]; expect(params.should_remove_source_branch).toBeTruthy(); expect(params.auto_merge_strategy).toBeUndefined(); @@ -418,128 +353,31 @@ describe('ReadyToMerge', () => { describe('initiateMergePolling', () => { it('should call simplePoll', () => { - vm.initiateMergePolling(); + createComponent(); + + wrapper.vm.initiateMergePolling(); expect(simplePoll).toHaveBeenCalledWith(expect.any(Function), { timeout: 0 }); }); it('should call handleMergePolling', () => { - jest.spyOn(vm, 'handleMergePolling').mockImplementation(() => {}); - - vm.initiateMergePolling(); - - expect(vm.handleMergePolling).toHaveBeenCalled(); - }); - }); - - describe('handleMergePolling', () => { - const returnPromise = (state) => - new Promise((resolve) => { - resolve({ - data: { - state, - source_branch_exists: true, - }, - }); - }); - - beforeEach(() => { - loadFixtures('merge_requests/merge_request_of_current_user.html'); - }); - - it('should call start and stop polling when MR merged', (done) => { - jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - let cpc = false; // continuePollingCalled - let spc = false; // stopPollingCalled - - vm.handleMergePolling( - () => { - cpc = true; - }, - () => { - spc = true; - }, - ); - setImmediate(() => { - expect(vm.service.poll).toHaveBeenCalled(); - expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested'); - expect(eventHub.$emit).toHaveBeenCalledWith('FetchActionsContent'); - expect(vm.initiateRemoveSourceBranchPolling).toHaveBeenCalled(); - expect(refreshUserMergeRequestCounts).toHaveBeenCalled(); - expect(cpc).toBeFalsy(); - expect(spc).toBeTruthy(); + createComponent(); - done(); - }); - }); - - it('updates status box', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - vm.handleMergePolling( - () => {}, - () => {}, - ); - - setImmediate(() => { - const statusBox = document.querySelector('.status-box'); - - expect(statusBox.classList.contains('status-box-mr-merged')).toBeTruthy(); - expect(statusBox.textContent).toContain('Merged'); - - done(); - }); - }); - - it('updates merge request count badge', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('merged')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - vm.handleMergePolling( - () => {}, - () => {}, - ); + jest.spyOn(wrapper.vm, 'handleMergePolling').mockImplementation(() => {}); - setImmediate(() => { - expect(document.querySelector('.js-merge-counter').textContent).toBe('0'); - - done(); - }); - }); - - it('should continue polling until MR is merged', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise('some_other_state')); - jest.spyOn(vm, 'initiateRemoveSourceBranchPolling').mockImplementation(() => {}); - - let cpc = false; // continuePollingCalled - let spc = false; // stopPollingCalled - - vm.handleMergePolling( - () => { - cpc = true; - }, - () => { - spc = true; - }, - ); - setImmediate(() => { - expect(cpc).toBeTruthy(); - expect(spc).toBeFalsy(); + wrapper.vm.initiateMergePolling(); - done(); - }); + expect(wrapper.vm.handleMergePolling).toHaveBeenCalled(); }); }); describe('initiateRemoveSourceBranchPolling', () => { it('should emit event and call simplePoll', () => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - vm.initiateRemoveSourceBranchPolling(); + wrapper.vm.initiateRemoveSourceBranchPolling(); expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]); expect(simplePoll).toHaveBeenCalled(); @@ -557,13 +395,15 @@ describe('ReadyToMerge', () => { }); it('should call start and stop polling when MR merged', (done) => { + createComponent(); + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(false)); + jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(false)); let cpc = false; // continuePollingCalled let spc = false; // stopPollingCalled - vm.handleRemoveBranchPolling( + wrapper.vm.handleRemoveBranchPolling( () => { cpc = true; }, @@ -572,7 +412,7 @@ describe('ReadyToMerge', () => { }, ); setImmediate(() => { - expect(vm.service.poll).toHaveBeenCalled(); + expect(wrapper.vm.service.poll).toHaveBeenCalled(); const args = eventHub.$emit.mock.calls[0]; @@ -590,12 +430,14 @@ describe('ReadyToMerge', () => { }); it('should continue polling until MR is merged', (done) => { - jest.spyOn(vm.service, 'poll').mockReturnValue(returnPromise(true)); + createComponent(); + + jest.spyOn(wrapper.vm.service, 'poll').mockReturnValue(returnPromise(true)); let cpc = false; // continuePollingCalled let spc = false; // stopPollingCalled - vm.handleRemoveBranchPolling( + wrapper.vm.handleRemoveBranchPolling( () => { cpc = true; }, @@ -616,49 +458,26 @@ describe('ReadyToMerge', () => { describe('Remove source branch checkbox', () => { describe('when user can merge but cannot delete branch', () => { it('should be disabled in the rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); + createComponent(); - expect(checkboxElement).toBeNull(); + expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false); }); }); describe('when user can merge and can delete branch', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { canRemoveSourceBranch: true }, }); }); it('isRemoveSourceBranchButtonDisabled should be false', () => { - expect(vm.isRemoveSourceBranchButtonDisabled).toBe(false); - }); - - it('removed source branch should be enabled in rendered output', () => { - const checkboxElement = vm.$el.querySelector('#remove-source-branch-input'); - - expect(checkboxElement).not.toBeNull(); + expect(wrapper.find('#remove-source-branch-input').props('disabled')).toBe(undefined); }); }); }); describe('render children components', () => { - let wrapper; - const localVue = createLocalVue(); - - const createLocalComponent = (customConfig = {}) => { - wrapper = shallowMount(localVue.extend(ReadyToMerge), { - localVue, - propsData: { - mr: createTestMr(customConfig), - service: createTestService(), - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - }); - const findCheckboxElement = () => wrapper.find(SquashBeforeMerge); const findCommitsHeaderElement = () => wrapper.find(CommitsHeader); const findCommitEditElements = () => wrapper.findAll(CommitEdit); @@ -667,7 +486,7 @@ describe('ReadyToMerge', () => { describe('squash checkbox', () => { it('should be rendered when squash before merge is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true }, }); @@ -675,13 +494,13 @@ describe('ReadyToMerge', () => { }); it('should not be rendered when squash before merge is disabled', () => { - createLocalComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: false } }); expect(findCheckboxElement().exists()).toBeFalsy(); }); it('should not be rendered when there is only 1 commit', () => { - createLocalComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); + createComponent({ mr: { commitsCount: 1, enableSquashBeforeMerge: true } }); expect(findCheckboxElement().exists()).toBeFalsy(); }); @@ -695,7 +514,7 @@ describe('ReadyToMerge', () => { `( 'is $state when squashIsReadonly returns $expectation ', ({ squashState, prop, expectation }) => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true, [squashState]: expectation }, }); @@ -704,7 +523,7 @@ describe('ReadyToMerge', () => { ); it('is not rendered for "Do not allow" option', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, enableSquashBeforeMerge: true, @@ -720,14 +539,14 @@ describe('ReadyToMerge', () => { describe('commits count collapsible header', () => { it('should be rendered when fast-forward is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitsHeaderElement().exists()).toBeTruthy(); }); describe('when fast-forward is enabled', () => { it('should be rendered if squash and squash before are enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, enableSquashBeforeMerge: true, @@ -740,7 +559,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash before merge is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, enableSquashBeforeMerge: false, @@ -753,7 +572,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: false, @@ -766,7 +585,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if commits count is 1', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -783,7 +602,7 @@ describe('ReadyToMerge', () => { describe('commits edit components', () => { describe('when fast-forward merge is enabled', () => { it('should not be rendered if squash is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: false, @@ -796,7 +615,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if squash before merge is disabled', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -809,7 +628,7 @@ describe('ReadyToMerge', () => { }); it('should not be rendered if there is only one commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squash: true, @@ -822,7 +641,7 @@ describe('ReadyToMerge', () => { }); it('should have one edit component if squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { ffOnlyEnabled: true, squashIsSelected: true, @@ -837,13 +656,13 @@ describe('ReadyToMerge', () => { }); it('should have one edit component when squash is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitEditElements().length).toBe(1); }); it('should have two edit components when squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, squashIsSelected: true, @@ -855,7 +674,7 @@ describe('ReadyToMerge', () => { }); it('should have one edit components when squash is enabled and there is 1 commit only', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 1, squash: true, @@ -867,13 +686,13 @@ describe('ReadyToMerge', () => { }); it('should have correct edit merge commit label', () => { - createLocalComponent(); + createComponent(); expect(findFirstCommitEditLabel()).toBe('Merge commit message'); }); it('should have correct edit squash commit label', () => { - createLocalComponent({ + createComponent({ mr: { commitsCount: 2, squashIsSelected: true, @@ -887,13 +706,13 @@ describe('ReadyToMerge', () => { describe('commits dropdown', () => { it('should not be rendered if squash is disabled', () => { - createLocalComponent(); + createComponent(); expect(findCommitDropdownElement().exists()).toBeFalsy(); }); it('should be rendered if squash is enabled and there is more than 1 commit', () => { - createLocalComponent({ + createComponent({ mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 }, }); @@ -902,83 +721,38 @@ describe('ReadyToMerge', () => { }); }); - describe('Merge controls', () => { - describe('when allowed to merge', () => { - beforeEach(() => { - vm = createComponent({ - mr: { isMergeAllowed: true, canRemoveSourceBranch: true }, - }); - }); - - it('shows remove source branch checkbox', () => { - expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).not.toBeNull(); - }); - - it('shows modify commit message button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); - }); - - it('does not show message about needing to resolve items', () => { - expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeNull(); - }); - }); - - describe('when not allowed to merge', () => { - beforeEach(() => { - vm = createComponent({ - mr: { isMergeAllowed: false }, - }); - }); - - it('does not show remove source branch checkbox', () => { - expect(vm.$el.querySelector('.js-remove-source-branch-checkbox')).toBeNull(); - }); - - it('shows message to resolve all items before being allowed to merge', () => { - expect(vm.$el.querySelector('.js-resolve-mr-widget-items-message')).toBeDefined(); - }); - }); - }); - describe('Merge request project settings', () => { describe('when the merge commit merge method is enabled', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { ffOnlyEnabled: false }, }); }); it('should not show fast forward message', () => { - expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeNull(); - }); - - it('should show "Modify commit message" button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeDefined(); + expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(false); }); }); describe('when the fast-forward merge method is enabled', () => { beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { ffOnlyEnabled: true }, }); }); it('should show fast forward message', () => { - expect(vm.$el.querySelector('.mr-fast-forward-message')).toBeDefined(); - }); - - it('should not show "Modify commit message" button', () => { - expect(vm.$el.querySelector('.js-modify-commit-message-button')).toBeNull(); + expect(wrapper.find('.mr-fast-forward-message').exists()).toBe(true); }); }); }); describe('with a mismatched SHA', () => { - const findMismatchShaBlock = () => vm.$el.querySelector('.js-sha-mismatch'); + const findMismatchShaBlock = () => wrapper.find('.js-sha-mismatch'); + const findMismatchShaTextBlock = () => findMismatchShaBlock().find(GlSprintf); beforeEach(() => { - vm = createComponent({ + createComponent({ mr: { isSHAMismatch: true, mergeRequestDiffsPath: '/merge_requests/1/diffs', @@ -987,17 +761,11 @@ describe('ReadyToMerge', () => { }); it('displays a warning message', () => { - expect(findMismatchShaBlock()).toExist(); + expect(findMismatchShaBlock().exists()).toBe(true); }); it('warns the user to refresh to review', () => { - expect(findMismatchShaBlock().textContent.trim()).toBe( - 'New changes were added. Reload the page to review them', - ); - }); - - it('displays link to the diffs tab', () => { - expect(findMismatchShaBlock().querySelector('a').href).toContain(vm.mr.mergeRequestDiffsPath); + expect(findMismatchShaTextBlock().element.outerHTML).toMatchSnapshot(); }); }); }); diff --git a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb index 23d4d86c79a..2c9c3a47650 100644 --- a/spec/graphql/resolvers/design_management/versions_resolver_spec.rb +++ b/spec/graphql/resolvers/design_management/versions_resolver_spec.rb @@ -41,6 +41,20 @@ RSpec.describe Resolvers::DesignManagement::VersionsResolver do it 'returns the ordered versions' do expect(result.to_a).to eq(all_versions) end + + context 'loading associations' do + it 'prevents N+1 queries when loading author' do + control_count = ActiveRecord::QueryRecorder.new(skip_cached: false) do + resolve_versions(object).items.map(&:author) + end.count + + create_list(:design_version, 3, issue: issue) + + expect do + resolve_versions(object).items.map(&:author) + end.not_to exceed_all_query_limit(control_count) + end + end end context 'when constrained' do diff --git a/spec/graphql/types/ci/pipeline_type_spec.rb b/spec/graphql/types/ci/pipeline_type_spec.rb index c67e86a7ee1..35d48229fa4 100644 --- a/spec/graphql/types/ci/pipeline_type_spec.rb +++ b/spec/graphql/types/ci/pipeline_type_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Types::Ci::PipelineType do it 'contains attributes related to a pipeline' do expected_fields = %w[ - id iid sha before_sha status detailed_status config_source + id iid sha before_sha complete status detailed_status config_source duration queued_duration coverage created_at updated_at started_at finished_at committed_at stages user retryable cancelable jobs source_job job downstream diff --git a/spec/graphql/types/design_management/version_type_spec.rb b/spec/graphql/types/design_management/version_type_spec.rb index 017cc1775a1..62335a65fdf 100644 --- a/spec/graphql/types/design_management/version_type_spec.rb +++ b/spec/graphql/types/design_management/version_type_spec.rb @@ -6,7 +6,7 @@ RSpec.describe GitlabSchema.types['DesignVersion'] do it { expect(described_class).to require_graphql_authorizations(:read_design) } it 'has the expected fields' do - expected_fields = %i[id sha designs design_at_version designs_at_version] + expected_fields = %i[id sha designs design_at_version designs_at_version author created_at] expect(described_class).to have_graphql_fields(*expected_fields) end diff --git a/spec/helpers/webpack_helper_spec.rb b/spec/helpers/webpack_helper_spec.rb new file mode 100644 index 00000000000..f9386c99dc3 --- /dev/null +++ b/spec/helpers/webpack_helper_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe WebpackHelper do + let(:source) { 'foo.js' } + let(:asset_path) { "/assets/webpack/#{source}" } + + describe '#prefetch_link_tag' do + it 'returns prefetch link tag' do + expect(helper.prefetch_link_tag(source)).to eq("<link rel=\"prefetch\" href=\"/#{source}\">") + end + end + + describe '#webpack_preload_asset_tag' do + before do + allow(Gitlab::Webpack::Manifest).to receive(:asset_paths).and_return([asset_path]) + end + + it 'preloads the resource by default' do + expect(helper).to receive(:preload_link_tag).with(asset_path, {}).and_call_original + + output = helper.webpack_preload_asset_tag(source) + + expect(output).to eq("<link rel=\"preload\" href=\"#{asset_path}\" as=\"script\" type=\"text/javascript\">") + end + + it 'prefetches the resource if explicitly asked' do + expect(helper).to receive(:prefetch_link_tag).with(asset_path).and_call_original + + output = helper.webpack_preload_asset_tag(source, prefetch: true) + + expect(output).to eq("<link rel=\"prefetch\" href=\"#{asset_path}\">") + end + end +end diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb index cdb123573f1..3c4769764d5 100644 --- a/spec/models/ci/job_artifact_spec.rb +++ b/spec/models/ci/job_artifact_spec.rb @@ -602,6 +602,34 @@ RSpec.describe Ci::JobArtifact do end end + context 'FastDestroyAll' do + let_it_be(:project) { create(:project) } + let_it_be(:pipeline) { create(:ci_pipeline, project: project) } + let_it_be(:job) { create(:ci_build, pipeline: pipeline, project: project) } + + let!(:job_artifact) { create(:ci_job_artifact, :archive, job: job) } + let(:subjects) { pipeline.job_artifacts } + + describe '.use_fast_destroy' do + it 'performs cascading delete with fast_destroy_all' do + expect(Ci::DeletedObject.count).to eq(0) + expect(subjects.count).to be > 0 + + expect { pipeline.destroy! }.not_to raise_error + + expect(subjects.count).to eq(0) + expect(Ci::DeletedObject.count).to be > 0 + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(project, :build_artifacts_size, -job_artifact.file.size) + + pipeline.destroy! + end + end + end + def file_type_limit_failure_message(type, limit_name) <<~MSG The artifact type `#{type}` is missing its counterpart plan limit which is expected to be named `#{limit_name}`. diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 7bb344a4ba3..e5ed8d89145 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -6936,6 +6936,32 @@ RSpec.describe Project, factory_default: :keep do end end + describe '#increment_statistic_value' do + let(:project) { build_stubbed(:project) } + + subject(:increment) do + project.increment_statistic_value(:build_artifacts_size, -10) + end + + it 'increments the value' do + expect(ProjectStatistics) + .to receive(:increment_statistic) + .with(project, :build_artifacts_size, -10) + + increment + end + + context 'when the project is scheduled for removal' do + let(:project) { build_stubbed(:project, pending_delete: true) } + + it 'does not increment the value' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + increment + end + end + end + def finish_job(export_job) export_job.start export_job.finish diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb index 44014f93444..579a9e664cf 100644 --- a/spec/models/wiki_page_spec.rb +++ b/spec/models/wiki_page_spec.rb @@ -620,16 +620,12 @@ RSpec.describe WikiPage do end describe "#versions" do - include_context 'subject is persisted page' + let(:subject) { create_wiki_page } it "returns an array of all commits for the page" do - 3.times { |i| subject.update(content: "content #{i}") } - - expect(subject.versions.count).to eq(4) - end - - it 'returns instances of WikiPageVersion' do - expect(subject.versions).to all( be_a(Gitlab::Git::WikiPageVersion) ) + expect do + 3.times { |i| subject.update(content: "content #{i}") } + end.to change { subject.versions.count }.by(3) end end @@ -777,8 +773,11 @@ RSpec.describe WikiPage do end describe '#historical?' do - include_context 'subject is persisted page' + let!(:container) { create(:project) } + + subject { create_wiki_page } + let(:wiki) { subject.wiki } let(:old_version) { subject.versions.last.id } let(:old_page) { wiki.find_page(subject.title, old_version) } let(:latest_version) { subject.versions.first.id } diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index 540647fb699..cc837e7544c 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -60,7 +60,7 @@ RSpec.describe ProjectPolicy do end it 'does not include the issues permissions' do - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident end it 'disables boards and lists permissions' do @@ -72,7 +72,7 @@ RSpec.describe ProjectPolicy do it 'does not include the issues permissions' do create(:jira_service, project: project) - expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue, :create_incident end end end diff --git a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb index ee0085718b3..9d98498ca8a 100644 --- a/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb +++ b/spec/requests/api/graphql/project/issue/design_collection/versions_spec.rb @@ -33,6 +33,7 @@ RSpec.describe 'Getting versions related to an issue' do let(:version_params) { nil } let(:version_query_fields) { ['edges { node { sha } }'] } + let(:edges_path) { %w[project issue designCollection versions edges] } let(:project) { issue.project } let(:current_user) { owner } @@ -50,8 +51,7 @@ RSpec.describe 'Getting versions related to an issue' do end def response_values(data = graphql_data, key = 'sha') - path = %w[project issue designCollection versions edges] - data.dig(*path).map { |e| e.dig('node', key) } + data.dig(*edges_path).map { |e| e.dig('node', key) } end before do @@ -64,6 +64,19 @@ RSpec.describe 'Getting versions related to an issue' do expect(response_values).to match_array([version_a, version_b, version_c, version_d].map(&:sha)) end + context 'with all fields requested' do + let(:version_query_fields) do + ['edges { node { id sha createdAt author { id } } }'] + end + + it 'returns correct data' do + post_graphql(query, current_user: current_user) + + keys = graphql_data.dig(*edges_path).first['node'].keys + expect(keys).to match_array(%w(id sha createdAt author)) + end + end + describe 'filter by sha' do let(:sha) { version_b.sha } diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb index e4eac9ee174..a13db1bb414 100644 --- a/spec/requests/api/merge_requests_spec.rb +++ b/spec/requests/api/merge_requests_spec.rb @@ -2154,7 +2154,7 @@ RSpec.describe API::MergeRequests do end end - describe 'PUT /projects/:id/merge_reuests/:merge_request_iid' do + describe 'PUT /projects/:id/merge_requests/:merge_request_iid' do it_behaves_like 'issuable update endpoint' do let(:entity) { merge_request } end @@ -2176,6 +2176,68 @@ RSpec.describe API::MergeRequests do end end + context 'when assignee_id=user2.id' do + let(:params) do + { + assignee_id: user2.id + } + end + + it 'sets the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to contain_exactly( + a_hash_including('name' => user2.name) + ) + end + end + + context 'when only assignee_ids are provided, and the list is empty' do + let(:params) do + { + assignee_ids: [] + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + + context 'when only assignee_ids are provided, and the list contains the sentinel value' do + let(:params) do + { + assignee_ids: [0] + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + + context 'when only assignee_id=0' do + let(:params) do + { + assignee_id: 0 + } + end + + it 'clears the assignees' do + put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user), params: params + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['assignees']).to be_empty + end + end + context 'accepts reviewer_ids' do let(:params) do { diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 1cbf1914c0c..f31cfcb8499 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -21,6 +21,10 @@ RSpec.describe JobEntity do subject { entity.as_json } + it 'contains complete to indicate if a pipeline is completed' do + expect(subject).to include(:complete) + end + it 'contains paths to job page action' do expect(subject).to include(:build_path) end diff --git a/spec/services/ci/destroy_pipeline_service_spec.rb b/spec/services/ci/destroy_pipeline_service_spec.rb index f226a129fac..302233cea5a 100644 --- a/spec/services/ci/destroy_pipeline_service_spec.rb +++ b/spec/services/ci/destroy_pipeline_service_spec.rb @@ -3,7 +3,8 @@ require 'spec_helper' RSpec.describe ::Ci::DestroyPipelineService do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } + let!(:pipeline) { create(:ci_pipeline, :success, project: project, sha: project.commit.id) } subject { described_class.new(project, user).execute(pipeline) } @@ -60,6 +61,10 @@ RSpec.describe ::Ci::DestroyPipelineService do expect { artifact.reload }.to raise_error(ActiveRecord::RecordNotFound) end + + it 'inserts deleted objects for object storage files' do + expect { subject }.to change { Ci::DeletedObject.count } + end end end end diff --git a/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb new file mode 100644 index 00000000000..b1a4741851b --- /dev/null +++ b/spec/services/ci/job_artifacts/destroy_associations_service_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Ci::JobArtifacts::DestroyAssociationsService do + let(:artifacts) { Ci::JobArtifact.all } + let(:service) { described_class.new(artifacts) } + + let_it_be(:artifact, refind: true) do + create(:ci_job_artifact) + end + + before do + artifact.file = fixture_file_upload(Rails.root.join('spec/fixtures/ci_build_artifacts.zip'), 'application/zip') + artifact.save! + end + + describe '#destroy_records' do + it 'removes artifacts without updating statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + expect { service.destroy_records }.to change { Ci::JobArtifact.count } + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.destroy_records }.not_to raise_error + end + end + end + + describe '#update_statistics' do + before do + service.destroy_records + end + + it 'updates project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + + service.update_statistics + end + + context 'when there are no artifacts' do + let(:artifacts) { Ci::JobArtifact.none } + + it 'does not raise error' do + expect { service.update_statistics }.not_to raise_error + end + end + end +end diff --git a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb index 52aaf73d67e..2cedbf93d74 100644 --- a/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb +++ b/spec/services/ci/job_artifacts/destroy_batch_service_spec.rb @@ -3,8 +3,6 @@ require 'spec_helper' RSpec.describe Ci::JobArtifacts::DestroyBatchService do - include ExclusiveLeaseHelpers - let(:artifacts) { Ci::JobArtifact.all } let(:service) { described_class.new(artifacts, pick_up_at: Time.current) } @@ -25,14 +23,6 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do expect { subject }.to change { Ci::DeletedObject.count }.by(1) end - it 'resets project statistics' do - expect(ProjectStatistics).to receive(:increment_statistic).once - .with(artifact.project, :build_artifacts_size, -artifact.file.size) - .and_call_original - - execute - end - it 'does not remove the files' do expect { execute }.not_to change { artifact.file.exists? } end @@ -44,6 +34,29 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do execute end + + context 'ProjectStatistics' do + it 'resets project statistics' do + expect(ProjectStatistics).to receive(:increment_statistic).once + .with(artifact.project, :build_artifacts_size, -artifact.file.size) + .and_call_original + + execute + end + + context 'with update_stats: false' do + it 'does not update project statistics' do + expect(ProjectStatistics).not_to receive(:increment_statistic) + + service.execute(update_stats: false) + end + + it 'returns size statistics' do + expect(service.execute(update_stats: false)).to match( + a_hash_including(statistics_updates: { artifact.project => -artifact.file.size })) + end + end + end end context 'when failed to destroy artifact' do @@ -65,16 +78,12 @@ RSpec.describe Ci::JobArtifacts::DestroyBatchService do context 'when there are no artifacts' do let(:artifacts) { Ci::JobArtifact.none } - before do - artifact.destroy! - end - it 'does not raise error' do expect { execute }.not_to raise_error end it 'reports the number of destroyed artifacts' do - is_expected.to eq(destroyed_artifacts_count: 0, status: :success) + is_expected.to eq(destroyed_artifacts_count: 0, statistics_updates: {}, status: :success) end end end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index d0f228fb3d9..3f506ec58b0 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -184,9 +184,9 @@ RSpec.describe Issues::BuildService do end it 'cannot set invalid type' do - expect do - build_issue(issue_type: 'invalid type') - end.to raise_error(ArgumentError, "'invalid type' is not a valid issue_type") + issue = build_issue(issue_type: 'invalid type') + + expect(issue).to be_issue end end end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index aa9eb0e6a0d..3ea2727dc60 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -25,6 +25,35 @@ RSpec.describe Labels::FindOrCreateService do project.add_developer(user) end + context 'when existing_labels_by_title is provided' do + let(:preloaded_label) { build(:label, title: 'Security') } + + before do + params.merge!( + existing_labels_by_title: { + 'Security' => preloaded_label + }) + end + + context 'when label exists' do + it 'returns preloaded label' do + expect(service.execute).to eq preloaded_label + end + end + + context 'when label does not exists' do + before do + params[:title] = 'Audit' + end + + it 'does not generates additional label search' do + service.execute + + expect(LabelsFinder).not_to receive(:new) + end + end + end + context 'when label does not exist at group level' do it 'creates a new label at project level' do expect { service.execute }.to change(project.labels, :count).by(1) diff --git a/spec/services/merge_requests/update_assignees_service_spec.rb b/spec/services/merge_requests/update_assignees_service_spec.rb index 113bfb0f31a..076161c9029 100644 --- a/spec/services/merge_requests/update_assignees_service_spec.rb +++ b/spec/services/merge_requests/update_assignees_service_spec.rb @@ -36,6 +36,22 @@ RSpec.describe MergeRequests::UpdateAssigneesService do end context 'when the parameters are valid' do + context 'when using sentinel values' do + let(:opts) { { assignee_ids: [0] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + + context 'the assignee_ids parameter is the empty list' do + let(:opts) { { assignee_ids: [] } } + + it 'removes all assignees' do + expect { update_merge_request }.to change(merge_request, :assignees).to([]) + end + end + it 'updates the MR, and queues the more expensive work for later' do expect_next(MergeRequests::HandleAssigneesChangeService, project: project, current_user: user) do |service| expect(service) diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 266c8d5ee84..35dc709b5d9 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -15,7 +15,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:base_guest_permissions) do %i[ - award_emoji create_issue create_merge_request_in create_note + award_emoji create_issue create_incident create_merge_request_in create_note create_project read_issue_board read_issue read_issue_iid read_issue_link read_label read_issue_board_list read_milestone read_note read_project read_project_for_iids read_project_member read_release read_snippet diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index 0a040557ffe..cfee26a0d6a 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -130,8 +130,8 @@ RSpec.shared_examples 'wiki controller actions' do it_behaves_like 'fetching history', :ok do let(:allow_read_wiki) { true } - it 'assigns @page_versions' do - expect(assigns(:page_versions)).to be_present + it 'assigns @commits' do + expect(assigns(:commits)).to be_present end end diff --git a/spec/support/shared_examples/policies/project_policy_shared_examples.rb b/spec/support/shared_examples/policies/project_policy_shared_examples.rb index d05e5eb9120..013c9b61b99 100644 --- a/spec/support/shared_examples/policies/project_policy_shared_examples.rb +++ b/spec/support/shared_examples/policies/project_policy_shared_examples.rb @@ -57,7 +57,7 @@ RSpec.shared_examples 'project policies as anonymous' do context 'when a project has pending invites' do let(:group) { create(:group, :public) } let(:project) { create(:project, :public, namespace: group) } - let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji] } + let(:user_permissions) { [:create_merge_request_in, :create_project, :create_issue, :create_note, :upload_file, :award_emoji, :create_incident] } let(:anonymous_permissions) { guest_permissions - user_permissions } let(:current_user) { anonymous } diff --git a/spec/views/layouts/_head.html.haml_spec.rb b/spec/views/layouts/_head.html.haml_spec.rb index ef0bd97cbcf..6752bdc8337 100644 --- a/spec/views/layouts/_head.html.haml_spec.rb +++ b/spec/views/layouts/_head.html.haml_spec.rb @@ -62,12 +62,6 @@ RSpec.describe 'layouts/_head' do expect(rendered).to match('<link rel="stylesheet" media="print" href="/stylesheets/highlight/themes/solarised-light.css" />') end - it 'preloads Monaco' do - render - - expect(rendered).to match('<link rel="preload" href="/assets/webpack/monaco.chunk.js" as="script" type="text/javascript">') - end - context 'when an asset_host is set and snowplow url is set' do let(:asset_host) { 'http://test.host' } let(:snowplow_collector_hostname) { 'www.snow.plow' } |