diff options
Diffstat (limited to 'spec')
49 files changed, 1816 insertions, 376 deletions
diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb index 21b6a6d45f5..b2d83a02290 100644 --- a/spec/controllers/projects/pipelines_settings_controller_spec.rb +++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb @@ -12,19 +12,22 @@ describe Projects::PipelinesSettingsController do end describe 'PATCH update' do - before do + subject do patch :update, namespace_id: project.namespace.to_param, project_id: project, - project: { - auto_devops_attributes: params - } + project: { auto_devops_attributes: params, + run_auto_devops_pipeline_implicit: 'false', + run_auto_devops_pipeline_explicit: auto_devops_pipeline } end context 'when updating the auto_devops settings' do let(:params) { { enabled: '', domain: 'mepmep.md' } } + let(:auto_devops_pipeline) { 'false' } it 'redirects to the settings page' do + subject + expect(response).to have_gitlab_http_status(302) expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.") end @@ -33,11 +36,32 @@ describe Projects::PipelinesSettingsController do let(:params) { { enabled: '' } } it 'allows enabled to be set to nil' do + subject project_auto_devops.reload expect(project_auto_devops.enabled).to be_nil end end + + context 'when run_auto_devops_pipeline is true' do + let(:auto_devops_pipeline) { 'true' } + + it 'queues a CreatePipelineWorker' do + expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) + + subject + end + end + + context 'when run_auto_devops_pipeline is not true' do + let(:auto_devops_pipeline) { 'false' } + + it 'does not queue a CreatePipelineWorker' do + expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args) + + subject + end + end end end end diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 50f8f13d261..a1b1d94ae06 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -500,6 +500,18 @@ describe 'Pipelines', :js do end it { expect(page).to have_content('Missing .gitlab-ci.yml file') } + it 'creates a pipeline after first request failed and a valid gitlab-ci.yml file is available when trying again' do + click_button project.default_branch + + stub_ci_pipeline_to_return_yaml_file + + page.within '.dropdown-menu' do + click_link 'master' + end + + expect { click_on 'Create pipeline' } + .to change { Ci::Pipeline.count }.by(1) + end end end end diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb index ea8f997409d..eb8e7265dd3 100644 --- a/spec/features/projects/settings/pipelines_settings_spec.rb +++ b/spec/features/projects/settings/pipelines_settings_spec.rb @@ -8,13 +8,14 @@ feature "Pipelines settings" do background do sign_in(user) project.team << [user, role] - visit project_pipelines_settings_path(project) end context 'for developer' do given(:role) { :developer } scenario 'to be disallowed to view' do + visit project_settings_ci_cd_path(project) + expect(page.status_code).to eq(404) end end @@ -23,6 +24,8 @@ feature "Pipelines settings" do given(:role) { :master } scenario 'be allowed to change' do + visit project_settings_ci_cd_path(project) + fill_in('Test coverage parsing', with: 'coverage_regex') click_on 'Save changes' @@ -32,6 +35,8 @@ feature "Pipelines settings" do end scenario 'updates auto_cancel_pending_pipelines' do + visit project_settings_ci_cd_path(project) + page.check('Auto-cancel redundant, pending pipelines') click_on 'Save changes' @@ -42,14 +47,119 @@ feature "Pipelines settings" do expect(checkbox).to be_checked end - scenario 'update auto devops settings' do - fill_in('project_auto_devops_attributes_domain', with: 'test.com') - page.choose('project_auto_devops_attributes_enabled_false') - click_on 'Save changes' + describe 'Auto DevOps' do + it 'update auto devops settings' do + visit project_settings_ci_cd_path(project) - expect(page.status_code).to eq(200) - expect(project.auto_devops).to be_present - expect(project.auto_devops).not_to be_enabled + fill_in('project_auto_devops_attributes_domain', with: 'test.com') + page.choose('project_auto_devops_attributes_enabled_false') + click_on 'Save changes' + + expect(page.status_code).to eq(200) + expect(project.auto_devops).to be_present + expect(project.auto_devops).not_to be_enabled + end + + describe 'Immediately run pipeline checkbox option', :js do + context 'when auto devops is set to instance default (enabled)' do + before do + stub_application_setting(auto_devops_enabled: true) + project.create_auto_devops!(enabled: nil) + visit project_settings_ci_cd_path(project) + end + + it 'does not show checkboxes on page-load' do + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) + end + + it 'selecting explicit disabled hides all checkboxes' do + page.choose('project_auto_devops_attributes_enabled_false') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) + end + + it 'selecting explicit enabled hides all checkboxes because we are already enabled' do + page.choose('project_auto_devops_attributes_enabled_true') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) + end + end + + context 'when auto devops is set to instance default (disabled)' do + before do + stub_application_setting(auto_devops_enabled: false) + project.create_auto_devops!(enabled: nil) + visit project_settings_ci_cd_path(project) + end + + it 'does not show checkboxes on page-load' do + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) + end + + it 'selecting explicit disabled hides all checkboxes' do + page.choose('project_auto_devops_attributes_enabled_false') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 1, visible: false) + end + + it 'selecting explicit enabled shows a checkbox' do + page.choose('project_auto_devops_attributes_enabled_true') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) + end + end + + context 'when auto devops is set to explicit disabled' do + before do + stub_application_setting(auto_devops_enabled: true) + project.create_auto_devops!(enabled: false) + visit project_settings_ci_cd_path(project) + end + + it 'does not show checkboxes on page-load' do + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper.hide', count: 2, visible: false) + end + + it 'selecting explicit enabled shows a checkbox' do + page.choose('project_auto_devops_attributes_enabled_true') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) + end + + it 'selecting instance default (enabled) shows a checkbox' do + page.choose('project_auto_devops_attributes_enabled_') + + expect(page).to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper:not(.hide)', count: 1) + end + end + + context 'when auto devops is set to explicit enabled' do + before do + stub_application_setting(auto_devops_enabled: false) + project.create_auto_devops!(enabled: true) + visit project_settings_ci_cd_path(project) + end + + it 'does not have any checkboxes' do + expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false) + end + end + + context 'when master contains a .gitlab-ci.yml file' do + let(:project) { create(:project, :repository) } + + before do + project.repository.create_file(user, '.gitlab-ci.yml', "script: ['test']", message: 'test', branch_name: project.default_branch) + stub_application_setting(auto_devops_enabled: true) + project.create_auto_devops!(enabled: false) + visit project_settings_ci_cd_path(project) + end + + it 'does not have any checkboxes' do + expect(page).not_to have_selector('.js-run-auto-devops-pipeline-checkbox-wrapper', visible: false) + end + end + end end end end diff --git a/spec/finders/runner_jobs_finder_spec.rb b/spec/finders/runner_jobs_finder_spec.rb new file mode 100644 index 00000000000..4275b1a7ff1 --- /dev/null +++ b/spec/finders/runner_jobs_finder_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe RunnerJobsFinder do + let(:project) { create(:project) } + let(:runner) { create(:ci_runner, :shared) } + + subject { described_class.new(runner, params).execute } + + describe '#execute' do + context 'when params is empty' do + let(:params) { {} } + let!(:job) { create(:ci_build, runner: runner, project: project) } + let!(:job1) { create(:ci_build, project: project) } + + it 'returns all jobs assigned to Runner' do + is_expected.to match_array(job) + is_expected.not_to match_array(job1) + end + end + + context 'when params contains status' do + HasStatus::AVAILABLE_STATUSES.each do |target_status| + context "when status is #{target_status}" do + let(:params) { { status: target_status } } + let!(:job) { create(:ci_build, runner: runner, project: project, status: target_status) } + + before do + exception_status = HasStatus::AVAILABLE_STATUSES - [target_status] + create(:ci_build, runner: runner, project: project, status: exception_status.first) + end + + it 'returns matched job' do + is_expected.to eq([job]) + end + end + end + end + end +end diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 5e272af6073..7266e1b84d1 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -82,4 +82,104 @@ describe AutoDevopsHelper do it { is_expected.to eq(false) } end end + + describe '.show_run_auto_devops_pipeline_checkbox_for_instance_setting?' do + subject { helper.show_run_auto_devops_pipeline_checkbox_for_instance_setting?(project) } + + context 'when master contains a .gitlab-ci.yml file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly enabled' do + before do + project.create_auto_devops!(enabled: true) + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly disabled' do + before do + project.create_auto_devops!(enabled: false) + end + + context 'when auto devops is enabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq(true) } + end + + context 'when auto devops is disabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to eq(false) } + end + end + + context 'when auto devops is set to instance setting' do + before do + project.create_auto_devops!(enabled: nil) + end + + it { is_expected.to eq(false) } + end + end + + describe '.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?' do + subject { helper.show_run_auto_devops_pipeline_checkbox_for_explicit_setting?(project) } + + context 'when master contains a .gitlab-ci.yml file' do + before do + allow(project.repository).to receive(:gitlab_ci_yml).and_return("script: ['test']") + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly enabled' do + before do + project.create_auto_devops!(enabled: true) + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is explicitly disabled' do + before do + project.create_auto_devops!(enabled: false) + end + + it { is_expected.to eq(true) } + end + + context 'when auto devops is set to instance setting' do + before do + project.create_auto_devops!(enabled: nil) + end + + context 'when auto devops is enabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: true) + end + + it { is_expected.to eq(false) } + end + + context 'when auto devops is disabled system-wide' do + before do + stub_application_setting(auto_devops_enabled: false) + end + + it { is_expected.to eq(true) } + end + end + end end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index cb851d828f2..d601cbdb39b 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -174,6 +174,7 @@ describe IssuablesHelper do expected_data = { 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}", + 'updateEndpoint' => "/#{@project.full_path}/issues/#{issue.iid}.json", 'canUpdate' => true, 'canDestroy' => true, 'issuableRef' => "##{issue.iid}", diff --git a/spec/javascripts/datetime_utility_spec.js b/spec/javascripts/datetime_utility_spec.js index 3391cade541..0f7bf9ec712 100644 --- a/spec/javascripts/datetime_utility_spec.js +++ b/spec/javascripts/datetime_utility_spec.js @@ -1,4 +1,4 @@ -import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; +import * as datetimeUtility from '~/lib/utils/datetime_utility'; (() => { describe('Date time utils', () => { @@ -89,10 +89,22 @@ import { timeIntervalInWords } from '~/lib/utils/datetime_utility'; describe('timeIntervalInWords', () => { it('should return string with number of minutes and seconds', () => { - expect(timeIntervalInWords(9.54)).toEqual('9 seconds'); - expect(timeIntervalInWords(1)).toEqual('1 second'); - expect(timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); - expect(timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + expect(datetimeUtility.timeIntervalInWords(9.54)).toEqual('9 seconds'); + expect(datetimeUtility.timeIntervalInWords(1)).toEqual('1 second'); + expect(datetimeUtility.timeIntervalInWords(200)).toEqual('3 minutes 20 seconds'); + expect(datetimeUtility.timeIntervalInWords(6008)).toEqual('100 minutes 8 seconds'); + }); + }); + + describe('dateInWords', () => { + const date = new Date('07/01/2016'); + + it('should return date in words', () => { + expect(datetimeUtility.dateInWords(date)).toEqual('July 1, 2016'); + }); + + it('should return abbreviated month name', () => { + expect(datetimeUtility.dateInWords(date, true)).toEqual('Jul 1, 2016'); }); }); })(); diff --git a/spec/javascripts/flash_spec.js b/spec/javascripts/flash_spec.js index b669aabcee4..97e3ab682c5 100644 --- a/spec/javascripts/flash_spec.js +++ b/spec/javascripts/flash_spec.js @@ -278,7 +278,7 @@ describe('Flash', () => { removeFlashClickListener(flashEl, false); - flashEl.parentNode.click(); + flashEl.click(); setTimeout(() => { expect(document.querySelector('.flash')).toBeNull(); diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js index 5662c7387fb..b47a8bf705f 100644 --- a/spec/javascripts/issue_show/components/app_spec.js +++ b/spec/javascripts/issue_show/components/app_spec.js @@ -35,11 +35,12 @@ describe('Issuable output', () => { canUpdate: true, canDestroy: true, endpoint: '/gitlab-org/gitlab-shell/issues/9/realtime_changes', + updateEndpoint: gl.TEST_HOST, issuableRef: '#1', initialTitleHtml: '', initialTitleText: '', - initialDescriptionHtml: '', - initialDescriptionText: '', + initialDescriptionHtml: 'test', + initialDescriptionText: 'test', markdownPreviewPath: '/', markdownDocsPath: '/', projectNamespace: '/', diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js index 360691a3546..163e5cdd062 100644 --- a/spec/javascripts/issue_show/components/description_spec.js +++ b/spec/javascripts/issue_show/components/description_spec.js @@ -1,11 +1,22 @@ import Vue from 'vue'; import descriptionComponent from '~/issue_show/components/description.vue'; +import * as taskList from '~/task_list'; +import mountComponent from '../../helpers/vue_mount_component_helper'; describe('Description component', () => { let vm; + let DescriptionComponent; + const props = { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + updatedAt: new Date().toString(), + taskStatus: '', + updateUrl: gl.TEST_HOST, + }; beforeEach(() => { - const Component = Vue.extend(descriptionComponent); + DescriptionComponent = Vue.extend(descriptionComponent); if (!document.querySelector('.issuable-meta')) { const metaData = document.createElement('div'); @@ -15,15 +26,11 @@ describe('Description component', () => { document.body.appendChild(metaData); } - vm = new Component({ - propsData: { - canUpdate: true, - descriptionHtml: 'test', - descriptionText: 'test', - updatedAt: new Date().toString(), - taskStatus: '', - }, - }).$mount(); + vm = mountComponent(DescriptionComponent, props); + }); + + afterEach(() => { + vm.$destroy(); }); it('animates description changes', (done) => { @@ -44,34 +51,46 @@ describe('Description component', () => { }); }); - // TODO: gl.TaskList no longer exists. rewrite these tests once we have a way to rewire ES modules - - // it('re-inits the TaskList when description changed', (done) => { - // spyOn(gl, 'TaskList'); - // vm.descriptionHtml = 'changed'; - // - // setTimeout(() => { - // expect( - // gl.TaskList, - // ).toHaveBeenCalled(); - // - // done(); - // }); - // }); - - // it('does not re-init the TaskList when canUpdate is false', (done) => { - // spyOn(gl, 'TaskList'); - // vm.canUpdate = false; - // vm.descriptionHtml = 'changed'; - // - // setTimeout(() => { - // expect( - // gl.TaskList, - // ).not.toHaveBeenCalled(); - // - // done(); - // }); - // }); + describe('TaskList', () => { + beforeEach(() => { + vm = mountComponent(DescriptionComponent, Object.assign({}, props, { + issuableType: 'issuableType', + })); + spyOn(taskList, 'default'); + }); + + it('re-inits the TaskList when description changed', (done) => { + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).toHaveBeenCalled(); + done(); + }); + }); + + it('does not re-init the TaskList when canUpdate is false', (done) => { + vm.canUpdate = false; + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).not.toHaveBeenCalled(); + done(); + }); + }); + + it('calls with issuableType dataType', (done) => { + vm.descriptionHtml = 'changed'; + + setTimeout(() => { + expect(taskList.default).toHaveBeenCalledWith({ + dataType: 'issuableType', + fieldName: 'description', + selector: '.detail-page-description', + }); + done(); + }); + }); + }); describe('taskStatus', () => { it('adds full taskStatus', (done) => { @@ -126,4 +145,8 @@ describe('Description component', () => { }); }); }); + + it('sets data-update-url', () => { + expect(vm.$el.querySelector('textarea').dataset.updateUrl).toEqual(gl.TEST_HOST); + }); }); diff --git a/spec/javascripts/issue_show/components/title_spec.js b/spec/javascripts/issue_show/components/title_spec.js index c1edc785d0f..5370f4e1fea 100644 --- a/spec/javascripts/issue_show/components/title_spec.js +++ b/spec/javascripts/issue_show/components/title_spec.js @@ -80,19 +80,19 @@ describe('Title component', () => { }); it('should not show by default', () => { - expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); it('should not show if canUpdate is false', () => { vm.showInlineEditButton = true; vm.canUpdate = false; - expect(vm.$el.querySelector('.note-action-button')).toBeNull(); + expect(vm.$el.querySelector('.btn-edit')).toBeNull(); }); it('should show if showInlineEditButton and canUpdate', () => { vm.showInlineEditButton = true; vm.canUpdate = true; - expect(vm.$el.querySelector('.note-action-button')).toBeDefined(); + expect(vm.$el.querySelector('.btn-edit')).toBeDefined(); }); it('should trigger open.form event when clicked', () => { @@ -100,7 +100,7 @@ describe('Title component', () => { vm.canUpdate = true; Vue.nextTick(() => { - vm.$el.querySelector('.note-action-button').click(); + vm.$el.querySelector('.btn-edit').click(); expect(eventHub.$emit).toHaveBeenCalledWith('open.form'); }); }); diff --git a/spec/javascripts/lib/utils/text_utility_spec.js b/spec/javascripts/lib/utils/text_utility_spec.js index b21bd958f90..1f46c225071 100644 --- a/spec/javascripts/lib/utils/text_utility_spec.js +++ b/spec/javascripts/lib/utils/text_utility_spec.js @@ -23,6 +23,14 @@ describe('text_utility', () => { }); }); + describe('capitalizeFirstCharacter', () => { + it('returns string with first letter capitalized', () => { + expect(textUtils.capitalizeFirstCharacter('gitlab')).toEqual('Gitlab'); + expect(textUtils.highCountTrim(105)).toBe('99+'); + expect(textUtils.highCountTrim(100)).toBe('99+'); + }); + }); + describe('humanize', () => { it('should remove underscores and uppercase the first letter', () => { expect(textUtils.humanize('foo_bar')).toEqual('Foo bar'); diff --git a/spec/javascripts/notes/components/issue_note_actions_spec.js b/spec/javascripts/notes/components/note_actions_spec.js index 7bcc061f167..ab81aabb992 100644 --- a/spec/javascripts/notes/components/issue_note_actions_spec.js +++ b/spec/javascripts/notes/components/note_actions_spec.js @@ -1,6 +1,6 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import issueActions from '~/notes/components/issue_note_actions.vue'; +import noteActions from '~/notes/components/note_actions.vue'; import { userDataMock } from '../mock_data'; describe('issse_note_actions component', () => { @@ -8,7 +8,7 @@ describe('issse_note_actions component', () => { let Component; beforeEach(() => { - Component = Vue.extend(issueActions); + Component = Vue.extend(noteActions); }); afterEach(() => { diff --git a/spec/javascripts/notes/components/issue_note_attachment_spec.js b/spec/javascripts/notes/components/note_attachment_spec.js index 8f33b874ad6..b14a518b622 100644 --- a/spec/javascripts/notes/components/issue_note_attachment_spec.js +++ b/spec/javascripts/notes/components/note_attachment_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import issueNoteAttachment from '~/notes/components/issue_note_attachment.vue'; +import noteAttachment from '~/notes/components/note_attachment.vue'; describe('issue note attachment', () => { it('should render properly', () => { @@ -11,7 +11,7 @@ describe('issue note attachment', () => { }, }; - const Component = Vue.extend(issueNoteAttachment); + const Component = Vue.extend(noteAttachment); const vm = new Component({ propsData: props, }).$mount(); diff --git a/spec/javascripts/notes/components/issue_note_awards_list_spec.js b/spec/javascripts/notes/components/note_awards_list_spec.js index c689c452143..15995ec5a05 100644 --- a/spec/javascripts/notes/components/issue_note_awards_list_spec.js +++ b/spec/javascripts/notes/components/note_awards_list_spec.js @@ -1,9 +1,9 @@ import Vue from 'vue'; import store from '~/notes/stores'; -import awardsNote from '~/notes/components/issue_note_awards_list.vue'; +import awardsNote from '~/notes/components/note_awards_list.vue'; import { noteableDataMock, notesDataMock } from '../mock_data'; -describe('issue_note_awards_list component', () => { +describe('note_awards_list component', () => { let vm; let awardsMock; diff --git a/spec/javascripts/notes/components/issue_note_edited_text_spec.js b/spec/javascripts/notes/components/note_edited_text_spec.js index 6603241eb64..e0b991c32ec 100644 --- a/spec/javascripts/notes/components/issue_note_edited_text_spec.js +++ b/spec/javascripts/notes/components/note_edited_text_spec.js @@ -1,12 +1,12 @@ import Vue from 'vue'; -import issueNoteEditedText from '~/notes/components/issue_note_edited_text.vue'; +import noteEditedText from '~/notes/components/note_edited_text.vue'; -describe('issue_note_edited_text', () => { +describe('note_edited_text', () => { let vm; let props; beforeEach(() => { - const Component = Vue.extend(issueNoteEditedText); + const Component = Vue.extend(noteEditedText); props = { actionText: 'Edited', className: 'foo-bar', diff --git a/spec/javascripts/notes/components/issue_note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js index 83ea18508ae..16a76b11321 100644 --- a/spec/javascripts/notes/components/issue_note_header_spec.js +++ b/spec/javascripts/notes/components/note_header_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import issueNoteHeader from '~/notes/components/issue_note_header.vue'; +import noteHeader from '~/notes/components/note_header.vue'; import store from '~/notes/stores'; -describe('issue_note_header component', () => { +describe('note_header component', () => { let vm; let Component; beforeEach(() => { - Component = Vue.extend(issueNoteHeader); + Component = Vue.extend(noteHeader); }); afterEach(() => { diff --git a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js b/spec/javascripts/notes/components/note_signed_out_widget_spec.js index f20d9ce9268..6cba8053888 100644 --- a/spec/javascripts/notes/components/issue_note_signed_out_widget_spec.js +++ b/spec/javascripts/notes/components/note_signed_out_widget_spec.js @@ -1,13 +1,13 @@ import Vue from 'vue'; -import issueNoteSignedOut from '~/notes/components/issue_note_signed_out_widget.vue'; +import noteSignedOut from '~/notes/components/note_signed_out_widget.vue'; import store from '~/notes/stores'; import { notesDataMock } from '../mock_data'; -describe('issue_note_signed_out_widget component', () => { +describe('note_signed_out_widget component', () => { let vm; beforeEach(() => { - const Component = Vue.extend(issueNoteSignedOut); + const Component = Vue.extend(noteSignedOut); store.dispatch('setNotesData', notesDataMock); vm = new Component({ diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js index 104da4473ce..a22b6bd3a67 100644 --- a/spec/javascripts/vue_shared/components/icon_spec.js +++ b/spec/javascripts/vue_shared/components/icon_spec.js @@ -11,7 +11,7 @@ describe('Sprite Icon Component', function () { icon = mountComponent(IconComponent, { name: 'test', - size: 99, + size: 32, cssClasses: 'extraclasses', }); }); @@ -34,12 +34,18 @@ describe('Sprite Icon Component', function () { }); it('should properly compute iconSizeClass', function () { - expect(icon.iconSizeClass).toBe('s99'); + expect(icon.iconSizeClass).toBe('s32'); + }); + + it('forbids invalid size prop', () => { + expect(icon.$options.props.size.validator(NaN)).toBeFalsy(); + expect(icon.$options.props.size.validator(0)).toBeFalsy(); + expect(icon.$options.props.size.validator(9001)).toBeFalsy(); }); it('should properly render img css', function () { const classList = icon.$el.classList; - const containsSizeClass = classList.contains('s99'); + const containsSizeClass = classList.contains('s32'); const containsCustomClass = classList.contains('extraclasses'); expect(containsSizeClass).toBe(true); expect(containsCustomClass).toBe(true); diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js new file mode 100644 index 00000000000..47af9534737 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pikaday_spec.js @@ -0,0 +1,29 @@ +import Vue from 'vue'; +import datePicker from '~/vue_shared/components/pikaday.vue'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('datePicker', () => { + let vm; + beforeEach(() => { + const DatePicker = Vue.extend(datePicker); + vm = mountComponent(DatePicker, { + label: 'label', + }); + }); + + it('should render label text', () => { + expect(vm.$el.querySelector('.dropdown-toggle-text').innerText.trim()).toEqual('label'); + }); + + it('should show calendar', () => { + expect(vm.$el.querySelector('.pika-single')).toBeDefined(); + }); + + it('should toggle when dropdown is clicked', () => { + const hidePicker = jasmine.createSpy(); + vm.$on('hidePicker', hidePicker); + + vm.$el.querySelector('.dropdown-menu-toggle').click(); + expect(hidePicker).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js new file mode 100644 index 00000000000..cce53193870 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('collapsedCalendarIcon', () => { + let vm; + beforeEach(() => { + const CollapsedCalendarIcon = Vue.extend(collapsedCalendarIcon); + vm = mountComponent(CollapsedCalendarIcon, { + containerClass: 'test-class', + text: 'text', + showIcon: false, + }); + }); + + it('should add class to container', () => { + expect(vm.$el.classList.contains('test-class')).toEqual(true); + }); + + it('should hide calendar icon if showIcon', () => { + expect(vm.$el.querySelector('.fa-calendar')).toBeNull(); + }); + + it('should render text', () => { + expect(vm.$el.querySelector('span').innerText.trim()).toEqual('text'); + }); + + it('should emit click event when container is clicked', () => { + const click = jasmine.createSpy(); + vm.$on('click', click); + + vm.$el.click(); + expect(click).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js new file mode 100644 index 00000000000..20363e78094 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js @@ -0,0 +1,91 @@ +import Vue from 'vue'; +import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('collapsedGroupedDatePicker', () => { + let vm; + beforeEach(() => { + const CollapsedGroupedDatePicker = Vue.extend(collapsedGroupedDatePicker); + vm = mountComponent(CollapsedGroupedDatePicker, { + showToggleSidebar: true, + }); + }); + + it('should render toggle sidebar if showToggleSidebar', (done) => { + expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeDefined(); + + vm.showToggleSidebar = false; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.issuable-sidebar-header')).toBeNull(); + done(); + }); + }); + + it('toggleCollapse events', () => { + const toggleCollapse = jasmine.createSpy(); + + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + Vue.nextTick(done); + }); + + it('should emit when sidebar is toggled', () => { + vm.$el.querySelector('.gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + + it('should emit when collapsed-calendar-icon is clicked', () => { + vm.$el.querySelector('.sidebar-collapsed-icon').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + }); + + describe('minDate and maxDate', () => { + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + vm.maxDate = new Date('07/17/2017'); + Vue.nextTick(done); + }); + + it('should render both collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(2); + expect(icons[0].innerText.trim()).toEqual('Jul 17 2016'); + expect(icons[1].innerText.trim()).toEqual('Jul 17 2017'); + }); + }); + + describe('minDate', () => { + beforeEach((done) => { + vm.minDate = new Date('07/17/2016'); + Vue.nextTick(done); + }); + + it('should render minDate in collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('From Jul 17 2016'); + }); + }); + + describe('maxDate', () => { + beforeEach((done) => { + vm.maxDate = new Date('07/17/2017'); + Vue.nextTick(done); + }); + + it('should render maxDate in collapsed-calendar-icon', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('Until Jul 17 2017'); + }); + }); + + describe('no dates', () => { + it('should render None', () => { + const icons = vm.$el.querySelectorAll('.sidebar-collapsed-icon'); + expect(icons.length).toEqual(1); + expect(icons[0].innerText.trim()).toEqual('None'); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js new file mode 100644 index 00000000000..926e11b4d30 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js @@ -0,0 +1,117 @@ +import Vue from 'vue'; +import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('sidebarDatePicker', () => { + let vm; + beforeEach(() => { + const SidebarDatePicker = Vue.extend(sidebarDatePicker); + vm = mountComponent(SidebarDatePicker, { + label: 'label', + isLoading: true, + }); + }); + + it('should emit toggleCollapse when collapsed toggle sidebar is clicked', () => { + const toggleCollapse = jasmine.createSpy(); + vm.$on('toggleCollapse', toggleCollapse); + + vm.$el.querySelector('.issuable-sidebar-header .gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + + it('should render collapsed-calendar-icon', () => { + expect(vm.$el.querySelector('.sidebar-collapsed-icon')).toBeDefined(); + }); + + it('should render label', () => { + expect(vm.$el.querySelector('.title').innerText.trim()).toEqual('label'); + }); + + it('should render loading-icon when isLoading', () => { + expect(vm.$el.querySelector('.fa-spin')).toBeDefined(); + }); + + it('should render value when not editing', () => { + expect(vm.$el.querySelector('.value-content')).toBeDefined(); + }); + + it('should render None if there is no selectedDate', () => { + expect(vm.$el.querySelector('.value-content span').innerText.trim()).toEqual('None'); + }); + + it('should render date-picker when editing', (done) => { + vm.editing = true; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.pika-label')).toBeDefined(); + done(); + }); + }); + + describe('editable', () => { + beforeEach((done) => { + vm.editable = true; + Vue.nextTick(done); + }); + + it('should render edit button', () => { + expect(vm.$el.querySelector('.title .btn-blank').innerText.trim()).toEqual('Edit'); + }); + + it('should enable editing when edit button is clicked', (done) => { + vm.isLoading = false; + Vue.nextTick(() => { + vm.$el.querySelector('.title .btn-blank').click(); + expect(vm.editing).toEqual(true); + done(); + }); + }); + }); + + it('should render date if selectedDate', (done) => { + vm.selectedDate = new Date('07/07/2017'); + Vue.nextTick(() => { + expect(vm.$el.querySelector('.value-content strong').innerText.trim()).toEqual('Jul 7, 2017'); + done(); + }); + }); + + describe('selectedDate and editable', () => { + beforeEach((done) => { + vm.selectedDate = new Date('07/07/2017'); + vm.editable = true; + Vue.nextTick(done); + }); + + it('should render remove button if selectedDate and editable', () => { + expect(vm.$el.querySelector('.value-content .btn-blank').innerText.trim()).toEqual('remove'); + }); + + it('should emit saveDate when remove button is clicked', () => { + const saveDate = jasmine.createSpy(); + vm.$on('saveDate', saveDate); + + vm.$el.querySelector('.value-content .btn-blank').click(); + expect(saveDate).toHaveBeenCalled(); + }); + }); + + describe('showToggleSidebar', () => { + beforeEach((done) => { + vm.showToggleSidebar = true; + Vue.nextTick(done); + }); + + it('should render toggle-sidebar when showToggleSidebar', () => { + expect(vm.$el.querySelector('.title .gutter-toggle')).toBeDefined(); + }); + + it('should emit toggleCollapse when toggle sidebar is clicked', () => { + const toggleCollapse = jasmine.createSpy(); + vm.$on('toggleCollapse', toggleCollapse); + + vm.$el.querySelector('.title .gutter-toggle').click(); + expect(toggleCollapse).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js new file mode 100644 index 00000000000..752a9e89d50 --- /dev/null +++ b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js @@ -0,0 +1,32 @@ +import Vue from 'vue'; +import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue'; +import mountComponent from '../../../helpers/vue_mount_component_helper'; + +describe('toggleSidebar', () => { + let vm; + beforeEach(() => { + const ToggleSidebar = Vue.extend(toggleSidebar); + vm = mountComponent(ToggleSidebar, { + collapsed: true, + }); + }); + + it('should render << when collapsed', () => { + expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-left')).toEqual(true); + }); + + it('should render >> when collapsed', () => { + vm.collapsed = false; + Vue.nextTick(() => { + expect(vm.$el.querySelector('.fa').classList.contains('fa-angle-double-right')).toEqual(true); + }); + }); + + it('should emit toggle event when button clicked', () => { + const toggle = jasmine.createSpy(); + vm.$on('toggle', toggle); + vm.$el.click(); + + expect(toggle).toHaveBeenCalled(); + }); +}); diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js index 7047053d131..45a0bb0650f 100644 --- a/spec/javascripts/zen_mode_spec.js +++ b/spec/javascripts/zen_mode_spec.js @@ -1,77 +1,93 @@ -/* eslint-disable space-before-function-paren, no-var, one-var, one-var-declaration-per-line, object-shorthand, comma-dangle, no-return-assign, new-cap, max-len */ /* global Mousetrap */ import Dropzone from 'dropzone'; import ZenMode from '~/zen_mode'; -(function() { - var enterZen, escapeKeydown, exitZen; - - describe('ZenMode', function() { - var fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; - preloadFixtures(fixtureName); - beforeEach(function() { - loadFixtures(fixtureName); - spyOn(Dropzone, 'forElement').and.callFake(function() { - return { - enable: function() { - return true; - } - }; - // Stub Dropzone.forElement(...).enable() - }); - this.zen = new ZenMode(); - // Set this manually because we can't actually scroll the window - return this.zen.scroll_position = 456; +describe('ZenMode', () => { + let zen; + const fixtureName = 'merge_requests/merge_request_with_comment.html.raw'; + + preloadFixtures(fixtureName); + + function enterZen() { + $('.notes-form .js-zen-enter').click(); + } + + function exitZen() { + $('.notes-form .js-zen-leave').click(); + } + + function escapeKeydown() { + $('.notes-form textarea').trigger($.Event('keydown', { + keyCode: 27, + })); + } + + beforeEach(() => { + loadFixtures(fixtureName); + + spyOn(Dropzone, 'forElement').and.callFake(() => ({ + enable: () => true, + })); + zen = new ZenMode(); + + // Set this manually because we can't actually scroll the window + zen.scroll_position = 456; + }); + + describe('on enter', () => { + it('pauses Mousetrap', () => { + spyOn(Mousetrap, 'pause'); + enterZen(); + expect(Mousetrap.pause).toHaveBeenCalled(); }); - describe('on enter', function() { - it('pauses Mousetrap', function() { - spyOn(Mousetrap, 'pause'); - enterZen(); - return expect(Mousetrap.pause).toHaveBeenCalled(); - }); - return it('removes textarea styling', function() { - $('.notes-form textarea').attr('style', 'height: 400px'); - enterZen(); - return expect($('.notes-form textarea')).not.toHaveAttr('style'); - }); + + it('removes textarea styling', () => { + $('.notes-form textarea').attr('style', 'height: 400px'); + enterZen(); + expect($('.notes-form textarea')).not.toHaveAttr('style'); }); - describe('in use', function() { - beforeEach(function() { - return enterZen(); - }); - return it('exits on Escape', function() { - escapeKeydown(); - return expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen'); - }); + }); + + describe('in use', () => { + beforeEach(enterZen); + + it('exits on Escape', () => { + escapeKeydown(); + expect($('.notes-form .zen-backdrop')).not.toHaveClass('fullscreen'); + }); + }); + + describe('on exit', () => { + beforeEach(enterZen); + + it('unpauses Mousetrap', () => { + spyOn(Mousetrap, 'unpause'); + exitZen(); + expect(Mousetrap.unpause).toHaveBeenCalled(); }); - return describe('on exit', function() { - beforeEach(function() { - return enterZen(); - }); - it('unpauses Mousetrap', function() { - spyOn(Mousetrap, 'unpause'); - exitZen(); - return expect(Mousetrap.unpause).toHaveBeenCalled(); - }); - return it('restores the scroll position', function() { - spyOn(this.zen, 'scrollTo'); - exitZen(); - return expect(this.zen.scrollTo).toHaveBeenCalled(); - }); + + it('restores the scroll position', () => { + spyOn(zen, 'scrollTo'); + exitZen(); + expect(zen.scrollTo).toHaveBeenCalled(); }); }); - enterZen = function() { - return $('.notes-form .js-zen-enter').click(); - }; + describe('enabling dropzone', () => { + beforeEach(() => { + enterZen(); + }); - exitZen = function() { - return $('.notes-form .js-zen-leave').click(); - }; + it('should not call dropzone if element is not dropzone valid', () => { + $('.div-dropzone').addClass('js-invalid-dropzone'); + exitZen(); + expect(Dropzone.forElement).not.toHaveBeenCalled(); + }); - escapeKeydown = function() { - return $('.notes-form textarea').trigger($.Event('keydown', { - keyCode: 27 - })); - }; -}).call(window); + it('should call dropzone if element is dropzone valid', () => { + $('.div-dropzone').removeClass('js-invalid-dropzone'); + exitZen(); + expect(Dropzone.forElement).toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/lib/api/helpers_spec.rb b/spec/lib/api/helpers_spec.rb new file mode 100644 index 00000000000..3c4deba4712 --- /dev/null +++ b/spec/lib/api/helpers_spec.rb @@ -0,0 +1,109 @@ +require 'spec_helper' + +describe API::Helpers do + subject { Class.new.include(described_class).new } + + describe '#find_namespace' do + let(:namespace) { create(:namespace) } + + shared_examples 'namespace finder' do + context 'when namespace exists' do + it 'returns requested namespace' do + expect(subject.find_namespace(existing_id)).to eq(namespace) + end + end + + context "when namespace doesn't exists" do + it 'returns nil' do + expect(subject.find_namespace(non_existing_id)).to be_nil + end + end + end + + context 'when ID is used as an argument' do + let(:existing_id) { namespace.id } + let(:non_existing_id) { 9999 } + + it_behaves_like 'namespace finder' + end + + context 'when PATH is used as an argument' do + let(:existing_id) { namespace.path } + let(:non_existing_id) { 'non-existing-path' } + + it_behaves_like 'namespace finder' + end + end + + shared_examples 'user namespace finder' do + let(:user1) { create(:user) } + + before do + allow(subject).to receive(:current_user).and_return(user1) + allow(subject).to receive(:header).and_return(nil) + allow(subject).to receive(:not_found!).and_raise('404 Namespace not found') + end + + context 'when namespace is group' do + let(:namespace) { create(:group) } + + context 'when user has access to group' do + before do + namespace.add_guest(user1) + namespace.save! + end + + it 'returns requested namespace' do + expect(namespace_finder).to eq(namespace) + end + end + + context "when user doesn't have access to group" do + it 'raises not found error' do + expect { namespace_finder }.to raise_error(RuntimeError, '404 Namespace not found') + end + end + end + + context "when namespace is user's personal namespace" do + let(:namespace) { create(:namespace) } + + context 'when user owns the namespace' do + before do + namespace.owner = user1 + namespace.save! + end + + it 'returns requested namespace' do + expect(namespace_finder).to eq(namespace) + end + end + + context "when user doesn't own the namespace" do + it 'raises not found error' do + expect { namespace_finder }.to raise_error(RuntimeError, '404 Namespace not found') + end + end + end + end + + describe '#find_namespace!' do + let(:namespace_finder) do + subject.find_namespace!(namespace.id) + end + + it_behaves_like 'user namespace finder' + end + + describe '#user_namespace' do + let(:namespace_finder) do + subject.user_namespace + end + + before do + allow(subject).to receive(:params).and_return({ id: namespace.id }) + end + + it_behaves_like 'user namespace finder' + end +end diff --git a/spec/lib/gitlab/diff/position_tracer_spec.rb b/spec/lib/gitlab/diff/position_tracer_spec.rb index e5138705443..ddc4f6c5b5c 100644 --- a/spec/lib/gitlab/diff/position_tracer_spec.rb +++ b/spec/lib/gitlab/diff/position_tracer_spec.rb @@ -1771,9 +1771,9 @@ describe Gitlab::Diff::PositionTracer do describe "merge of target branch" do let(:merge_commit) do - update_file_again_commit + second_create_file_commit - merge_request = create(:merge_request, source_branch: second_create_file_commit.sha, target_branch: branch_name, source_project: project) + merge_request = create(:merge_request, source_branch: second_branch_name, target_branch: branch_name, source_project: project) repository.merge(current_user, merge_request.diff_head_sha, merge_request, "Merge branches") diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb index a1f4e65b8d4..a871ed0df0e 100644 --- a/spec/lib/gitlab/gitaly_client_spec.rb +++ b/spec/lib/gitlab/gitaly_client_spec.rb @@ -278,4 +278,20 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do end end end + + describe 'timeouts' do + context 'with default values' do + before do + stub_application_setting(gitaly_timeout_default: 55) + stub_application_setting(gitaly_timeout_medium: 30) + stub_application_setting(gitaly_timeout_fast: 10) + end + + it 'returns expected values' do + expect(described_class.default_timeout).to be(55) + expect(described_class.medium_timeout).to be(30) + expect(described_class.fast_timeout).to be(10) + end + end + end end diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index 48d56628ed5..ef51e3cc8df 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -137,22 +137,22 @@ describe Gitlab::SQL::Pattern do end end - describe '.to_fuzzy_arel' do - subject(:to_fuzzy_arel) { Issue.to_fuzzy_arel(:title, query) } + describe '.fuzzy_arel_match' do + subject(:fuzzy_arel_match) { Issue.fuzzy_arel_match(:title, query) } context 'with a word equal to 3 chars' do let(:query) { 'foo' } it 'returns a single ILIKE condition' do - expect(to_fuzzy_arel.to_sql).to match(/title.*I?LIKE '\%foo\%'/) + expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE '\%foo\%'/) end end context 'with a word shorter than 3 chars' do let(:query) { 'fo' } - it 'returns nil' do - expect(to_fuzzy_arel).to be_nil + it 'returns a single equality condition' do + expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE 'fo'/) end end @@ -160,7 +160,23 @@ describe Gitlab::SQL::Pattern do let(:query) { 'foo baz' } it 'returns a joining LIKE condition using a AND' do - expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/) + expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%'/) + end + end + + context 'with two words both shorter than 3 chars' do + let(:query) { 'fo ba' } + + it 'returns a single ILIKE condition' do + expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE 'fo ba'/) + end + end + + context 'with two words, one shorter 3 chars' do + let(:query) { 'foo ba' } + + it 'returns a single ILIKE condition using the longer word' do + expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%'/) end end @@ -168,7 +184,7 @@ describe Gitlab::SQL::Pattern do let(:query) { 'foo "really bar" baz' } it 'returns a joining LIKE condition using a AND' do - expect(to_fuzzy_arel.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/) + expect(fuzzy_arel_match.to_sql).to match(/title.+I?LIKE '\%foo\%' AND .*title.*I?LIKE '\%baz\%' AND .*title.*I?LIKE '\%really bar\%'/) end end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 51bf4e65e5d..0b7e16cc33c 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -219,6 +219,65 @@ describe ApplicationSetting do expect(subject).to be_valid end end + + context 'gitaly timeouts' do + [:gitaly_timeout_default, :gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name| + it do + is_expected.to validate_presence_of(timeout_name) + is_expected.to validate_numericality_of(timeout_name).only_integer + .is_greater_than_or_equal_to(0) + end + end + + [:gitaly_timeout_medium, :gitaly_timeout_fast].each do |timeout_name| + it "validates that #{timeout_name} is lower than timeout_default" do + subject[:gitaly_timeout_default] = 50 + subject[timeout_name] = 100 + + expect(subject).to be_invalid + end + end + + it 'accepts all timeouts equal' do + subject.gitaly_timeout_default = 0 + subject.gitaly_timeout_medium = 0 + subject.gitaly_timeout_fast = 0 + + expect(subject).to be_valid + end + + it 'accepts timeouts in descending order' do + subject.gitaly_timeout_default = 50 + subject.gitaly_timeout_medium = 30 + subject.gitaly_timeout_fast = 20 + + expect(subject).to be_valid + end + + it 'rejects timeouts in ascending order' do + subject.gitaly_timeout_default = 20 + subject.gitaly_timeout_medium = 30 + subject.gitaly_timeout_fast = 50 + + expect(subject).to be_invalid + end + + it 'rejects medium timeout larger than default' do + subject.gitaly_timeout_default = 30 + subject.gitaly_timeout_medium = 50 + subject.gitaly_timeout_fast = 20 + + expect(subject).to be_invalid + end + + it 'rejects medium timeout smaller than fast' do + subject.gitaly_timeout_default = 30 + subject.gitaly_timeout_medium = 15 + subject.gitaly_timeout_fast = 20 + + expect(subject).to be_invalid + end + end end describe '.current' do diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 584dfe9a5c1..a93e7e233a8 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -473,7 +473,7 @@ describe Ci::Runner do end describe '.search' do - let(:runner) { create(:ci_runner, token: '123abc') } + let(:runner) { create(:ci_runner, token: '123abc', description: 'test runner') } it 'returns runners with a matching token' do expect(described_class.search(runner.token)).to eq([runner]) diff --git a/spec/models/concerns/has_variable_spec.rb b/spec/models/concerns/has_variable_spec.rb index f4b24e6d1d9..f87869a2fdc 100644 --- a/spec/models/concerns/has_variable_spec.rb +++ b/spec/models/concerns/has_variable_spec.rb @@ -9,6 +9,24 @@ describe HasVariable do it { is_expected.not_to allow_value('foo bar').for(:key) } it { is_expected.not_to allow_value('foo/bar').for(:key) } + describe '#key=' do + context 'when the new key is nil' do + it 'strips leading and trailing whitespaces' do + subject.key = nil + + expect(subject.key).to eq('') + end + end + + context 'when the new key has leadind and trailing whitespaces' do + it 'strips leading and trailing whitespaces' do + subject.key = ' my key ' + + expect(subject.key).to eq('my key') + end + end + end + describe '#value' do before do subject.value = 'secret' diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb index 4dfbb14952e..a53b59c4e08 100644 --- a/spec/models/concerns/issuable_spec.rb +++ b/spec/models/concerns/issuable_spec.rb @@ -67,6 +67,7 @@ describe Issuable do describe ".search" do let!(:searchable_issue) { create(:issue, title: "Searchable awesome issue") } + let!(:searchable_issue2) { create(:issue, title: 'Aw') } it 'returns issues with a matching title' do expect(issuable_class.search(searchable_issue.title)) @@ -86,8 +87,8 @@ describe Issuable do expect(issuable_class.search('searchable issue')).to eq([searchable_issue]) end - it 'returns all issues with a query shorter than 3 chars' do - expect(issuable_class.search('zz')).to eq(issuable_class.all) + it 'returns issues with a matching title for a query shorter than 3 chars' do + expect(issuable_class.search(searchable_issue2.title.downcase)).to eq([searchable_issue2]) end end @@ -95,6 +96,7 @@ describe Issuable do let!(:searchable_issue) do create(:issue, title: "Searchable awesome issue", description: 'Many cute kittens') end + let!(:searchable_issue2) { create(:issue, title: "Aw", description: "Cu") } it 'returns issues with a matching title' do expect(issuable_class.full_search(searchable_issue.title)) @@ -133,8 +135,8 @@ describe Issuable do expect(issuable_class.full_search('many kittens')).to eq([searchable_issue]) end - it 'returns all issues with a query shorter than 3 chars' do - expect(issuable_class.search('zz')).to eq(issuable_class.all) + it 'returns issues with a matching description for a query shorter than 3 chars' do + expect(issuable_class.full_search(searchable_issue2.description.downcase)).to eq([searchable_issue2]) end end @@ -283,7 +285,7 @@ describe Issuable do 'labels' => [[labels[0].hook_attrs], [labels[1].hook_attrs]] )) - issue.to_hook_data(user, old_labels: [labels[0]]) + issue.to_hook_data(user, old_associations: { labels: [labels[0]] }) end end @@ -302,7 +304,7 @@ describe Issuable do 'total_time_spent' => [1, 2] )) - issue.to_hook_data(user, old_total_time_spent: 1) + issue.to_hook_data(user, old_associations: { total_time_spent: 1 }) end end @@ -322,7 +324,7 @@ describe Issuable do 'assignees' => [[user.hook_attrs], [user.hook_attrs, user2.hook_attrs]] )) - issue.to_hook_data(user, old_assignees: [user]) + issue.to_hook_data(user, old_associations: { assignees: [user] }) end end @@ -345,7 +347,7 @@ describe Issuable do 'assignee' => [user.hook_attrs, user2.hook_attrs] )) - merge_request.to_hook_data(user, old_assignees: [user]) + merge_request.to_hook_data(user, old_associations: { assignees: [user] }) end end end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 3cf8fc816ff..728028746d8 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -259,7 +259,7 @@ describe MergeRequest do end describe '#source_branch_sha' do - let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) } + let(:last_branch_commit) { subject.source_project.repository.commit(Gitlab::Git::BRANCH_REF_PREFIX + subject.source_branch) } context 'with diffs' do subject { create(:merge_request, :with_diffs) } @@ -273,6 +273,21 @@ describe MergeRequest do it 'returns the sha of the source branch last commit' do expect(subject.source_branch_sha).to eq(last_branch_commit.sha) end + + context 'when there is a tag name matching the branch name' do + let(:tag_name) { subject.source_branch } + + it 'returns the sha of the source branch last commit' do + subject.source_project.repository.add_tag(subject.author, + tag_name, + subject.target_branch_sha, + 'Add a tag') + + expect(subject.source_branch_sha).to eq(last_branch_commit.sha) + + subject.source_project.repository.rm_tag(subject.author, tag_name) + end + end end context 'when the merge request is being created' do @@ -933,7 +948,7 @@ describe MergeRequest do context 'with a completely different branch' do before do - subject.update(target_branch: 'v1.0.0') + subject.update(target_branch: 'csv') end it_behaves_like 'returning all SHA' @@ -941,7 +956,7 @@ describe MergeRequest do context 'with a branch having no difference' do before do - subject.update(target_branch: 'v1.1.0') + subject.update(target_branch: 'branch-merged') subject.reload # make sure commits were not cached end diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index de3ca300ae3..e09d89d235d 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -88,7 +88,7 @@ describe Snippet do end describe '.search' do - let(:snippet) { create(:snippet) } + let(:snippet) { create(:snippet, title: 'test snippet') } it 'returns snippets with a matching title' do expect(described_class.search(snippet.title)).to eq([snippet]) diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb index 17dc3bb4f48..4f4e634829d 100644 --- a/spec/policies/group_policy_spec.rb +++ b/spec/policies/group_policy_spec.rb @@ -56,6 +56,7 @@ describe GroupPolicy do expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) expect_disallowed(*owner_permissions) + expect_disallowed(:read_namespace) end end @@ -63,7 +64,7 @@ describe GroupPolicy do let(:current_user) { guest } it do - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_disallowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -75,7 +76,7 @@ describe GroupPolicy do let(:current_user) { reporter } it do - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_allowed(*reporter_permissions) expect_disallowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -87,7 +88,7 @@ describe GroupPolicy do let(:current_user) { developer } it do - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_disallowed(*master_permissions) @@ -99,7 +100,7 @@ describe GroupPolicy do let(:current_user) { master } it do - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -113,7 +114,7 @@ describe GroupPolicy do it do allow(Group).to receive(:supports_nested_groups?).and_return(true) - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) @@ -127,7 +128,7 @@ describe GroupPolicy do it do allow(Group).to receive(:supports_nested_groups?).and_return(true) - expect_allowed(:read_group) + expect_allowed(:read_group, :read_namespace) expect_allowed(*reporter_permissions) expect_allowed(*developer_permissions) expect_allowed(*master_permissions) diff --git a/spec/policies/namespace_policy_spec.rb b/spec/policies/namespace_policy_spec.rb index e52ff02e5f0..1fdf95ad716 100644 --- a/spec/policies/namespace_policy_spec.rb +++ b/spec/policies/namespace_policy_spec.rb @@ -1,20 +1,42 @@ require 'spec_helper' describe NamespacePolicy do - let(:current_user) { create(:user) } - let(:namespace) { current_user.namespace } + let(:user) { create(:user) } + let(:owner) { create(:user) } + let(:admin) { create(:admin) } + let(:namespace) { create(:namespace, owner: owner) } + + let(:owner_permissions) { [:create_projects, :admin_namespace, :read_namespace] } subject { described_class.new(current_user, namespace) } - context "create projects" do - context "user namespace" do - it { is_expected.to be_allowed(:create_projects) } - end + context 'with no user' do + let(:current_user) { nil } + + it { is_expected.to be_banned } + end + + context 'regular user' do + let(:current_user) { user } + + it { is_expected.to be_disallowed(*owner_permissions) } + end + + context 'owner' do + let(:current_user) { owner } + + it { is_expected.to be_allowed(*owner_permissions) } - context "user who has exceeded project limit" do - let(:current_user) { create(:user, projects_limit: 0) } + context 'user who has exceeded project limit' do + let(:owner) { create(:user, projects_limit: 0) } it { is_expected.not_to be_allowed(:create_projects) } end end + + context 'admin' do + let(:current_user) { admin } + + it { is_expected.to be_allowed(*owner_permissions) } + end end diff --git a/spec/requests/api/namespaces_spec.rb b/spec/requests/api/namespaces_spec.rb index e60716d46d7..98102fcd6a7 100644 --- a/spec/requests/api/namespaces_spec.rb +++ b/spec/requests/api/namespaces_spec.rb @@ -91,4 +91,127 @@ describe API::Namespaces do end end end + + describe 'GET /namespaces/:id' do + let(:owned_group) { group1 } + let(:user2) { create(:user) } + + shared_examples 'can access namespace' do + it 'returns namespace details' do + get api("/namespaces/#{namespace_id}", request_actor) + + expect(response).to have_gitlab_http_status(200) + + expect(json_response['id']).to eq(requested_namespace.id) + expect(json_response['path']).to eq(requested_namespace.path) + expect(json_response['name']).to eq(requested_namespace.name) + end + end + + shared_examples 'namespace reader' do + let(:requested_namespace) { owned_group } + + before do + owned_group.add_owner(request_actor) + end + + context 'when namespace exists' do + context 'when requested by ID' do + context 'when requesting group' do + let(:namespace_id) { owned_group.id } + + it_behaves_like 'can access namespace' + end + + context 'when requesting personal namespace' do + let(:namespace_id) { request_actor.namespace.id } + let(:requested_namespace) { request_actor.namespace } + + it_behaves_like 'can access namespace' + end + end + + context 'when requested by path' do + context 'when requesting group' do + let(:namespace_id) { owned_group.path } + + it_behaves_like 'can access namespace' + end + + context 'when requesting personal namespace' do + let(:namespace_id) { request_actor.namespace.path } + let(:requested_namespace) { request_actor.namespace } + + it_behaves_like 'can access namespace' + end + end + end + + context "when namespace doesn't exist" do + it 'returns not-found' do + get api('/namespaces/9999', request_actor) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when unauthenticated' do + it 'returns authentication error' do + get api("/namespaces/#{group1.id}") + + expect(response).to have_gitlab_http_status(401) + end + end + + context 'when authenticated as regular user' do + let(:request_actor) { user } + + context 'when requested namespace is not owned by user' do + context 'when requesting group' do + it 'returns not-found' do + get api("/namespaces/#{group2.id}", request_actor) + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when requesting personal namespace' do + it 'returns not-found' do + get api("/namespaces/#{user2.namespace.id}", request_actor) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'when requested namespace is owned by user' do + it_behaves_like 'namespace reader' + end + end + + context 'when authenticated as admin' do + let(:request_actor) { admin } + + context 'when requested namespace is not owned by user' do + context 'when requesting group' do + let(:namespace_id) { group2.id } + let(:requested_namespace) { group2 } + + it_behaves_like 'can access namespace' + end + + context 'when requesting personal namespace' do + let(:namespace_id) { user2.namespace.id } + let(:requested_namespace) { user2.namespace } + + it_behaves_like 'can access namespace' + end + end + + context 'when requested namespace is owned by user' do + it_behaves_like 'namespace reader' + end + end + end end diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb index fe38a7b3251..ec5cad4f4fd 100644 --- a/spec/requests/api/runners_spec.rb +++ b/spec/requests/api/runners_spec.rb @@ -354,6 +354,140 @@ describe API::Runners do end end + describe 'GET /runners/:id/jobs' do + set(:job_1) { create(:ci_build) } + let!(:job_2) { create(:ci_build, :running, runner: shared_runner, project: project) } + let!(:job_3) { create(:ci_build, :failed, runner: shared_runner, project: project) } + let!(:job_4) { create(:ci_build, :running, runner: specific_runner, project: project) } + let!(:job_5) { create(:ci_build, :failed, runner: specific_runner, project: project) } + + context 'admin user' do + context 'when runner exists' do + context 'when runner is shared' do + it 'return jobs' do + get api("/runners/#{shared_runner.id}/jobs", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + end + end + + context 'when runner is specific' do + it 'return jobs' do + get api("/runners/#{specific_runner.id}/jobs", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + end + end + + context 'when valid status is provided' do + it 'return filtered jobs' do + get api("/runners/#{specific_runner.id}/jobs?status=failed", admin) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when invalid status is provided' do + it 'return 400' do + get api("/runners/#{specific_runner.id}/jobs?status=non-existing", admin) + + expect(response).to have_gitlab_http_status(400) + end + end + end + + context "when runner doesn't exist" do + it 'returns 404' do + get api('/runners/9999/jobs', admin) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context "runner project's administrative user" do + context 'when runner exists' do + context 'when runner is shared' do + it 'returns 403' do + get api("/runners/#{shared_runner.id}/jobs", user) + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'when runner is specific' do + it 'return jobs' do + get api("/runners/#{specific_runner.id}/jobs", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(2) + end + end + + context 'when valid status is provided' do + it 'return filtered jobs' do + get api("/runners/#{specific_runner.id}/jobs?status=failed", user) + + expect(response).to have_gitlab_http_status(200) + expect(response).to include_pagination_headers + + expect(json_response).to be_an(Array) + expect(json_response.length).to eq(1) + expect(json_response.first).to include('id' => job_5.id) + end + end + + context 'when invalid status is provided' do + it 'return 400' do + get api("/runners/#{specific_runner.id}/jobs?status=non-existing", user) + + expect(response).to have_gitlab_http_status(400) + end + end + end + + context "when runner doesn't exist" do + it 'returns 404' do + get api('/runners/9999/jobs', user) + + expect(response).to have_gitlab_http_status(404) + end + end + end + + context 'other authorized user' do + it 'does not return jobs' do + get api("/runners/#{specific_runner.id}/jobs", user2) + + expect(response).to have_gitlab_http_status(403) + end + end + + context 'unauthorized user' do + it 'does not return jobs' do + get api("/runners/#{specific_runner.id}/jobs") + + expect(response).to have_gitlab_http_status(401) + end + end + end + describe 'GET /projects/:id/runners' do context 'authorized user with master privileges' do it "returns project's runners" do diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb new file mode 100644 index 00000000000..d74d98c6079 --- /dev/null +++ b/spec/services/issuable/destroy_service_spec.rb @@ -0,0 +1,38 @@ +require 'spec_helper' + +describe Issuable::DestroyService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + subject(:service) { described_class.new(project, user) } + + describe '#execute' do + context 'when issuable is an issue' do + let!(:issue) { create(:issue, project: project, author: user) } + + it 'destroys the issue' do + expect { service.execute(issue) }.to change { project.issues.count }.by(-1) + end + + it 'updates open issues count cache' do + expect_any_instance_of(Projects::OpenIssuesCountService).to receive(:refresh_cache) + + service.execute(issue) + end + end + + context 'when issuable is a merge request' do + let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user) } + + it 'destroys the merge request' do + expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1) + end + + it 'updates open merge requests count cache' do + expect_any_instance_of(Projects::OpenMergeRequestsCountService).to receive(:refresh_cache) + + service.execute(merge_request) + end + end + end +end diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index b46c419de14..fee293760f5 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -29,13 +29,27 @@ describe MergeRequests::BuildService do before do project.team << [user, :guest] + end + def stub_compare allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare) allow(project).to receive(:commit).and_return(commit_1) allow(project).to receive(:commit).and_return(commit_2) end - describe 'execute' do + describe '#execute' do + it 'calls the compare service with the correct arguments' do + expect(CompareService).to receive(:new) + .with(project, Gitlab::Git::BRANCH_REF_PREFIX + source_branch) + .and_call_original + + expect_any_instance_of(CompareService).to receive(:execute) + .with(project, Gitlab::Git::BRANCH_REF_PREFIX + target_branch) + .and_call_original + + merge_request + end + context 'missing source branch' do let(:source_branch) { '' } @@ -52,6 +66,10 @@ describe MergeRequests::BuildService do let(:target_branch) { nil } let(:commits) { Commit.decorate([commit_1], project) } + before do + stub_compare + end + it 'creates compare object with target branch as default branch' do expect(merge_request.compare).to be_present expect(merge_request.target_branch).to eq(project.default_branch) @@ -77,6 +95,10 @@ describe MergeRequests::BuildService do context 'no commits in the diff' do let(:commits) { [] } + before do + stub_compare + end + it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) end @@ -89,6 +111,10 @@ describe MergeRequests::BuildService do context 'one commit in the diff' do let(:commits) { Commit.decorate([commit_1], project) } + before do + stub_compare + end + it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) end @@ -149,6 +175,10 @@ describe MergeRequests::BuildService do context 'more than one commit in the diff' do let(:commits) { Commit.decorate([commit_1, commit_2], project) } + before do + stub_compare + end + it 'allows the merge request to be created' do expect(merge_request.can_be_created).to eq(true) end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index 5ce6ca70c83..7a66b809550 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -65,7 +65,7 @@ describe MergeRequests::UpdateService, :mailer do end end - it 'mathces base expectations' do + it 'matches base expectations' do expect(@merge_request).to be_valid expect(@merge_request.title).to eq('New title') expect(@merge_request.assignee).to eq(user2) @@ -78,9 +78,17 @@ describe MergeRequests::UpdateService, :mailer do end it 'executes hooks with update action' do - expect(service) - .to have_received(:execute_hooks) - .with(@merge_request, 'update', old_labels: [], old_assignees: [user3], old_total_time_spent: 0) + expect(service).to have_received(:execute_hooks) + .with( + @merge_request, + 'update', + old_associations: { + labels: [], + mentioned_users: [user2], + assignees: [user3], + total_time_spent: 0 + } + ) end it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do diff --git a/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb new file mode 100644 index 00000000000..50e59954f73 --- /dev/null +++ b/spec/services/projects/hashed_storage/migrate_attachments_service_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe Projects::HashedStorage::MigrateAttachmentsService do + subject(:service) { described_class.new(project) } + let(:project) { create(:project) } + let(:legacy_storage) { Storage::LegacyProject.new(project) } + let(:hashed_storage) { Storage::HashedProject.new(project) } + + let!(:upload) { Upload.find_by(path: file_uploader.relative_path) } + let(:file_uploader) { build(:file_uploader, project: project) } + let(:old_path) { File.join(base_path(legacy_storage), upload.path) } + let(:new_path) { File.join(base_path(hashed_storage), upload.path) } + + context '#execute' do + context 'when succeeds' do + it 'moves attachments to hashed storage layout' do + expect(File.file?(old_path)).to be_truthy + expect(File.file?(new_path)).to be_falsey + expect(File.exist?(base_path(legacy_storage))).to be_truthy + expect(File.exist?(base_path(hashed_storage))).to be_falsey + expect(FileUtils).to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage)).and_call_original + + service.execute + + expect(File.exist?(base_path(hashed_storage))).to be_truthy + expect(File.exist?(base_path(legacy_storage))).to be_falsey + expect(File.file?(old_path)).to be_falsey + expect(File.file?(new_path)).to be_truthy + end + end + + context 'when original folder does not exist anymore' do + before do + FileUtils.rm_rf(base_path(legacy_storage)) + end + + it 'skips moving folders and go to next' do + expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage)) + + service.execute + + expect(File.exist?(base_path(hashed_storage))).to be_falsey + expect(File.file?(new_path)).to be_falsey + end + end + + context 'when target folder already exists' do + before do + FileUtils.mkdir_p(base_path(hashed_storage)) + end + + it 'raises AttachmentMigrationError' do + expect(FileUtils).not_to receive(:mv).with(base_path(legacy_storage), base_path(hashed_storage)) + + expect { service.execute }.to raise_error(Projects::HashedStorage::AttachmentMigrationError) + end + end + end + + def base_path(storage) + FileUploader.dynamic_path_builder(storage.disk_path) + end +end diff --git a/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb new file mode 100644 index 00000000000..3a3e47fd9c0 --- /dev/null +++ b/spec/services/projects/hashed_storage/migrate_repository_service_spec.rb @@ -0,0 +1,76 @@ +require 'spec_helper' + +describe Projects::HashedStorage::MigrateRepositoryService do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:project) { create(:project, :empty_repo, :wiki_repo) } + let(:service) { described_class.new(project) } + let(:legacy_storage) { Storage::LegacyProject.new(project) } + let(:hashed_storage) { Storage::HashedProject.new(project) } + + describe '#execute' do + before do + allow(service).to receive(:gitlab_shell) { gitlab_shell } + end + + context 'when succeeds' do + it 'renames project and wiki repositories' do + service.execute + + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy + end + + it 'updates project to be hashed and not read-only' do + service.execute + + expect(project.hashed_storage?(:repository)).to be_truthy + expect(project.repository_read_only).to be_falsey + end + + it 'move operation is called for both repositories' do + expect_move_repository(project.disk_path, hashed_storage.disk_path) + expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + + service.execute + end + end + + context 'when one move fails' do + it 'rollsback repositories to original name' do + from_name = project.disk_path + to_name = hashed_storage.disk_path + allow(service).to receive(:move_repository).and_call_original + allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only + + expect(service).to receive(:rollback_folder_move).and_call_original + + service.execute + + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey + expect(project.repository_read_only?).to be_falsey + end + + context 'when rollback fails' do + let(:from_name) { legacy_storage.disk_path } + let(:to_name) { hashed_storage.disk_path } + + before do + hashed_storage.ensure_storage_path_exists + gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) + end + + it 'does not try to move nil repository over hashed' do + expect(gitlab_shell).not_to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name) + expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + + service.execute + end + end + end + + def expect_move_repository(from_name, to_name) + expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original + end + end +end diff --git a/spec/services/projects/hashed_storage_migration_service_spec.rb b/spec/services/projects/hashed_storage_migration_service_spec.rb index b71b47c59b6..466f0b5d7c2 100644 --- a/spec/services/projects/hashed_storage_migration_service_spec.rb +++ b/spec/services/projects/hashed_storage_migration_service_spec.rb @@ -1,74 +1,44 @@ require 'spec_helper' describe Projects::HashedStorageMigrationService do - let(:gitlab_shell) { Gitlab::Shell.new } let(:project) { create(:project, :empty_repo, :wiki_repo) } - let(:service) { described_class.new(project) } - let(:legacy_storage) { Storage::LegacyProject.new(project) } - let(:hashed_storage) { Storage::HashedProject.new(project) } + subject(:service) { described_class.new(project) } describe '#execute' do - before do - allow(service).to receive(:gitlab_shell) { gitlab_shell } - end - - context 'when succeeds' do - it 'renames project and wiki repositories' do - service.execute + context 'repository migration' do + let(:repository_service) { Projects::HashedStorage::MigrateRepositoryService.new(project, subject.logger) } - expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_truthy - expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_truthy - end + it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do + expect(Projects::HashedStorage::MigrateRepositoryService).to receive(:new).with(project, subject.logger).and_return(repository_service) + expect(repository_service).to receive(:execute) - it 'updates project to be hashed and not read-only' do service.execute - - expect(project.hashed_storage?(:repository)).to be_truthy - expect(project.repository_read_only).to be_falsey end - it 'move operation is called for both repositories' do - expect_move_repository(project.disk_path, hashed_storage.disk_path) - expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") + it 'does not delegate migration if repository is already migrated' do + project.storage_version = ::Project::LATEST_STORAGE_VERSION + expect(Projects::HashedStorage::MigrateRepositoryService).not_to receive(:new) service.execute end end - context 'when one move fails' do - it 'rollsback repositories to original name' do - from_name = project.disk_path - to_name = hashed_storage.disk_path - allow(service).to receive(:move_repository).and_call_original - allow(service).to receive(:move_repository).with(from_name, to_name).once { false } # will disable first move only + context 'attachments migration' do + let(:attachments_service) { Projects::HashedStorage::MigrateAttachmentsService.new(project, subject.logger) } - expect(service).to receive(:rollback_folder_move).and_call_original + it 'delegates migration to Projects::HashedStorage::MigrateRepositoryService' do + expect(Projects::HashedStorage::MigrateAttachmentsService).to receive(:new).with(project, subject.logger).and_return(attachments_service) + expect(attachments_service).to receive(:execute) service.execute - - expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.git")).to be_falsey - expect(gitlab_shell.exists?(project.repository_storage_path, "#{hashed_storage.disk_path}.wiki.git")).to be_falsey end - context 'when rollback fails' do - before do - from_name = legacy_storage.disk_path - to_name = hashed_storage.disk_path + it 'does not delegate migration if attachments are already migrated' do + project.storage_version = ::Project::LATEST_STORAGE_VERSION + expect(Projects::HashedStorage::MigrateAttachmentsService).not_to receive(:new) - hashed_storage.ensure_storage_path_exists - gitlab_shell.mv_repository(project.repository_storage_path, from_name, to_name) - end - - it 'does not try to move nil repository over hashed' do - expect_move_repository("#{project.disk_path}.wiki", "#{hashed_storage.disk_path}.wiki") - - service.execute - end + service.execute end end - - def expect_move_repository(from_name, to_name) - expect(gitlab_shell).to receive(:mv_repository).with(project.repository_storage_path, from_name, to_name).and_call_original - end end end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 3da222e2ed8..fcd71857af3 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,198 +1,222 @@ require 'spec_helper' -describe Projects::UpdateService, '#execute' do +describe Projects::UpdateService do include ProjectForksHelper - let(:gitlab_shell) { Gitlab::Shell.new } let(:user) { create(:user) } - let(:admin) { create(:admin) } - let(:project) do create(:project, creator: user, namespace: user.namespace) end - context 'when changing visibility level' do - context 'when visibility_level is INTERNAL' do - it 'updates the project to internal' do - result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) - - expect(result).to eq({ status: :success }) - expect(project).to be_internal - end - end - - context 'when visibility_level is PUBLIC' do - it 'updates the project to public' do - result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - expect(result).to eq({ status: :success }) - expect(project).to be_public - end - end - - context 'when visibility levels are restricted to PUBLIC only' do - before do - stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) - end + describe '#execute' do + let(:gitlab_shell) { Gitlab::Shell.new } + let(:admin) { create(:admin) } + context 'when changing visibility level' do context 'when visibility_level is INTERNAL' do it 'updates the project to internal' do result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) + expect(result).to eq({ status: :success }) expect(project).to be_internal end end context 'when visibility_level is PUBLIC' do - it 'does not update the project to public' do + it 'updates the project to public' do result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + expect(result).to eq({ status: :success }) + expect(project).to be_public + end + end - expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' }) - expect(project).to be_private + context 'when visibility levels are restricted to PUBLIC only' do + before do + stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) end - context 'when updated by an admin' do - it 'updates the project to public' do - result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + context 'when visibility_level is INTERNAL' do + it 'updates the project to internal' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) expect(result).to eq({ status: :success }) - expect(project).to be_public + expect(project).to be_internal end end - end - end - context 'When project visibility is higher than parent group' do - let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + context 'when visibility_level is PUBLIC' do + it 'does not update the project to public' do + result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - before do - project.update(namespace: group, visibility_level: group.visibility_level) + expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' }) + expect(project).to be_private + end + + context 'when updated by an admin' do + it 'updates the project to public' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + expect(result).to eq({ status: :success }) + expect(project).to be_public + end + end + end end - it 'does not update project visibility level' do - result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) + context 'When project visibility is higher than parent group' do + let(:group) { create(:group, visibility_level: Gitlab::VisibilityLevel::INTERNAL) } + + before do + project.update(namespace: group, visibility_level: group.visibility_level) + end + + it 'does not update project visibility level' do + result = update_project(project, admin, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) - expect(project.reload).to be_internal + expect(result).to eq({ status: :error, message: 'Visibility level public is not allowed in a internal group.' }) + expect(project.reload).to be_internal + end end end - end - describe 'when updating project that has forks' do - let(:project) { create(:project, :internal) } - let(:forked_project) { fork_project(project) } + describe 'when updating project that has forks' do + let(:project) { create(:project, :internal) } + let(:forked_project) { fork_project(project) } - it 'updates forks visibility level when parent set to more restrictive' do - opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } + it 'updates forks visibility level when parent set to more restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PRIVATE } - expect(project).to be_internal - expect(forked_project).to be_internal + expect(project).to be_internal + expect(forked_project).to be_internal - expect(update_project(project, admin, opts)).to eq({ status: :success }) + expect(update_project(project, admin, opts)).to eq({ status: :success }) - expect(project).to be_private - expect(forked_project.reload).to be_private - end + expect(project).to be_private + expect(forked_project.reload).to be_private + end - it 'does not update forks visibility level when parent set to less restrictive' do - opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } + it 'does not update forks visibility level when parent set to less restrictive' do + opts = { visibility_level: Gitlab::VisibilityLevel::PUBLIC } - expect(project).to be_internal - expect(forked_project).to be_internal + expect(project).to be_internal + expect(forked_project).to be_internal - expect(update_project(project, admin, opts)).to eq({ status: :success }) + expect(update_project(project, admin, opts)).to eq({ status: :success }) - expect(project).to be_public - expect(forked_project.reload).to be_internal + expect(project).to be_public + expect(forked_project.reload).to be_internal + end end - end - context 'when updating a default branch' do - let(:project) { create(:project, :repository) } + context 'when updating a default branch' do + let(:project) { create(:project, :repository) } - it 'changes a default branch' do - update_project(project, admin, default_branch: 'feature') + it 'changes a default branch' do + update_project(project, admin, default_branch: 'feature') - expect(Project.find(project.id).default_branch).to eq 'feature' - end + expect(Project.find(project.id).default_branch).to eq 'feature' + end - it 'does not change a default branch' do - # The branch 'unexisted-branch' does not exist. - update_project(project, admin, default_branch: 'unexisted-branch') + it 'does not change a default branch' do + # The branch 'unexisted-branch' does not exist. + update_project(project, admin, default_branch: 'unexisted-branch') - expect(Project.find(project.id).default_branch).to eq 'master' + expect(Project.find(project.id).default_branch).to eq 'master' + end end - end - context 'when updating a project that contains container images' do - before do - stub_container_registry_config(enabled: true) - stub_container_registry_tags(repository: /image/, tags: %w[rc1]) - create(:container_repository, project: project, name: :image) - end + context 'when updating a project that contains container images' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + end - it 'does not allow to rename the project' do - result = update_project(project, admin, path: 'renamed') + it 'does not allow to rename the project' do + result = update_project(project, admin, path: 'renamed') - expect(result).to include(status: :error) - expect(result[:message]).to match(/contains container registry tags/) - end + expect(result).to include(status: :error) + expect(result[:message]).to match(/contains container registry tags/) + end - it 'allows to update other settings' do - result = update_project(project, admin, public_builds: true) + it 'allows to update other settings' do + result = update_project(project, admin, public_builds: true) - expect(result[:status]).to eq :success - expect(project.reload.public_builds).to be true + expect(result[:status]).to eq :success + expect(project.reload.public_builds).to be true + end end - end - context 'when renaming a project' do - let(:repository_storage) { 'default' } - let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } + context 'when renaming a project' do + let(:repository_storage) { 'default' } + let(:repository_storage_path) { Gitlab.config.repositories.storages[repository_storage]['path'] } - context 'with legacy storage' do - before do - gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") - end + context 'with legacy storage' do + before do + gitlab_shell.add_repository(repository_storage, "#{user.namespace.full_path}/existing") + end + + after do + gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + end + + it 'does not allow renaming when new path matches existing repository on disk' do + result = update_project(project, admin, path: 'existing') - after do - gitlab_shell.remove_repository(repository_storage_path, "#{user.namespace.full_path}/existing") + expect(result).to include(status: :error) + expect(result[:message]).to match('There is already a repository with that name on disk') + expect(project).not_to be_valid + expect(project.errors.messages).to have_key(:base) + expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') + end end - it 'does not allow renaming when new path matches existing repository on disk' do - result = update_project(project, admin, path: 'existing') + context 'with hashed storage' do + let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } - expect(result).to include(status: :error) - expect(result[:message]).to match('There is already a repository with that name on disk') - expect(project).not_to be_valid - expect(project.errors.messages).to have_key(:base) - expect(project.errors.messages[:base]).to include('There is already a repository with that name on disk') + before do + stub_application_setting(hashed_storage_enabled: true) + end + + it 'does not check if new path matches existing repository on disk' do + expect(project).not_to receive(:repository_with_same_path_already_exists?) + + result = update_project(project, admin, path: 'existing') + + expect(result).to include(status: :success) + end end end - context 'with hashed storage' do - let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) } + context 'when passing invalid parameters' do + it 'returns an error result when record cannot be updated' do + result = update_project(project, admin, { name: 'foo&bar' }) - before do - stub_application_setting(hashed_storage_enabled: true) + expect(result).to eq({ + status: :error, + message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." + }) end + end + end - it 'does not check if new path matches existing repository on disk' do - expect(project).not_to receive(:repository_with_same_path_already_exists?) + describe '#run_auto_devops_pipeline?' do + subject { described_class.new(project, user, params).run_auto_devops_pipeline? } - result = update_project(project, admin, path: 'existing') + context 'when neither pipeline setting is true' do + let(:params) { {} } - expect(result).to include(status: :success) - end + it { is_expected.to eq(false) } + end + + context 'when run_auto_devops_pipeline_explicit is true' do + let(:params) { { run_auto_devops_pipeline_explicit: 'true' } } + + it { is_expected.to eq(true) } end - end - context 'when passing invalid parameters' do - it 'returns an error result when record cannot be updated' do - result = update_project(project, admin, { name: 'foo&bar' }) + context 'when run_auto_devops_pipeline_implicit is true' do + let(:params) { { run_auto_devops_pipeline_implicit: 'true' } } - expect(result).to eq({ - status: :error, - message: "Name can contain only letters, digits, emojis, '_', '.', dash, space. It must start with letter, digit, emoji or '_'." - }) + it { is_expected.to eq(true) } end end diff --git a/spec/workers/create_pipeline_worker_spec.rb b/spec/workers/create_pipeline_worker_spec.rb new file mode 100644 index 00000000000..02cb0f46cb4 --- /dev/null +++ b/spec/workers/create_pipeline_worker_spec.rb @@ -0,0 +1,36 @@ +require 'spec_helper' + +describe CreatePipelineWorker do + describe '#perform' do + let(:worker) { described_class.new } + + context 'when a project not found' do + it 'does not call the Service' do + expect(Ci::CreatePipelineService).not_to receive(:new) + expect { worker.perform(99, create(:user).id, 'master', :web) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when a user not found' do + let(:project) { create(:project) } + + it 'does not call the Service' do + expect(Ci::CreatePipelineService).not_to receive(:new) + expect { worker.perform(project.id, 99, project.default_branch, :web) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context 'when everything is ok' do + let(:project) { create(:project) } + let(:user) { create(:user) } + let(:create_pipeline_service) { instance_double(Ci::CreatePipelineService) } + + it 'calls the Service' do + expect(Ci::CreatePipelineService).to receive(:new).with(project, user, ref: project.default_branch).and_return(create_pipeline_service) + expect(create_pipeline_service).to receive(:execute).with(:web, any_args) + + worker.perform(project.id, user.id, project.default_branch, :web) + end + end + end +end diff --git a/spec/workers/project_migrate_hashed_storage_worker_spec.rb b/spec/workers/project_migrate_hashed_storage_worker_spec.rb index f5226dee0ad..2e3951e7afc 100644 --- a/spec/workers/project_migrate_hashed_storage_worker_spec.rb +++ b/spec/workers/project_migrate_hashed_storage_worker_spec.rb @@ -1,29 +1,53 @@ require 'spec_helper' -describe ProjectMigrateHashedStorageWorker do +describe ProjectMigrateHashedStorageWorker, :clean_gitlab_redis_shared_state do describe '#perform' do let(:project) { create(:project, :empty_repo) } let(:pending_delete_project) { create(:project, :empty_repo, pending_delete: true) } - it 'skips when project no longer exists' do - nonexistent_id = 999999999999 + context 'when have exclusive lease' do + before do + lease = subject.lease_for(project.id) - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - subject.perform(nonexistent_id) - end + allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease) + allow(lease).to receive(:try_obtain).and_return(true) + end + + it 'skips when project no longer exists' do + nonexistent_id = 999999999999 + + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + subject.perform(nonexistent_id) + end + + it 'skips when project is pending delete' do + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - it 'skips when project is pending delete' do - expect(::Projects::HashedStorageMigrationService).not_to receive(:new) + subject.perform(pending_delete_project.id) + end - subject.perform(pending_delete_project.id) + it 'delegates removal to service class' do + service = double('service') + expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service) + expect(service).to receive(:execute) + + subject.perform(project.id) + end end - it 'delegates removal to service class' do - service = double('service') - expect(::Projects::HashedStorageMigrationService).to receive(:new).with(project, subject.logger).and_return(service) - expect(service).to receive(:execute) + context 'when dont have exclusive lease' do + before do + lease = subject.lease_for(project.id) + + allow(Gitlab::ExclusiveLease).to receive(:new).and_return(lease) + allow(lease).to receive(:try_obtain).and_return(false) + end + + it 'skips when dont have lease' do + expect(::Projects::HashedStorageMigrationService).not_to receive(:new) - subject.perform(project.id) + subject.perform(project.id) + end end end end diff --git a/spec/workers/stuck_ci_jobs_worker_spec.rb b/spec/workers/stuck_ci_jobs_worker_spec.rb index ac6f4fefb4e..bdc64c6785b 100644 --- a/spec/workers/stuck_ci_jobs_worker_spec.rb +++ b/spec/workers/stuck_ci_jobs_worker_spec.rb @@ -105,8 +105,8 @@ describe StuckCiJobsWorker do job.project.update(pending_delete: true) end - it 'does not drop job' do - expect_any_instance_of(Ci::Build).not_to receive(:drop) + it 'does drop job' do + expect_any_instance_of(Ci::Build).to receive(:drop).and_call_original worker.perform end end @@ -117,7 +117,7 @@ describe StuckCiJobsWorker do let(:worker2) { described_class.new } it 'is guard by exclusive lease when executed concurrently' do - expect(worker).to receive(:drop).at_least(:once) + expect(worker).to receive(:drop).at_least(:once).and_call_original expect(worker2).not_to receive(:drop) worker.perform allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(false) @@ -125,8 +125,8 @@ describe StuckCiJobsWorker do end it 'can be executed in sequence' do - expect(worker).to receive(:drop).at_least(:once) - expect(worker2).to receive(:drop).at_least(:once) + expect(worker).to receive(:drop).at_least(:once).and_call_original + expect(worker2).to receive(:drop).at_least(:once).and_call_original worker.perform worker2.perform end |