diff options
Diffstat (limited to 'spec')
49 files changed, 982 insertions, 139 deletions
diff --git a/spec/controllers/admin/health_check_controller_spec.rb b/spec/controllers/admin/health_check_controller_spec.rb new file mode 100644 index 00000000000..0b8e0c8a065 --- /dev/null +++ b/spec/controllers/admin/health_check_controller_spec.rb @@ -0,0 +1,25 @@ +require 'spec_helper' + +describe Admin::HealthCheckController, broken_storage: true do + let(:admin) { create(:admin) } + + before do + sign_in(admin) + end + + describe 'GET show' do + it 'loads the git storage health information' do + get :show + + expect(assigns[:failing_storage_statuses]).not_to be_nil + end + end + + describe 'POST reset_storage_health' do + it 'resets all storage health information' do + expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + + post :reset_storage_health + end + end +end diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index 1641bddea11..331903a5543 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -108,6 +108,30 @@ describe ApplicationController do end end + describe 'rescue from Gitlab::Git::Storage::Inaccessible' do + controller(described_class) do + def index + raise Gitlab::Git::Storage::Inaccessible.new('broken', 100) + end + end + + it 'renders a 503 when storage is not available' do + sign_in(create(:user)) + + get :index + + expect(response.status).to eq(503) + end + + it 'renders includes a Retry-After header' do + sign_in(create(:user)) + + get :index + + expect(response.headers['Retry-After']).to eq(100) + end + end + describe 'response format' do controller(described_class) do def index diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 34095ef6250..8ecd8b6ca71 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -107,6 +107,20 @@ describe ProjectsController do end end + context 'when the storage is not available', broken_storage: true do + let(:project) { create(:project, :broken_storage) } + before do + project.add_developer(user) + sign_in(user) + end + + it 'renders a 503' do + get :show, namespace_id: project.namespace, id: project + + expect(response).to have_http_status(503) + end + end + context "project with empty repo" do let(:empty_project) { create(:project_empty_repo, :public) } diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index be3f219e8bf..3f8e7030b1c 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -54,6 +54,12 @@ FactoryGirl.define do avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) } end + trait :broken_storage do + after(:create) do |project| + project.update_column(:repository_storage, 'broken') + end + end + # Test repository - https://gitlab.com/gitlab-org/gitlab-test trait :repository do path { 'gitlabhq' } diff --git a/spec/features/admin/admin_health_check_spec.rb b/spec/features/admin/admin_health_check_spec.rb index 106e7370a98..37fd3e171eb 100644 --- a/spec/features/admin/admin_health_check_spec.rb +++ b/spec/features/admin/admin_health_check_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "Admin Health Check" do +feature "Admin Health Check", feature: true, broken_storage: true do include StubENV before do @@ -55,4 +55,26 @@ feature "Admin Health Check" do expect(page).to have_content('The server is on fire') end end + + context 'with repository storage failures' do + before do + # Track a failure + Gitlab::Git::Storage::CircuitBreaker.for_storage('broken').perform { nil } rescue nil + visit admin_health_check_path + end + + it 'shows storage failure information' do + hostname = Gitlab::Environment.hostname + + expect(page).to have_content('broken: failed storage access attempt on host:') + expect(page).to have_content("#{hostname}: 1 of 10 failures.") + end + + it 'allows resetting storage failures' do + click_button 'Reset git storage health information' + + expect(page).to have_content('Git storage health information has been reset') + expect(page).not_to have_content('failed storage access attempt') + end + end end diff --git a/spec/features/merge_requests/closes_issues_spec.rb b/spec/features/merge_requests/closes_issues_spec.rb index 0e97254eada..299b4f5708a 100644 --- a/spec/features/merge_requests/closes_issues_spec.rb +++ b/spec/features/merge_requests/closes_issues_spec.rb @@ -26,17 +26,11 @@ feature 'Merge Request closing issues message', js: true do wait_for_requests end - context 'not closing or mentioning any issue' do - it 'does not display closing issue message' do - expect(page).not_to have_css('.mr-widget-footer') - end - end - context 'closing issues but not mentioning any other issue' do let(:merge_request_description) { "Description\n\nclosing #{issue_1.to_reference}, #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") + expect(page).to have_content("Closes #{issue_1.to_reference} and #{issue_2.to_reference}") end end @@ -44,7 +38,7 @@ feature 'Merge Request closing issues message', js: true do let(:merge_request_description) { "Description\n\nRefers to #{issue_1.to_reference} and #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") + expect(page).to have_content("Mentions #{issue_1.to_reference} and #{issue_2.to_reference}") end end @@ -52,8 +46,8 @@ feature 'Merge Request closing issues message', js: true do let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Closes issue #{issue_1.to_reference}.") - expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") + expect(page).to have_content("Closes #{issue_1.to_reference}") + expect(page).to have_content("Mentions #{issue_2.to_reference}") end end @@ -61,7 +55,7 @@ feature 'Merge Request closing issues message', js: true do let(:merge_request_title) { "closing #{issue_1.to_reference}, #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Closes issues #{issue_1.to_reference} and #{issue_2.to_reference}") + expect(page).to have_content("Closes #{issue_1.to_reference} and #{issue_2.to_reference}") end end @@ -69,7 +63,7 @@ feature 'Merge Request closing issues message', js: true do let(:merge_request_title) { "Refers to #{issue_1.to_reference} and #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Issues #{issue_1.to_reference} and #{issue_2.to_reference} are mentioned but will not be closed.") + expect(page).to have_content("Mentions #{issue_1.to_reference} and #{issue_2.to_reference}") end end @@ -77,8 +71,8 @@ feature 'Merge Request closing issues message', js: true do let(:merge_request_title) { "closes #{issue_1.to_reference}\n\n refers to #{issue_2.to_reference}" } it 'does not display closing issue message' do - expect(page).to have_content("Closes issue #{issue_1.to_reference}. Issue #{issue_2.to_reference} is mentioned but will not be closed.") - expect(page).to have_content("Issue #{issue_2.to_reference} is mentioned but will not be closed.") + expect(page).to have_content("Closes #{issue_1.to_reference}") + expect(page).to have_content("Mentions #{issue_2.to_reference}") end end end diff --git a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb index 574f5fe353e..ac46cc1f0e4 100644 --- a/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb +++ b/spec/features/merge_requests/merge_when_pipeline_succeeds_spec.rb @@ -41,8 +41,8 @@ feature 'Merge When Pipeline Succeeds', :js do it 'activates the Merge when pipeline succeeds feature' do click_button "Merge when pipeline succeeds" - expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will not be removed." + expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" + expect(page).to have_content "The source branch will not be removed" expect(page).to have_selector ".js-cancel-auto-merge" visit_merge_request(merge_request) # Needed to refresh the page expect(page).to have_content /enabled an automatic merge when the pipeline for \h{8} succeeds/i @@ -97,11 +97,11 @@ feature 'Merge When Pipeline Succeeds', :js do describe 'enabling Merge when pipeline succeeds via dropdown' do it 'activates the Merge when pipeline succeeds feature' do - click_button 'Select merge moment' + find('.js-merge-moment').click click_link 'Merge when pipeline succeeds' - expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds." - expect(page).to have_content "The source branch will not be removed." + expect(page).to have_content "Set by #{user.name} to be merged automatically when the pipeline succeeds" + expect(page).to have_content "The source branch will not be removed" expect(page).to have_link "Cancel automatic merge" end end diff --git a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb index 5c6eec44ff7..59e67420333 100644 --- a/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb +++ b/spec/features/merge_requests/only_allow_merge_if_build_succeeds_spec.rb @@ -43,7 +43,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t wait_for_requests expect(page).to have_button 'Merge when pipeline succeeds' - expect(page).not_to have_button 'Select merge moment' + expect(page).not_to have_button '.js-merge-moment' end end @@ -56,7 +56,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t wait_for_requests expect(page).to have_css('button[disabled="disabled"]', text: 'Merge') - expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure') end end @@ -69,7 +69,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t wait_for_requests expect(page).not_to have_button 'Merge' - expect(page).to have_content('Please retry the job or push a new commit to fix the failure.') + expect(page).to have_content('Please retry the job or push a new commit to fix the failure') end end @@ -113,7 +113,7 @@ feature 'Only allow merge requests to be merged if the pipeline succeeds', js: t expect(page).to have_button 'Merge when pipeline succeeds' - click_button 'Select merge moment' + page.find('.js-merge-moment').click expect(page).to have_content 'Merge immediately' end end diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb index 43cab65d287..95c50df1896 100644 --- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb +++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb @@ -3,17 +3,17 @@ require 'rails_helper' feature 'Merge Requests > User uses quick actions', js: true do include QuickActionsHelpers - let(:user) { create(:user) } - let(:project) { create(:project, :public, :repository) } - let(:merge_request) { create(:merge_request, source_project: project) } - let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } - it_behaves_like 'issuable record that supports quick actions in its description and notes', :merge_request do let(:issuable) { create(:merge_request, source_project: project) } let(:new_url_opts) { { merge_request: { source_branch: 'feature', target_branch: 'master' } } } end describe 'merge-request-only commands' do + let(:user) { create(:user) } + let(:project) { create(:project, :public, :repository) } + let(:merge_request) { create(:merge_request, source_project: project) } + let!(:milestone) { create(:milestone, project: project, title: 'ASAP') } + before do project.team << [user, :master] sign_in(user) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 236a7c29634..37a5e6b474e 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -411,4 +411,48 @@ describe ProjectsHelper do end end end + + describe '#has_projects_or_name?' do + let(:projects) do + create(:project) + Project.all + end + + it 'returns true when there are projects' do + expect(helper.has_projects_or_name?(projects, {})).to eq(true) + end + + it 'returns true when there are no projects but a name is given' do + expect(helper.has_projects_or_name?(Project.none, name: 'foo')).to eq(true) + end + + it 'returns false when there are no projects and there is no name' do + expect(helper.has_projects_or_name?(Project.none, {})).to eq(false) + end + end + + describe '#any_projects?' do + before do + create(:project) + end + + it 'returns true when projects will be returned' do + expect(helper.any_projects?(Project.all)).to eq(true) + end + + it 'returns false when no projects will be returned' do + expect(helper.any_projects?(Project.none)).to eq(false) + end + + it 'only executes a single query when a LIMIT is applied' do + relation = Project.limit(1) + recorder = ActiveRecord::QueryRecorder.new do + 2.times do + helper.any_projects?(relation) + end + end + + expect(recorder.count).to eq(1) + end + end end diff --git a/spec/helpers/storage_health_helper_spec.rb b/spec/helpers/storage_health_helper_spec.rb new file mode 100644 index 00000000000..874498e6338 --- /dev/null +++ b/spec/helpers/storage_health_helper_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe StorageHealthHelper do + describe '#failing_storage_health_message' do + let(:health) do + Gitlab::Git::Storage::Health.new( + "<script>alert('storage name');)</script>", + [] + ) + end + + it 'escapes storage names' do + escaped_storage_name = '<script>alert('storage name');)</script>' + + result = helper.failing_storage_health_message(health) + + expect(result).to include(escaped_storage_name) + end + end +end diff --git a/spec/initializers/6_validations_spec.rb b/spec/initializers/6_validations_spec.rb index 0877770c167..83283f03940 100644 --- a/spec/initializers/6_validations_spec.rb +++ b/spec/initializers/6_validations_spec.rb @@ -23,6 +23,16 @@ describe '6_validations' do end end + context 'when one of the settings is incorrect' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/paths/a/b/c', 'failure_count_threshold' => 'not a number' }) + end + + it 'throws an error' do + expect { validate_storages_config }.to raise_error(/failure_count_threshold/) + end + end + context 'with invalid storage names' do before do mock_storages('name with spaces' => { 'path' => 'tmp/tests/paths/a/b/c' }) @@ -84,6 +94,17 @@ describe '6_validations' do expect { validate_storages_paths }.not_to raise_error end end + + describe 'inaccessible storage' do + before do + mock_storages('foo' => { 'path' => 'tmp/tests/a/path/that/does/not/exist' }) + end + + it 'passes through with a warning' do + expect(Rails.logger).to receive(:error) + expect { validate_storages_paths }.not_to raise_error + end + end end def mock_storages(storages) diff --git a/spec/initializers/settings_spec.rb b/spec/initializers/settings_spec.rb index ebdabcf93f1..e5ec90cb8f9 100644 --- a/spec/initializers/settings_spec.rb +++ b/spec/initializers/settings_spec.rb @@ -2,6 +2,17 @@ require 'spec_helper' require_relative '../../config/initializers/1_settings' describe Settings do + describe '#repositories' do + it 'assigns the default failure attributes' do + repository_settings = Gitlab.config.repositories.storages['broken'] + + expect(repository_settings['failure_count_threshold']).to eq(10) + expect(repository_settings['failure_wait_time']).to eq(30) + expect(repository_settings['failure_reset_time']).to eq(1800) + expect(repository_settings['storage_timeout']).to eq(5) + end + end + describe '#host_without_www' do context 'URL with protocol' do it 'returns the host' do diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js index ab8a3f6c64c..7ee998c8fce 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js @@ -1,7 +1,6 @@ import Vue from 'vue'; import deploymentComponent from '~/vue_merge_request_widget/components/mr_widget_deployment'; import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service'; -import { statusIconEntityMap } from '~/vue_shared/ci_status_icons'; const deploymentMockData = [ { @@ -43,15 +42,6 @@ describe('MRWidgetDeployment', () => { }); }); - describe('computed', () => { - describe('svg', () => { - it('should have the proper SVG icon', () => { - const vm = createComponent(deploymentMockData); - expect(vm.svg).toEqual(statusIconEntityMap.icon_status_success); - }); - }); - }); - describe('methods', () => { let vm = createComponent(); const deployment = deploymentMockData[0]; diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js index 6adcbc73ed7..2ae3adc1f93 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js @@ -52,10 +52,10 @@ const createComponent = () => { }; const messages = { - loadingMetrics: 'Loading deployment statistics.', + loadingMetrics: 'Loading deployment statistics', hasMetrics: 'Memory usage unchanged from 0MB to 0MB', - loadFailed: 'Failed to load deployment statistics.', - metricsUnavailable: 'Deployment statistics are not available currently.', + loadFailed: 'Failed to load deployment statistics', + metricsUnavailable: 'Deployment statistics are not available currently', }; describe('MemoryUsage', () => { diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js index 647b59520f8..c763487d12f 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js @@ -81,13 +81,12 @@ describe('MRWidgetPipeline', () => { expect(el.querySelectorAll('.ci-status-icon.ci-status-icon-success').length).toEqual(1); expect(el.querySelector('.pipeline-id').textContent).toContain(`#${pipeline.id}`); expect(el.innerText).toContain('passed'); - expect(el.innerText).toContain('with stages'); expect(el.querySelector('.pipeline-id').getAttribute('href')).toEqual(pipeline.path); expect(el.querySelectorAll('.stage-container').length).toEqual(2); expect(el.querySelector('.js-ci-error')).toEqual(null); expect(el.querySelector('.js-commit-link').getAttribute('href')).toEqual(pipeline.commit.commit_path); expect(el.querySelector('.js-commit-link').textContent).toContain(pipeline.commit.short_id); - expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%.`); + expect(el.querySelector('.js-mr-coverage').textContent).toContain(`Coverage ${pipeline.coverage}%`); }); it('should list single stage', (done) => { @@ -95,7 +94,6 @@ describe('MRWidgetPipeline', () => { Vue.nextTick(() => { expect(el.querySelectorAll('.stage-container button').length).toEqual(1); - expect(el.innerText).toContain('with stage'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js index f6e0c3dfb74..f86fb6a0b4b 100644 --- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js +++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js @@ -22,15 +22,16 @@ describe('MRWidgetRelatedLinks', () => { }); describe('computed', () => { + const data = { + relatedLinks: { + closing: '/foo', + mentioned: '/foo', + assignToMe: '/foo', + }, + }; + describe('hasLinks', () => { it('should return correct value when we have links reference', () => { - const data = { - relatedLinks: { - closing: '/foo', - mentioned: '/foo', - assignToMe: '/foo', - }, - }; const vm = createComponent(data); expect(vm.hasLinks).toBeTruthy(); @@ -44,44 +45,24 @@ describe('MRWidgetRelatedLinks', () => { expect(vm.hasLinks).toBeFalsy(); }); }); - }); - - describe('methods', () => { - const data = { - relatedLinks: { - closing: '<a href="#">#23</a> and <a>#42</a>', - mentioned: '<a href="#">#7</a>', - }, - }; - const vm = createComponent(data); - - describe('hasMultipleIssues', () => { - it('should return true if the given text has multiple issues', () => { - expect(vm.hasMultipleIssues(data.relatedLinks.closing)).toBeTruthy(); - }); - - it('should return false if the given text has one issue', () => { - expect(vm.hasMultipleIssues(data.relatedLinks.mentioned)).toBeFalsy(); - }); - }); - describe('issueLabel', () => { - it('should return true if the given text has multiple issues', () => { - expect(vm.issueLabel('closing')).toEqual('issues'); - }); - - it('should return false if the given text has one issue', () => { - expect(vm.issueLabel('mentioned')).toEqual('issue'); + describe('closesText', () => { + it('returns correct text for open merge request', () => { + data.state = 'open'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Closes'); }); - }); - describe('verbLabel', () => { - it('should return true if the given text has multiple issues', () => { - expect(vm.verbLabel('closing')).toEqual('are'); + it('returns correct text for closed merge request', () => { + data.state = 'closed'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Did not close'); }); - it('should return false if the given text has one issue', () => { - expect(vm.verbLabel('mentioned')).toEqual('is'); + it('returns correct tense for merged request', () => { + data.state = 'merged'; + const vm = createComponent(data); + expect(vm.closesText).toEqual('Closed'); }); }); }); @@ -95,8 +76,8 @@ describe('MRWidgetRelatedLinks', () => { }); const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - expect(content).toContain('Closes issues #23 and #42'); - expect(content).not.toContain('mentioned'); + expect(content).toContain('Closes #23 and #42'); + expect(content).not.toContain('Mentions'); }); it('should have only have mentioned issues text', () => { @@ -106,8 +87,7 @@ describe('MRWidgetRelatedLinks', () => { }, }); - expect(vm.$el.innerText).toContain('issue #7'); - expect(vm.$el.innerText).toContain('is mentioned but will not be closed.'); + expect(vm.$el.innerText).toContain('Mentions #7'); expect(vm.$el.innerText).not.toContain('Closes'); }); @@ -120,9 +100,8 @@ describe('MRWidgetRelatedLinks', () => { }); const content = vm.$el.textContent.replace(/\n(\s)+/g, ' ').trim(); - expect(content).toContain('Closes issue #7.'); - expect(content).toContain('issues #23 and #42'); - expect(content).toContain('are mentioned but will not be closed.'); + expect(content).toContain('Closes #7'); + expect(content).toContain('Mentions #23 and #42'); }); it('should have assing issues link', () => { diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js index cac2f561a0b..4869fb17d96 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js @@ -12,7 +12,7 @@ describe('MRWidgetArchived', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('This project is archived, write access has been disabled.'); + expect(el.innerText).toContain('This project is archived, write access has been disabled'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js index 47b4ba893e0..6042d7384d5 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js @@ -24,8 +24,8 @@ describe('MRWidgetAutoMergeFailed', () => { it('should have correct elements', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically.'); + expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeFalsy(); + expect(vm.$el.innerText).toContain('This merge request failed to be merged automatically'); expect(vm.$el.innerText).toContain(mergeError); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js index 3be11d47227..6b7aa935ad3 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js @@ -12,7 +12,7 @@ describe('MRWidgetChecking', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(el.querySelector('button').classList.contains('btn-success')).toBeTruthy(); expect(el.querySelector('button').disabled).toBeTruthy(); - expect(el.innerText).toContain('Checking ability to merge automatically.'); + expect(el.innerText).toContain('Checking ability to merge automatically'); expect(el.querySelector('i')).toBeDefined(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js index e7ae85caec4..3b7b7d93662 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js @@ -29,15 +29,16 @@ describe('MRWidgetConflicts', () => { describe('template', () => { it('should have correct elements', () => { const el = createComponent().$el; - const resolveButton = el.querySelectorAll('.btn-group .btn')[0]; - const mergeLocallyButton = el.querySelectorAll('.btn-group .btn')[1]; + const resolveButton = el.querySelector('.js-resolve-conflicts-button'); + const mergeButton = el.querySelector('.mr-widget-body .btn'); + const mergeLocallyButton = el.querySelector('.js-merge-locally-button'); - expect(el.textContent).toContain('There are merge conflicts.'); + expect(el.textContent).toContain('There are merge conflicts'); expect(el.textContent).not.toContain('ask someone with write access'); expect(el.querySelector('.btn-success').disabled).toBeTruthy(); - expect(el.querySelectorAll('.btn-group .btn').length).toBe(2); expect(resolveButton.textContent).toContain('Resolve conflicts'); expect(resolveButton.getAttribute('href')).toEqual(path); + expect(mergeButton.textContent).toContain('Merge'); expect(mergeLocallyButton.textContent).toContain('Merge locally'); }); @@ -59,8 +60,8 @@ describe('MRWidgetConflicts', () => { it('should not have action buttons', (done) => { Vue.nextTick(() => { expect(vm.$el.querySelectorAll('.btn').length).toBe(1); - expect(vm.$el.querySelector('a.js-resolve-conflicts-button')).toEqual(null); - expect(vm.$el.querySelector('a.js-merge-locally-button')).toEqual(null); + expect(vm.$el.querySelector('.js-resolve-conflicts-button')).toEqual(null); + expect(vm.$el.querySelector('.js-merge-locally-button')).toEqual(null); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js index 587b83430d9..cef365eec8a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js @@ -94,7 +94,7 @@ describe('MRWidgetFailedToMerge', () => { expect(el.querySelector('button').innerText).toContain('Merge'); expect(el.querySelector('.js-refresh-button').innerText).toContain('Refresh now'); expect(el.querySelector('.js-refresh-label')).toEqual(null); - expect(el.innerText).not.toContain('Refreshing now...'); + expect(el.innerText).not.toContain('Refreshing now'); setTimeout(() => { expect(el.innerText).toContain('Refreshing in 9 seconds'); done(); @@ -115,7 +115,7 @@ describe('MRWidgetFailedToMerge', () => { vm.refresh(); Vue.nextTick(() => { expect(el.innerText).not.toContain('Merge failed. Refreshing'); - expect(el.innerText).toContain('Refreshing now...'); + expect(el.innerText).toContain('Refreshing now'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js index 8d8b90cea16..9a71d0b47d7 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js @@ -162,10 +162,10 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { it('should have correct elements', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds.'); + expect(el.innerText).toContain('to be merged automatically when the pipeline succeeds'); expect(el.innerText).toContain('The changes will be merged into'); expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch will not be removed.'); + expect(el.innerText).toContain('The source branch will not be removed'); expect(el.querySelector('.js-cancel-auto-merge').innerText).toContain('Cancel automatic merge'); expect(el.querySelector('.js-cancel-auto-merge').getAttribute('disabled')).toBeFalsy(); expect(el.querySelector('.js-remove-source-branch').innerText).toContain('Remove source branch'); @@ -186,8 +186,8 @@ describe('MRWidgetMergeWhenPipelineSucceeds', () => { Vue.nextTick(() => { const normalizedText = el.innerText.replace(/\s+/g, ' '); - expect(normalizedText).toContain('The source branch will be removed.'); - expect(normalizedText).not.toContain('The source branch will not be removed.'); + expect(normalizedText).toContain('The source branch will be removed'); + expect(normalizedText).not.toContain('The source branch will not be removed'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js index 6628010112d..afaa750199a 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js @@ -142,19 +142,19 @@ describe('MRWidgetMerged', () => { expect(el.querySelector('.js-mr-widget-author')).toBeDefined(); expect(el.innerText).toContain('The changes were merged into'); expect(el.innerText).toContain(targetBranch); - expect(el.innerText).toContain('The source branch has been removed.'); + expect(el.innerText).toContain('The source branch has been removed'); expect(el.innerText).toContain('Revert'); expect(el.innerText).toContain('Cherry-pick'); - expect(el.innerText).not.toContain('You can remove source branch now.'); - expect(el.innerText).not.toContain('The source branch is being removed.'); + expect(el.innerText).not.toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch is being removed'); }); it('should not show source branch removed text', (done) => { vm.mr.sourceBranchRemoved = false; Vue.nextTick(() => { - expect(el.innerText).toContain('You can remove source branch now.'); - expect(el.innerText).not.toContain('The source branch has been removed.'); + expect(el.innerText).toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch has been removed'); done(); }); }); @@ -164,9 +164,9 @@ describe('MRWidgetMerged', () => { vm.mr.sourceBranchRemoved = false; Vue.nextTick(() => { - expect(el.innerText).toContain('The source branch is being removed.'); - expect(el.innerText).not.toContain('You can remove source branch now.'); - expect(el.innerText).not.toContain('The source branch has been removed.'); + expect(el.innerText).toContain('The source branch is being removed'); + expect(el.innerText).not.toContain('You can remove source branch now'); + expect(el.innerText).not.toContain('The source branch has been removed'); done(); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js index 98674d12afb..720effb5c1c 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js @@ -49,7 +49,7 @@ describe('MRWidgetMissingBranch', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(content).toContain('source branch does not exist.'); - expect(content).toContain('Please restore the source branch or use a different source branch.'); + expect(content).toContain('Please restore it or use a different source branch'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js index 61e00f4cf79..33f20ab132d 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js @@ -11,7 +11,7 @@ describe('MRWidgetNotAllowed', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(vm.$el.innerText).toContain('Ready to be merged automatically.'); - expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request.'); + expect(vm.$el.innerText).toContain('Ask someone with write access to this repository to merge this request'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js index b293d118571..d0702f9f503 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js @@ -10,7 +10,7 @@ describe('MRWidgetPipelineBlocked', () => { it('should have correct elements', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed.'); + expect(vm.$el.innerText).toContain('Pipeline blocked. The pipeline for this merge request requires a manual action to proceed'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js index 807fba705d4..78bac1c61a5 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js @@ -10,7 +10,7 @@ describe('MRWidgetPipelineFailed', () => { it('should have correct elements', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure.'); + expect(vm.$el.innerText).toContain('The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js index 732b516badd..c607c9746a4 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_ready_to_merge_spec.js @@ -72,7 +72,7 @@ describe('MRWidgetReadyToMerge', () => { const withDesc = 'Include description in commit message'; const withoutDesc = "Don't include description in commit message"; - it('should return message wit description', () => { + it('should return message with description', () => { expect(vm.commitMessageLinkTitle).toEqual(withDesc); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js index 5fb1d69a8b3..4c67504b642 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_sha_mismatch_spec.js @@ -10,7 +10,7 @@ describe('MRWidgetSHAMismatch', () => { it('should have correct elements', () => { expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy(); expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy(); - expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging.'); + expect(vm.$el.innerText).toContain('The source branch HEAD has recently changed. Please reload the page and review the changes before merging'); }); }); }); diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js index 45bd1a69964..2cb3aaa6951 100644 --- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js +++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_wip_spec.js @@ -78,7 +78,7 @@ describe('MRWidgetWIP', () => { it('should have correct elements', () => { expect(el.classList.contains('mr-widget-body')).toBeTruthy(); - expect(el.innerText).toContain('This merge request is currently Work In Progress and therefore unable to merge'); + expect(el.innerText).toContain('This is a Work in Progress'); expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy(); expect(el.querySelector('button').innerText).toContain('Merge'); expect(el.querySelector('.js-remove-wip').innerText).toContain('Resolve WIP status'); diff --git a/spec/javascripts/vue_mr_widget/mock_data.js b/spec/javascripts/vue_mr_widget/mock_data.js index e6f96d5588b..ad2f28b24f0 100644 --- a/spec/javascripts/vue_mr_widget/mock_data.js +++ b/spec/javascripts/vue_mr_widget/mock_data.js @@ -30,6 +30,7 @@ export default { "merge_user_id": null, "merge_when_pipeline_succeeds": false, "source_branch": "daaaa", + "source_branch_link": "daaaa", "source_project_id": 19, "target_branch": "master", "target_project_id": 19, diff --git a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb index f43d89d7ccd..16704ff5e77 100644 --- a/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb +++ b/spec/lib/gitlab/cache/ci/project_pipeline_status_spec.rb @@ -48,8 +48,9 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do described_class.load_in_batch_for_projects([project_without_status]) end - it 'only connects to redis_cache twice' do - # Once to load, once to store in the cache + it 'only connects to redis twice' do + # Stub circuitbreaker so it doesn't count the redis connections in there + stub_circuit_breaker(project_without_status) expect(Gitlab::Redis::Cache).to receive(:with).exactly(2).and_call_original described_class.load_in_batch_for_projects([project_without_status]) @@ -301,4 +302,13 @@ describe Gitlab::Cache::Ci::ProjectPipelineStatus, :clean_gitlab_redis_cache do end end end + + def stub_circuit_breaker(project) + fake_circuitbreaker = double + allow(fake_circuitbreaker).to receive(:perform).and_yield + allow(project.repository.raw_repository) + .to receive(:circuit_breaker).and_return(fake_circuitbreaker) + allow(project.repository) + .to receive(:circuit_breaker).and_return(fake_circuitbreaker) + end end diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb index 730fdb112d9..26d7a364f5b 100644 --- a/spec/lib/gitlab/git/commit_spec.rb +++ b/spec/lib/gitlab/git/commit_spec.rb @@ -114,7 +114,7 @@ describe Gitlab::Git::Commit, seed_helper: true do describe '.find' do it "should return first head commit if without params" do expect(Gitlab::Git::Commit.last(repository).id).to eq( - repository.raw.head.target.oid + repository.rugged.head.target.oid ) end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 9bfad0c9bdf..20d6b58d6d1 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -22,7 +22,6 @@ describe Gitlab::Git::Repository, seed_helper: true do describe "Respond to" do subject { repository } - it { is_expected.to respond_to(:raw) } it { is_expected.to respond_to(:rugged) } it { is_expected.to respond_to(:root_ref) } it { is_expected.to respond_to(:tags) } @@ -55,6 +54,20 @@ describe Gitlab::Git::Repository, seed_helper: true do end describe "#rugged" do + describe 'when storage is broken', broken_storage: true do + it 'raises a storage exception when storage is not available' do + broken_repo = described_class.new('broken', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Storage::Inaccessible) + end + end + + it 'raises a no repository exception when there is no repo' do + broken_repo = described_class.new('default', 'a/path.git') + + expect { broken_repo.rugged }.to raise_error(Gitlab::Git::Repository::NoRepository) + end + context 'with no Git env stored' do before do expect(Gitlab::Git::Env).to receive(:all).and_return({}) @@ -757,13 +770,13 @@ describe Gitlab::Git::Repository, seed_helper: true do end end - describe "#commits_between" do + describe "#rugged_commits_between" do context 'two SHAs' do let(:first_sha) { 'b0e52af38d7ea43cf41d8a6f2471351ac036d6c9' } let(:second_sha) { '0e50ec4d3c7ce42ab74dda1d422cb2cbffe1e326' } it 'returns the number of commits between' do - expect(repository.commits_between(first_sha, second_sha).count).to eq(3) + expect(repository.rugged_commits_between(first_sha, second_sha).count).to eq(3) end end @@ -772,11 +785,11 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:branch) { 'master' } it 'returns the number of commits between a sha and a branch' do - expect(repository.commits_between(sha, branch).count).to eq(5) + expect(repository.rugged_commits_between(sha, branch).count).to eq(5) end it 'returns the number of commits between a branch and a sha' do - expect(repository.commits_between(branch, sha).count).to eq(0) # sha is before branch + expect(repository.rugged_commits_between(branch, sha).count).to eq(0) # sha is before branch end end @@ -785,7 +798,7 @@ describe Gitlab::Git::Repository, seed_helper: true do let(:second_branch) { 'master' } it 'returns the number of commits between' do - expect(repository.commits_between(first_branch, second_branch).count).to eq(17) + expect(repository.rugged_commits_between(first_branch, second_branch).count).to eq(17) end end end diff --git a/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb new file mode 100644 index 00000000000..b2886628601 --- /dev/null +++ b/spec/lib/gitlab/git/storage/circuit_breaker_spec.rb @@ -0,0 +1,294 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::CircuitBreaker, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:circuit_breaker) { described_class.new('default') } + let(:hostname) { Gitlab::Environment.hostname } + let(:cache_key) { "storage_accessible:default:#{hostname}" } + + def value_from_redis(name) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmget(cache_key, name) + end.first + end + + def set_in_redis(name, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, name, value) + end.first + end + + describe '.reset_all!' do + it 'clears all entries form redis' do + set_in_redis(:failure_count, 10) + + described_class.reset_all! + + key_exists = Gitlab::Git::Storage.redis.with { |redis| redis.exists(cache_key) } + + expect(key_exists).to be_falsey + end + end + + describe '.for_storage' do + it 'only builds a single circuitbreaker per storage' do + expect(described_class).to receive(:new).once.and_call_original + + breaker = described_class.for_storage('default') + + expect(breaker).to be_a(described_class) + expect(described_class.for_storage('default')).to eq(breaker) + end + end + + describe '#initialize' do + it 'assigns the settings' do + expect(circuit_breaker.hostname).to eq(hostname) + expect(circuit_breaker.storage).to eq('default') + expect(circuit_breaker.storage_path).to eq(TestEnv.repos_path) + expect(circuit_breaker.failure_count_threshold).to eq(10) + expect(circuit_breaker.failure_wait_time).to eq(30) + expect(circuit_breaker.failure_reset_time).to eq(1800) + expect(circuit_breaker.storage_timeout).to eq(5) + end + end + + describe '#perform' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { |b| circuit_breaker.perform(&b) } + .to raise_error(Gitlab::Git::Storage::CircuitOpen) + end + + it 'yields the block' do + expect { |b| circuit_breaker.perform(&b) } + .to yield_control + end + + it 'checks if the storage is available' do + expect(circuit_breaker).to receive(:check_storage_accessible!) + + circuit_breaker.perform { 'hello world' } + end + + it 'returns the value of the block' do + result = circuit_breaker.perform { 'return value' } + + expect(result).to eq('return value') + end + + it 'raises possible errors' do + expect { circuit_breaker.perform { raise Rugged::OSError.new('Broken') } } + .to raise_error(Rugged::OSError) + end + + context 'with the feature disabled' do + it 'returns the block without checking accessibility' do + stub_feature_flags(git_storage_circuit_breaker: false) + + expect(circuit_breaker).not_to receive(:circuit_broken?) + + result = circuit_breaker.perform { 'hello' } + + expect(result).to eq('hello') + end + end + end + + describe '#circuit_broken?' do + it 'is closed when there is no last failure' do + set_in_redis(:last_failure, nil) + set_in_redis(:failure_count, 0) + + expect(circuit_breaker.circuit_broken?).to be_falsey + end + + it 'is open when there was a recent failure' do + Timecop.freeze do + set_in_redis(:last_failure, 1.second.ago.to_f) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + end + + it 'is open when there are to many failures' do + set_in_redis(:last_failure, 1.day.ago.to_f) + set_in_redis(:failure_count, 200) + + expect(circuit_breaker.circuit_broken?).to be_truthy + end + end + + describe "storage_available?" do + context 'when the storage is available' do + it 'tracks that the storage was accessible an raises the error' do + expect(circuit_breaker).to receive(:track_storage_accessible) + + circuit_breaker.storage_available? + end + + it 'only performs the check once' do + expect(Gitlab::Git::Storage::ForkedStorageCheck) + .to receive(:storage_available?).once.and_call_original + + 2.times { circuit_breaker.storage_available? } + end + end + + context 'when storage is not available' do + let(:circuit_breaker) { described_class.new('broken') } + + it 'tracks that the storage was inaccessible' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + circuit_breaker.storage_available? + end + end + end + + describe '#check_storage_accessible!' do + it 'raises an exception with retry time when the circuit is open' do + allow(circuit_breaker).to receive(:circuit_broken?).and_return(true) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::CircuitOpen) + expect(exception.retry_after).to eq(30) + end + end + + context 'when the storage is not available' do + let(:circuit_breaker) { described_class.new('broken') } + + it 'raises an error' do + expect(circuit_breaker).to receive(:track_storage_inaccessible) + + expect { circuit_breaker.check_storage_accessible! } + .to raise_error do |exception| + expect(exception).to be_kind_of(Gitlab::Git::Storage::Inaccessible) + expect(exception.retry_after).to eq(30) + end + end + end + end + + describe '#track_storage_inaccessible' do + around(:each) do |example| + Timecop.freeze + + example.run + + Timecop.return + end + + it 'records the failure time in redis' do + circuit_breaker.track_storage_inaccessible + + failure_time = value_from_redis(:last_failure) + + expect(Time.at(failure_time.to_i)).to be_within(1.second).of(Time.now) + end + + it 'sets the failure time on the breaker without reloading' do + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to eq(Time.now) + end + + it 'increments the failure count in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(value_from_redis(:failure_count).to_i).to be(11) + end + + it 'increments the failure count on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_inaccessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(11) + end + end + + describe '#track_storage_accessible' do + it 'sets the failure count to zero in redis' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:failure_count).to_i).to be(0) + end + + it 'sets the failure count to zero on the breaker without reloading' do + set_in_redis(:failure_count, 10) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.failure_count).to eq(0) + end + + it 'removes the last failure time from redis' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(circuit_breaker).not_to receive(:get_failure_info) + expect(circuit_breaker.last_failure).to be_nil + end + + it 'removes the last failure time from the breaker without reloading' do + set_in_redis(:last_failure, Time.now.to_i) + + circuit_breaker.track_storage_accessible + + expect(value_from_redis(:last_failure)).to be_empty + end + + it 'wont connect to redis when there are no failures' do + expect(Gitlab::Git::Storage.redis).to receive(:with).once + .and_call_original + expect(circuit_breaker).to receive(:track_storage_accessible) + .and_call_original + + circuit_breaker.track_storage_accessible + end + end + + describe '#no_failures?' do + it 'is false when a failure was tracked' do + set_in_redis(:last_failure, Time.now.to_i) + set_in_redis(:failure_count, 1) + + expect(circuit_breaker.no_failures?).to be_falsey + end + end + + describe '#last_failure' do + it 'returns the last failure time' do + time = Time.parse("2017-05-26 17:52:30") + set_in_redis(:last_failure, time.to_i) + + expect(circuit_breaker.last_failure).to eq(time) + end + end + + describe '#failure_count' do + it 'returns the failure count' do + set_in_redis(:failure_count, 7) + + expect(circuit_breaker.failure_count).to eq(7) + end + end + + describe '#cache_key' do + it 'includes storage and host' do + expect(circuit_breaker.cache_key).to eq(cache_key) + end + end +end diff --git a/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb new file mode 100644 index 00000000000..12366151f44 --- /dev/null +++ b/spec/lib/gitlab/git/storage/forked_storage_check_spec.rb @@ -0,0 +1,58 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::ForkedStorageCheck, skip_database_cleaner: true do + let(:existing_path) do + existing_path = TestEnv.repos_path + FileUtils.mkdir_p(existing_path) + existing_path + end + + describe '.storage_accessible?' do + it 'detects when a storage is not available' do + expect(described_class.storage_available?('/non/existant/path')).to be_falsey + end + + it 'detects when a storage is available' do + expect(described_class.storage_available?(existing_path)).to be_truthy + end + + it 'returns false when the check takes to long' do + # We're forking a process here that takes too long + # It will be killed it's parent process will be killed by it's parent + # and waited for inside `Gitlab::Git::Storage::ForkedStorageCheck.timeout_check` + allow(described_class).to receive(:check_filesystem_in_process) do + Process.spawn("sleep 10") + end + result = true + + runtime = Benchmark.realtime do + result = described_class.storage_available?(existing_path, 0.5) + end + + expect(result).to be_falsey + expect(runtime).to be < 1.0 + end + + describe 'when using paths with spaces' do + let(:test_dir) { Rails.root.join('tmp', 'tests', 'storage_check') } + let(:path_with_spaces) { File.join(test_dir, 'path with spaces') } + + around do |example| + FileUtils.mkdir_p(path_with_spaces) + example.run + FileUtils.rm_r(test_dir) + end + + it 'works for paths with spaces' do + expect(described_class.storage_available?(path_with_spaces)).to be_truthy + end + + it 'works for a realpath with spaces' do + symlink_location = File.join(test_dir, 'a symlink') + FileUtils.ln_s(path_with_spaces, symlink_location) + + expect(described_class.storage_available?(symlink_location)).to be_truthy + end + end + end +end diff --git a/spec/lib/gitlab/git/storage/health_spec.rb b/spec/lib/gitlab/git/storage/health_spec.rb new file mode 100644 index 00000000000..2d3af387971 --- /dev/null +++ b/spec/lib/gitlab/git/storage/health_spec.rb @@ -0,0 +1,87 @@ +require 'spec_helper' + +describe Gitlab::Git::Storage::Health, clean_gitlab_redis_shared_state: true, broken_storage: true do + let(:host1_key) { 'storage_accessible:broken:web01' } + let(:host2_key) { 'storage_accessible:default:kiq01' } + + def set_in_redis(cache_key, value) + Gitlab::Git::Storage.redis.with do |redis| + redis.hmset(cache_key, :failure_count, value) + end.first + end + + describe '.for_failing_storages' do + it 'only includes health status for failures' do + set_in_redis(host1_key, 10) + set_in_redis(host2_key, 0) + + expect(described_class.for_failing_storages.map(&:storage_name)) + .to contain_exactly('broken') + end + end + + describe '.load_for_keys' do + let(:subject) do + results = Gitlab::Git::Storage.redis.with do |redis| + fake_future = double + allow(fake_future).to receive(:value).and_return([host1_key]) + described_class.load_for_keys({ 'broken' => fake_future }, redis) + end + + # Make sure the `Redis#future is loaded + results.inject({}) do |result, (name, info)| + info.each { |i| i[:failure_count] = i[:failure_count].value.to_i } + + result[name] = info + + result + end + end + + it 'loads when there is no info in redis' do + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 0 }]) + end + + it 'reads the correct values for a storage from redis' do + set_in_redis(host1_key, 5) + set_in_redis(host2_key, 7) + + expect(subject).to eq('broken' => [{ name: host1_key, failure_count: 5 }]) + end + end + + describe '.for_all_storages' do + it 'loads health status for all configured storages' do + healths = described_class.for_all_storages + + expect(healths.map(&:storage_name)).to contain_exactly('default', 'broken') + end + end + + describe '#failing_info' do + it 'only contains storages that have failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 0 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.failing_info).to contain_exactly({ name: host2_key, failure_count: 3 }) + end + end + + describe '#total_failures' do + it 'sums up all the failures' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 3 }]) + + expect(health.total_failures).to eq(5) + end + end + + describe '#failing_on_hosts' do + it 'collects only the failing hostnames' do + health = described_class.new('broken', [{ name: host1_key, failure_count: 2 }, + { name: host2_key, failure_count: 0 }]) + + expect(health.failing_on_hosts).to contain_exactly('web01') + end + end +end diff --git a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb index 8abc4320c59..26574df8bb5 100644 --- a/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb +++ b/spec/lib/gitlab/health_checks/fs_shards_check_spec.rb @@ -44,6 +44,15 @@ describe Gitlab::HealthChecks::FsShardsCheck do describe '#readiness' do subject { described_class.readiness } + context 'storage has a tripped circuitbreaker', broken_storage: true do + let(:repository_storages) { ['broken'] } + let(:storages_paths) do + Gitlab.config.repositories.storages + end + + it { is_expected.to include(result_class.new(false, 'circuitbreaker tripped', shard: 'broken')) } + end + context 'storage points to not existing folder' do let(:storages_paths) do { @@ -51,6 +60,10 @@ describe Gitlab::HealthChecks::FsShardsCheck do }.with_indifferent_access end + before do + allow(described_class).to receive(:storage_circuitbreaker_test) { true } + end + it { is_expected.to include(result_class.new(false, 'cannot stat storage', shard: :default)) } end @@ -109,6 +122,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0)) end end @@ -127,6 +141,7 @@ describe Gitlab::HealthChecks::FsShardsCheck do expect(metrics).to include(an_object_having_attributes(name: :filesystem_access_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_read_latency_seconds, value: be >= 0)) expect(metrics).to include(an_object_having_attributes(name: :filesystem_write_latency_seconds, value: be >= 0)) + expect(metrics).to include(an_object_having_attributes(name: :filesystem_circuitbreaker_latency_seconds, value: be >= 0)) end it 'cleans up files used for metrics' do diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index be3908e8f6a..3db19d06305 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -20,9 +20,10 @@ describe Mattermost::Session, type: :request do describe '#with session' do let(:location) { 'http://location.tld' } + let(:cookie_header) {'MMOAUTH=taskik8az7rq8k6rkpuas7htia; Path=/;'} let!(:stub) do WebMock.stub_request(:get, "#{mattermost_url}/api/v3/oauth/gitlab/login") - .to_return(headers: { 'location' => location }, status: 307) + .to_return(headers: { 'location' => location, 'Set-Cookie' => cookie_header }, status: 307) end context 'without oauth uri' do @@ -34,9 +35,9 @@ describe Mattermost::Session, type: :request do context 'with oauth_uri' do let!(:doorkeeper) do Doorkeeper::Application.create( - name: "GitLab Mattermost", + name: 'GitLab Mattermost', redirect_uri: "#{mattermost_url}/signup/gitlab/complete\n#{mattermost_url}/login/gitlab/complete", - scopes: "") + scopes: '') end context 'without token_uri' do diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index f876baaa805..4ddda5b638c 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1,11 +1,12 @@ require 'spec_helper' -describe Repository do +describe Repository, models: true do include RepoHelpers TestBlob = Struct.new(:path) let(:project) { create(:project, :repository) } let(:repository) { project.repository } + let(:broken_repository) { create(:project, :broken_storage).repository } let(:user) { create(:user) } let(:commit_options) do @@ -27,12 +28,27 @@ describe Repository do let(:author_email) { 'user@example.org' } let(:author_name) { 'John Doe' } + def expect_to_raise_storage_error + expect { yield }.to raise_error do |exception| + storage_exceptions = [Gitlab::Git::Storage::Inaccessible, Gitlab::Git::CommandError, GRPC::Unavailable] + expect(exception.class).to be_in(storage_exceptions) + end + end + describe '#branch_names_contains' do subject { repository.branch_names_contains(sample_commit.id) } it { is_expected.to include('master') } it { is_expected.not_to include('feature') } it { is_expected.not_to include('fix') } + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.branch_names_contains(sample_commit.id) + end + end + end end describe '#tag_names_contains' do @@ -143,6 +159,14 @@ describe Repository do subject { repository.last_commit_for_path(sample_commit.id, '.gitignore').id } it { is_expected.to eq('c1acaa58bbcbc3eafe538cb8274ba387047b69f8') } + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_id_for_path(sample_commit.id, '.gitignore') + end + end + end end context 'when Gitaly feature last_commit_for_path is enabled' do @@ -169,6 +193,14 @@ describe Repository do expect(cache).to receive(:fetch).with(key).and_return('c1acaa5') is_expected.to eq('c1acaa5') end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.last_commit_for_path(sample_commit.id, '.gitignore').id + end + end + end end context 'when Gitaly feature last_commit_for_path is enabled' do @@ -216,6 +248,12 @@ describe Repository do expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.find_commits_by_message('s') } + end + end end describe '#blob_at' do @@ -541,6 +579,14 @@ describe Repository do expect(results).to match_array([]) end + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error do + broken_repository.search_files_by_content('feature', 'master') + end + end + end + describe 'result' do subject { results.first } @@ -569,6 +615,22 @@ describe Repository do expect(results).to match_array([]) end + + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.search_files_by_name('files', 'master') } + end + end + end + + describe '#fetch_ref' do + describe 'when storage is broken', broken_storage: true do + it 'should raise a storage error' do + path = broken_repository.path_to_repo + + expect_to_raise_storage_error { broken_repository.fetch_ref(path, '1', '2') } + end + end end describe '#create_ref' do @@ -986,6 +1048,12 @@ describe Repository do expect(repository.exists?).to eq(false) end + + context 'with broken storage', broken_storage: true do + it 'should raise a storage error' do + expect_to_raise_storage_error { broken_repository.exists? } + end + end end describe '#exists?' do diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index a6bd6052006..0103fb6040e 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1976,4 +1976,28 @@ describe User do expect(user.allow_password_authentication?).to be_falsey end end + + describe '#personal_projects_count' do + it 'returns the number of personal projects using a single query' do + user = build(:user) + projects = double(:projects, count: 1) + + expect(user).to receive(:personal_projects).once.and_return(projects) + + 2.times do + expect(user.personal_projects_count).to eq(1) + end + end + end + + describe '#projects_limit_left' do + it 'returns the number of projects that can be created by the user' do + user = build(:user) + + allow(user).to receive(:projects_limit).and_return(10) + allow(user).to receive(:personal_projects_count).and_return(5) + + expect(user.projects_limit_left).to eq(5) + end + end end diff --git a/spec/requests/api/circuit_breakers_spec.rb b/spec/requests/api/circuit_breakers_spec.rb new file mode 100644 index 00000000000..76521e55994 --- /dev/null +++ b/spec/requests/api/circuit_breakers_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' + +describe API::CircuitBreakers do + let(:user) { create(:user) } + let(:admin) { create(:admin) } + + describe 'GET circuit_breakers/repository_storage' do + it 'returns a 401 for anonymous users' do + get api('/circuit_breakers/repository_storage') + + expect(response).to have_http_status(401) + end + + it 'returns a 403 for users' do + get api('/circuit_breakers/repository_storage', user) + + expect(response).to have_http_status(403) + end + + it 'returns an Array of storages' do + expect(Gitlab::Git::Storage::Health).to receive(:for_all_storages) do + [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] + end + + get api('/circuit_breakers/repository_storage', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + expect(json_response.first['storage_name']).to eq('broken') + expect(json_response.first['failing_on_hosts']).to eq(['web01']) + expect(json_response.first['total_failures']).to eq(4) + end + + describe 'GET circuit_breakers/repository_storage/failing' do + it 'returns an array of failing storages' do + expect(Gitlab::Git::Storage::Health).to receive(:for_failing_storages) do + [Gitlab::Git::Storage::Health.new('broken', [{ name: 'prefix:broken:web01', failure_count: 4 }])] + end + + get api('/circuit_breakers/repository_storage/failing', admin) + + expect(response).to have_http_status(200) + expect(json_response).to be_kind_of(Array) + end + end + end + + describe 'DELETE circuit_breakers/repository_storage' do + it 'clears all circuit_breakers' do + expect(Gitlab::Git::Storage::CircuitBreaker).to receive(:reset_all!) + + delete api('/circuit_breakers/repository_storage', admin) + + expect(response).to have_http_status(204) + end + end +end diff --git a/spec/requests/api/environments_spec.rb b/spec/requests/api/environments_spec.rb index 4c5ded7a492..87716c6fe3a 100644 --- a/spec/requests/api/environments_spec.rb +++ b/spec/requests/api/environments_spec.rb @@ -13,7 +13,14 @@ describe API::Environments do describe 'GET /projects/:id/environments' do context 'as member of the project' do it 'returns project environments' do - project_data_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + project_data_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get api("/projects/#{project.id}/environments", user) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index b9ebf6c4c16..9baac12821f 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -186,7 +186,14 @@ describe API::Projects do context 'and with simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + expected_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get api('/projects?simple=true', user) @@ -689,6 +696,7 @@ describe API::Projects do expect(response).to have_http_status(200) expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) + expect(json_response['default_branch']).to eq(public_project.default_branch) expect(json_response.keys).not_to include('permissions') end end diff --git a/spec/requests/api/v3/projects_spec.rb b/spec/requests/api/v3/projects_spec.rb index c211cc20e53..fca5b5b5d82 100644 --- a/spec/requests/api/v3/projects_spec.rb +++ b/spec/requests/api/v3/projects_spec.rb @@ -82,7 +82,14 @@ describe API::V3::Projects do context 'GET /projects?simple=true' do it 'returns a simplified version of all the projects' do - expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace) + expected_keys = %w( + id description default_branch tag_list + ssh_url_to_repo http_url_to_repo web_url + name name_with_namespace + path path_with_namespace + star_count forks_count + created_at last_activity_at + ) get v3_api('/projects?simple=true', user) @@ -644,6 +651,7 @@ describe API::V3::Projects do expect(response).to have_http_status(200) expect(json_response['id']).to eq(public_project.id) expect(json_response['description']).to eq(public_project.description) + expect(json_response['default_branch']).to eq(public_project.default_branch) expect(json_response.keys).not_to include('permissions') end end diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index b78ecfb61c4..30fa0ee6873 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -424,6 +424,26 @@ describe QuickActions::InterpretService do end end + context 'assign command with me alias' do + let(:content) { "/assign me" } + + context 'Issue' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, issue) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end + + context 'Merge Request' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, merge_request) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end + end + it_behaves_like 'empty command' do let(:content) { '/assign @abcd1234' } let(:issuable) { issue } diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb index 32835e391a8..68f0ce8afb3 100644 --- a/spec/support/features/issuable_slash_commands_shared_examples.rb +++ b/spec/support/features/issuable_slash_commands_shared_examples.rb @@ -279,6 +279,17 @@ shared_examples 'issuable record that supports quick actions in its description expect(issuable.subscribed?(master, project)).to be_falsy end end + + context "with a note assigning the #{issuable_type} to the current user" do + it "assigns the #{issuable_type} to the current user" do + write_note("/assign me") + + expect(page).not_to have_content '/assign me' + expect(page).to have_content 'Commands applied' + + expect(issuable.reload.assignees).to eq [master] + end + end end describe "preview of note on #{issuable_type}" do diff --git a/spec/support/stored_repositories.rb b/spec/support/stored_repositories.rb index df18926d58c..f3deae0f455 100644 --- a/spec/support/stored_repositories.rb +++ b/spec/support/stored_repositories.rb @@ -2,4 +2,16 @@ RSpec.configure do |config| config.before(:each, :repository) do TestEnv.clean_test_path end + + config.before(:all, :broken_storage) do + FileUtils.rm_rf Gitlab.config.repositories.storages.broken['path'] + end + + config.before(:each, :broken_storage) do + allow(Gitlab::GitalyClient).to receive(:call) do + raise GRPC::Unavailable.new('Gitaly broken in this spec') + end + + Gitlab::Git::Storage::CircuitBreaker.reset_all! + end end |
