diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-11 06:13:09 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-11 06:13:09 +0000 |
commit | be7d70b884e6fa66c52862f38bf0f39b0631868b (patch) | |
tree | 235616671718bf2f39855f663677b61a55a8d68c /spec | |
parent | 848ba57883b4ea9164bcb56a16c0fcb2b55b56e6 (diff) | |
download | gitlab-ce-be7d70b884e6fa66c52862f38bf0f39b0631868b.tar.gz |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
15 files changed, 764 insertions, 25 deletions
diff --git a/spec/features/projects/files/user_creates_directory_spec.rb b/spec/features/projects/files/user_creates_directory_spec.rb index 46b93d738e1..5ad7641a5be 100644 --- a/spec/features/projects/files/user_creates_directory_spec.rb +++ b/spec/features/projects/files/user_creates_directory_spec.rb @@ -98,12 +98,14 @@ RSpec.describe 'Projects > Files > User creates a directory', :js do expect(page).to have_content(fork_message) find('.add-to-tree').click + wait_for_requests click_link('New directory') fill_in(:dir_name, with: 'new_directory') fill_in(:commit_message, with: 'New commit message', visible: true) click_button('Create directory') fork = user.fork_of(project2.reload) + wait_for_requests expect(current_path).to eq(project_new_merge_request_path(fork)) end diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js index 6f186ba3227..18b68d91e01 100644 --- a/spec/frontend/lib/utils/url_utility_spec.js +++ b/spec/frontend/lib/utils/url_utility_spec.js @@ -1004,4 +1004,39 @@ describe('URL utility', () => { expect(urlUtils.isSameOriginUrl(url)).toBe(expected); }); }); + + describe('constructWebIDEPath', () => { + let originalGl; + const projectIDEPath = '/foo/bar'; + const sourceProj = 'my_-fancy-proj/boo'; + const targetProj = 'boo/another-fancy-proj'; + const mrIid = '7'; + + beforeEach(() => { + originalGl = window.gl; + window.gl = { webIDEPath: projectIDEPath }; + }); + + afterEach(() => { + window.gl = originalGl; + }); + + it.each` + sourceProjectFullPath | targetProjectFullPath | iid | expectedPath + ${undefined} | ${undefined} | ${undefined} | ${projectIDEPath} + ${undefined} | ${undefined} | ${mrIid} | ${projectIDEPath} + ${undefined} | ${targetProj} | ${undefined} | ${projectIDEPath} + ${undefined} | ${targetProj} | ${mrIid} | ${projectIDEPath} + ${sourceProj} | ${undefined} | ${undefined} | ${projectIDEPath} + ${sourceProj} | ${targetProj} | ${undefined} | ${projectIDEPath} + ${sourceProj} | ${undefined} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`} + ${sourceProj} | ${sourceProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=`} + ${sourceProj} | ${targetProj} | ${mrIid} | ${`/-/ide/project/${sourceProj}/merge_requests/${mrIid}?target_project=${encodeURIComponent(targetProj)}`} + `( + 'returns $expectedPath for "$sourceProjectFullPath + $targetProjectFullPath + $iid"', + ({ expectedPath, ...args } = {}) => { + expect(urlUtils.constructWebIDEPath(args)).toBe(expectedPath); + }, + ); + }); }); diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js index 0733cffe4f4..eb957c635ac 100644 --- a/spec/frontend/repository/components/breadcrumbs_spec.js +++ b/spec/frontend/repository/components/breadcrumbs_spec.js @@ -2,6 +2,7 @@ import { GlDropdown } from '@gitlab/ui'; import { shallowMount, RouterLinkStub } from '@vue/test-utils'; import Breadcrumbs from '~/repository/components/breadcrumbs.vue'; import UploadBlobModal from '~/repository/components/upload_blob_modal.vue'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; const defaultMockRoute = { name: 'blobPath', @@ -10,7 +11,7 @@ const defaultMockRoute = { describe('Repository breadcrumbs component', () => { let wrapper; - const factory = (currentPath, extraProps = {}, mockRoute = {}) => { + const factory = (currentPath, extraProps = {}, mockRoute = {}, newDirModal = true) => { const $apollo = { queries: { userPermissions: { @@ -34,10 +35,12 @@ describe('Repository breadcrumbs component', () => { }, $apollo, }, + provide: { glFeatures: { newDirModal } }, }); }; const findUploadBlobModal = () => wrapper.find(UploadBlobModal); + const findNewDirectoryModal = () => wrapper.find(NewDirectoryModal); afterEach(() => { wrapper.destroy(); @@ -121,4 +124,37 @@ describe('Repository breadcrumbs component', () => { expect(findUploadBlobModal().exists()).toBe(true); }); }); + + describe('renders the new directory modal', () => { + describe('with the feature flag enabled', () => { + beforeEach(() => { + window.gon.features = { + newDirModal: true, + }; + factory('/', { canEditTree: true }); + }); + + it('does not render the modal while loading', () => { + expect(findNewDirectoryModal().exists()).toBe(false); + }); + + it('renders the modal once loaded', async () => { + wrapper.setData({ $apollo: { queries: { userPermissions: { loading: false } } } }); + + await wrapper.vm.$nextTick(); + + expect(findNewDirectoryModal().exists()).toBe(true); + }); + }); + + describe('with the feature flag disabled', () => { + it('does not render the modal', () => { + window.gon.features = { + newDirModal: false, + }; + factory('/', { canEditTree: true }, {}, {}, false); + expect(findNewDirectoryModal().exists()).toBe(false); + }); + }); + }); }); diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js new file mode 100644 index 00000000000..fe7f024e3ea --- /dev/null +++ b/spec/frontend/repository/components/new_directory_modal_spec.js @@ -0,0 +1,203 @@ +import { GlModal, GlFormTextarea, GlToggle } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import createFlash from '~/flash'; +import httpStatusCodes from '~/lib/utils/http_status'; +import { visitUrl } from '~/lib/utils/url_utility'; +import NewDirectoryModal from '~/repository/components/new_directory_modal.vue'; + +jest.mock('~/flash'); +jest.mock('~/lib/utils/url_utility', () => ({ + visitUrl: jest.fn(), +})); + +const initialProps = { + modalTitle: 'Create New Directory', + modalId: 'modal-new-directory', + commitMessage: 'Add new directory', + targetBranch: 'some-target-branch', + originalBranch: 'master', + canPushCode: true, + path: 'create_dir', +}; + +const defaultFormValue = { + dirName: 'foo', + originalBranch: initialProps.originalBranch, + branchName: initialProps.targetBranch, + commitMessage: initialProps.commitMessage, + createNewMr: true, +}; + +describe('NewDirectoryModal', () => { + let wrapper; + let mock; + + const createComponent = (props = {}) => { + wrapper = shallowMount(NewDirectoryModal, { + propsData: { + ...initialProps, + ...props, + }, + attrs: { + static: true, + visible: true, + }, + }); + }; + + const findModal = () => wrapper.findComponent(GlModal); + const findDirName = () => wrapper.find('[name="dir_name"]'); + const findBranchName = () => wrapper.find('[name="branch_name"]'); + const findCommitMessage = () => wrapper.findComponent(GlFormTextarea); + const findMrToggle = () => wrapper.findComponent(GlToggle); + + const fillForm = async (inputValue = {}) => { + const { + dirName = defaultFormValue.dirName, + branchName = defaultFormValue.branchName, + commitMessage = defaultFormValue.commitMessage, + createNewMr = true, + } = inputValue; + + await findDirName().vm.$emit('input', dirName); + await findBranchName().vm.$emit('input', branchName); + await findCommitMessage().vm.$emit('input', commitMessage); + await findMrToggle().vm.$emit('change', createNewMr); + await nextTick; + }; + + const submitForm = async () => { + const mockEvent = { preventDefault: jest.fn() }; + findModal().vm.$emit('primary', mockEvent); + await waitForPromises(); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders modal component', () => { + createComponent(); + + const { modalTitle: title } = initialProps; + + expect(findModal().props()).toMatchObject({ + title, + size: 'md', + actionPrimary: { + text: NewDirectoryModal.i18n.PRIMARY_OPTIONS_TEXT, + }, + actionCancel: { + text: 'Cancel', + }, + }); + }); + + describe('form', () => { + it.each` + component | defaultValue | canPushCode | targetBranch | originalBranch | exist + ${findDirName} | ${undefined} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${initialProps.targetBranch} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findBranchName} | ${undefined} | ${false} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${false} + ${findCommitMessage} | ${initialProps.commitMessage} | ${true} | ${initialProps.targetBranch} | ${initialProps.originalBranch} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'new-target-branch'} | ${'master'} | ${true} + ${findMrToggle} | ${'true'} | ${true} | ${'master'} | ${'master'} | ${true} + `( + 'has the correct form fields ', + ({ component, defaultValue, canPushCode, targetBranch, originalBranch, exist }) => { + createComponent({ + canPushCode, + targetBranch, + originalBranch, + }); + const formField = component(); + + if (!exist) { + expect(formField.exists()).toBe(false); + return; + } + + expect(formField.exists()).toBe(true); + expect(formField.attributes('value')).toBe(defaultValue); + }, + ); + }); + + describe('form submission', () => { + beforeEach(async () => { + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + }); + + describe('valid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('passes the formData', async () => { + const { + dirName, + branchName, + commitMessage, + originalBranch, + createNewMr, + } = defaultFormValue; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm(); + await submitForm(); + + expect(mock.history.post[0].data.get('dir_name')).toEqual(dirName); + expect(mock.history.post[0].data.get('branch_name')).toEqual(branchName); + expect(mock.history.post[0].data.get('commit_message')).toEqual(commitMessage); + expect(mock.history.post[0].data.get('original_branch')).toEqual(originalBranch); + expect(mock.history.post[0].data.get('create_merge_request')).toEqual(String(createNewMr)); + }); + + it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => { + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {}); + await fillForm({ createNewMr: false }); + await submitForm(); + expect(mock.history.post[0].data.get('create_merge_request')).toBeNull(); + }); + + it('redirects to the new directory', async () => { + const response = { filePath: 'new-dir-path' }; + mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(visitUrl).toHaveBeenCalledWith(response.filePath); + }); + }); + + describe('invalid form', () => { + beforeEach(() => { + createComponent(); + }); + + it('disables submit button', async () => { + await fillForm({ dirName: '', branchName: '', commitMessage: '' }); + expect(findModal().props('actionPrimary').attributes[0].disabled).toBe(true); + }); + + it('creates a flash error', async () => { + mock.onPost(initialProps.path).timeout(); + + await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' }); + await submitForm(); + + expect(createFlash).toHaveBeenCalledWith({ + message: NewDirectoryModal.i18n.ERROR_MESSAGE, + }); + }); + }); + }); +}); diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index bb82fa706fd..3f822db601f 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -24,4 +24,32 @@ describe('Repository router spec', () => { expect(componentsForRoute).toContain(component); } }); + + describe('Storing Web IDE path globally', () => { + const proj = 'foo-bar-group/foo-bar-proj'; + let originalGl; + + beforeEach(() => { + originalGl = window.gl; + }); + + afterEach(() => { + window.gl = originalGl; + }); + + it.each` + path | branch | expectedPath + ${'/'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/tree/feat(test)'} | ${'feat(test)'} | ${`/-/ide/project/${proj}/edit/feat(test)/-/`} + ${'/-/tree/main'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/`} + ${'/-/tree/main/app/assets'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/app/assets/`} + ${'/-/blob/main/file.md'} | ${'main'} | ${`/-/ide/project/${proj}/edit/main/-/file.md`} + `('generates the correct Web IDE url for $path', ({ path, branch, expectedPath } = {}) => { + const router = createRouter(proj, branch); + + router.push(path); + expect(window.gl.webIDEPath).toBe(expectedPath); + }); + }); }); diff --git a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb index c7c00f54c0c..51a63e66b93 100644 --- a/spec/graphql/resolvers/project_pipelines_resolver_spec.rb +++ b/spec/graphql/resolvers/project_pipelines_resolver_spec.rb @@ -11,15 +11,23 @@ RSpec.describe Resolvers::ProjectPipelinesResolver do let(:current_user) { create(:user) } - before do - project.add_developer(current_user) + context 'when the user does have access' do + before do + project.add_developer(current_user) + end + + it 'resolves only MRs for the passed merge request' do + expect(resolve_pipelines).to contain_exactly(pipeline) + end end - def resolve_pipelines - resolve(described_class, obj: project, ctx: { current_user: current_user }) + context 'when the user does not have access' do + it 'does not return pipeline data' do + expect(resolve_pipelines).to be_empty + end end - it 'resolves only MRs for the passed merge request' do - expect(resolve_pipelines).to contain_exactly(pipeline) + def resolve_pipelines + resolve(described_class, obj: project, ctx: { current_user: current_user }) end end diff --git a/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb new file mode 100644 index 00000000000..4f437e57600 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/check_result_spec.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::CheckResult do + subject(:check_result) { described_class } + + let(:time) { Time.current } + + around do |example| + freeze_time do + example.run + end + end + + describe '.default_payload' do + it 'returns the expected defaults' do + expect(check_result.default_payload).to eq({ last_run_at: time }) + end + end + + describe '.success' do + subject(:success) { check_result.success(payload: payload) } + + let(:payload) { {} } + + it 'creates a success result' do + expect(success.status).to eq described_class::SUCCESS_STATUS + end + + it 'uses the default payload' do + expect(success.payload).to eq described_class.default_payload + end + + context 'when given a payload' do + let(:payload) { { last_run_at: time + 1.day, test: 'test' } } + + it 'uses the payload passed' do + expect(success.payload).to eq payload + end + end + end + + describe '.failed' do + subject(:failed) { check_result.failed(payload: payload) } + + let(:payload) { {} } + + it 'creates a failure result' do + expect(failed.status).to eq described_class::FAILED_STATUS + end + + it 'uses the default payload' do + expect(failed.payload).to eq described_class.default_payload + end + + context 'when given a payload' do + let(:payload) { { last_run_at: time + 1.day, test: 'test' } } + + it 'uses the payload passed' do + expect(failed.payload).to eq payload + end + end + end + + describe '.from_hash' do + subject(:from_hash) { described_class.from_hash(hash) } + + let(:status) { described_class::SUCCESS_STATUS } + let(:payload) { { test: 'test' } } + let(:hash) do + { + status: status, + payload: payload + } + end + + it 'returns the expected status and payload' do + expect(from_hash.status).to eq status + expect(from_hash.payload).to eq payload + end + end + + describe '#to_hash' do + subject(:to_hash) { described_class.new(**hash).to_hash } + + let(:status) { described_class::SUCCESS_STATUS } + let(:payload) { { test: 'test' } } + let(:hash) do + { + status: status, + payload: payload + } + end + + it 'returns the expected hash' do + expect(to_hash).to eq hash + end + end + + describe '#failed?' do + subject(:failed) { described_class.new(status: status).failed? } + + context 'when it has failed' do + let(:status) { described_class::FAILED_STATUS } + + it 'returns true' do + expect(failed).to eq true + end + end + + context 'when it has succeeded' do + let(:status) { described_class::SUCCESS_STATUS } + + it 'returns false' do + expect(failed).to eq false + end + end + end + + describe '#success?' do + subject(:success) { described_class.new(status: status).success? } + + context 'when it has failed' do + let(:status) { described_class::FAILED_STATUS } + + it 'returns false' do + expect(success).to eq false + end + end + + context 'when it has succeeded' do + let(:status) { described_class::SUCCESS_STATUS } + + it 'returns true' do + expect(success).to eq true + end + end + end +end diff --git a/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb new file mode 100644 index 00000000000..e5475d04d86 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/redis_interface_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::RedisInterface, :clean_gitlab_redis_shared_state do + subject(:redis_interface) { described_class.new } + + let(:merge_check) { double(cache_key: '13') } + let(:result_hash) { { 'test' => 'test' } } + let(:expected_key) { "#{merge_check.cache_key}:#{described_class::VERSION}" } + + describe '#save_check' do + it 'saves the hash' do + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(expected_key) }).to be_nil + + redis_interface.save_check(merge_check: merge_check, result_hash: result_hash) + + expect(Gitlab::Redis::SharedState.with { |redis| redis.get(expected_key) }).to eq result_hash.to_json + end + end + + describe '#retrieve_check' do + it 'returns the hash' do + Gitlab::Redis::SharedState.with { |redis| redis.set(expected_key, result_hash.to_json) } + + expect(redis_interface.retrieve_check(merge_check: merge_check)).to eq result_hash + end + end +end diff --git a/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb new file mode 100644 index 00000000000..d376dcb5b18 --- /dev/null +++ b/spec/lib/gitlab/merge_requests/mergeability/results_store_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::MergeRequests::Mergeability::ResultsStore do + subject(:results_store) { described_class.new(merge_request: merge_request, interface: interface) } + + let(:merge_check) { double } + let(:interface) { double } + let(:merge_request) { double } + + describe '#read' do + it 'calls #retrieve on the interface' do + expect(interface).to receive(:retrieve_check).with(merge_check: merge_check) + + results_store.read(merge_check: merge_check) + end + end + + describe '#write' do + let(:result_hash) { double } + + it 'calls #save_check on the interface' do + expect(interface).to receive(:save_check).with(merge_check: merge_check, result_hash: result_hash) + + results_store.write(merge_check: merge_check, result_hash: result_hash) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 1eb54ee73f9..3711f304bd2 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -3089,7 +3089,7 @@ RSpec.describe MergeRequest, factory_default: :keep do end end - describe '#mergeable_state?' do + shared_examples 'for mergeable_state' do subject { create(:merge_request) } it 'checks if merge request can be merged' do @@ -3130,33 +3130,61 @@ RSpec.describe MergeRequest, factory_default: :keep do end context 'when failed' do - context 'when #mergeable_ci_state? is false' do - before do - allow(subject).to receive(:mergeable_ci_state?) { false } - end + shared_examples 'failed skip_ci_check' do + context 'when #mergeable_ci_state? is false' do + before do + allow(subject).to receive(:mergeable_ci_state?) { false } + end - it 'returns false' do - expect(subject.mergeable_state?).to be_falsey + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + + it 'returns true when skipping ci check' do + expect(subject.mergeable_state?(skip_ci_check: true)).to be(true) + end end - it 'returns true when skipping ci check' do - expect(subject.mergeable_state?(skip_ci_check: true)).to be(true) + context 'when #mergeable_discussions_state? is false' do + before do + allow(subject).to receive(:mergeable_discussions_state?) { false } + end + + it 'returns false' do + expect(subject.mergeable_state?).to be_falsey + end + + it 'returns true when skipping discussions check' do + expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true) + end end end - context 'when #mergeable_discussions_state? is false' do + context 'when improved_mergeability_checks is on' do + it_behaves_like 'failed skip_ci_check' + end + + context 'when improved_mergeability_checks is off' do before do - allow(subject).to receive(:mergeable_discussions_state?) { false } + stub_feature_flags(improved_mergeability_checks: false) end - it 'returns false' do - expect(subject.mergeable_state?).to be_falsey - end + it_behaves_like 'failed skip_ci_check' + end + end + end - it 'returns true when skipping discussions check' do - expect(subject.mergeable_state?(skip_discussions_check: true)).to be(true) - end + describe '#mergeable_state?' do + context 'when merge state caching is on' do + it_behaves_like 'for mergeable_state' + end + + context 'when merge state caching is off' do + before do + stub_feature_flags(mergeability_caching: false) end + + it_behaves_like 'for mergeable_state' end end diff --git a/spec/services/merge_requests/mergeability/check_base_service_spec.rb b/spec/services/merge_requests/mergeability/check_base_service_spec.rb new file mode 100644 index 00000000000..f07522b43cb --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_base_service_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckBaseService do + subject(:check_base_service) { described_class.new(merge_request: merge_request, params: params) } + + let(:merge_request) { double } + let(:params) { double } + + describe '#merge_request' do + it 'returns the merge_request' do + expect(check_base_service.merge_request).to eq merge_request + end + end + + describe '#params' do + it 'returns the params' do + expect(check_base_service.params).to eq params + end + end + + describe '#skip?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end + + describe '#cacheable?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end + + describe '#cache_key?' do + it 'raises NotImplementedError' do + expect { check_base_service.skip? }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb new file mode 100644 index 00000000000..6fbbecd7c0e --- /dev/null +++ b/spec/services/merge_requests/mergeability/check_ci_status_service_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::CheckCiStatusService do + subject(:check_ci_status) { described_class.new(merge_request: merge_request, params: params) } + + let(:merge_request) { build(:merge_request) } + let(:params) { { skip_ci_check: skip_check } } + let(:skip_check) { false } + + describe '#execute' do + before do + expect(merge_request).to receive(:mergeable_ci_state?).and_return(mergeable) + end + + context 'when the merge request is in a mergable state' do + let(:mergeable) { true } + + it 'returns a check result with status success' do + expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::SUCCESS_STATUS + end + end + + context 'when the merge request is not in a mergeable state' do + let(:mergeable) { false } + + it 'returns a check result with status failed' do + expect(check_ci_status.execute.status).to eq Gitlab::MergeRequests::Mergeability::CheckResult::FAILED_STATUS + end + end + end + + describe '#skip?' do + context 'when skip check is true' do + let(:skip_check) { true } + + it 'returns true' do + expect(check_ci_status.skip?).to eq true + end + end + + context 'when skip check is false' do + let(:skip_check) { false } + + it 'returns false' do + expect(check_ci_status.skip?).to eq false + end + end + end + + describe '#cacheable?' do + it 'returns false' do + expect(check_ci_status.cacheable?).to eq false + end + end +end diff --git a/spec/services/merge_requests/mergeability/run_checks_service_spec.rb b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb new file mode 100644 index 00000000000..170d99f4642 --- /dev/null +++ b/spec/services/merge_requests/mergeability/run_checks_service_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe MergeRequests::Mergeability::RunChecksService do + subject(:run_checks) { described_class.new(merge_request: merge_request, params: {}) } + + let_it_be(:merge_request) { create(:merge_request) } + + describe '#CHECKS' do + it 'contains every subclass of the base checks service' do + expect(described_class::CHECKS).to contain_exactly(*MergeRequests::Mergeability::CheckBaseService.subclasses) + end + end + + describe '#execute' do + subject(:execute) { run_checks.execute } + + let(:params) { {} } + let(:success_result) { Gitlab::MergeRequests::Mergeability::CheckResult.success } + + context 'when every check is skipped' do + before do + MergeRequests::Mergeability::CheckBaseService.subclasses.each do |subclass| + expect_next_instance_of(subclass) do |service| + expect(service).to receive(:skip?).and_return(true) + end + end + end + + it 'is still a success' do + expect(execute.all?(&:success?)).to eq(true) + end + end + + context 'when a check is skipped' do + it 'does not execute the check' do + expect_next_instance_of(MergeRequests::Mergeability::CheckCiStatusService) do |service| + expect(service).to receive(:skip?).and_return(true) + expect(service).not_to receive(:execute) + end + + expect(execute).to match_array([]) + end + end + + context 'when a check is not skipped' do + let(:cacheable) { true } + let(:merge_check) { instance_double(MergeRequests::Mergeability::CheckCiStatusService) } + + before do + expect(MergeRequests::Mergeability::CheckCiStatusService).to receive(:new).and_return(merge_check) + expect(merge_check).to receive(:skip?).and_return(false) + allow(merge_check).to receive(:cacheable?).and_return(cacheable) + allow(merge_check).to receive(:execute).and_return(success_result) + end + + context 'when the check is cacheable' do + context 'when the check is cached' do + it 'returns the cached result' do + expect_next_instance_of(Gitlab::MergeRequests::Mergeability::ResultsStore) do |service| + expect(service).to receive(:read).with(merge_check: merge_check).and_return(success_result) + end + + expect(execute).to match_array([success_result]) + end + end + + context 'when the check is not cached' do + it 'writes and returns the result' do + expect_next_instance_of(Gitlab::MergeRequests::Mergeability::ResultsStore) do |service| + expect(service).to receive(:read).with(merge_check: merge_check).and_return(nil) + expect(service).to receive(:write).with(merge_check: merge_check, result_hash: success_result.to_hash).and_return(true) + end + + expect(execute).to match_array([success_result]) + end + end + end + + context 'when check is not cacheable' do + let(:cacheable) { false } + + it 'does not call the results store' do + expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) + + expect(execute).to match_array([success_result]) + end + end + + context 'when mergeability_caching is turned off' do + before do + stub_feature_flags(mergeability_caching: false) + end + + it 'does not call the results store' do + expect(Gitlab::MergeRequests::Mergeability::ResultsStore).not_to receive(:new) + + expect(execute).to match_array([success_result]) + end + end + end + end +end diff --git a/spec/support/database/cross-database-modification-allowlist.yml b/spec/support/database/cross-database-modification-allowlist.yml index 87126bdcdc8..819a77a697b 100644 --- a/spec/support/database/cross-database-modification-allowlist.yml +++ b/spec/support/database/cross-database-modification-allowlist.yml @@ -1338,3 +1338,4 @@ - "./spec/workers/repository_cleanup_worker_spec.rb" - "./spec/workers/stage_update_worker_spec.rb" - "./spec/workers/stuck_merge_jobs_worker_spec.rb" +- "./ee/spec/requests/api/graphql/project/pipelines/dast_profile_spec.rb" diff --git a/spec/support/database/cross-join-allowlist.yml b/spec/support/database/cross-join-allowlist.yml index de4d2f8156a..87a91c80671 100644 --- a/spec/support/database/cross-join-allowlist.yml +++ b/spec/support/database/cross-join-allowlist.yml @@ -7,7 +7,6 @@ - "./ee/spec/features/merge_trains/user_adds_to_merge_train_when_pipeline_succeeds_spec.rb" - "./ee/spec/features/projects/pipelines/pipeline_spec.rb" - "./ee/spec/finders/ee/namespaces/projects_finder_spec.rb" -- "./ee/spec/finders/security/findings_finder_spec.rb" - "./ee/spec/graphql/ee/resolvers/namespace_projects_resolver_spec.rb" - "./ee/spec/lib/ee/gitlab/background_migration/migrate_approver_to_approval_rules_spec.rb" - "./ee/spec/lib/ee/gitlab/background_migration/migrate_security_scans_spec.rb" |