diff options
author | Jacob Schatz <jschatz@gitlab.com> | 2018-02-23 20:36:28 +0000 |
---|---|---|
committer | Jacob Schatz <jschatz@gitlab.com> | 2018-02-23 20:36:28 +0000 |
commit | a90a22a7743028a260d7b1f3105b80700a10287e (patch) | |
tree | bebc8c433c47000c0eac1426150de70a74206992 /spec | |
parent | 11aa990da7794038aef09dd023b85e81b5ac6c4f (diff) | |
parent | 296a4e6825a3528917bb385123cdf62ae3d1944e (diff) | |
download | gitlab-ce-a90a22a7743028a260d7b1f3105b80700a10287e.tar.gz |
Merge branch 'master' into 'boards-bundle-refactor'
# Conflicts:
# config/webpack.config.js
Diffstat (limited to 'spec')
101 files changed, 3880 insertions, 1014 deletions
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb index 73fff6eb5ca..b7257fac608 100644 --- a/spec/controllers/autocomplete_controller_spec.rb +++ b/spec/controllers/autocomplete_controller_spec.rb @@ -109,15 +109,17 @@ describe AutocompleteController do end context 'limited users per page' do - let(:per_page) { 2 } - before do + 25.times do + create(:user) + end + sign_in(user) - get(:users, per_page: per_page) + get(:users) end it { expect(json_response).to be_kind_of(Array) } - it { expect(json_response.size).to eq(per_page) } + it { expect(json_response.size).to eq(20) } end context 'unauthenticated user' do diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 79bbc29e80d..4770e187db6 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -86,6 +86,7 @@ describe Boards::IssuesController do context 'with unauthorized user' do before do + allow(Ability).to receive(:allowed?).and_call_original allow(Ability).to receive(:allowed?).with(user, :read_project, project).and_return(true) allow(Ability).to receive(:allowed?).with(user, :read_issue, project).and_return(false) end diff --git a/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb new file mode 100644 index 00000000000..27f558e1b5d --- /dev/null +++ b/spec/controllers/concerns/controller_with_cross_project_access_check_spec.rb @@ -0,0 +1,146 @@ +require 'spec_helper' + +describe ControllerWithCrossProjectAccessCheck do + let(:user) { create(:user) } + + before do + sign_in user + end + + render_views + + context 'When reading cross project is not allowed' do + before do + allow(Ability).to receive(:allowed).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_cross_project, :global) + .and_return(false) + end + + describe '#requires_cross_project_access' do + controller(ApplicationController) do + # `described_class` is not available in this context + include ControllerWithCrossProjectAccessCheck # rubocop:disable RSpec/DescribedClass + + requires_cross_project_access :index, show: false, + unless: -> { unless_condition }, + if: -> { if_condition } + + def index + render nothing: true + end + + def show + render nothing: true + end + + def unless_condition + false + end + + def if_condition + true + end + end + + it 'renders a 404 with trying to access a cross project page' do + message = "This page is unavailable because you are not allowed to read "\ + "information across multiple projects." + + get :index + + expect(response).to have_gitlab_http_status(404) + expect(response.body).to match(/#{message}/) + end + + it 'is skipped when the `if` condition returns false' do + expect(controller).to receive(:if_condition).and_return(false) + + get :index + + expect(response).to have_gitlab_http_status(200) + end + + it 'is skipped when the `unless` condition returns true' do + expect(controller).to receive(:unless_condition).and_return(true) + + get :index + + expect(response).to have_gitlab_http_status(200) + end + + it 'correctly renders an action that does not require cross project access' do + get :show, id: 'nothing' + + expect(response).to have_gitlab_http_status(200) + end + end + + describe '#skip_cross_project_access_check' do + controller(ApplicationController) do + # `described_class` is not available in this context + include ControllerWithCrossProjectAccessCheck # rubocop:disable RSpec/DescribedClass + + requires_cross_project_access + + skip_cross_project_access_check index: true, show: false, + unless: -> { unless_condition }, + if: -> { if_condition } + + def index + render nothing: true + end + + def show + render nothing: true + end + + def edit + render nothing: true + end + + def unless_condition + false + end + + def if_condition + true + end + end + + it 'renders a success when the check is skipped' do + get :index + + expect(response).to have_gitlab_http_status(200) + end + + it 'is executed when the `if` condition returns false' do + expect(controller).to receive(:if_condition).and_return(false) + + get :index + + expect(response).to have_gitlab_http_status(404) + end + + it 'is executed when the `unless` condition returns true' do + expect(controller).to receive(:unless_condition).and_return(true) + + get :index + + expect(response).to have_gitlab_http_status(404) + end + + it 'does not skip the check on an action that is not skipped' do + get :show, id: 'hello' + + expect(response).to have_gitlab_http_status(404) + end + + it 'does not skip the check on an action that was not defined to skip' do + get :edit, id: 'hello' + + expect(response).to have_gitlab_http_status(404) + end + end + end +end diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb index 775f9db1c6e..e14ba29fa70 100644 --- a/spec/controllers/projects/clusters/gcp_controller_spec.rb +++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb @@ -161,7 +161,7 @@ describe Projects::Clusters::GcpController do it 'renders the cluster form with an error' do go - expect(response).to set_flash[:alert] + expect(response).to set_flash.now[:alert] expect(response).to render_template('new') end end diff --git a/spec/controllers/projects/merge_requests/creations_controller_spec.rb b/spec/controllers/projects/merge_requests/creations_controller_spec.rb index 92db7284e0e..24310b847e8 100644 --- a/spec/controllers/projects/merge_requests/creations_controller_spec.rb +++ b/spec/controllers/projects/merge_requests/creations_controller_spec.rb @@ -17,7 +17,7 @@ describe Projects::MergeRequests::CreationsController do before do fork_project.add_master(user) - + Projects::ForkService.new(project, user).execute(fork_project) sign_in(user) end @@ -125,4 +125,66 @@ describe Projects::MergeRequests::CreationsController do end end end + + describe 'GET #branch_to' do + before do + allow(Ability).to receive(:allowed?).and_call_original + end + + it 'fetches the commit if a user has access' do + expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true } + + get :branch_to, + namespace_id: fork_project.namespace, + project_id: fork_project, + target_project_id: project.id, + ref: 'master' + + expect(assigns(:commit)).not_to be_nil + expect(response).to have_gitlab_http_status(200) + end + + it 'does not load the commit when the user cannot read the project' do + expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false } + + get :branch_to, + namespace_id: fork_project.namespace, + project_id: fork_project, + target_project_id: project.id, + ref: 'master' + + expect(assigns(:commit)).to be_nil + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'GET #update_branches' do + before do + allow(Ability).to receive(:allowed?).and_call_original + end + + it 'lists the branches of another fork if the user has access' do + expect(Ability).to receive(:allowed?).with(user, :read_project, project) { true } + + get :update_branches, + namespace_id: fork_project.namespace, + project_id: fork_project, + target_project_id: project.id + + expect(assigns(:target_branches)).not_to be_empty + expect(response).to have_gitlab_http_status(200) + end + + it 'does not list branches when the user cannot read the project' do + expect(Ability).to receive(:allowed?).with(user, :read_project, project) { false } + + get :update_branches, + namespace_id: fork_project.namespace, + project_id: fork_project, + target_project_id: project.id + + expect(response).to have_gitlab_http_status(200) + expect(assigns(:target_branches)).to eq([]) + end + end end diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb index e9e7d357d9c..2192fd5cae2 100644 --- a/spec/controllers/projects/pages_domains_controller_spec.rb +++ b/spec/controllers/projects/pages_domains_controller_spec.rb @@ -46,7 +46,46 @@ describe Projects::PagesDomainsController do post(:create, request_params.merge(pages_domain: pages_domain_params)) end.to change { PagesDomain.count }.by(1) - expect(response).to redirect_to(project_pages_path(project)) + created_domain = PagesDomain.reorder(:id).last + + expect(created_domain).to be_present + expect(response).to redirect_to(project_pages_domain_path(project, created_domain)) + end + end + + describe 'POST verify' do + let(:params) { request_params.merge(id: pages_domain.domain) } + + def stub_service + service = double(:service) + + expect(VerifyPagesDomainService).to receive(:new) { service } + + service + end + + it 'handles verification success' do + expect(stub_service).to receive(:execute).and_return(status: :success) + + post :verify, params + + expect(response).to redirect_to project_pages_domain_path(project, pages_domain) + expect(flash[:notice]).to eq('Successfully verified domain ownership') + end + + it 'handles verification failure' do + expect(stub_service).to receive(:execute).and_return(status: :failed) + + post :verify, params + + expect(response).to redirect_to project_pages_domain_path(project, pages_domain) + expect(flash[:alert]).to eq('Failed to verify domain ownership') + end + + it 'returns a 404 response for an unknown domain' do + post :verify, request_params.merge(id: 'unknown-domain') + + expect(response).to have_gitlab_http_status(404) end end diff --git a/spec/controllers/projects/prometheus_controller_spec.rb b/spec/controllers/projects/prometheus/metrics_controller_spec.rb index bbfe78d305a..f17f819feee 100644 --- a/spec/controllers/projects/prometheus_controller_spec.rb +++ b/spec/controllers/projects/prometheus/metrics_controller_spec.rb @@ -1,20 +1,20 @@ -require('spec_helper') +require 'spec_helper' -describe Projects::PrometheusController do +describe Projects::Prometheus::MetricsController do let(:user) { create(:user) } - let!(:project) { create(:project) } + let(:project) { create(:project) } let(:prometheus_service) { double('prometheus_service') } before do allow(controller).to receive(:project).and_return(project) - allow(project).to receive(:prometheus_service).and_return(prometheus_service) + allow(project).to receive(:find_or_initialize_service).with('prometheus').and_return(prometheus_service) project.add_master(user) sign_in(user) end - describe 'GET #active_metrics' do + describe 'GET #active_common' do context 'when prometheus metrics are enabled' do context 'when data is not present' do before do @@ -22,7 +22,7 @@ describe Projects::PrometheusController do end it 'returns no content response' do - get :active_metrics, project_params(format: :json) + get :active_common, project_params(format: :json) expect(response).to have_gitlab_http_status(204) end @@ -36,7 +36,7 @@ describe Projects::PrometheusController do end it 'returns no content response' do - get :active_metrics, project_params(format: :json) + get :active_common, project_params(format: :json) expect(response).to have_gitlab_http_status(200) expect(json_response).to eq(sample_response.deep_stringify_keys) @@ -45,7 +45,7 @@ describe Projects::PrometheusController do context 'when requesting non json response' do it 'returns not found response' do - get :active_metrics, project_params + get :active_common, project_params expect(response).to have_gitlab_http_status(404) end diff --git a/spec/controllers/projects/uploads_controller_spec.rb b/spec/controllers/projects/uploads_controller_spec.rb index d572085661d..eca9baed9c9 100644 --- a/spec/controllers/projects/uploads_controller_spec.rb +++ b/spec/controllers/projects/uploads_controller_spec.rb @@ -7,4 +7,12 @@ describe Projects::UploadsController do end it_behaves_like 'handle uploads' + + context 'when the URL the old style, without /-/system' do + it 'responds with a redirect to the login page' do + get :show, namespace_id: 'project', project_id: 'avatar', filename: 'foo.png', secret: 'bar' + + expect(response).to redirect_to(new_user_session_path) + end + end end diff --git a/spec/controllers/search_controller_spec.rb b/spec/controllers/search_controller_spec.rb index 37f961d0c94..30c06ddf744 100644 --- a/spec/controllers/search_controller_spec.rb +++ b/spec/controllers/search_controller_spec.rb @@ -16,6 +16,32 @@ describe SearchController do expect(assigns[:search_objects].first).to eq note end + context 'when the user cannot read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?) + .with(user, :read_cross_project, :global) { false } + end + + it 'still allows accessing the search page' do + get :show + + expect(response).to have_gitlab_http_status(200) + end + + it 'still blocks searches without a project_id' do + get :show, search: 'hello' + + expect(response).to have_gitlab_http_status(404) + end + + it 'allows searches with a project_id' do + get :show, search: 'hello', project_id: create(:project, :public).id + + expect(response).to have_gitlab_http_status(200) + end + end + context 'on restricted projects' do context 'when signed out' do before do diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 2898c4b119e..b0acf4a49ac 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -74,6 +74,31 @@ describe UsersController do end end end + + context 'json with events' do + let(:project) { create(:project) } + before do + project.add_developer(user) + Gitlab::DataBuilder::Push.build_sample(project, user) + + sign_in(user) + end + + it 'loads events' do + get :show, username: user, format: :json + + expect(assigns(:events)).not_to be_empty + end + + it 'hides events if the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + get :show, username: user, format: :json + + expect(assigns(:events)).to be_empty + end + end end describe 'GET #calendar' do diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb index 61b04708da2..35b44e1c52e 100644 --- a/spec/factories/pages_domains.rb +++ b/spec/factories/pages_domains.rb @@ -1,6 +1,25 @@ FactoryBot.define do factory :pages_domain, class: 'PagesDomain' do - domain 'my.domain.com' + sequence(:domain) { |n| "my#{n}.domain.com" } + verified_at { Time.now } + enabled_until { 1.week.from_now } + + trait :disabled do + verified_at nil + enabled_until nil + end + + trait :unverified do + verified_at nil + end + + trait :reverify do + enabled_until { 1.hour.from_now } + end + + trait :expired do + enabled_until { 1.hour.ago } + end trait :with_certificate do certificate '-----BEGIN CERTIFICATE----- diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index a01c129defd..7eeed7da998 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -19,7 +19,7 @@ describe "Admin Runners" do end it 'has all necessary texts' do - expect(page).to have_text "How to setup" + expect(page).to have_text "Setup a shared Runner manually" expect(page).to have_text "Runners with last contact more than a minute ago: 1" end @@ -54,7 +54,7 @@ describe "Admin Runners" do end it 'has all necessary texts including no runner message' do - expect(page).to have_text "How to setup" + expect(page).to have_text "Setup a shared Runner manually" expect(page).to have_text "Runners with last contact more than a minute ago: 0" expect(page).to have_text 'No runners found' end diff --git a/spec/features/auto_deploy_spec.rb b/spec/features/auto_deploy_spec.rb deleted file mode 100644 index 9aef68b7156..00000000000 --- a/spec/features/auto_deploy_spec.rb +++ /dev/null @@ -1,77 +0,0 @@ -require 'spec_helper' - -describe 'Auto deploy' do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - - shared_examples 'same behavior between KubernetesService and Platform::Kubernetes' do - context 'when no deployment service is active' do - before do - trun_off - end - - it 'does not show a button to set up auto deploy' do - visit project_path(project) - expect(page).to have_no_content('Set up auto deploy') - end - end - - context 'when a deployment service is active' do - before do - trun_on - visit project_path(project) - end - - it 'shows a button to set up auto deploy' do - expect(page).to have_link('Set up auto deploy') - end - - it 'includes OpenShift as an available template', :js do - click_link 'Set up auto deploy' - click_button 'Apply a GitLab CI Yaml template' - - within '.gitlab-ci-yml-selector' do - expect(page).to have_content('OpenShift') - end - end - - it 'creates a merge request using "auto-deploy" branch', :js do - click_link 'Set up auto deploy' - click_button 'Apply a GitLab CI Yaml template' - within '.gitlab-ci-yml-selector' do - click_on 'OpenShift' - end - wait_for_requests - click_button 'Commit changes' - - expect(page).to have_content('New Merge Request From auto-deploy into master') - end - end - end - - context 'when user configured kubernetes from Integration > Kubernetes' do - before do - create :kubernetes_service, project: project - project.add_master(user) - sign_in user - end - - let(:trun_on) { project.deployment_platform.update!(active: true) } - let(:trun_off) { project.deployment_platform.update!(active: false) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end - - context 'when user configured kubernetes from CI/CD > Clusters' do - before do - create(:cluster, :provided_by_gcp, projects: [project]) - project.add_master(user) - sign_in user - end - - let(:trun_on) { project.deployment_platform.cluster.update!(enabled: true) } - let(:trun_off) { project.deployment_platform.cluster.update!(enabled: false) } - - it_behaves_like 'same behavior between KubernetesService and Platform::Kubernetes' - end -end diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb index 8ac9821b879..7f1d1934103 100644 --- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb +++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb @@ -11,7 +11,7 @@ feature 'project owner sees a link to create a license file in empty project', : scenario 'project master creates a license file from a template' do visit project_path(project) - click_on 'LICENSE' + click_on 'Add License' expect(page).to have_content('New file') expect(current_path).to eq( diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 6f097ad16c7..b5104747d00 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -140,7 +140,7 @@ feature 'New project' do find('#import-project-tab').click end - context 'from git repository url' do + context 'from git repository url, "Repo by URL"' do before do first('.import_git').click end @@ -157,6 +157,18 @@ feature 'New project' do expect(git_import_instructions).to be_visible expect(git_import_instructions).to have_content 'Git repository URL' end + + it 'keeps "Import project" tab open after form validation error' do + collision_project = create(:project, name: 'test-name-collision', namespace: user.namespace) + + fill_in 'project_import_url', with: collision_project.http_url_to_repo + fill_in 'project_path', with: collision_project.path + + click_on 'Create project' + + expect(page).to have_css('#import-project-pane.active') + expect(page).not_to have_css('.toggle-import-form.hide') + end end context 'from GitHub' do diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb index 3f1ef0b2a47..a96f2c186a4 100644 --- a/spec/features/projects/pages_spec.rb +++ b/spec/features/projects/pages_spec.rb @@ -60,7 +60,6 @@ feature 'Pages' do fill_in 'Domain', with: 'my.test.domain.com' click_button 'Create New Domain' - expect(page).to have_content('Domains (1)') expect(page).to have_content('my.test.domain.com') end end @@ -159,7 +158,6 @@ feature 'Pages' do fill_in 'Key (PEM)', with: certificate_key click_button 'Create New Domain' - expect(page).to have_content('Domains (1)') expect(page).to have_content('my.test.domain.com') end end diff --git a/spec/features/projects/show_project_spec.rb b/spec/features/projects/show_project_spec.rb index 0b94c9eae5d..0a014e9f080 100644 --- a/spec/features/projects/show_project_spec.rb +++ b/spec/features/projects/show_project_spec.rb @@ -17,4 +17,321 @@ describe 'Project show page', :feature do expect(page).to have_content("This project was scheduled for deletion, but failed with the following message: #{project.delete_error}") end end + + describe 'stat button existence' do + # For "New file", "Add License" functionality, + # see spec/features/projects/files/project_owner_creates_license_file_spec.rb + # see spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb + + let(:user) { create(:user) } + + describe 'empty project' do + let(:project) { create(:project, :public, :empty_repo) } + let(:presenter) { project.present(current_user: user) } + + describe 'as a normal user' do + before do + sign_in(user) + + visit project_path(project) + end + + it 'no Auto DevOps button if can not manage pipelines' do + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it '"Auto DevOps enabled" button not linked' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_text('Auto DevOps enabled') + end + end + end + + describe 'as a master' do + before do + project.add_master(user) + sign_in(user) + + visit project_path(project) + end + + it '"New file" button linked to new file page' do + page.within('.project-stats') do + expect(page).to have_link('New file', href: project_new_blob_path(project, project.default_branch || 'master')) + end + end + + it '"Add Readme" button linked to new file populated for a readme' do + page.within('.project-stats') do + expect(page).to have_link('Add Readme', href: presenter.add_readme_path) + end + end + + it '"Add License" button linked to new file populated for a license' do + page.within('.project-stats') do + expect(page).to have_link('Add License', href: presenter.add_license_path) + end + end + + describe 'Auto DevOps button' do + it '"Enable Auto DevOps" button linked to settings page' do + page.within('.project-stats') do + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it '"Auto DevOps enabled" anchor linked to settings page' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + end + + describe 'Kubernetes cluster button' do + it '"Add Kubernetes cluster" button linked to clusters page' do + page.within('.project-stats') do + expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project)) + end + end + + it '"Kubernetes cluster" anchor linked to cluster page' do + cluster = create(:cluster, :provided_by_gcp, projects: [project]) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster)) + end + end + end + end + end + + describe 'populated project' do + let(:project) { create(:project, :public, :repository) } + let(:presenter) { project.present(current_user: user) } + + describe 'as a normal user' do + before do + sign_in(user) + + visit project_path(project) + end + + it 'no Auto DevOps button if can not manage pipelines' do + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it '"Auto DevOps enabled" button not linked' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_text('Auto DevOps enabled') + end + end + + it 'no Kubernetes cluster button if can not manage clusters' do + page.within('.project-stats') do + expect(page).not_to have_link('Add Kubernetes cluster') + expect(page).not_to have_link('Kubernetes configured') + end + end + end + + describe 'as a master' do + before do + allow_any_instance_of(AutoDevopsHelper).to receive(:show_auto_devops_callout?).and_return(false) + project.add_master(user) + sign_in(user) + + visit project_path(project) + end + + it 'no "Add Changelog" button if the project already has a changelog' do + expect(project.repository.changelog).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add Changelog') + end + end + + it 'no "Add License" button if the project already has a license' do + expect(project.repository.license_blob).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add License') + end + end + + it 'no "Add Contribution guide" button if the project already has a contribution guide' do + expect(project.repository.contribution_guide).not_to be_nil + + page.within('.project-stats') do + expect(page).not_to have_link('Add Contribution guide') + end + end + + describe 'GitLab CI configuration button' do + it '"Set up CI/CD" button linked to new file populated for a .gitlab-ci.yml' do + expect(project.repository.gitlab_ci_yml).to be_nil + + page.within('.project-stats') do + expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path) + end + end + + it 'no "Set up CI/CD" button if the project already has a .gitlab-ci.yml' do + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + expect(project.repository.gitlab_ci_yml).not_to be_nil + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end + end + + it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end + end + end + + describe 'Auto DevOps button' do + it '"Enable Auto DevOps" button linked to settings page' do + page.within('.project-stats') do + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it '"Enable Auto DevOps" button linked to settings page' do + project.create_auto_devops!(enabled: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings')) + end + end + + it 'no Auto DevOps button if Auto DevOps callout is shown' do + allow_any_instance_of(AutoDevopsHelper).to receive(:show_auto_devops_callout?).and_return(true) + + visit project_path(project) + + expect(page).to have_selector('.js-autodevops-banner') + + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + + it 'no "Enable Auto DevOps" button when .gitlab-ci.yml already exists' do + Files::CreateService.new( + project, + project.creator, + start_branch: 'master', + branch_name: 'master', + commit_message: "Add .gitlab-ci.yml", + file_path: '.gitlab-ci.yml', + file_content: File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) + ).execute + + expect(project.repository.gitlab_ci_yml).not_to be_nil + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Enable Auto DevOps') + expect(page).not_to have_link('Auto DevOps enabled') + end + end + end + + describe 'Kubernetes cluster button' do + it '"Add Kubernetes cluster" button linked to clusters page' do + page.within('.project-stats') do + expect(page).to have_link('Add Kubernetes cluster', href: new_project_cluster_path(project)) + end + end + + it '"Kubernetes cluster" button linked to cluster page' do + cluster = create(:cluster, :provided_by_gcp, projects: [project]) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Kubernetes configured', href: project_cluster_path(project, cluster)) + end + end + end + + describe '"Set up Koding" button' do + it 'no "Set up Koding" button if Koding disabled' do + stub_application_setting(koding_enabled?: false) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up Koding') + end + end + + it 'no "Set up Koding" button if the project already has a .koding.yml' do + stub_application_setting(koding_enabled?: true) + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:koding_url).and_return('http://koding.example.com') + expect(project.repository.changelog).not_to be_nil + allow_any_instance_of(Repository).to receive(:koding_yml).and_return(project.repository.changelog) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up Koding') + end + end + + it '"Set up Koding" button linked to new file populated for a .koding.yml' do + stub_application_setting(koding_enabled?: true) + + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_link('Set up Koding', href: presenter.add_koding_stack_path) + end + end + end + end + end + end end diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb index b66a7dea598..645d12da09f 100644 --- a/spec/features/projects_spec.rb +++ b/spec/features/projects_spec.rb @@ -25,6 +25,24 @@ feature 'Project' do end end + describe 'shows tip about push to create git command' do + let(:user) { create(:user) } + + before do + sign_in user + visit new_project_path + end + + it 'shows the command in a popover', :js do + page.within '.profile-settings-sidebar' do + click_link 'Show command' + end + + expect(page).to have_css('.popover .push-to-create-popover #push_to_create_tip') + expect(page).to have_content 'Private projects can be created in your personal namespace with:' + end + end + describe 'description' do let(:project) { create(:project, :repository) } let(:path) { project_path(project) } diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index aec9de6c7ca..df65c2d2f83 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -7,6 +7,20 @@ feature 'Runners' do sign_in(user) end + context 'when user opens runners page' do + given(:project) { create(:project) } + + background do + project.add_master(user) + end + + scenario 'user can see a button to install runners on kubernetes clusters' do + visit runners_path(project) + + expect(page).to have_link('Install Runner on Kubernetes', href: project_clusters_path(project)) + end + end + context 'when a project has enabled shared_runners' do given(:project) { create(:project) } diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb deleted file mode 100644 index 917fad74ef1..00000000000 --- a/spec/features/signup_spec.rb +++ /dev/null @@ -1,103 +0,0 @@ -require 'spec_helper' - -feature 'Signup' do - describe 'signup with no errors' do - context "when sending confirmation email" do - before do - stub_application_setting(send_user_confirmation_email: true) - end - - it 'creates the user account and sends a confirmation email' do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq users_almost_there_path - expect(page).to have_content("Please check your email to confirm your account") - end - end - - context "when sigining up with different cased emails" do - it "creates the user successfully" do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email.capitalize - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq dashboard_projects_path - expect(page).to have_content("Welcome! You have signed up successfully.") - end - end - - context "when not sending confirmation email" do - before do - stub_application_setting(send_user_confirmation_email: false) - end - - it 'creates the user account and goes to dashboard' do - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq dashboard_projects_path - expect(page).to have_content("Welcome! You have signed up successfully.") - end - end - end - - describe 'signup with errors' do - it "displays the errors" do - existing_user = create(:user) - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: existing_user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq user_registration_path - expect(page).to have_content("errors prohibited this user from being saved") - expect(page).to have_content("Email has already been taken") - expect(page).to have_content("Email confirmation doesn't match") - end - - it 'does not redisplay the password' do - existing_user = create(:user) - user = build(:user) - - visit root_path - - fill_in 'new_user_name', with: user.name - fill_in 'new_user_username', with: user.username - fill_in 'new_user_email', with: existing_user.email - fill_in 'new_user_password', with: user.password - click_button "Register" - - expect(current_path).to eq user_registration_path - expect(page.body).not_to match(/#{user.password}/) - end - end -end diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb index 4662367d843..b625e7065cc 100644 --- a/spec/features/tags/master_views_tags_spec.rb +++ b/spec/features/tags/master_views_tags_spec.rb @@ -13,7 +13,7 @@ feature 'Master views tags' do before do visit project_path(project) - click_on 'README' + click_on 'Add Readme' fill_in :commit_message, with: 'Add a README file', visible: true click_button 'Commit changes' visit project_tags_path(project) diff --git a/spec/features/login_spec.rb b/spec/features/users/login_spec.rb index 6dfabcc7225..6ef235cf870 100644 --- a/spec/features/login_spec.rb +++ b/spec/features/users/login_spec.rb @@ -1,6 +1,26 @@ require 'spec_helper' feature 'Login' do + scenario 'Successful user signin invalidates password reset token' do + user = create(:user) + + expect(user.reset_password_token).to be_nil + + visit new_user_password_path + fill_in 'user_email', with: user.email + click_button 'Reset password' + + user.reload + expect(user.reset_password_token).not_to be_nil + + find('a[href="#login-pane"]').click + gitlab_sign_in(user) + expect(current_path).to eq root_path + + user.reload + expect(user.reset_password_token).to be_nil + end + describe 'initial login after setup' do it 'allows the initial admin to create a password' do # This behavior is dependent on there only being one user diff --git a/spec/features/logout_spec.rb b/spec/features/users/logout_spec.rb index 635729efa53..635729efa53 100644 --- a/spec/features/logout_spec.rb +++ b/spec/features/users/logout_spec.rb diff --git a/spec/features/users/projects_spec.rb b/spec/features/users/projects_spec.rb deleted file mode 100644 index f079771cee1..00000000000 --- a/spec/features/users/projects_spec.rb +++ /dev/null @@ -1,29 +0,0 @@ -require 'spec_helper' - -describe 'Projects tab on a user profile', :js do - let(:user) { create(:user) } - let!(:project) { create(:project, namespace: user.namespace) } - let!(:project2) { create(:project, namespace: user.namespace) } - - before do - allow(Project).to receive(:default_per_page).and_return(1) - - sign_in(user) - - visit user_path(user) - - page.within('.user-profile-nav') do - click_link('Personal projects') - end - - wait_for_requests - end - - it 'paginates results' do - expect(page).to have_content(project2.name) - - click_link('Next') - - expect(page).to have_content(project.name) - end -end diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb new file mode 100644 index 00000000000..b5bbb2c0ea5 --- /dev/null +++ b/spec/features/users/show_spec.rb @@ -0,0 +1,17 @@ +require 'spec_helper' + +describe 'User page' do + let(:user) { create(:user) } + + it 'shows all the tabs' do + visit(user_path(user)) + + page.within '.nav-links' do + expect(page).to have_link('Activity') + expect(page).to have_link('Groups') + expect(page).to have_link('Contributed projects') + expect(page).to have_link('Personal projects') + expect(page).to have_link('Snippets') + end + end +end diff --git a/spec/features/users/signup_spec.rb b/spec/features/users/signup_spec.rb new file mode 100644 index 00000000000..5d539f0ccbe --- /dev/null +++ b/spec/features/users/signup_spec.rb @@ -0,0 +1,135 @@ +require 'spec_helper' + +describe 'Signup' do + let(:new_user) { build_stubbed(:user) } + + describe 'username validation', :js do + before do + visit root_path + click_link 'Register' + end + + it 'does not show an error border if the username is available' do + fill_in 'new_user_username', with: 'new-user' + wait_for_requests + + expect(find('.username')).not_to have_css '.gl-field-error-outline' + end + + it 'does not show an error border if the username contains dots (.)' do + fill_in 'new_user_username', with: 'new.user.username' + wait_for_requests + + expect(find('.username')).not_to have_css '.gl-field-error-outline' + end + + it 'shows an error border if the username already exists' do + existing_user = create(:user) + + fill_in 'new_user_username', with: existing_user.username + wait_for_requests + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + + it 'shows an error border if the username contains special characters' do + fill_in 'new_user_username', with: 'new$user!username' + wait_for_requests + + expect(find('.username')).to have_css '.gl-field-error-outline' + end + end + + context 'with no errors' do + context "when sending confirmation email" do + before do + stub_application_setting(send_user_confirmation_email: true) + end + + it 'creates the user account and sends a confirmation email' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + + expect { click_button 'Register' }.to change { User.count }.by(1) + + expect(current_path).to eq users_almost_there_path + expect(page).to have_content("Please check your email to confirm your account") + end + end + + context "when sigining up with different cased emails" do + it "creates the user successfully" do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email.capitalize + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + + context "when not sending confirmation email" do + before do + stub_application_setting(send_user_confirmation_email: false) + end + + it 'creates the user account and goes to dashboard' do + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: new_user.email + fill_in 'new_user_email_confirmation', with: new_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq dashboard_projects_path + expect(page).to have_content("Welcome! You have signed up successfully.") + end + end + end + + context 'with errors' do + it "displays the errors" do + existing_user = create(:user) + + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq user_registration_path + expect(page).to have_content("errors prohibited this user from being saved") + expect(page).to have_content("Email has already been taken") + expect(page).to have_content("Email confirmation doesn't match") + end + + it 'does not redisplay the password' do + existing_user = create(:user) + + visit root_path + + fill_in 'new_user_name', with: new_user.name + fill_in 'new_user_username', with: new_user.username + fill_in 'new_user_email', with: existing_user.email + fill_in 'new_user_password', with: new_user.password + click_button "Register" + + expect(current_path).to eq user_registration_path + expect(page.body).not_to match(/#{new_user.password}/) + end + end +end diff --git a/spec/features/user_page_spec.rb b/spec/features/users/user_browses_projects_on_user_page_spec.rb index 19c587e53c8..a70637c8370 100644 --- a/spec/features/user_page_spec.rb +++ b/spec/features/users/user_browses_projects_on_user_page_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'User page', :js do +describe 'Users > User browses projects on user page', :js do let!(:user) { create :user } let!(:private_project) do create :project, :private, name: 'private', namespace: user.namespace do |project| @@ -26,6 +26,28 @@ describe 'User page', :js do end end + it 'paginates projects', :js do + project = create(:project, namespace: user.namespace) + project2 = create(:project, namespace: user.namespace) + allow(Project).to receive(:default_per_page).and_return(1) + + sign_in(user) + + visit user_path(user) + + page.within('.user-profile-nav') do + click_link('Personal projects') + end + + wait_for_requests + + expect(page).to have_content(project2.name) + + click_link('Next') + + expect(page).to have_content(project.name) + end + context 'when not signed in' do it 'renders user public project' do visit user_path(user) diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb deleted file mode 100644 index a9973cdf214..00000000000 --- a/spec/features/users_spec.rb +++ /dev/null @@ -1,114 +0,0 @@ -require 'spec_helper' - -feature 'Users', :js do - let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') } - - scenario 'GET /users/sign_in creates a new user account' do - visit new_user_session_path - click_link 'Register' - fill_in 'new_user_name', with: 'Name Surname' - fill_in 'new_user_username', with: 'Great' - fill_in 'new_user_email', with: 'name@mail.com' - fill_in 'new_user_email_confirmation', with: 'name@mail.com' - fill_in 'new_user_password', with: 'password1234' - expect { click_button 'Register' }.to change { User.count }.by(1) - end - - scenario 'Successful user signin invalidates password reset token' do - expect(user.reset_password_token).to be_nil - - visit new_user_password_path - fill_in 'user_email', with: user.email - click_button 'Reset password' - - user.reload - expect(user.reset_password_token).not_to be_nil - - find('a[href="#login-pane"]').click - gitlab_sign_in(user) - expect(current_path).to eq root_path - - user.reload - expect(user.reset_password_token).to be_nil - end - - scenario 'Should show one error if email is already taken' do - visit new_user_session_path - click_link 'Register' - fill_in 'new_user_name', with: 'Another user name' - fill_in 'new_user_username', with: 'anotheruser' - fill_in 'new_user_email', with: user.email - fill_in 'new_user_email_confirmation', with: user.email - fill_in 'new_user_password', with: '12341234' - expect { click_button 'Register' }.to change { User.count }.by(0) - expect(page).to have_text('Email has already been taken') - expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}' - end - - describe 'redirect alias routes' do - before do - expect(user).to be_persisted - end - - scenario '/u/user1 redirects to user page' do - visit '/u/user1' - - expect(current_path).to eq user_path(user) - expect(page).to have_text(user.name) - end - - scenario '/u/user1/groups redirects to user groups page' do - visit '/u/user1/groups' - - expect(current_path).to eq user_groups_path(user) - end - - scenario '/u/user1/projects redirects to user projects page' do - visit '/u/user1/projects' - - expect(current_path).to eq user_projects_path(user) - end - end - - feature 'username validation' do - let(:loading_icon) { '.fa.fa-spinner' } - let(:username_input) { 'new_user_username' } - - before do - visit new_user_session_path - click_link 'Register' - end - - scenario 'doesn\'t show an error border if the username is available' do - fill_in username_input, with: 'new-user' - wait_for_requests - expect(find('.username')).not_to have_css '.gl-field-error-outline' - end - - scenario 'does not show an error border if the username contains dots (.)' do - fill_in username_input, with: 'new.user.username' - wait_for_requests - expect(find('.username')).not_to have_css '.gl-field-error-outline' - end - - scenario 'shows an error border if the username already exists' do - fill_in username_input, with: user.username - wait_for_requests - expect(find('.username')).to have_css '.gl-field-error-outline' - end - - scenario 'shows an error border if the username contains special characters' do - fill_in username_input, with: 'new$user!username' - wait_for_requests - expect(find('.username')).to have_css '.gl-field-error-outline' - end - end - - def errors_on_page(page) - page.find('#error_explanation').find('ul').all('li').map { |item| item.text }.join("\n") - end - - def number_of_errors_on_page(page) - page.find('#error_explanation').find('ul').all('li').count - end -end diff --git a/spec/finders/concerns/finder_methods_spec.rb b/spec/finders/concerns/finder_methods_spec.rb new file mode 100644 index 00000000000..a4ad331f613 --- /dev/null +++ b/spec/finders/concerns/finder_methods_spec.rb @@ -0,0 +1,70 @@ +require 'spec_helper' + +describe FinderMethods do + let(:finder_class) do + Class.new do + include FinderMethods + + attr_reader :current_user + + def initialize(user) + @current_user = user + end + + def execute + Project.all + end + end + end + + let(:user) { create(:user) } + let(:finder) { finder_class.new(user) } + let(:authorized_project) { create(:project) } + let(:unauthorized_project) { create(:project) } + + before do + authorized_project.add_developer(user) + end + + describe '#find_by!' do + it 'returns the project if the user has access' do + expect(finder.find_by!(id: authorized_project.id)).to eq(authorized_project) + end + + it 'raises not found when the project is not found' do + expect { finder.find_by!(id: 0) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises not found the user does not have access' do + expect { finder.find_by!(id: unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + describe '#find' do + it 'returns the project if the user has access' do + expect(finder.find(authorized_project.id)).to eq(authorized_project) + end + + it 'raises not found when the project is not found' do + expect { finder.find(0) }.to raise_error(ActiveRecord::RecordNotFound) + end + + it 'raises not found the user does not have access' do + expect { finder.find(unauthorized_project.id) }.to raise_error(ActiveRecord::RecordNotFound) + end + end + + describe '#find_by' do + it 'returns the project if the user has access' do + expect(finder.find_by(id: authorized_project.id)).to eq(authorized_project) + end + + it 'returns nil when the project is not found' do + expect(finder.find_by(id: 0)).to be_nil + end + + it 'returns nil when the user does not have access' do + expect(finder.find_by(id: unauthorized_project.id)).to be_nil + end + end +end diff --git a/spec/finders/concerns/finder_with_cross_project_access_spec.rb b/spec/finders/concerns/finder_with_cross_project_access_spec.rb new file mode 100644 index 00000000000..c784fb87972 --- /dev/null +++ b/spec/finders/concerns/finder_with_cross_project_access_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe FinderWithCrossProjectAccess do + let(:finder_class) do + Class.new do + prepend FinderWithCrossProjectAccess + include FinderMethods + + requires_cross_project_access if: -> { requires_access? } + + attr_reader :current_user + + def initialize(user) + @current_user = user + end + + def execute + Issue.all + end + end + end + + let(:user) { create(:user) } + subject(:finder) { finder_class.new(user) } + let!(:result) { create(:issue) } + + before do + result.project.add_master(user) + end + + def expect_access_check_on_result + expect(finder).not_to receive(:requires_access?) + expect(Ability).to receive(:allowed?).with(user, :read_issue, result).and_call_original + end + + context 'when the user cannot read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) + .and_return(false) + end + + describe '#execute' do + it 'returns a issue if the check is disabled' do + expect(finder).to receive(:requires_access?).and_return(false) + + expect(finder.execute).to include(result) + end + + it 'returns an empty relation when the check is enabled' do + expect(finder).to receive(:requires_access?).and_return(true) + + expect(finder.execute).to be_empty + end + + it 'only queries once when check is enabled' do + expect(finder).to receive(:requires_access?).and_return(true) + + expect { finder.execute }.not_to exceed_query_limit(1) + end + + it 'only queries once when check is disabled' do + expect(finder).to receive(:requires_access?).and_return(false) + + expect { finder.execute }.not_to exceed_query_limit(1) + end + end + + describe '#find' do + it 'checks the accessibility of the subject directly' do + expect_access_check_on_result + + finder.find(result.id) + end + + it 'returns the issue' do + expect(finder.find(result.id)).to eq(result) + end + end + + describe '#find_by' do + it 'checks the accessibility of the subject directly' do + expect_access_check_on_result + + finder.find_by(id: result.id) + end + end + + describe '#find_by!' do + it 'checks the accessibility of the subject directly' do + expect_access_check_on_result + + finder.find_by!(id: result.id) + end + + it 're-enables the check after the find failed' do + finder.find_by!(id: 9999) rescue ActiveRecord::RecordNotFound + + expect(finder.instance_variable_get(:@should_skip_cross_project_check)) + .to eq(false) + end + end + end + + context 'when the user can read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) + .and_return(true) + end + + it 'returns the result' do + expect(finder).not_to receive(:requires_access?) + + expect(finder.execute).to include(result) + end + end +end diff --git a/spec/finders/events_finder_spec.rb b/spec/finders/events_finder_spec.rb index 18d6c0cfd74..62968e83292 100644 --- a/spec/finders/events_finder_spec.rb +++ b/spec/finders/events_finder_spec.rb @@ -26,6 +26,14 @@ describe EventsFinder do expect(events).not_to include(opened_merge_request_event) end + + it 'returns nothing when the current user cannot read cross project' do + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + events = described_class.new(source: user, current_user: user).execute + + expect(events).to be_empty + end end context 'when targeting a project' do diff --git a/spec/finders/milestones_finder_spec.rb b/spec/finders/milestones_finder_spec.rb index 0b3cf7ece5f..656d120311a 100644 --- a/spec/finders/milestones_finder_spec.rb +++ b/spec/finders/milestones_finder_spec.rb @@ -70,4 +70,12 @@ describe MilestonesFinder do expect(result.to_a).to contain_exactly(milestone_1) end end + + describe '#find_by' do + it 'finds a single milestone' do + finder = described_class.new(project_ids: [project_1.id], state: 'all') + + expect(finder.find_by(iid: milestone_3.iid)).to eq(milestone_3) + end + end end diff --git a/spec/finders/snippets_finder_spec.rb b/spec/finders/snippets_finder_spec.rb index 54a07eccaba..1ae0bd988f2 100644 --- a/spec/finders/snippets_finder_spec.rb +++ b/spec/finders/snippets_finder_spec.rb @@ -162,8 +162,26 @@ describe SnippetsFinder do end end - describe "#execute" do - # Snippet visibility scenarios are included in more details in spec/support/snippet_visibility.rb - include_examples 'snippet visibility', described_class + describe '#execute' do + let(:project) { create(:project, :public) } + let!(:project_snippet) { create(:project_snippet, :public, project: project) } + let!(:personal_snippet) { create(:personal_snippet, :public) } + let(:user) { create(:user) } + subject(:finder) { described_class.new(user) } + + it 'returns project- and personal snippets' do + expect(finder.execute).to contain_exactly(project_snippet, personal_snippet) + end + + context 'when the user cannot read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + it 'returns only personal snippets when the user cannot read cross project' do + expect(finder.execute).to contain_exactly(personal_snippet) + end + end end end diff --git a/spec/finders/user_recent_events_finder_spec.rb b/spec/finders/user_recent_events_finder_spec.rb new file mode 100644 index 00000000000..3ca0f7c3c89 --- /dev/null +++ b/spec/finders/user_recent_events_finder_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe UserRecentEventsFinder do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:project_owner) { project.creator } + let!(:event) { create(:event, project: project, author: project_owner) } + + subject(:finder) { described_class.new(user, project_owner) } + + describe '#execute' do + it 'does not include the event when a user does not have access to the project' do + expect(finder.execute).to be_empty + end + + context 'when the user has access to a project' do + before do + project.add_developer(user) + end + + it 'includes the event' do + expect(finder.execute).to include(event) + end + + it 'does not include the event if the user cannot read cross project' do + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + expect(finder.execute).to be_empty + end + end + end +end diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json index e8c17298b43..ed8ed9085c0 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/basic.json @@ -4,6 +4,9 @@ "domain": { "type": "string" }, "url": { "type": "uri" }, "project_id": { "type": "integer" }, + "verified": { "type": "boolean" }, + "verification_code": { "type": ["string", "null"] }, + "enabled_until": { "type": ["date", "null"] }, "certificate_expiration": { "type": "object", "properties": { @@ -14,6 +17,6 @@ "additionalProperties": false } }, - "required": ["domain", "url", "project_id"], + "required": ["domain", "url", "project_id", "verified", "verification_code", "enabled_until"], "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json index 08db8d47050..b57d544f896 100644 --- a/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json +++ b/spec/fixtures/api/schemas/public_api/v4/pages_domain/detail.json @@ -3,6 +3,9 @@ "properties": { "domain": { "type": "string" }, "url": { "type": "uri" }, + "verified": { "type": "boolean" }, + "verification_code": { "type": ["string", "null"] }, + "enabled_until": { "type": ["date", "null"] }, "certificate": { "type": "object", "properties": { @@ -15,6 +18,6 @@ "additionalProperties": false } }, - "required": ["domain", "url"], + "required": ["domain", "url", "verified", "verification_code", "enabled_until"], "additionalProperties": false } diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb new file mode 100644 index 00000000000..7ba24ba2956 --- /dev/null +++ b/spec/helpers/dashboard_helper_spec.rb @@ -0,0 +1,24 @@ +require 'spec_helper' + +describe DashboardHelper do + let(:user) { build(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?) { true } + end + + describe '#dashboard_nav_links' do + it 'has all the expected links by default' do + menu_items = [:projects, :groups, :activity, :milestones, :snippets] + + expect(helper.dashboard_nav_links).to contain_exactly(*menu_items) + end + + it 'does not contain cross project elements when the user cannot read cross project' do + expect(helper).to receive(:can?).with(user, :read_cross_project) { false } + + expect(helper.dashboard_nav_links).not_to include(:activity, :milestones) + end + end +end diff --git a/spec/helpers/explore_helper_spec.rb b/spec/helpers/explore_helper_spec.rb new file mode 100644 index 00000000000..12651d80e36 --- /dev/null +++ b/spec/helpers/explore_helper_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' + +describe ExploreHelper do + let(:user) { build(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?) { true } + end + + describe '#explore_nav_links' do + it 'has all the expected links by default' do + menu_items = [:projects, :groups, :snippets] + + expect(helper.explore_nav_links).to contain_exactly(*menu_items) + end + end +end diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb index 5f608fe18d9..b48c252acd3 100644 --- a/spec/helpers/groups_helper_spec.rb +++ b/spec/helpers/groups_helper_spec.rb @@ -201,4 +201,39 @@ describe GroupsHelper do end end end + + describe '#group_sidebar_links' do + let(:group) { create(:group, :public) } + let(:user) { create(:user) } + before do + allow(helper).to receive(:current_user) { user } + allow(helper).to receive(:can?) { true } + helper.instance_variable_set(:@group, group) + end + + it 'returns all the expected links' do + links = [ + :overview, :activity, :issues, :labels, :milestones, :merge_requests, + :group_members, :settings + ] + + expect(helper.group_sidebar_links).to include(*links) + end + + it 'includes settings when the user can admin the group' do + expect(helper).to receive(:current_user) { user } + expect(helper).to receive(:can?).with(user, :admin_group, group) { false } + + expect(helper.group_sidebar_links).not_to include(:settings) + end + + it 'excludes cross project features when the user cannot read cross project' do + cross_project_features = [:activity, :issues, :labels, :milestones, + :merge_requests] + + expect(helper).to receive(:can?).with(user, :read_cross_project) { false } + + expect(helper.group_sidebar_links).not_to include(*cross_project_features) + end + end end diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index ddf881a7b6f..aeef5352333 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -113,21 +113,6 @@ describe IssuesHelper do end end - describe "milestone_options" do - it "gets closed milestone from current issue" do - closed_milestone = create(:closed_milestone, project: project) - milestone1 = create(:milestone, project: project) - milestone2 = create(:milestone, project: project) - issue.update_attributes(milestone_id: closed_milestone.id) - - options = milestone_options(issue) - - expect(options).to have_selector('option[selected]', text: closed_milestone.title) - expect(options).to have_selector('option', text: milestone1.title) - expect(options).to have_selector('option', text: milestone2.title) - end - end - describe "#link_to_discussions_to_resolve" do describe "passing only a merge request" do let(:merge_request) { create(:merge_request) } diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb new file mode 100644 index 00000000000..e840c927d59 --- /dev/null +++ b/spec/helpers/nav_helper_spec.rb @@ -0,0 +1,53 @@ +require 'spec_helper' + +describe NavHelper do + describe '#header_links' do + before do + allow(helper).to receive(:session) { {} } + end + + context 'when the user is logged in' do + let(:user) { build(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?) { true } + end + + it 'has all the expected links by default' do + menu_items = [:user_dropdown, :search, :issues, :merge_requests, :todos] + + expect(helper.header_links).to contain_exactly(*menu_items) + end + + it 'contains the impersonation link while impersonating' do + expect(helper).to receive(:session) { { impersonator_id: 1 } } + + expect(helper.header_links).to include(:admin_impersonation) + end + + context 'when the user cannot read cross project' do + before do + allow(helper).to receive(:can?).with(user, :read_cross_project) { false } + end + + it 'does not contain cross project elements when the user cannot read cross project' do + expect(helper.header_links).not_to include(:issues, :merge_requests, :todos, :search) + end + + it 'shows the search box when the user cannot read cross project and he is visiting a project' do + helper.instance_variable_set(:@project, create(:project)) + + expect(helper.header_links).to include(:search) + end + end + end + + it 'returns only the sign in and search when the user is not logged in' do + allow(helper).to receive(:current_user).and_return(nil) + allow(helper).to receive(:can?).with(nil, :read_cross_project) { true } + + expect(helper.header_links).to contain_exactly(:sign_in, :search) + end + end +end diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb index 749aa25e632..e2a0c4322ff 100644 --- a/spec/helpers/preferences_helper_spec.rb +++ b/spec/helpers/preferences_helper_spec.rb @@ -77,103 +77,6 @@ describe PreferencesHelper do end end - describe '#default_project_view' do - context 'user not signed in' do - before do - helper.instance_variable_set(:@project, project) - stub_user - end - - context 'when repository is empty' do - let(:project) { create(:project_empty_repo, :public) } - - it 'returns activity if user has repository access' do - allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true) - - expect(helper.default_project_view).to eq('activity') - end - - it 'returns activity if user does not have repository access' do - allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false) - - expect(helper.default_project_view).to eq('activity') - end - end - - context 'when repository is not empty' do - let(:project) { create(:project, :public, :repository) } - - it 'returns files and readme if user has repository access' do - allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(true) - - expect(helper.default_project_view).to eq('files') - end - - it 'returns activity if user does not have repository access' do - allow(helper).to receive(:can?).with(nil, :download_code, project).and_return(false) - - expect(helper.default_project_view).to eq('activity') - end - end - end - - context 'user signed in' do - let(:user) { create(:user, :readme) } - let(:project) { create(:project, :public, :repository) } - - before do - helper.instance_variable_set(:@project, project) - allow(helper).to receive(:current_user).and_return(user) - end - - context 'when the user is allowed to see the code' do - it 'returns the project view' do - allow(helper).to receive(:can?).with(user, :download_code, project).and_return(true) - - expect(helper.default_project_view).to eq('readme') - end - end - - context 'with wikis enabled and the right policy for the user' do - before do - project.project_feature.update_attribute(:issues_access_level, 0) - allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) - end - - it 'returns wiki if the user has the right policy' do - allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(true) - - expect(helper.default_project_view).to eq('wiki') - end - - it 'returns customize_workflow if the user does not have the right policy' do - allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) - - expect(helper.default_project_view).to eq('customize_workflow') - end - end - - context 'with issues as a feature available' do - it 'return issues' do - allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) - allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) - - expect(helper.default_project_view).to eq('projects/issues/issues') - end - end - - context 'with no activity, no wikies and no issues' do - it 'returns customize_workflow as default' do - project.project_feature.update_attribute(:issues_access_level, 0) - allow(helper).to receive(:can?).with(user, :download_code, project).and_return(false) - allow(helper).to receive(:can?).with(user, :read_wiki, project).and_return(false) - - expect(helper.default_project_view).to eq('customize_workflow') - end - end - end - end - def stub_user(messages = {}) if messages.empty? allow(helper).to receive(:current_user).and_return(nil) diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index b67fee2fcc0..ce96e90e2d7 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -75,6 +75,12 @@ describe ProjectsHelper do describe "#project_list_cache_key", :clean_gitlab_redis_shared_state do let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).with(user, :read_cross_project) { true } + end it "includes the route" do expect(helper.project_list_cache_key(project)).to include(project.route.cache_key) @@ -106,6 +112,10 @@ describe ProjectsHelper do expect(helper.project_list_cache_key(project).last).to start_with('v') end + it 'includes wether or not the user can read cross project' do + expect(helper.project_list_cache_key(project)).to include('cross-project:true') + end + it "includes the pipeline status when there is a status" do create(:ci_pipeline, :success, project: project, sha: project.commit.sha) @@ -264,32 +274,6 @@ describe ProjectsHelper do end end - describe '#license_short_name' do - let(:project) { create(:project) } - - context 'when project.repository has a license_key' do - it 'returns the nickname of the license if present' do - allow(project.repository).to receive(:license_key).and_return('agpl-3.0') - - expect(helper.license_short_name(project)).to eq('GNU AGPLv3') - end - - it 'returns the name of the license if nickname is not present' do - allow(project.repository).to receive(:license_key).and_return('mit') - - expect(helper.license_short_name(project)).to eq('MIT License') - end - end - - context 'when project.repository has no license_key but a license_blob' do - it 'returns LICENSE' do - allow(project.repository).to receive(:license_key).and_return(nil) - - expect(helper.license_short_name(project)).to eq('LICENSE') - end - end - end - describe '#sanitized_import_error' do let(:project) { create(:project, :repository) } @@ -462,6 +446,22 @@ describe ProjectsHelper do end end + describe('#push_to_create_project_command') do + let(:user) { create(:user, username: 'john') } + + it 'returns the command to push to create project over HTTP' do + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'http' } + + expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream http://test.host/john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') + end + + it 'returns the command to push to create project over SSH' do + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:enabled_git_access_protocol) { 'ssh' } + + expect(helper.push_to_create_project_command(user)).to eq('git push --set-upstream git@localhost:john/$(git rev-parse --show-toplevel | xargs basename).git $(git rev-parse --abbrev-ref HEAD)') + end + end + describe '#any_projects?' do let!(:project) { create(:project) } diff --git a/spec/helpers/users_helper_spec.rb b/spec/helpers/users_helper_spec.rb index 03f78de8e91..6332217b920 100644 --- a/spec/helpers/users_helper_spec.rb +++ b/spec/helpers/users_helper_spec.rb @@ -14,4 +14,17 @@ describe UsersHelper do is_expected.to include("title=\"#{user.email}\"") end end + + describe '#profile_tabs' do + subject(:tabs) { helper.profile_tabs } + + before do + allow(helper).to receive(:current_user).and_return(user) + allow(helper).to receive(:can?).and_return(true) + end + + it 'includes all the expected tabs' do + expect(tabs).to include(:activity, :groups, :contributed, :projects, :snippets) + end + end end diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js index 8ce33d410a7..e0ea3649646 100644 --- a/spec/javascripts/pipelines/async_button_spec.js +++ b/spec/javascripts/pipelines/async_button_spec.js @@ -15,7 +15,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'repeat', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); }); @@ -39,8 +40,9 @@ describe('Pipelines Async Button', () => { describe('With confirm dialog', () => { it('should call the service when confimation is positive', () => { - eventHub.$on('actionConfirmationModal', (data) => { - expect(data.id).toEqual(123); + eventHub.$on('openConfirmationModal', (data) => { + expect(data.pipelineId).toEqual(123); + expect(data.type).toEqual('explode'); }); component = new AsyncButtonComponent({ @@ -49,7 +51,8 @@ describe('Pipelines Async Button', () => { title: 'Foo', icon: 'fa fa-foo', cssClass: 'bar', - id: 123, + pipelineId: 123, + type: 'explode', }, }).$mount(); diff --git a/spec/lib/banzai/commit_renderer_spec.rb b/spec/lib/banzai/commit_renderer_spec.rb index 84adaebdcbe..e7ebb2a332f 100644 --- a/spec/lib/banzai/commit_renderer_spec.rb +++ b/spec/lib/banzai/commit_renderer_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Banzai::CommitRenderer do describe '.render' do it 'renders a commit description and title' do - user = double(:user) + user = build(:user) project = create(:project, :repository) expect(Banzai::ObjectRenderer).to receive(:new).with(project, user).and_call_original diff --git a/spec/lib/banzai/filter/issuable_state_filter_spec.rb b/spec/lib/banzai/filter/issuable_state_filter_spec.rb index cacb33d3372..17347768a49 100644 --- a/spec/lib/banzai/filter/issuable_state_filter_spec.rb +++ b/spec/lib/banzai/filter/issuable_state_filter_spec.rb @@ -77,6 +77,14 @@ describe Banzai::Filter::IssuableStateFilter do expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)} (closed)") end + it 'skips cross project references if the user cannot read cross project' do + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + link = create_link(closed_issue.to_reference(other_project), issue: closed_issue.id, reference_type: 'issue') + doc = filter(link, context.merge(project: other_project)) + + expect(doc.css('a').last.text).to eq("#{closed_issue.to_reference(other_project)}") + end + it 'does not append state when filter is not enabled' do link = create_link('text', issue: closed_issue.id, reference_type: 'issue') context = { current_user: user } diff --git a/spec/lib/banzai/filter/redactor_filter_spec.rb b/spec/lib/banzai/filter/redactor_filter_spec.rb index 5a7858e77f3..9a2e521fdcf 100644 --- a/spec/lib/banzai/filter/redactor_filter_spec.rb +++ b/spec/lib/banzai/filter/redactor_filter_spec.rb @@ -6,7 +6,7 @@ describe Banzai::Filter::RedactorFilter do it 'ignores non-GFM links' do html = %(See <a href="https://google.com/">Google</a>) - doc = filter(html, current_user: double) + doc = filter(html, current_user: build(:user)) expect(doc.css('a').length).to eq 1 end diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb index 2424c3fdc66..1fa89137972 100644 --- a/spec/lib/banzai/redactor_spec.rb +++ b/spec/lib/banzai/redactor_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Banzai::Redactor do - let(:user) { build(:user) } + let(:user) { create(:user) } let(:project) { build(:project) } let(:redactor) { described_class.new(project, user) } @@ -88,6 +88,55 @@ describe Banzai::Redactor do end end + context 'when the user cannot read cross project' do + include ActionView::Helpers::UrlHelper + let(:project) { create(:project) } + let(:other_project) { create(:project, :public) } + + def create_link(issuable) + type = issuable.class.name.underscore.downcase + link_to(issuable.to_reference, '', + class: 'gfm has-tooltip', + title: issuable.title, + data: { + reference_type: type, + "#{type}": issuable.id + }) + end + + before do + project.add_developer(user) + + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global) { false } + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + it 'skips links to issues within the same project' do + issue = create(:issue, project: project) + link = create_link(issue) + doc = Nokogiri::HTML.fragment(link) + + redactor.redact([doc]) + result = doc.css('a').last + + expect(result['class']).to include('has-tooltip') + expect(result['title']).to eq(issue.title) + end + + it 'removes info from a cross project reference' do + issue = create(:issue, project: other_project) + link = create_link(issue) + doc = Nokogiri::HTML.fragment(link) + + redactor.redact([doc]) + result = doc.css('a').last + + expect(result['class']).not_to include('has-tooltip') + expect(result['title']).to be_empty + end + end + describe '#redact_nodes' do it 'redacts an Array of nodes' do doc = Nokogiri::HTML.fragment('<a href="foo">foo</a>') diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 4cef3bdb24b..0a63567ee40 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -19,19 +19,58 @@ describe Banzai::ReferenceParser::IssueParser do it 'returns the nodes when the user can read the issue' do expect(Ability).to receive(:issues_readable_by_user) - .with([issue], user) - .and_return([issue]) + .with([issue], user) + .and_return([issue]) expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) end it 'returns an empty Array when the user can not read the issue' do expect(Ability).to receive(:issues_readable_by_user) - .with([issue], user) - .and_return([]) + .with([issue], user) + .and_return([]) expect(subject.nodes_visible_to_user(user, [link])).to eq([]) end + + context 'when the user cannot read cross project' do + let(:issue) { create(:issue) } + + before do + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + allow(Ability).to receive(:allowed?).with(user, :read_cross_project, :global) { false } + end + + it 'returns the nodes when the user can read the issue' do + expect(Ability).to receive(:allowed?) + .with(user, :read_issue_iid, issue) + .and_return(true) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([link]) + end + + it 'returns an empty Array when the user can not read the issue' do + expect(Ability).to receive(:allowed?) + .with(user, :read_issue_iid, issue) + .and_return(false) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + + context 'when the issue is not cross project' do + let(:issue) { create(:issue, project: project) } + + it 'does not check `can_read_reference` if the issue is not cross project' do + expect(Ability).to receive(:issues_readable_by_user) + .with([issue], user) + .and_return([]) + + expect(subject).not_to receive(:can_read_reference?).with(user, issue) + + expect(subject.nodes_visible_to_user(user, [link])).to eq([]) + end + end + end end context 'when the link does not have a data-issue attribute' do diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb new file mode 100644 index 00000000000..c76adcbe2f5 --- /dev/null +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_dependencies/untracked_file_spec.rb @@ -0,0 +1,262 @@ +require 'spec_helper' + +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploadsDependencies::UntrackedFile, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + + before(:all) do + ensure_temporary_tracking_table_exists + end + + describe '#upload_path' do + def assert_upload_path(file_path, expected_upload_path) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.upload_path).to eq(expected_upload_path) + end + + context 'for an appearance logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') + end + end + + context 'for an appearance header_logo file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') + end + end + + context 'for a user avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') + end + end + + context 'for a group avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') + end + end + + context 'for a project avatar file path' do + it 'returns the file path relative to the CarrierWave root' do + assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the file path relative to the project directory in uploads' do + project = create_project + random_hex = SecureRandom.hex + + assert_upload_path("/#{get_full_path(project)}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") + end + end + end + + describe '#uploader' do + def assert_uploader(file_path, expected_uploader) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.uploader).to eq(expected_uploader) + end + + context 'for an appearance logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for an appearance header_logo file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns AttachmentUploader as a string' do + assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') + end + end + + context 'for a user avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a group avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project avatar file path' do + it 'returns AvatarUploader as a string' do + assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns FileUploader as a string' do + project = create_project + + assert_uploader("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') + end + end + end + + describe '#model_type' do + def assert_model_type(file_path, expected_model_type) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_type).to eq(expected_model_type) + end + + context 'for an appearance logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for an appearance header_logo file path' do + it 'returns Appearance as a string' do + assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns Note as a string' do + assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') + end + end + + context 'for a user avatar file path' do + it 'returns User as a string' do + assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') + end + end + + context 'for a group avatar file path' do + it 'returns Namespace as a string' do + assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') + end + end + + context 'for a project avatar file path' do + it 'returns Project as a string' do + assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns Project as a string' do + project = create_project + + assert_model_type("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", 'Project') + end + end + end + + describe '#model_id' do + def assert_model_id(file_path, expected_model_id) + untracked_file = create_untracked_file(file_path) + + expect(untracked_file.model_id).to eq(expected_model_id) + end + + context 'for an appearance logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) + end + end + + context 'for an appearance header_logo file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) + end + end + + context 'for a pre-Markdown Note attachment file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) + end + end + + context 'for a user avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a group avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project avatar file path' do + it 'returns the ID as a string' do + assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + it 'returns the ID as a string' do + project = create_project + + assert_model_id("/#{get_full_path(project)}/#{SecureRandom.hex}/Some file.jpg", project.id) + end + end + end + + describe '#file_size' do + context 'for an appearance logo file path' do + let(:appearance) { create_or_update_appearance(logo: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(appearance, 'Appearance').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project avatar file path' do + let(:project) { create_project(avatar: true) } + let(:untracked_file) { described_class.create!(path: get_uploads(project, 'Project').first.path) } + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + + context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do + let(:project) { create_project } + let(:untracked_file) { create_untracked_file("/#{get_full_path(project)}/#{get_uploads(project, 'Project').first.path}") } + + before do + add_markdown_attachment(project) + end + + it 'returns the file size' do + expect(untracked_file.file_size).to eq(1062) + end + end + end + + def create_untracked_file(path_relative_to_upload_dir) + described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") + end +end diff --git a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb index c8fa252439a..0d2074eed22 100644 --- a/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/populate_untracked_uploads_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' -# This migration is using UploadService, which sets uploads.secret that is only -# added to the DB schema in 20180129193323. Since the test isn't isolated, we -# just use the latest schema when testing this migration. -# Ideally, the test should not use factories nor UploadService, and rely on the -# `table` helper instead. -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do - include TrackUntrackedUploadsHelpers +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers subject { described_class.new } - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } - let!(:uploads) { described_class::Upload } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:notes) { table(:notes) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } before do ensure_temporary_tracking_table_exists @@ -19,30 +21,30 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end context 'with untracked files and tracked files in untracked_files_for_uploads' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user1) { create(:user, :with_avatar) } - let!(:user2) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :legacy_storage, :with_avatar) } - let!(:project2) { create(:project, :legacy_storage, :with_avatar) } + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user1) { create_user(avatar: true) } + let!(:user2) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let!(:project2) { create_project(avatar: true) } before do - UploadService.new(project1, uploaded_file, FileUploader).execute # Markdown upload - UploadService.new(project2, uploaded_file, FileUploader).execute # Markdown upload + add_markdown_attachment(project1) + add_markdown_attachment(project2) # File records created by PrepareUntrackedUploads - untracked_files_for_uploads.create!(path: appearance.uploads.first.path) - untracked_files_for_uploads.create!(path: appearance.uploads.last.path) - untracked_files_for_uploads.create!(path: user1.uploads.first.path) - untracked_files_for_uploads.create!(path: user2.uploads.first.path) - untracked_files_for_uploads.create!(path: project1.uploads.first.path) - untracked_files_for_uploads.create!(path: project2.uploads.first.path) - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project1.full_path}/#{project1.uploads.last.path}") - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/#{project2.uploads.last.path}") + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').first.path) + untracked_files_for_uploads.create!(path: get_uploads(appearance, 'Appearance').last.path) + untracked_files_for_uploads.create!(path: get_uploads(user1, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(user2, 'User').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project1, 'Project').first.path) + untracked_files_for_uploads.create!(path: get_uploads(project2, 'Project').first.path) + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project1).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project1, 'Project').last.path}") + untracked_files_for_uploads.create!(path: "#{legacy_project_uploads_dir(project2).sub("#{MigrationsHelpers::TrackUntrackedUploadsHelpers::PUBLIC_DIR}/", '')}/#{get_uploads(project2, 'Project').last.path}") # Untrack 4 files - user2.uploads.delete_all - project2.uploads.delete_all # 2 files: avatar and a Markdown upload - appearance.uploads.where("path like '%header_logo%'").delete_all + get_uploads(user2, 'User').delete_all + get_uploads(project2, 'Project').delete_all # 2 files: avatar and a Markdown upload + get_uploads(appearance, 'Appearance').where("path like '%header_logo%'").delete_all end it 'adds untracked files to the uploads table' do @@ -50,9 +52,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(1, untracked_files_for_uploads.reorder(:id).last.id) end.to change { uploads.count }.from(4).to(8) - expect(user2.uploads.count).to eq(1) - expect(project2.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(project2, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'deletes rows after processing them' do @@ -66,9 +68,9 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra it 'does not create duplicate uploads of already tracked files' do subject.perform(1, untracked_files_for_uploads.last.id) - expect(user1.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(appearance.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) end it 'uses the start and end batch ids [only 1st half]' do @@ -80,11 +82,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(1) - expect(appearance.uploads.count).to eq(2) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(0) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(1) + expect(get_uploads(appearance, 'Appearance').count).to eq(2) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(0) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -99,11 +101,11 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra subject.perform(start_id, end_id) end.to change { uploads.count }.from(4).to(6) - expect(user1.uploads.count).to eq(1) - expect(user2.uploads.count).to eq(0) - expect(appearance.uploads.count).to eq(1) - expect(project1.uploads.count).to eq(2) - expect(project2.uploads.count).to eq(2) + expect(get_uploads(user1, 'User').count).to eq(1) + expect(get_uploads(user2, 'User').count).to eq(0) + expect(get_uploads(appearance, 'Appearance').count).to eq(1) + expect(get_uploads(project1, 'Project').count).to eq(2) + expect(get_uploads(project2, 'Project').count).to eq(2) # Only 4 have been either confirmed or added to uploads expect(untracked_files_for_uploads.count).to eq(4) @@ -122,7 +124,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'does not block a whole batch because of one bad path' do - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a") expect(untracked_files_for_uploads.count).to eq(9) expect(uploads.count).to eq(4) @@ -133,7 +135,7 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra end it 'an unparseable path is shown in error output' do - bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{project2.full_path}/._7d37bf4c747916390e596744117d5d1a" + bad_path = "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(project2)}/._7d37bf4c747916390e596744117d5d1a" untracked_files_for_uploads.create!(path: bad_path) expect(Rails.logger).to receive(:error).with(/Error parsing path "#{bad_path}":/) @@ -152,363 +154,100 @@ describe Gitlab::BackgroundMigration::PopulateUntrackedUploads, :sidekiq, :migra describe 'upload outcomes for each path pattern' do shared_examples_for 'non_markdown_file' do - let!(:expected_upload_attrs) { model.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let!(:expected_upload_attrs) { model_uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - model.uploads.delete_all + model_uploads.delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { model_uploads.count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(expected_upload_attrs) + expect(model_uploads.first.attributes).to include(expected_upload_attrs) end end context 'for an appearance logo file path' do - let(:model) { create_or_update_appearance(logo: uploaded_file) } + let(:model) { create_or_update_appearance(logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for an appearance header_logo file path' do - let(:model) { create_or_update_appearance(header_logo: uploaded_file) } + let(:model) { create_or_update_appearance(header_logo: true) } + let(:model_uploads) { get_uploads(model, 'Appearance') } it_behaves_like 'non_markdown_file' end context 'for a pre-Markdown Note attachment file path' do - let(:model) { create(:note, :with_attachment) } - let!(:expected_upload_attrs) { Upload.where(model_type: 'Note', model_id: model.id).first.attributes.slice('path', 'uploader', 'size', 'checksum') } + let(:model) { create_note(attachment: true) } + let!(:expected_upload_attrs) { get_uploads(model, 'Note').first.attributes.slice('path', 'uploader', 'size', 'checksum') } let!(:untracked_file) { untracked_files_for_uploads.create!(path: expected_upload_attrs['path']) } before do - Upload.where(model_type: 'Note', model_id: model.id).delete_all + get_uploads(model, 'Note').delete_all end # Can't use the shared example because Note doesn't have an `uploads` association it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { Upload.where(model_type: 'Note', model_id: model.id).count }.from(0).to(1) + end.to change { get_uploads(model, 'Note').count }.from(0).to(1) - expect(Upload.where(model_type: 'Note', model_id: model.id).first.attributes).to include(expected_upload_attrs) + expect(get_uploads(model, 'Note').first.attributes).to include(expected_upload_attrs) end end context 'for a user avatar file path' do - let(:model) { create(:user, :with_avatar) } + let(:model) { create_user(avatar: true) } + let(:model_uploads) { get_uploads(model, 'User') } it_behaves_like 'non_markdown_file' end context 'for a group avatar file path' do - let(:model) { create(:group, :with_avatar) } + let(:model) { create_group(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Namespace') } it_behaves_like 'non_markdown_file' end context 'for a project avatar file path' do - let(:model) { create(:project, :legacy_storage, :with_avatar) } + let(:model) { create_project(avatar: true) } + let(:model_uploads) { get_uploads(model, 'Project') } it_behaves_like 'non_markdown_file' end context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:model) { create(:project, :legacy_storage) } + let(:model) { create_project } before do # Upload the file - UploadService.new(model, uploaded_file, FileUploader).execute + add_markdown_attachment(model) # Create the untracked_files_for_uploads record - untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{model.full_path}/#{model.uploads.first.path}") + untracked_files_for_uploads.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}/#{get_full_path(model)}/#{get_uploads(model, 'Project').first.path}") # Save the expected upload attributes - @expected_upload_attrs = model.reload.uploads.first.attributes.slice('path', 'uploader', 'size', 'checksum') + @expected_upload_attrs = get_uploads(model, 'Project').first.attributes.slice('path', 'uploader', 'size', 'checksum') # Untrack the file - model.reload.uploads.delete_all + get_uploads(model, 'Project').delete_all end it 'creates an Upload record' do expect do subject.perform(1, untracked_files_for_uploads.last.id) - end.to change { model.reload.uploads.count }.from(0).to(1) + end.to change { get_uploads(model, 'Project').count }.from(0).to(1) - expect(model.uploads.first.attributes).to include(@expected_upload_attrs) + expect(get_uploads(model, 'Project').first.attributes).to include(@expected_upload_attrs) end end end end - -describe Gitlab::BackgroundMigration::PopulateUntrackedUploads::UntrackedFile do - include TrackUntrackedUploadsHelpers - - let(:upload_class) { Gitlab::BackgroundMigration::PopulateUntrackedUploads::Upload } - - before(:all) do - ensure_temporary_tracking_table_exists - end - - describe '#upload_path' do - def assert_upload_path(file_path, expected_upload_path) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.upload_path).to eq(expected_upload_path) - end - - context 'for an appearance logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/logo/1/some_logo.jpg', 'uploads/-/system/appearance/logo/1/some_logo.jpg') - end - end - - context 'for an appearance header_logo file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/appearance/header_logo/1/some_logo.jpg', 'uploads/-/system/appearance/header_logo/1/some_logo.jpg') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/note/attachment/1234/some_attachment.pdf', 'uploads/-/system/note/attachment/1234/some_attachment.pdf') - end - end - - context 'for a user avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/user/avatar/1234/avatar.jpg', 'uploads/-/system/user/avatar/1234/avatar.jpg') - end - end - - context 'for a group avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/group/avatar/1234/avatar.jpg', 'uploads/-/system/group/avatar/1234/avatar.jpg') - end - end - - context 'for a project avatar file path' do - it 'returns the file path relative to the CarrierWave root' do - assert_upload_path('/-/system/project/avatar/1234/avatar.jpg', 'uploads/-/system/project/avatar/1234/avatar.jpg') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the file path relative to the project directory in uploads' do - project = create(:project, :legacy_storage) - random_hex = SecureRandom.hex - - assert_upload_path("/#{project.full_path}/#{random_hex}/Some file.jpg", "#{random_hex}/Some file.jpg") - end - end - end - - describe '#uploader' do - def assert_uploader(file_path, expected_uploader) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.uploader).to eq(expected_uploader) - end - - context 'for an appearance logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for an appearance header_logo file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/appearance/header_logo/1/some_logo.jpg', 'AttachmentUploader') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns AttachmentUploader as a string' do - assert_uploader('/-/system/note/attachment/1234/some_attachment.pdf', 'AttachmentUploader') - end - end - - context 'for a user avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/user/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a group avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/group/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project avatar file path' do - it 'returns AvatarUploader as a string' do - assert_uploader('/-/system/project/avatar/1234/avatar.jpg', 'AvatarUploader') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns FileUploader as a string' do - project = create(:project, :legacy_storage) - - assert_uploader("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'FileUploader') - end - end - end - - describe '#model_type' do - def assert_model_type(file_path, expected_model_type) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_type).to eq(expected_model_type) - end - - context 'for an appearance logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for an appearance header_logo file path' do - it 'returns Appearance as a string' do - assert_model_type('/-/system/appearance/header_logo/1/some_logo.jpg', 'Appearance') - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns Note as a string' do - assert_model_type('/-/system/note/attachment/1234/some_attachment.pdf', 'Note') - end - end - - context 'for a user avatar file path' do - it 'returns User as a string' do - assert_model_type('/-/system/user/avatar/1234/avatar.jpg', 'User') - end - end - - context 'for a group avatar file path' do - it 'returns Namespace as a string' do - assert_model_type('/-/system/group/avatar/1234/avatar.jpg', 'Namespace') - end - end - - context 'for a project avatar file path' do - it 'returns Project as a string' do - assert_model_type('/-/system/project/avatar/1234/avatar.jpg', 'Project') - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns Project as a string' do - project = create(:project, :legacy_storage) - - assert_model_type("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", 'Project') - end - end - end - - describe '#model_id' do - def assert_model_id(file_path, expected_model_id) - untracked_file = create_untracked_file(file_path) - - expect(untracked_file.model_id).to eq(expected_model_id) - end - - context 'for an appearance logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/logo/1/some_logo.jpg', 1) - end - end - - context 'for an appearance header_logo file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/appearance/header_logo/1/some_logo.jpg', 1) - end - end - - context 'for a pre-Markdown Note attachment file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/note/attachment/1234/some_attachment.pdf', 1234) - end - end - - context 'for a user avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/user/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a group avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/group/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project avatar file path' do - it 'returns the ID as a string' do - assert_model_id('/-/system/project/avatar/1234/avatar.jpg', 1234) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - it 'returns the ID as a string' do - project = create(:project, :legacy_storage) - - assert_model_id("/#{project.full_path}/#{SecureRandom.hex}/Some file.jpg", project.id) - end - end - end - - describe '#file_size' do - context 'for an appearance logo file path' do - let(:appearance) { create_or_update_appearance(logo: uploaded_file) } - let(:untracked_file) { described_class.create!(path: appearance.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(appearance.logo.size) - end - end - - context 'for a project avatar file path' do - let(:project) { create(:project, :legacy_storage, avatar: uploaded_file) } - let(:untracked_file) { described_class.create!(path: project.uploads.first.path) } - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.avatar.size) - end - end - - context 'for a project Markdown attachment (notes, issues, MR descriptions) file path' do - let(:project) { create(:project, :legacy_storage) } - let(:untracked_file) { create_untracked_file("/#{project.full_path}/#{project.uploads.first.path}") } - - before do - UploadService.new(project, uploaded_file, FileUploader).execute - end - - it 'returns the file size' do - expect(untracked_file.file_size).to eq(35255) - end - - it 'returns the same thing that CarrierWave would return' do - expect(untracked_file.file_size).to eq(project.uploads.first.size) - end - end - end - - def create_untracked_file(path_relative_to_upload_dir) - described_class.create!(path: "#{Gitlab::BackgroundMigration::PrepareUntrackedUploads::RELATIVE_UPLOAD_DIR}#{path_relative_to_upload_dir}") - end -end diff --git a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb index ca77e64ae40..35750d89c35 100644 --- a/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb +++ b/spec/lib/gitlab/background_migration/prepare_untracked_uploads_spec.rb @@ -1,10 +1,16 @@ require 'spec_helper' -describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180129193323 do - include TrackUntrackedUploadsHelpers - include MigrationsHelpers - - let!(:untracked_files_for_uploads) { described_class::UntrackedFile } +# Rollback DB to 10.5 (later than this was originally written for) because it still needs to work. +describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migration, schema: 20180208183958 do + include MigrationsHelpers::TrackUntrackedUploadsHelpers + + let!(:untracked_files_for_uploads) { table(:untracked_files_for_uploads) } + let!(:appearances) { table(:appearances) } + let!(:namespaces) { table(:namespaces) } + let!(:projects) { table(:projects) } + let!(:routes) { table(:routes) } + let!(:uploads) { table(:uploads) } + let!(:users) { table(:users) } around do |example| # Especially important so the follow-up migration does not get run @@ -15,19 +21,17 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat shared_examples 'prepares the untracked_files_for_uploads table' do context 'when files were uploaded before and after hashed storage was enabled' do - let!(:appearance) { create_or_update_appearance(logo: uploaded_file, header_logo: uploaded_file) } - let!(:user) { create(:user, :with_avatar) } - let!(:project1) { create(:project, :with_avatar, :legacy_storage) } - let(:project2) { create(:project) } # instantiate after enabling hashed_storage + let!(:appearance) { create_or_update_appearance(logo: true, header_logo: true) } + let!(:user) { create_user(avatar: true) } + let!(:project1) { create_project(avatar: true) } + let(:project2) { create_project } # instantiate after enabling hashed_storage before do # Markdown upload before enabling hashed_storage - UploadService.new(project1, uploaded_file, FileUploader).execute - - stub_application_setting(hashed_storage_enabled: true) + add_markdown_attachment(project1) # Markdown upload after enabling hashed_storage - UploadService.new(project2, uploaded_file, FileUploader).execute + add_markdown_attachment(project2, hashed_storage: true) end it 'has a path field long enough for really long paths' do @@ -61,7 +65,7 @@ describe Gitlab::BackgroundMigration::PrepareUntrackedUploads, :sidekiq, :migrat it 'does not add hashed files to the untracked_files_for_uploads table' do described_class.new.perform - hashed_file_path = project2.uploads.where(uploader: 'FileUploader').first.path + hashed_file_path = get_uploads(project2, 'Project').where(uploader: 'FileUploader').first.path expect(untracked_files_for_uploads.where("path like '%#{hashed_file_path}%'").exists?).to be_falsey end diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb index f1655854486..49a179ba875 100644 --- a/spec/lib/gitlab/contributions_calendar_spec.rb +++ b/spec/lib/gitlab/contributions_calendar_spec.rb @@ -118,6 +118,19 @@ describe Gitlab::ContributionsCalendar do expect(calendar.events_by_date(today)).to contain_exactly(e1) expect(calendar(contributor).events_by_date(today)).to contain_exactly(e1, e2, e3) end + + context 'when the user cannot read read cross project' do + before do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + it 'does not return any events' do + create_event(public_project, today) + + expect(calendar(user).events_by_date(today)).to be_empty + end + end end describe '#starting_year' do diff --git a/spec/lib/gitlab/cross_project_access/check_collection_spec.rb b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb new file mode 100644 index 00000000000..a9e7575240e --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/check_collection_spec.rb @@ -0,0 +1,55 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess::CheckCollection do + subject(:collection) { described_class.new } + + describe '#add_collection' do + it 'merges the checks of 2 collections' do + initial_check = double('check') + collection.add_check(initial_check) + + other_collection = described_class.new + other_check = double('other_check') + other_collection.add_check(other_check) + + shared_check = double('shared check') + other_collection.add_check(shared_check) + collection.add_check(shared_check) + + collection.add_collection(other_collection) + + expect(collection.checks).to contain_exactly(initial_check, shared_check, other_check) + end + end + + describe '#should_run?' do + def fake_check(run, skip) + check = double("Check: run=#{run} - skip={skip}") + allow(check).to receive(:should_run?).and_return(run) + allow(check).to receive(:should_skip?).and_return(skip) + allow(check).to receive(:skip).and_return(skip) + + check + end + + it 'returns true if one of the check says it should run' do + check = fake_check(true, false) + other_check = fake_check(false, false) + + collection.add_check(check) + collection.add_check(other_check) + + expect(collection.should_run?(double)).to be_truthy + end + + it 'returns false if one of the check says it should be skipped' do + check = fake_check(true, false) + other_check = fake_check(false, true) + + collection.add_check(check) + collection.add_check(other_check) + + expect(collection.should_run?(double)).to be_falsey + end + end +end diff --git a/spec/lib/gitlab/cross_project_access/check_info_spec.rb b/spec/lib/gitlab/cross_project_access/check_info_spec.rb new file mode 100644 index 00000000000..bc9dbf2bece --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/check_info_spec.rb @@ -0,0 +1,111 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess::CheckInfo do + let(:dummy_controller) { double } + + before do + allow(dummy_controller).to receive(:action_name).and_return('index') + end + + describe '#should_run?' do + it 'runs when an action is defined' do + info = described_class.new({ index: true }, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'runs when the action is missing' do + info = described_class.new({}, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'does not run when the action is excluded' do + info = described_class.new({ index: false }, nil, nil, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'runs when the `if` conditional is true' do + info = described_class.new({}, -> { true }, nil, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'does not run when the if condition is false' do + info = described_class.new({}, -> { false }, nil, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'does not run when the `unless` check is true' do + info = described_class.new({}, nil, -> { true }, false) + + expect(info.should_run?(dummy_controller)).to be_falsy + end + + it 'runs when the `unless` check is false' do + info = described_class.new({}, nil, -> { false }, false) + + expect(info.should_run?(dummy_controller)).to be_truthy + end + + it 'returns the the oposite of #should_skip? when the check is a skip' do + info = described_class.new({}, nil, nil, true) + + expect(info).to receive(:should_skip?).with(dummy_controller).and_return(false) + expect(info.should_run?(dummy_controller)).to be_truthy + end + end + + describe '#should_skip?' do + it 'skips when an action is defined' do + info = described_class.new({ index: true }, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'does not skip when the action is not defined' do + info = described_class.new({}, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'does not skip when the action is excluded' do + info = described_class.new({ index: false }, nil, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'skips when the `if` conditional is true' do + info = described_class.new({ index: true }, -> { true }, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'does not skip the `if` conditional is false' do + info = described_class.new({ index: true }, -> { false }, nil, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'does not skip when the `unless` check is true' do + info = described_class.new({ index: true }, nil, -> { true }, true) + + expect(info.should_skip?(dummy_controller)).to be_falsy + end + + it 'skips when `unless` check is false' do + info = described_class.new({ index: true }, nil, -> { false }, true) + + expect(info.should_skip?(dummy_controller)).to be_truthy + end + + it 'returns the the oposite of #should_run? when the check is not a skip' do + info = described_class.new({}, nil, nil, false) + + expect(info).to receive(:should_run?).with(dummy_controller).and_return(false) + expect(info.should_skip?(dummy_controller)).to be_truthy + end + end +end diff --git a/spec/lib/gitlab/cross_project_access/class_methods_spec.rb b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb new file mode 100644 index 00000000000..5349685e633 --- /dev/null +++ b/spec/lib/gitlab/cross_project_access/class_methods_spec.rb @@ -0,0 +1,46 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess::ClassMethods do + let(:dummy_class) do + Class.new do + extend Gitlab::CrossProjectAccess::ClassMethods + end + end + let(:dummy_proc) { lambda { false } } + + describe '#requires_cross_project_access' do + it 'creates a correct check when a hash is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: false }, + positive_condition: dummy_proc, + negative_condition: dummy_proc) + + dummy_class.requires_cross_project_access( + hello: true, world: false, if: dummy_proc, unless: dummy_proc + ) + end + + it 'creates a correct check when an array is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: true }, + positive_condition: nil, + negative_condition: nil) + + dummy_class.requires_cross_project_access(:hello, :world) + end + + it 'creates a correct check when an array and a hash is passed' do + expect(Gitlab::CrossProjectAccess) + .to receive(:add_check).with(dummy_class, + actions: { hello: true, world: true }, + positive_condition: dummy_proc, + negative_condition: dummy_proc) + + dummy_class.requires_cross_project_access( + :hello, :world, if: dummy_proc, unless: dummy_proc + ) + end + end +end diff --git a/spec/lib/gitlab/cross_project_access_spec.rb b/spec/lib/gitlab/cross_project_access_spec.rb new file mode 100644 index 00000000000..614b0473c7e --- /dev/null +++ b/spec/lib/gitlab/cross_project_access_spec.rb @@ -0,0 +1,84 @@ +require 'spec_helper' + +describe Gitlab::CrossProjectAccess do + let(:super_class) { Class.new } + let(:descendant_class) { Class.new(super_class) } + let(:current_instance) { described_class.new } + + before do + allow(described_class).to receive(:instance).and_return(current_instance) + end + + describe '#add_check' do + it 'keeps track of the properties to check' do + expect do + described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { true }, + negative_condition: -> { false }) + end.to change { described_class.checks.size }.by(1) + end + + it 'builds the check correctly' do + check_collection = described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + check = check_collection.checks.first + + expect(check.actions).to eq(index: true) + expect(check.positive_condition.call).to eq('positive') + expect(check.negative_condition.call).to eq('negative') + end + + it 'merges the checks of a parent class into existing checks of a subclass' do + subclass_collection = described_class.add_check(descendant_class) + + expect(subclass_collection).to receive(:add_collection).and_call_original + + described_class.add_check(super_class) + end + + it 'merges the existing checks of a superclass into the checks of a subclass' do + super_collection = described_class.add_check(super_class) + descendant_collection = described_class.add_check(descendant_class) + + expect(descendant_collection.checks).to include(*super_collection.checks) + end + end + + describe '#find_check' do + it 'returns a check when it was defined for a superclass' do + expected_check = described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + expect(described_class.find_check(descendant_class.new)) + .to eq(expected_check) + end + + it 'caches the result for a subclass' do + described_class.add_check(super_class, + actions: { index: true }, + positive_condition: -> { 'positive' }, + negative_condition: -> { 'negative' }) + + expect(described_class.instance).to receive(:closest_parent).once.and_call_original + + 2.times { described_class.find_check(descendant_class.new) } + end + + it 'returns the checks for the closest class if there are more checks available' do + described_class.add_check(super_class, + actions: { index: true }) + expected_check = described_class.add_check(descendant_class, + actions: { index: true, show: false }) + + check = described_class.find_check(descendant_class.new) + + expect(check).to eq(expected_check) + end + end +end diff --git a/spec/lib/gitlab/diff/highlight_spec.rb b/spec/lib/gitlab/diff/highlight_spec.rb index cd602ccab8e..73d60c021c8 100644 --- a/spec/lib/gitlab/diff/highlight_spec.rb +++ b/spec/lib/gitlab/diff/highlight_spec.rb @@ -72,6 +72,28 @@ describe Gitlab::Diff::Highlight do expect(subject[5].text).to eq(code) expect(subject[5].text).to be_html_safe end + + context 'when the inline diff marker has an invalid range' do + before do + allow_any_instance_of(Gitlab::Diff::InlineDiffMarker).to receive(:mark).and_raise(RangeError) + end + + it 'keeps the original rich line' do + code = %q{+ raise RuntimeError, "System commands must be given as an array of strings"} + + expect(subject[5].text).to eq(code) + expect(subject[5].text).not_to be_html_safe + end + + it 'reports to Sentry if configured' do + allow(Gitlab::Sentry).to receive(:enabled?).and_return(true) + + expect(Gitlab::Sentry).to receive(:context) + expect(Raven).to receive(:capture_exception) + + subject + end + end end end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 67271d769a0..d601a383a98 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -600,6 +600,33 @@ describe Gitlab::Git::Repository, seed_helper: true do end end + describe '#branch_names_contains_sha' do + shared_examples 'returning the right branches' do + let(:head_id) { repository.rugged.head.target.oid } + let(:new_branch) { head_id } + + before do + repository.create_branch(new_branch, 'master') + end + + after do + repository.delete_branch(new_branch) + end + + it 'displays that branch' do + expect(repository.branch_names_contains_sha(head_id)).to include('master', new_branch) + end + end + + context 'when Gitaly is enabled' do + it_behaves_like 'returning the right branches' + end + + context 'when Gitaly is disabled', :disable_gitaly do + it_behaves_like 'returning the right branches' + end + end + describe "#refs_hash" do subject { repository.refs_hash } @@ -2204,7 +2231,7 @@ describe Gitlab::Git::Repository, seed_helper: true do context 'sparse checkout', :skip_gitaly_mock do let(:expected_files) { %w(files files/js files/js/application.js) } - before do + it 'checks out only the files in the diff' do allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| m.call(*args) do worktree_path = args[0] @@ -2216,11 +2243,34 @@ describe Gitlab::Git::Repository, seed_helper: true do expect(Dir[files_pattern]).to eq(expected) end end - end - it 'checkouts only the files in the diff' do subject end + + context 'when the diff contains a rename' do + let(:repo) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged } + let(:end_sha) { new_commit_move_file(repo).oid } + + after do + # Erase our commits so other tests get the original repo + repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged + repo.references.update('refs/heads/master', SeedRepo::LastCommit::ID) + end + + it 'does not include the renamed file in the sparse checkout' do + allow(repository).to receive(:with_worktree).and_wrap_original do |m, *args| + m.call(*args) do + worktree_path = args[0] + files_pattern = File.join(worktree_path, '**', '*') + + expect(Dir[files_pattern]).not_to include('CHANGELOG') + expect(Dir[files_pattern]).not_to include('encoding/CHANGELOG') + end + end + + subject + end + end end context 'with an ASCII-8BIT diff', :skip_gitaly_mock do @@ -2230,7 +2280,7 @@ describe Gitlab::Git::Repository, seed_helper: true do allow(repository).to receive(:run_git!).and_call_original allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT')) - expect(subject.length).to eq(40) + expect(subject).to match(/\h{40}/) end end end diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb index 186b2d9279d..215f1ecc9c5 100644 --- a/spec/lib/gitlab/git_access_wiki_spec.rb +++ b/spec/lib/gitlab/git_access_wiki_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' describe Gitlab::GitAccessWiki do let(:access) { described_class.new(user, project, 'web', authentication_abilities: authentication_abilities, redirected_path: redirected_path) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :wiki_repo) } let(:user) { create(:user) } let(:changes) { ['6f6d7e7ed 570e7b2ab refs/heads/master'] } let(:redirected_path) { nil } @@ -48,6 +48,18 @@ describe Gitlab::GitAccessWiki do it 'give access to download wiki code' do expect { subject }.not_to raise_error end + + context 'when the wiki repository does not exist' do + it 'returns not found' do + wiki_repo = project.wiki.repository + FileUtils.rm_rf(wiki_repo.path) + + # Sanity check for rm_rf + expect(wiki_repo.exists?).to eq(false) + + expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.') + end + end end context 'when wiki feature is disabled' do diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb index cbc7ce1c1b0..c50e73cecfc 100644 --- a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb @@ -84,4 +84,30 @@ describe Gitlab::GitalyClient::RepositoryService do expect(client.has_local_branches?).to be(true) end end + + describe '#rebase_in_progress?' do + let(:rebase_id) { 1 } + + it 'sends a repository_rebase_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_rebase_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.rebase_in_progress?(rebase_id) + end + end + + describe '#squash_in_progress?' do + let(:squash_id) { 1 } + + it 'sends a repository_squash_in_progress message' do + expect_any_instance_of(Gitaly::RepositoryService::Stub) + .to receive(:is_squash_in_progress) + .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash)) + .and_return(double(in_progress: true)) + + client.squash_in_progress?(squash_id) + end + end end diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb index 60a134be939..b24c9882c0c 100644 --- a/spec/lib/gitlab/middleware/go_spec.rb +++ b/spec/lib/gitlab/middleware/go_spec.rb @@ -3,19 +3,30 @@ require 'spec_helper' describe Gitlab::Middleware::Go do let(:app) { double(:app) } let(:middleware) { described_class.new(app) } + let(:env) do + { + 'rack.input' => '', + 'REQUEST_METHOD' => 'GET' + } + end describe '#call' do describe 'when go-get=0' do + before do + env['QUERY_STRING'] = 'go-get=0' + end + it 'skips go-import generation' do - env = { 'rack.input' => '', - 'QUERY_STRING' => 'go-get=0' } expect(app).to receive(:call).with(env).and_return('no-go') middleware.call(env) end end describe 'when go-get=1' do - let(:current_user) { nil } + before do + env['QUERY_STRING'] = 'go-get=1' + env['PATH_INFO'] = "/#{path}" + end shared_examples 'go-get=1' do |enabled_protocol:| context 'with simple 2-segment project path' do @@ -54,21 +65,75 @@ describe Gitlab::Middleware::Go do project.update_attribute(:visibility_level, Project::PRIVATE) end - context 'with access to the project' do + shared_examples 'unauthorized' do + it 'returns the 2-segment group path' do + expect_response_with_path(go, enabled_protocol, group.full_path) + end + end + + context 'when not authenticated' do + it_behaves_like 'unauthorized' + end + + context 'when authenticated' do let(:current_user) { project.creator } before do project.team.add_master(current_user) end - it 'returns the full project path' do - expect_response_with_path(go, enabled_protocol, project.full_path) + shared_examples 'authenticated' do + context 'with access to the project' do + it 'returns the full project path' do + expect_response_with_path(go, enabled_protocol, project.full_path) + end + end + + context 'without access to the project' do + before do + project.team.find_member(current_user).destroy + end + + it_behaves_like 'unauthorized' + end end - end - context 'without access to the project' do - it 'returns the 2-segment group path' do - expect_response_with_path(go, enabled_protocol, group.full_path) + context 'using warden' do + before do + env['warden'] = double(authenticate: current_user) + end + + context 'when active' do + it_behaves_like 'authenticated' + end + + context 'when blocked' do + before do + current_user.block! + end + + it_behaves_like 'unauthorized' + end + end + + context 'using a personal access token' do + let(:personal_access_token) { create(:personal_access_token, user: current_user) } + + before do + env['HTTP_PRIVATE_TOKEN'] = personal_access_token.token + end + + context 'with api scope' do + it_behaves_like 'authenticated' + end + + context 'with read_user scope' do + before do + personal_access_token.update_attribute(:scopes, [:read_user]) + end + + it_behaves_like 'unauthorized' + end end end end @@ -138,12 +203,6 @@ describe Gitlab::Middleware::Go do end def go - env = { - 'rack.input' => '', - 'QUERY_STRING' => 'go-get=1', - 'PATH_INFO' => "/#{path}", - 'warden' => double(authenticate: current_user) - } middleware.call(env) end diff --git a/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb index 2b488101496..c95719eff1d 100644 --- a/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb +++ b/spec/lib/gitlab/prometheus/queries/matched_metrics_query_spec.rb @@ -24,7 +24,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricsQuery do context 'with one group where two metrics is found' do before do - allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) allow(client).to receive(:label_values).and_return(metric_names) end @@ -70,7 +70,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricsQuery do context 'with one group where only one metric is found' do before do - allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) allow(client).to receive(:label_values).and_return('metric_a') end @@ -99,7 +99,7 @@ describe Gitlab::Prometheus::Queries::MatchedMetricsQuery do let(:second_metric_group) { simple_metric_group(name: 'nameb', metrics: simple_metrics(added_metric_name: 'metric_c')) } before do - allow(metric_group_class).to receive(:all).and_return([simple_metric_group, second_metric_group]) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group, second_metric_group]) allow(client).to receive(:label_values).and_return('metric_c') end diff --git a/spec/lib/gitlab/prometheus_client_spec.rb b/spec/lib/gitlab/prometheus_client_spec.rb index 5d86007f71f..4c3b8deefb9 100644 --- a/spec/lib/gitlab/prometheus_client_spec.rb +++ b/spec/lib/gitlab/prometheus_client_spec.rb @@ -19,41 +19,41 @@ describe Gitlab::PrometheusClient do # - execute_query: A query call shared_examples 'failure response' do context 'when request returns 400 with an error message' do - it 'raises a Gitlab::PrometheusError error' do + it 'raises a Gitlab::PrometheusClient::Error error' do req_stub = stub_prometheus_request(query_url, status: 400, body: { error: 'bar!' }) expect { execute_query } - .to raise_error(Gitlab::PrometheusError, 'bar!') + .to raise_error(Gitlab::PrometheusClient::Error, 'bar!') expect(req_stub).to have_been_requested end end context 'when request returns 400 without an error message' do - it 'raises a Gitlab::PrometheusError error' do + it 'raises a Gitlab::PrometheusClient::Error error' do req_stub = stub_prometheus_request(query_url, status: 400) expect { execute_query } - .to raise_error(Gitlab::PrometheusError, 'Bad data received') + .to raise_error(Gitlab::PrometheusClient::Error, 'Bad data received') expect(req_stub).to have_been_requested end end context 'when request returns 500' do - it 'raises a Gitlab::PrometheusError error' do + it 'raises a Gitlab::PrometheusClient::Error error' do req_stub = stub_prometheus_request(query_url, status: 500, body: { message: 'FAIL!' }) expect { execute_query } - .to raise_error(Gitlab::PrometheusError, '500 - {"message":"FAIL!"}') + .to raise_error(Gitlab::PrometheusClient::Error, '500 - {"message":"FAIL!"}') expect(req_stub).to have_been_requested end end context 'when request returns non json data' do - it 'raises a Gitlab::PrometheusError error' do + it 'raises a Gitlab::PrometheusClient::Error error' do req_stub = stub_prometheus_request(query_url, status: 200, body: 'not json') expect { execute_query } - .to raise_error(Gitlab::PrometheusError, 'Parsing response failed') + .to raise_error(Gitlab::PrometheusClient::Error, 'Parsing response failed') expect(req_stub).to have_been_requested end end @@ -65,27 +65,27 @@ describe Gitlab::PrometheusClient do subject { described_class.new(RestClient::Resource.new(prometheus_url)) } context 'exceptions are raised' do - it 'raises a Gitlab::PrometheusError error when a SocketError is rescued' do + it 'raises a Gitlab::PrometheusClient::Error error when a SocketError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, SocketError) expect { subject.send(:get, '/', {}) } - .to raise_error(Gitlab::PrometheusError, "Can't connect to #{prometheus_url}") + .to raise_error(Gitlab::PrometheusClient::Error, "Can't connect to #{prometheus_url}") expect(req_stub).to have_been_requested end - it 'raises a Gitlab::PrometheusError error when a SSLError is rescued' do + it 'raises a Gitlab::PrometheusClient::Error error when a SSLError is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, OpenSSL::SSL::SSLError) expect { subject.send(:get, '/', {}) } - .to raise_error(Gitlab::PrometheusError, "#{prometheus_url} contains invalid SSL data") + .to raise_error(Gitlab::PrometheusClient::Error, "#{prometheus_url} contains invalid SSL data") expect(req_stub).to have_been_requested end - it 'raises a Gitlab::PrometheusError error when a RestClient::Exception is rescued' do + it 'raises a Gitlab::PrometheusClient::Error error when a RestClient::Exception is rescued' do req_stub = stub_prometheus_request_with_exception(prometheus_url, RestClient::Exception) expect { subject.send(:get, '/', {}) } - .to raise_error(Gitlab::PrometheusError, "Network connection error") + .to raise_error(Gitlab::PrometheusClient::Error, "Network connection error") expect(req_stub).to have_been_requested end end diff --git a/spec/lib/gitlab/quick_actions/command_definition_spec.rb b/spec/lib/gitlab/quick_actions/command_definition_spec.rb index f44a562dc63..b03c1e23ca3 100644 --- a/spec/lib/gitlab/quick_actions/command_definition_spec.rb +++ b/spec/lib/gitlab/quick_actions/command_definition_spec.rb @@ -40,7 +40,7 @@ describe Gitlab::QuickActions::CommandDefinition do end describe "#available?" do - let(:opts) { { go: false } } + let(:opts) { OpenStruct.new(go: false) } context "when the command has a condition block" do before do @@ -78,7 +78,7 @@ describe Gitlab::QuickActions::CommandDefinition do it "doesn't execute the command" do expect(context).not_to receive(:instance_exec) - subject.execute(context, {}, nil) + subject.execute(context, nil) expect(context.run).to be false end @@ -95,7 +95,7 @@ describe Gitlab::QuickActions::CommandDefinition do end it "doesn't execute the command" do - subject.execute(context, {}, nil) + subject.execute(context, nil) expect(context.run).to be false end @@ -109,7 +109,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is provided an argument" do it "executes the command" do - subject.execute(context, {}, true) + subject.execute(context, true) expect(context.run).to be true end @@ -117,7 +117,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is not provided an argument" do it "executes the command" do - subject.execute(context, {}, nil) + subject.execute(context, nil) expect(context.run).to be true end @@ -131,7 +131,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is provided an argument" do it "executes the command" do - subject.execute(context, {}, true) + subject.execute(context, true) expect(context.run).to be true end @@ -139,7 +139,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is not provided an argument" do it "doesn't execute the command" do - subject.execute(context, {}, nil) + subject.execute(context, nil) expect(context.run).to be false end @@ -153,7 +153,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is provided an argument" do it "executes the command" do - subject.execute(context, {}, true) + subject.execute(context, true) expect(context.run).to be true end @@ -161,7 +161,7 @@ describe Gitlab::QuickActions::CommandDefinition do context "when the command is not provided an argument" do it "executes the command" do - subject.execute(context, {}, nil) + subject.execute(context, nil) expect(context.run).to be true end @@ -175,7 +175,7 @@ describe Gitlab::QuickActions::CommandDefinition do end it 'executes the command passing the parsed param' do - subject.execute(context, {}, 'something ') + subject.execute(context, 'something ') expect(context.received_arg).to eq('something') end @@ -192,7 +192,7 @@ describe Gitlab::QuickActions::CommandDefinition do end it 'returns nil' do - result = subject.explain({}, {}, nil) + result = subject.explain({}, nil) expect(result).to be_nil end @@ -204,7 +204,7 @@ describe Gitlab::QuickActions::CommandDefinition do end it 'returns this static string' do - result = subject.explain({}, {}, nil) + result = subject.explain({}, nil) expect(result).to eq 'Explanation' end @@ -216,7 +216,7 @@ describe Gitlab::QuickActions::CommandDefinition do end it 'invokes the proc' do - result = subject.explain({}, {}, 'explanation') + result = subject.explain({}, 'explanation') expect(result).to eq 'Dynamic explanation' end diff --git a/spec/lib/gitlab/quick_actions/dsl_spec.rb b/spec/lib/gitlab/quick_actions/dsl_spec.rb index ff59dc48bcb..067a30fd7e2 100644 --- a/spec/lib/gitlab/quick_actions/dsl_spec.rb +++ b/spec/lib/gitlab/quick_actions/dsl_spec.rb @@ -76,7 +76,7 @@ describe Gitlab::QuickActions::Dsl do expect(dynamic_description_def.name).to eq(:dynamic_description) expect(dynamic_description_def.aliases).to eq([]) - expect(dynamic_description_def.to_h(noteable: 'issue')[:description]).to eq('A dynamic description for ISSUE') + expect(dynamic_description_def.to_h(OpenStruct.new(noteable: 'issue'))[:description]).to eq('A dynamic description for ISSUE') expect(dynamic_description_def.explanation).to eq('') expect(dynamic_description_def.params).to eq(['The first argument', 'The second argument']) expect(dynamic_description_def.condition_block).to be_nil diff --git a/spec/lib/gitlab/sql/pattern_spec.rb b/spec/lib/gitlab/sql/pattern_spec.rb index ef51e3cc8df..5b5052de372 100644 --- a/spec/lib/gitlab/sql/pattern_spec.rb +++ b/spec/lib/gitlab/sql/pattern_spec.rb @@ -154,6 +154,12 @@ describe Gitlab::SQL::Pattern do it 'returns a single equality condition' do expect(fuzzy_arel_match.to_sql).to match(/title.*I?LIKE 'fo'/) end + + it 'uses LOWER instead of ILIKE when LOWER is enabled' do + rel = Issue.fuzzy_arel_match(:title, query, lower_exact_match: true) + + expect(rel.to_sql).to match(/LOWER\(.*title.*\).*=.*'fo'/) + end end context 'with two words both equal to 3 chars' do diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index f65e41dfea3..db9d9158b29 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -115,6 +115,9 @@ describe GoogleApi::CloudPlatform::Client do "initial_node_count": cluster_size, "node_config": { "machine_type": machine_type + }, + "legacy_abac": { + "enabled": true } } } ) diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb new file mode 100644 index 00000000000..fe428ea657d --- /dev/null +++ b/spec/mailers/emails/pages_domains_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' +require 'email_spec' + +describe Emails::PagesDomains do + include EmailSpec::Matchers + include_context 'gitlab email notification' + + set(:project) { create(:project) } + set(:domain) { create(:pages_domain, project: project) } + set(:user) { project.owner } + + shared_examples 'a pages domain email' do + it_behaves_like 'an email sent from GitLab' + it_behaves_like 'it should not have Gmail Actions links' + it_behaves_like 'a user cannot unsubscribe through footer link' + + it 'has the expected content' do + aggregate_failures do + is_expected.to have_subject(email_subject) + is_expected.to have_body_text(project.human_name) + is_expected.to have_body_text(domain.domain) + is_expected.to have_body_text domain.url + is_expected.to have_body_text project_pages_domain_url(project, domain) + is_expected.to have_body_text help_page_url('user/project/pages/getting_started_part_three.md', anchor: 'dns-txt-record') + end + end + end + + describe '#pages_domain_enabled_email' do + let(:email_subject) { "#{project.path} | GitLab Pages domain '#{domain.domain}' has been enabled" } + + subject { Notify.pages_domain_enabled_email(domain, user) } + + it_behaves_like 'a pages domain email' + + it { is_expected.to have_body_text 'has been enabled' } + end + + describe '#pages_domain_disabled_email' do + let(:email_subject) { "#{project.path} | GitLab Pages domain '#{domain.domain}' has been disabled" } + + subject { Notify.pages_domain_disabled_email(domain, user) } + + it_behaves_like 'a pages domain email' + + it { is_expected.to have_body_text 'has been disabled' } + end + + describe '#pages_domain_verification_succeeded_email' do + let(:email_subject) { "#{project.path} | Verification succeeded for GitLab Pages domain '#{domain.domain}'" } + + subject { Notify.pages_domain_verification_succeeded_email(domain, user) } + + it_behaves_like 'a pages domain email' + + it { is_expected.to have_body_text 'successfully verified' } + end + + describe '#pages_domain_verification_failed_email' do + let(:email_subject) { "#{project.path} | ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'" } + + subject { Notify.pages_domain_verification_failed_email(domain, user) } + + it_behaves_like 'a pages domain email' + + it 'says verification has failed and when the domain is enabled until' do + is_expected.to have_body_text 'Verification has failed' + is_expected.to have_body_text domain.enabled_until.strftime('%F %T') + end + end +end diff --git a/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb new file mode 100644 index 00000000000..afcaefa0591 --- /dev/null +++ b/spec/migrations/enqueue_verify_pages_domain_workers_spec.rb @@ -0,0 +1,23 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180216121030_enqueue_verify_pages_domain_workers') + +describe EnqueueVerifyPagesDomainWorkers, :sidekiq, :migration do + around do |example| + Sidekiq::Testing.fake! do + example.run + end + end + + describe '#up' do + it 'enqueues a verification worker for every domain' do + domains = 1.upto(3).map { |i| PagesDomain.create!(domain: "my#{i}.domain.com") } + + expect { migrate! }.to change(PagesDomainVerificationWorker.jobs, :size).by(3) + + enqueued_ids = PagesDomainVerificationWorker.jobs.map { |job| job['args'] } + expected_ids = domains.map { |domain| [domain.id] } + + expect(enqueued_ids).to match_array(expected_ids) + end + end +end diff --git a/spec/migrations/track_untracked_uploads_spec.rb b/spec/migrations/track_untracked_uploads_spec.rb index fe4d5b8a279..2fccfb3f12c 100644 --- a/spec/migrations/track_untracked_uploads_spec.rb +++ b/spec/migrations/track_untracked_uploads_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' require Rails.root.join('db', 'post_migrate', '20171103140253_track_untracked_uploads') describe TrackUntrackedUploads, :migration, :sidekiq do - include TrackUntrackedUploadsHelpers + include MigrationsHelpers::TrackUntrackedUploadsHelpers it 'correctly schedules the follow-up background migration' do Sidekiq::Testing.fake! do diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb index 38fb98d4f50..cd175dba6da 100644 --- a/spec/models/ability_spec.rb +++ b/spec/models/ability_spec.rb @@ -204,6 +204,78 @@ describe Ability do end end + describe '.merge_requests_readable_by_user' do + context 'with an admin' do + it 'returns all merge requests' do + user = build(:user, admin: true) + merge_request = build(:merge_request) + + expect(described_class.merge_requests_readable_by_user([merge_request], user)) + .to eq([merge_request]) + end + end + + context 'without a user' do + it 'returns merge_requests that are publicly visible' do + hidden_merge_request = build(:merge_request) + visible_merge_request = build(:merge_request, source_project: build(:project, :public)) + + merge_requests = described_class + .merge_requests_readable_by_user([hidden_merge_request, visible_merge_request]) + + expect(merge_requests).to eq([visible_merge_request]) + end + end + + context 'with a user' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:cross_project_merge_request) do + create(:merge_request, source_project: create(:project, :public)) + end + let(:other_merge_request) { create(:merge_request) } + let(:all_merge_requests) do + [merge_request, cross_project_merge_request, other_merge_request] + end + + subject(:readable_merge_requests) do + described_class.merge_requests_readable_by_user(all_merge_requests, user) + end + + before do + project.add_developer(user) + end + + it 'returns projects visible to the user' do + expect(readable_merge_requests).to contain_exactly(merge_request, cross_project_merge_request) + end + + context 'when a user cannot read cross project and a filter is passed' do + before do + allow(described_class).to receive(:allowed?).and_call_original + expect(described_class).to receive(:allowed?).with(user, :read_cross_project) { false } + end + + subject(:readable_merge_requests) do + read_cross_project_filter = -> (merge_requests) do + merge_requests.select { |mr| mr.source_project == project } + end + described_class.merge_requests_readable_by_user( + all_merge_requests, user, + filters: { read_cross_project: read_cross_project_filter } + ) + end + + it 'returns only MRs of the specified project without checking access on others' do + expect(described_class).not_to receive(:allowed?).with(user, :read_merge_request, cross_project_merge_request) + + expect(readable_merge_requests).to contain_exactly(merge_request) + end + end + end + end + describe '.issues_readable_by_user' do context 'with an admin user' do it 'returns all given issues' do @@ -250,6 +322,29 @@ describe Ability do expect(issues).to eq([visible_issue]) end end + + context 'when the user cannot read cross project' do + let(:user) { create(:user) } + let(:issue) { create(:issue) } + let(:other_project_issue) { create(:issue) } + let(:project) { issue.project } + + before do + project.add_developer(user) + + allow(described_class).to receive(:allowed?).and_call_original + allow(described_class).to receive(:allowed?).with(user, :read_cross_project, any_args) { false } + end + + it 'excludes issues from other projects whithout checking separatly when passing a scope' do + expect(described_class).not_to receive(:allowed?).with(user, :read_issue, other_project_issue) + + filters = { read_cross_project: -> (issues) { issues.where(project: project) } } + result = described_class.issues_readable_by_user(Issue.all, user, filters: filters) + + expect(result).to contain_exactly(issue) + end + end end describe '.project_disabled_features_rules' do diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 2b6b6a61182..c27313ed88b 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -277,7 +277,7 @@ describe Ci::Build do allow_any_instance_of(Project).to receive(:jobs_cache_index).and_return(1) end - it { is_expected.to be_an(Array).and all(include(key: "key_1")) } + it { is_expected.to be_an(Array).and all(include(key: "key-1")) } end context 'when project does not have jobs_cache_index' do diff --git a/spec/models/concerns/protected_ref_access_spec.rb b/spec/models/concerns/protected_ref_access_spec.rb new file mode 100644 index 00000000000..a62ca391e25 --- /dev/null +++ b/spec/models/concerns/protected_ref_access_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe ProtectedRefAccess do + subject(:protected_ref_access) do + create(:protected_branch, :masters_can_push).push_access_levels.first + end + + let(:project) { protected_ref_access.project } + + describe '#check_access' do + it 'is always true for admins' do + admin = create(:admin) + + expect(protected_ref_access.check_access(admin)).to be_truthy + end + + it 'is true for masters' do + master = create(:user) + project.add_master(master) + + expect(protected_ref_access.check_access(master)).to be_truthy + end + + it 'is for developers of the project' do + developer = create(:user) + project.add_developer(developer) + + expect(protected_ref_access.check_access(developer)).to be_falsy + end + end +end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index f5c9f551e65..feed7968f09 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -221,27 +221,55 @@ describe Issue do end describe '#referenced_merge_requests' do - it 'returns the referenced merge requests' do - project = create(:project, :public) - - mr1 = create(:merge_request, - source_project: project, - source_branch: 'master', - target_branch: 'feature') + let(:project) { create(:project, :public) } + let(:issue) do + create(:issue, description: merge_request.to_reference, project: project) + end + let!(:merge_request) do + create(:merge_request, + source_project: project, + source_branch: 'master', + target_branch: 'feature') + end + it 'returns the referenced merge requests' do mr2 = create(:merge_request, source_project: project, source_branch: 'feature', target_branch: 'master') - issue = create(:issue, description: mr1.to_reference, project: project) - create(:note_on_issue, noteable: issue, note: mr2.to_reference, project_id: project.id) - expect(issue.referenced_merge_requests).to eq([mr1, mr2]) + expect(issue.referenced_merge_requests).to eq([merge_request, mr2]) + end + + it 'returns cross project referenced merge requests' do + other_project = create(:project, :public) + cross_project_merge_request = create(:merge_request, source_project: other_project) + create(:note_on_issue, + noteable: issue, + note: cross_project_merge_request.to_reference(issue.project), + project_id: issue.project.id) + + expect(issue.referenced_merge_requests).to eq([merge_request, cross_project_merge_request]) + end + + it 'excludes cross project references if the user cannot read cross project' do + user = create(:user) + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + other_project = create(:project, :public) + cross_project_merge_request = create(:merge_request, source_project: other_project) + create(:note_on_issue, + noteable: issue, + note: cross_project_merge_request.to_reference(issue.project), + project_id: issue.project.id) + + expect(issue.referenced_merge_requests(user)).to eq([merge_request]) end end @@ -309,7 +337,7 @@ describe Issue do end describe '#related_branches' do - let(:user) { build(:admin) } + let(:user) { create(:admin) } before do allow(subject.project.repository).to receive(:branch_names) diff --git a/spec/models/notification_recipient_spec.rb b/spec/models/notification_recipient_spec.rb new file mode 100644 index 00000000000..eda0e1da835 --- /dev/null +++ b/spec/models/notification_recipient_spec.rb @@ -0,0 +1,16 @@ +require 'spec_helper' + +describe NotificationRecipient do + let(:user) { create(:user) } + let(:project) { create(:project, namespace: user.namespace) } + let(:target) { create(:issue, project: project) } + + subject(:recipient) { described_class.new(user, :watch, target: target, project: project) } + + it 'denies access to a target when cross project access is denied' do + allow(Ability).to receive(:allowed?).and_call_original + expect(Ability).to receive(:allowed?).with(user, :read_cross_project, :global).and_return(false) + + expect(recipient.has_access?).to be_falsy + end +end diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb index 9d12f96c642..95713d8b85b 100644 --- a/spec/models/pages_domain_spec.rb +++ b/spec/models/pages_domain_spec.rb @@ -1,6 +1,10 @@ require 'spec_helper' describe PagesDomain do + using RSpec::Parameterized::TableSyntax + + subject(:pages_domain) { described_class.new } + describe 'associations' do it { is_expected.to belong_to(:project) } end @@ -64,19 +68,51 @@ describe PagesDomain do end end + describe 'validations' do + it { is_expected.to validate_presence_of(:verification_code) } + end + + describe '#verification_code' do + subject { pages_domain.verification_code } + + it 'is set automatically with 128 bits of SecureRandom data' do + expect(SecureRandom).to receive(:hex).with(16) { 'verification code' } + + is_expected.to eq('verification code') + end + end + + describe '#keyed_verification_code' do + subject { pages_domain.keyed_verification_code } + + it { is_expected.to eq("gitlab-pages-verification-code=#{pages_domain.verification_code}") } + end + + describe '#verification_domain' do + subject { pages_domain.verification_domain } + + it { is_expected.to be_nil } + + it 'is a well-known subdomain if the domain is present' do + pages_domain.domain = 'example.com' + + is_expected.to eq('_gitlab-pages-verification-code.example.com') + end + end + describe '#url' do subject { domain.url } context 'without the certificate' do let(:domain) { build(:pages_domain, certificate: '') } - it { is_expected.to eq('http://my.domain.com') } + it { is_expected.to eq("http://#{domain.domain}") } end context 'with a certificate' do let(:domain) { build(:pages_domain, :with_certificate) } - it { is_expected.to eq('https://my.domain.com') } + it { is_expected.to eq("https://#{domain.domain}") } end end @@ -154,4 +190,108 @@ describe PagesDomain do # We test only existence of output, since the output is long it { is_expected.not_to be_empty } end + + describe '#update_daemon' do + it 'runs when the domain is created' do + domain = build(:pages_domain) + + expect(domain).to receive(:update_daemon) + + domain.save! + end + + it 'runs when the domain is destroyed' do + domain = create(:pages_domain) + + expect(domain).to receive(:update_daemon) + + domain.destroy! + end + + it 'delegates to Projects::UpdatePagesConfigurationService' do + service = instance_double('Projects::UpdatePagesConfigurationService') + expect(Projects::UpdatePagesConfigurationService).to receive(:new) { service } + expect(service).to receive(:execute) + + create(:pages_domain) + end + + context 'configuration updates when attributes change' do + set(:project1) { create(:project) } + set(:project2) { create(:project) } + set(:domain) { create(:pages_domain) } + + where(:attribute, :old_value, :new_value, :update_expected) do + now = Time.now + future = now + 1.day + + :project | nil | :project1 | true + :project | :project1 | :project1 | false + :project | :project1 | :project2 | true + :project | :project1 | nil | true + + # domain can't be set to nil + :domain | 'a.com' | 'a.com' | false + :domain | 'a.com' | 'b.com' | true + + # verification_code can't be set to nil + :verification_code | 'foo' | 'foo' | false + :verification_code | 'foo' | 'bar' | false + + :verified_at | nil | now | false + :verified_at | now | now | false + :verified_at | now | future | false + :verified_at | now | nil | false + + :enabled_until | nil | now | true + :enabled_until | now | now | false + :enabled_until | now | future | false + :enabled_until | now | nil | true + end + + with_them do + it 'runs if a relevant attribute has changed' do + a = old_value.is_a?(Symbol) ? send(old_value) : old_value + b = new_value.is_a?(Symbol) ? send(new_value) : new_value + + domain.update!(attribute => a) + + if update_expected + expect(domain).to receive(:update_daemon) + else + expect(domain).not_to receive(:update_daemon) + end + + domain.update!(attribute => b) + end + end + + context 'TLS configuration' do + set(:domain_with_tls) { create(:pages_domain, :with_key, :with_certificate) } + + let(:cert1) { domain_with_tls.certificate } + let(:cert2) { cert1 + ' ' } + let(:key1) { domain_with_tls.key } + let(:key2) { key1 + ' ' } + + it 'updates when added' do + expect(domain).to receive(:update_daemon) + + domain.update!(key: key1, certificate: cert1) + end + + it 'updates when changed' do + expect(domain_with_tls).to receive(:update_daemon) + + domain_with_tls.update!(key: key2, certificate: cert2) + end + + it 'updates when removed' do + expect(domain_with_tls).to receive(:update_daemon) + + domain_with_tls.update!(key: nil, certificate: nil) + end + end + end + end end diff --git a/spec/models/project_services/prometheus_service_spec.rb b/spec/models/project_services/prometheus_service_spec.rb index ed17e019d42..6693e5783a5 100644 --- a/spec/models/project_services/prometheus_service_spec.rb +++ b/spec/models/project_services/prometheus_service_spec.rb @@ -223,8 +223,8 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do context 'with cluster for all environments without prometheus installed' do context 'without environment supplied' do - it 'raises PrometheusError because cluster was not found' do - expect { service.client }.to raise_error(Gitlab::PrometheusError, /couldn't find cluster with Prometheus installed/) + it 'raises PrometheusClient::Error because cluster was not found' do + expect { service.client }.to raise_error(Gitlab::PrometheusClient::Error, /couldn't find cluster with Prometheus installed/) end end @@ -242,8 +242,8 @@ describe PrometheusService, :use_clean_rails_memory_store_caching do context 'with prod environment supplied' do let!(:environment) { create(:environment, project: project, name: 'prod') } - it 'raises PrometheusError because cluster was not found' do - expect { service.client }.to raise_error(Gitlab::PrometheusError, /couldn't find cluster with Prometheus installed/) + it 'raises PrometheusClient::Error because cluster was not found' do + expect { service.client }.to raise_error(Gitlab::PrometheusClient::Error, /couldn't find cluster with Prometheus installed/) end end end diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index ee04d74d848..56c2d7b953e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1473,6 +1473,13 @@ describe Project do expect(project.user_can_push_to_empty_repo?(user)).to be_truthy end + + it 'returns false when the repo is not empty' do + project.add_master(user) + expect(project).to receive(:empty_repo?).and_return(false) + + expect(project.user_can_push_to_empty_repo?(user)).to be_falsey + end end describe '#container_registry_url' do diff --git a/spec/policies/issuable_policy_spec.rb b/spec/policies/issuable_policy_spec.rb index 2cf669e8191..d1bf98995e7 100644 --- a/spec/policies/issuable_policy_spec.rb +++ b/spec/policies/issuable_policy_spec.rb @@ -1,12 +1,14 @@ require 'spec_helper' describe IssuablePolicy, models: true do + let(:user) { create(:user) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project) } + let(:policies) { described_class.new(user, issue) } + describe '#rules' do context 'when discussion is locked for the issuable' do - let(:user) { create(:user) } - let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project, discussion_locked: true) } - let(:policies) { described_class.new(user, issue) } context 'when the user is not a project member' do it 'can not create a note' do diff --git a/spec/policies/issue_policy_spec.rb b/spec/policies/issue_policy_spec.rb index a4af9361ea6..793b724bfca 100644 --- a/spec/policies/issue_policy_spec.rb +++ b/spec/policies/issue_policy_spec.rb @@ -30,41 +30,41 @@ describe IssuePolicy do end it 'does not allow non-members to read issues' do - expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(non_member, issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end @@ -73,37 +73,37 @@ describe IssuePolicy do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow non-members to read confidential issues' do - expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(non_member, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end end end @@ -123,36 +123,36 @@ describe IssuePolicy do end it 'allows guests to read issues' do - expect(permissions(guest, issue)).to be_allowed(:read_issue) + expect(permissions(guest, issue)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue)).to be_disallowed(:update_issue, :admin_issue) - expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(guest, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(guest, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows reporters to read, update, and admin issues' do - expect(permissions(reporter, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters from group links to read, update, and admin issues' do - expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their issues' do - expect(permissions(author, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, issue)).to be_disallowed(:admin_issue) - expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(author, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(author, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end it 'allows issue assignees to read and update their issues' do - expect(permissions(assignee, issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue) + expect(permissions(assignee, issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid) expect(permissions(assignee, issue_no_assignee)).to be_disallowed(:update_issue, :admin_issue) end @@ -161,32 +161,32 @@ describe IssuePolicy do let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) } it 'does not allow guests to read confidential issues' do - expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(guest, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporters to read, update, and admin confidential issues' do - expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows reporter from group links to read, update, and admin confidential issues' do - expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :update_issue, :admin_issue) - expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) + expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to be_allowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue authors to read and update their confidential issues' do - expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(author, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(author, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(author, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end it 'allows issue assignees to read and update their confidential issues' do - expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :update_issue) + expect(permissions(assignee, confidential_issue)).to be_allowed(:read_issue, :read_issue_iid, :update_issue) expect(permissions(assignee, confidential_issue)).to be_disallowed(:admin_issue) - expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :update_issue, :admin_issue) + expect(permissions(assignee, confidential_issue_no_assignee)).to be_disallowed(:read_issue, :read_issue_iid, :update_issue, :admin_issue) end end end diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb new file mode 100644 index 00000000000..f8c93d91ec5 --- /dev/null +++ b/spec/presenters/project_presenter_spec.rb @@ -0,0 +1,397 @@ +require 'spec_helper' + +describe ProjectPresenter do + let(:user) { create(:user) } + + describe '#license_short_name' do + let(:project) { create(:project) } + let(:presenter) { described_class.new(project, current_user: user) } + + context 'when project.repository has a license_key' do + it 'returns the nickname of the license if present' do + allow(project.repository).to receive(:license_key).and_return('agpl-3.0') + + expect(presenter.license_short_name).to eq('GNU AGPLv3') + end + + it 'returns the name of the license if nickname is not present' do + allow(project.repository).to receive(:license_key).and_return('mit') + + expect(presenter.license_short_name).to eq('MIT License') + end + end + + context 'when project.repository has no license_key but a license_blob' do + it 'returns LICENSE' do + allow(project.repository).to receive(:license_key).and_return(nil) + + expect(presenter.license_short_name).to eq('LICENSE') + end + end + end + + describe '#default_view' do + let(:presenter) { described_class.new(project, current_user: user) } + + context 'user not signed in' do + let(:user) { nil } + + context 'when repository is empty' do + let(:project) { create(:project_empty_repo, :public) } + + it 'returns activity if user has repository access' do + allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true) + + expect(presenter.default_view).to eq('activity') + end + + it 'returns activity if user does not have repository access' do + allow(project).to receive(:can?).with(nil, :download_code, project).and_return(false) + + expect(presenter.default_view).to eq('activity') + end + end + + context 'when repository is not empty' do + let(:project) { create(:project, :public, :repository) } + + it 'returns files and readme if user has repository access' do + allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(true) + + expect(presenter.default_view).to eq('files') + end + + it 'returns activity if user does not have repository access' do + allow(presenter).to receive(:can?).with(nil, :download_code, project).and_return(false) + + expect(presenter.default_view).to eq('activity') + end + end + end + + context 'user signed in' do + let(:user) { create(:user, :readme) } + let(:project) { create(:project, :public, :repository) } + + context 'when the user is allowed to see the code' do + it 'returns the project view' do + allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(true) + + expect(presenter.default_view).to eq('readme') + end + end + + context 'with wikis enabled and the right policy for the user' do + before do + project.project_feature.update_attribute(:issues_access_level, 0) + allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + end + + it 'returns wiki if the user has the right policy' do + allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(true) + + expect(presenter.default_view).to eq('wiki') + end + + it 'returns customize_workflow if the user does not have the right policy' do + allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(presenter.default_view).to eq('customize_workflow') + end + end + + context 'with issues as a feature available' do + it 'return issues' do + allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(presenter.default_view).to eq('projects/issues/issues') + end + end + + context 'with no activity, no wikies and no issues' do + it 'returns customize_workflow as default' do + project.project_feature.update_attribute(:issues_access_level, 0) + allow(presenter).to receive(:can?).with(user, :download_code, project).and_return(false) + allow(presenter).to receive(:can?).with(user, :read_wiki, project).and_return(false) + + expect(presenter.default_view).to eq('customize_workflow') + end + end + end + end + + describe '#can_current_user_push_code?' do + let(:project) { create(:project, :repository) } + let(:presenter) { described_class.new(project, current_user: user) } + + context 'empty repo' do + let(:project) { create(:project) } + + it 'returns true if user can push_code' do + project.add_developer(user) + + expect(presenter.can_current_user_push_code?).to be(true) + end + + it 'returns false if user cannot push_code' do + project.add_reporter(user) + + expect(presenter.can_current_user_push_code?).to be(false) + end + end + + context 'not empty repo' do + let(:project) { create(:project, :repository) } + + it 'returns true if user can push to default branch' do + project.add_developer(user) + + expect(presenter.can_current_user_push_code?).to be(true) + end + + it 'returns false if default branch is protected' do + project.add_developer(user) + create(:protected_branch, project: project, name: project.default_branch) + + expect(presenter.can_current_user_push_code?).to be(false) + end + end + end + + context 'statistics anchors' do + let(:project) { create(:project, :repository) } + let(:presenter) { described_class.new(project, current_user: user) } + + describe '#files_anchor_data' do + it 'returns files data' do + expect(presenter.files_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Files (0 Bytes)', + link: presenter.project_tree_path(project))) + end + end + + describe '#commits_anchor_data' do + it 'returns commits data' do + expect(presenter.commits_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Commits (0)', + link: presenter.project_commits_path(project, project.repository.root_ref))) + end + end + + describe '#branches_anchor_data' do + it 'returns branches data' do + expect(presenter.branches_anchor_data).to eq(OpenStruct.new(enabled: true, + label: "Branches (#{project.repository.branches.size})", + link: presenter.project_branches_path(project))) + end + end + + describe '#tags_anchor_data' do + it 'returns tags data' do + expect(presenter.tags_anchor_data).to eq(OpenStruct.new(enabled: true, + label: "Tags (#{project.repository.tags.size})", + link: presenter.project_tags_path(project))) + end + end + + describe '#new_file_anchor_data' do + it 'returns new file data if user can push' do + project.add_developer(user) + + expect(presenter.new_file_anchor_data).to eq(OpenStruct.new(enabled: false, + label: "New file", + link: presenter.project_new_blob_path(project, 'master'), + class_modifier: 'new')) + end + + it 'returns nil if user cannot push' do + expect(presenter.new_file_anchor_data).to be_nil + end + end + + describe '#readme_anchor_data' do + context 'when user can push and README does not exists' do + it 'returns anchor data' do + project.add_developer(user) + allow(project.repository).to receive(:readme).and_return(nil) + + expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Add Readme', + link: presenter.add_readme_path)) + end + end + + context 'when README exists' do + it 'returns anchor data' do + allow(project.repository).to receive(:readme).and_return(double(name: 'readme')) + + expect(presenter.readme_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Readme', + link: presenter.readme_path)) + end + end + end + + describe '#changelog_anchor_data' do + context 'when user can push and CHANGELOG does not exists' do + it 'returns anchor data' do + project.add_developer(user) + allow(project.repository).to receive(:changelog).and_return(nil) + + expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Add Changelog', + link: presenter.add_changelog_path)) + end + end + + context 'when CHANGELOG exists' do + it 'returns anchor data' do + allow(project.repository).to receive(:changelog).and_return(double(name: 'foo')) + + expect(presenter.changelog_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Changelog', + link: presenter.changelog_path)) + end + end + end + + describe '#license_anchor_data' do + context 'when user can push and LICENSE does not exists' do + it 'returns anchor data' do + project.add_developer(user) + allow(project.repository).to receive(:license_blob).and_return(nil) + + expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Add License', + link: presenter.add_license_path)) + end + end + + context 'when LICENSE exists' do + it 'returns anchor data' do + allow(project.repository).to receive(:license_blob).and_return(double(name: 'foo')) + + expect(presenter.license_anchor_data).to eq(OpenStruct.new(enabled: true, + label: presenter.license_short_name, + link: presenter.license_path)) + end + end + end + + describe '#contribution_guide_anchor_data' do + context 'when user can push and CONTRIBUTING does not exists' do + it 'returns anchor data' do + project.add_developer(user) + allow(project.repository).to receive(:contribution_guide).and_return(nil) + + expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Add Contribution guide', + link: presenter.add_contribution_guide_path)) + end + end + + context 'when CONTRIBUTING exists' do + it 'returns anchor data' do + allow(project.repository).to receive(:contribution_guide).and_return(double(name: 'foo')) + + expect(presenter.contribution_guide_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Contribution guide', + link: presenter.contribution_guide_path)) + end + end + end + + describe '#autodevops_anchor_data' do + context 'when Auto Devops is enabled' do + it 'returns anchor data' do + allow(project).to receive(:auto_devops_enabled?).and_return(true) + + expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Auto DevOps enabled', + link: nil)) + end + end + + context 'when user can admin pipeline and CI yml does not exists' do + it 'returns anchor data' do + project.add_master(user) + allow(project).to receive(:auto_devops_enabled?).and_return(false) + allow(project.repository).to receive(:gitlab_ci_yml).and_return(nil) + + expect(presenter.autodevops_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Enable Auto DevOps', + link: presenter.project_settings_ci_cd_path(project, anchor: 'js-general-pipeline-settings'))) + end + end + end + + describe '#kubernetes_cluster_anchor_data' do + context 'when user can create Kubernetes cluster' do + it 'returns link to cluster if only one exists' do + project.add_master(user) + cluster = create(:cluster, projects: [project]) + + expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_cluster_path(project, cluster))) + end + + it 'returns link to clusters page if more than one exists' do + project.add_master(user) + create(:cluster, projects: [project]) + create(:cluster, projects: [project]) + + expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: true, + label: 'Kubernetes configured', + link: presenter.project_clusters_path(project))) + end + + it 'returns link to create a cluster if no cluster exists' do + project.add_master(user) + + expect(presenter.kubernetes_cluster_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Add Kubernetes cluster', + link: presenter.new_project_cluster_path(project))) + end + end + + context 'when user cannot create Kubernetes cluster' do + it 'returns nil' do + expect(presenter.kubernetes_cluster_anchor_data).to be_nil + end + end + end + + describe '#koding_anchor_data' do + it 'returns link to setup Koding if user can push and no koding YML exists' do + project.add_developer(user) + allow(project.repository).to receive(:koding_yml).and_return(nil) + allow(Gitlab::CurrentSettings).to receive(:koding_enabled?).and_return(true) + + expect(presenter.koding_anchor_data).to eq(OpenStruct.new(enabled: false, + label: 'Set up Koding', + link: presenter.add_koding_stack_path)) + end + + it 'returns nil if user cannot push' do + expect(presenter.koding_anchor_data).to be_nil + end + + it 'returns nil if koding is not enabled' do + project.add_developer(user) + allow(Gitlab::CurrentSettings).to receive(:koding_enabled?).and_return(false) + + expect(presenter.koding_anchor_data).to be_nil + end + + it 'returns nil if koding YML already exists' do + project.add_developer(user) + allow(project.repository).to receive(:koding_yml).and_return(double) + allow(Gitlab::CurrentSettings).to receive(:koding_enabled?).and_return(true) + + expect(presenter.koding_anchor_data).to be_nil + end + end + end +end diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb index c7df6251d74..827f4c04324 100644 --- a/spec/requests/api/internal_spec.rb +++ b/spec/requests/api/internal_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe API::Internal do let(:user) { create(:user) } let(:key) { create(:key, user: user) } - let(:project) { create(:project, :repository) } + let(:project) { create(:project, :repository, :wiki_repo) } let(:secret_token) { Gitlab::Shell.secret_token } let(:gl_repository) { "project-#{project.id}" } let(:reference_counter) { double('ReferenceCounter') } diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb index 942e5b2bb1b..c6fdda203ad 100644 --- a/spec/requests/git_http_spec.rb +++ b/spec/requests/git_http_spec.rb @@ -150,7 +150,7 @@ describe 'Git HTTP requests' do let(:path) { "/#{wiki.repository.full_path}.git" } context "when the project is public" do - let(:project) { create(:project, :repository, :public, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :wiki_enabled) } it_behaves_like 'pushes require Basic HTTP Authentication' @@ -177,7 +177,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :public, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :public, :repository_disabled, :wiki_enabled) } it_behaves_like 'pulls are allowed' it_behaves_like 'pushes are allowed' @@ -198,7 +198,7 @@ describe 'Git HTTP requests' do end context "when the project is private" do - let(:project) { create(:project, :repository, :private, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :wiki_enabled) } it_behaves_like 'pulls require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication' @@ -210,7 +210,7 @@ describe 'Git HTTP requests' do end context 'but the repo is disabled' do - let(:project) { create(:project, :repository, :private, :repository_disabled, :wiki_enabled) } + let(:project) { create(:project, :wiki_repo, :private, :repository_disabled, :wiki_enabled) } it 'allows clones' do download(path, user: user.username, password: user.password) do |response| diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb index 91aefa84d0e..56d025f0176 100644 --- a/spec/routing/routing_spec.rb +++ b/spec/routing/routing_spec.rb @@ -37,6 +37,22 @@ describe UsersController, "routing" do it "to #calendar_activities" do expect(get("/users/User/calendar_activities")).to route_to('users#calendar_activities', username: 'User') end + + describe 'redirect alias routes' do + include RSpec::Rails::RequestExampleGroup + + it '/u/user1 redirects to /user1' do + expect(get("/u/user1")).to redirect_to('/user1') + end + + it '/u/user1/groups redirects to /user1/groups' do + expect(get("/u/user1/groups")).to redirect_to('/users/user1/groups') + end + + it '/u/user1/projects redirects to /user1/projects' do + expect(get("/u/user1/projects")).to redirect_to('/users/user1/projects') + end + end end # search GET /search(.:format) search#show diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb index 75553afc033..38d84cf0ceb 100644 --- a/spec/services/merge_requests/create_from_issue_service_spec.rb +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -24,7 +24,7 @@ describe MergeRequests::CreateFromIssueService do end it 'delegates issue search to IssuesFinder' do - expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original + expect_any_instance_of(IssuesFinder).to receive(:find_by).once.and_call_original described_class.new(project, user, issue_iid: -1).execute end diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 836ffb7cea0..62fdf870090 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1678,6 +1678,78 @@ describe NotificationService, :mailer do end end + describe 'Pages domains' do + set(:project) { create(:project) } + set(:domain) { create(:pages_domain, project: project) } + set(:u_blocked) { create(:user, :blocked) } + set(:u_silence) { create_user_with_notification(:disabled, 'silent', project) } + set(:u_owner) { project.owner } + set(:u_master1) { create(:user) } + set(:u_master2) { create(:user) } + set(:u_developer) { create(:user) } + + before do + project.add_master(u_blocked) + project.add_master(u_silence) + project.add_master(u_master1) + project.add_master(u_master2) + project.add_developer(u_developer) + + reset_delivered_emails! + end + + %i[ + pages_domain_enabled + pages_domain_disabled + pages_domain_verification_succeeded + pages_domain_verification_failed + ].each do |sym| + describe "##{sym}" do + subject(:notify!) { notification.send(sym, domain) } + + it 'emails current watching masters' do + expect(Notify).to receive(:"#{sym}_email").at_least(:once).and_call_original + + notify! + + should_only_email(u_master1, u_master2, u_owner) + end + + it 'emails nobody if the project is missing' do + domain.project = nil + + notify! + + should_not_email_anyone + end + end + end + + describe '#pages_domain_verification_failed' do + it 'emails current watching masters' do + notification.pages_domain_verification_failed(domain) + + should_only_email(u_master1, u_master2, u_owner) + end + end + + describe '#pages_domain_enabled' do + it 'emails current watching masters' do + notification.pages_domain_enabled(domain) + + should_only_email(u_master1, u_master2, u_owner) + end + end + + describe '#pages_domain_disabled' do + it 'emails current watching masters' do + notification.pages_domain_disabled(domain) + + should_only_email(u_master1, u_master2, u_owner) + end + end + end + def build_team(project) @u_watcher = create_global_setting_for(create(:user), :watch) @u_participating = create_global_setting_for(create(:user), :participating) diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index ae160d104f1..f793f55e51b 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -522,6 +522,22 @@ describe QuickActions::InterpretService do let(:issuable) { merge_request } end + context 'only group milestones available' do + let(:group) { create(:group) } + let(:project) { create(:project, :public, namespace: group) } + let(:milestone) { create(:milestone, group: group, title: '10.0') } + + it_behaves_like 'milestone command' do + let(:content) { "/milestone %#{milestone.title}" } + let(:issuable) { issue } + end + + it_behaves_like 'milestone command' do + let(:content) { "/milestone %#{milestone.title}" } + let(:issuable) { merge_request } + end + end + it_behaves_like 'remove_milestone command' do let(:content) { '/remove_milestone' } let(:issuable) { issue } diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 5e6c24f5730..562b89e6767 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -943,7 +943,8 @@ describe TodoService do described_class.new.mark_todos_as_done_by_ids(todo, john_doe) - expect_any_instance_of(TodosFinder).not_to receive(:execute) + # Make sure no TodosFinder is inialized to perform counting + expect(TodosFinder).not_to receive(:new) expect(john_doe.todos_done_count).to eq(1) expect(john_doe.todos_pending_count).to eq(1) diff --git a/spec/services/verify_pages_domain_service_spec.rb b/spec/services/verify_pages_domain_service_spec.rb new file mode 100644 index 00000000000..576db1dde2d --- /dev/null +++ b/spec/services/verify_pages_domain_service_spec.rb @@ -0,0 +1,270 @@ +require 'spec_helper' + +describe VerifyPagesDomainService do + using RSpec::Parameterized::TableSyntax + include EmailHelpers + + let(:error_status) { { status: :error, message: "Couldn't verify #{domain.domain}" } } + + subject(:service) { described_class.new(domain) } + + describe '#execute' do + context 'verification code recognition (verified domain)' do + where(:domain_sym, :code_sym) do + :domain | :verification_code + :domain | :keyed_verification_code + + :verification_domain | :verification_code + :verification_domain | :keyed_verification_code + end + + with_them do + set(:domain) { create(:pages_domain) } + + let(:domain_name) { domain.send(domain_sym) } + let(:verification_code) { domain.send(code_sym) } + + it 'verifies and enables the domain' do + stub_resolver(domain_name => ['something else', verification_code]) + + expect(service.execute).to eq(status: :success) + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'verifies and enables when the code is contained partway through a TXT record' do + stub_resolver(domain_name => "something #{verification_code} else") + + expect(service.execute).to eq(status: :success) + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'does not verify when the code is not present' do + stub_resolver(domain_name => 'something else') + + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).to be_enabled + end + end + + context 'verified domain' do + set(:domain) { create(:pages_domain) } + + it 'unverifies (but does not disable) when the right code is not present' do + stub_resolver(domain.domain => 'something else') + + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end + + it 'unverifies (but does not disable) when no records are present' do + stub_resolver + + expect(service.execute).to eq(error_status) + expect(domain).not_to be_verified + expect(domain).to be_enabled + end + end + + context 'expired domain' do + set(:domain) { create(:pages_domain, :expired) } + + it 'verifies and enables when the right code is present' do + stub_resolver(domain.domain => domain.keyed_verification_code) + + expect(service.execute).to eq(status: :success) + + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'disables when the right code is not present' do + error_status[:message] += '. It is now disabled.' + + stub_resolver + + expect(service.execute).to eq(error_status) + + expect(domain).not_to be_verified + expect(domain).not_to be_enabled + end + end + end + + context 'timeout behaviour' do + let(:domain) { create(:pages_domain) } + + it 'sets a timeout on the DNS query' do + expect(stub_resolver).to receive(:timeouts=).with(described_class::RESOLVER_TIMEOUT_SECONDS) + + service.execute + end + end + + context 'email notifications' do + let(:notification_service) { instance_double('NotificationService') } + + where(:factory, :verification_succeeds, :expected_notification) do + nil | true | nil + nil | false | :verification_failed + :reverify | true | nil + :reverify | false | :verification_failed + :unverified | true | :verification_succeeded + :unverified | false | nil + :expired | true | nil + :expired | false | :disabled + :disabled | true | :enabled + :disabled | false | nil + end + + with_them do + let(:domain) { create(:pages_domain, *[factory].compact) } + + before do + allow(service).to receive(:notification_service) { notification_service } + + if verification_succeeds + stub_resolver(domain.domain => domain.verification_code) + else + stub_resolver + end + end + + it 'sends a notification if appropriate' do + if expected_notification + expect(notification_service).to receive(:"pages_domain_#{expected_notification}").with(domain) + end + + service.execute + end + end + + context 'pages verification disabled' do + let(:domain) { create(:pages_domain, :disabled) } + + before do + stub_application_setting(pages_domain_verification_enabled: false) + allow(service).to receive(:notification_service) { notification_service } + end + + it 'skips email notifications' do + expect(notification_service).not_to receive(:pages_domain_enabled) + + service.execute + end + end + end + + context 'pages configuration updates' do + context 'enabling a disabled domain' do + let(:domain) { create(:pages_domain, :disabled) } + + it 'schedules an update' do + stub_resolver(domain.domain => domain.verification_code) + + expect(domain).to receive(:update_daemon) + + service.execute + end + end + + context 'verifying an enabled domain' do + let(:domain) { create(:pages_domain) } + + it 'schedules an update' do + stub_resolver(domain.domain => domain.verification_code) + + expect(domain).not_to receive(:update_daemon) + + service.execute + end + end + + context 'disabling an expired domain' do + let(:domain) { create(:pages_domain, :expired) } + + it 'schedules an update' do + stub_resolver + + expect(domain).to receive(:update_daemon) + + service.execute + end + end + + context 'failing to verify a disabled domain' do + let(:domain) { create(:pages_domain, :disabled) } + + it 'does not schedule an update' do + stub_resolver + + expect(domain).not_to receive(:update_daemon) + + service.execute + end + end + end + + context 'no verification code' do + let(:domain) { create(:pages_domain) } + + it 'returns an error' do + domain.verification_code = '' + + disallow_resolver! + + expect(service.execute).to eq(status: :error, message: "No verification code set for #{domain.domain}") + end + end + + context 'pages domain verification is disabled' do + let(:domain) { create(:pages_domain, :disabled) } + + before do + stub_application_setting(pages_domain_verification_enabled: false) + end + + it 'extends domain validity by unconditionally reverifying' do + disallow_resolver! + + service.execute + + expect(domain).to be_verified + expect(domain).to be_enabled + end + + it 'does not shorten any grace period' do + grace = Time.now + 1.year + domain.update!(enabled_until: grace) + disallow_resolver! + + service.execute + + expect(domain.enabled_until).to be_like_time(grace) + end + end + end + + def disallow_resolver! + expect(Resolv::DNS).not_to receive(:open) + end + + def stub_resolver(stubbed_lookups = {}) + resolver = instance_double('Resolv::DNS') + allow(resolver).to receive(:timeouts=) + + expect(Resolv::DNS).to receive(:open).and_yield(resolver) + + allow(resolver).to receive(:getresources) { [] } + stubbed_lookups.each do |domain, records| + records = Array(records).map { |txt| Resolv::DNS::Resource::IN::TXT.new(txt) } + allow(resolver).to receive(:getresources).with(domain, Resolv::DNS::Resource::IN::TXT) { records } + end + + resolver + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 85de0a14631..c0f3366fb52 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -154,6 +154,22 @@ RSpec.configure do |config| Sidekiq.redis(&:flushall) end + # The :each scope runs "inside" the example, so this hook ensures the DB is in the + # correct state before any examples' before hooks are called. This prevents a + # problem where `ScheduleIssuesClosedAtTypeChange` (or any migration that depends + # on background migrations being run inline during test setup) can be broken by + # altering Sidekiq behavior in an unrelated spec like so: + # + # around do |example| + # Sidekiq::Testing.fake! do + # example.run + # end + # end + config.before(:context, :migration) do + schema_migrate_down! + end + + # Each example may call `migrate!`, so we must ensure we are migrated down every time config.before(:each, :migration) do schema_migrate_down! end @@ -169,6 +185,14 @@ RSpec.configure do |config| config.around(:each, :postgresql) do |example| example.run if Gitlab::Database.postgresql? end + + # This makes sure the `ApplicationController#can?` method is stubbed with the + # original implementation for all view specs. + config.before(:each, type: :view) do + allow(view).to receive(:can?) do |*args| + Ability.allowed?(*args) + end + end end # add simpler way to match asset paths containing digest strings diff --git a/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb new file mode 100644 index 00000000000..016bcfa9b1b --- /dev/null +++ b/spec/support/migrations_helpers/track_untracked_uploads_helpers.rb @@ -0,0 +1,128 @@ +module MigrationsHelpers + module TrackUntrackedUploadsHelpers + PUBLIC_DIR = File.join(Rails.root, 'tmp', 'tests', 'public') + UPLOADS_DIR = File.join(PUBLIC_DIR, 'uploads') + SYSTEM_DIR = File.join(UPLOADS_DIR, '-', 'system') + UPLOAD_FILENAME = 'image.png'.freeze + FIXTURE_FILE_PATH = File.join(Rails.root, 'spec', 'fixtures', 'dk.png') + FIXTURE_CHECKSUM = 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'.freeze + + def create_or_update_appearance(logo: false, header_logo: false) + appearance = appearances.first_or_create(title: 'foo', description: 'bar', logo: (UPLOAD_FILENAME if logo), header_logo: (UPLOAD_FILENAME if header_logo)) + + add_upload(appearance, 'Appearance', 'logo', 'AttachmentUploader') if logo + add_upload(appearance, 'Appearance', 'header_logo', 'AttachmentUploader') if header_logo + + appearance + end + + def create_group(avatar: false) + index = unique_index(:group) + group = namespaces.create(name: "group#{index}", path: "group#{index}", avatar: (UPLOAD_FILENAME if avatar)) + + add_upload(group, 'Group', 'avatar', 'AvatarUploader') if avatar + + group + end + + def create_note(attachment: false) + note = notes.create(attachment: (UPLOAD_FILENAME if attachment)) + + add_upload(note, 'Note', 'attachment', 'AttachmentUploader') if attachment + + note + end + + def create_project(avatar: false) + group = create_group + project = projects.create(namespace_id: group.id, path: "project#{unique_index(:project)}", avatar: (UPLOAD_FILENAME if avatar)) + routes.create(path: "#{group.path}/#{project.path}", source_id: project.id, source_type: 'Project') # so Project.find_by_full_path works + + add_upload(project, 'Project', 'avatar', 'AvatarUploader') if avatar + + project + end + + def create_user(avatar: false) + user = users.create(email: "foo#{unique_index(:user)}@bar.com", avatar: (UPLOAD_FILENAME if avatar), projects_limit: 100) + + add_upload(user, 'User', 'avatar', 'AvatarUploader') if avatar + + user + end + + def unique_index(name = :unnamed) + @unique_index ||= {} + @unique_index[name] ||= 0 + @unique_index[name] += 1 + end + + def add_upload(model, model_type, attachment_type, uploader) + file_path = upload_file_path(model, model_type, attachment_type) + path_relative_to_public = file_path.sub("#{PUBLIC_DIR}/", '') + create_file(file_path) + + uploads.create!( + size: 1062, + path: path_relative_to_public, + model_id: model.id, + model_type: model_type == 'Group' ? 'Namespace' : model_type, + uploader: uploader, + checksum: FIXTURE_CHECKSUM + ) + end + + def add_markdown_attachment(project, hashed_storage: false) + project_dir = hashed_storage ? hashed_project_uploads_dir(project) : legacy_project_uploads_dir(project) + attachment_dir = File.join(project_dir, SecureRandom.hex) + attachment_file_path = File.join(attachment_dir, UPLOAD_FILENAME) + project_attachment_path_relative_to_project = attachment_file_path.sub("#{project_dir}/", '') + create_file(attachment_file_path) + + uploads.create!( + size: 1062, + path: project_attachment_path_relative_to_project, + model_id: project.id, + model_type: 'Project', + uploader: 'FileUploader', + checksum: FIXTURE_CHECKSUM + ) + end + + def legacy_project_uploads_dir(project) + namespace = namespaces.find_by(id: project.namespace_id) + File.join(UPLOADS_DIR, namespace.path, project.path) + end + + def hashed_project_uploads_dir(project) + File.join(UPLOADS_DIR, '@hashed', 'aa', 'aaaaaaaaaaaa') + end + + def upload_file_path(model, model_type, attachment_type) + dir = File.join(upload_dir(model_type.downcase, attachment_type.to_s), model.id.to_s) + File.join(dir, UPLOAD_FILENAME) + end + + def upload_dir(model_type, attachment_type) + File.join(SYSTEM_DIR, model_type, attachment_type) + end + + def create_file(path) + File.delete(path) if File.exist?(path) + FileUtils.mkdir_p(File.dirname(path)) + FileUtils.cp(FIXTURE_FILE_PATH, path) + end + + def get_uploads(model, model_type) + uploads.where(model_type: model_type, model_id: model.id) + end + + def get_full_path(project) + routes.find_by(source_id: project.id, source_type: 'Project').path + end + + def ensure_temporary_tracking_table_exists + Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) + end + end +end diff --git a/spec/support/prometheus/additional_metrics_shared_examples.rb b/spec/support/prometheus/additional_metrics_shared_examples.rb index dbbd4ad4d40..c7c3346d39e 100644 --- a/spec/support/prometheus/additional_metrics_shared_examples.rb +++ b/spec/support/prometheus/additional_metrics_shared_examples.rb @@ -12,11 +12,12 @@ RSpec.shared_examples 'additional metrics query' do let(:client) { double('prometheus_client') } let(:query_result) { described_class.new(client).query(*query_params) } - let(:environment) { create(:environment, slug: 'environment-slug') } + let(:project) { create(:project) } + let(:environment) { create(:environment, slug: 'environment-slug', project: project) } before do allow(client).to receive(:label_values).and_return(metric_names) - allow(metric_group_class).to receive(:all).and_return([simple_metric_group(metrics: [simple_metric])]) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group(metrics: [simple_metric])]) end context 'metrics query context' do @@ -24,13 +25,14 @@ RSpec.shared_examples 'additional metrics query' do shared_examples 'query context containing environment slug and filter' do it 'contains ci_environment_slug' do - expect(subject).to receive(:query_metrics).with(hash_including(ci_environment_slug: environment.slug)) + expect(subject).to receive(:query_metrics).with(project, hash_including(ci_environment_slug: environment.slug)) subject.query(*query_params) end it 'contains environment filter' do expect(subject).to receive(:query_metrics).with( + project, hash_including( environment_filter: "container_name!=\"POD\",environment=\"#{environment.slug}\"" ) @@ -48,7 +50,7 @@ RSpec.shared_examples 'additional metrics query' do it_behaves_like 'query context containing environment slug and filter' it 'query context contains kube_namespace' do - expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: kube_namespace)) + expect(subject).to receive(:query_metrics).with(project, hash_including(kube_namespace: kube_namespace)) subject.query(*query_params) end @@ -72,7 +74,7 @@ RSpec.shared_examples 'additional metrics query' do it_behaves_like 'query context containing environment slug and filter' it 'query context contains empty kube_namespace' do - expect(subject).to receive(:query_metrics).with(hash_including(kube_namespace: '')) + expect(subject).to receive(:query_metrics).with(project, hash_including(kube_namespace: '')) subject.query(*query_params) end @@ -81,7 +83,7 @@ RSpec.shared_examples 'additional metrics query' do context 'with one group where two metrics is found' do before do - allow(metric_group_class).to receive(:all).and_return([simple_metric_group]) + allow(metric_group_class).to receive(:common_metrics).and_return([simple_metric_group]) end context 'some queries return results' do @@ -117,7 +119,7 @@ RSpec.shared_examples 'additional metrics query' do let(:metrics) { [simple_metric(queries: [simple_query])] } before do - allow(metric_group_class).to receive(:all).and_return( + allow(metric_group_class).to receive(:common_metrics).and_return( [ simple_metric_group(name: 'group_a', metrics: [simple_metric(queries: [simple_query])]), simple_metric_group(name: 'group_b', metrics: [simple_metric(title: 'title_b', queries: [simple_query('b')])]) diff --git a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb index 7ce80c82439..ea7dbade171 100644 --- a/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/uploads_actions_shared_examples.rb @@ -89,6 +89,19 @@ shared_examples 'handle uploads' do end end + context "when neither the uploader nor the model exists" do + before do + allow_any_instance_of(Upload).to receive(:build_uploader).and_return(nil) + allow(controller).to receive(:find_model).and_return(nil) + end + + it "responds with status 404" do + show_upload + + expect(response).to have_gitlab_http_status(404) + end + end + context "when the file doesn't exist" do before do allow_any_instance_of(FileUploader).to receive(:exists?).and_return(false) diff --git a/spec/support/snippet_visibility.rb b/spec/support/snippet_visibility.rb index 1cb904823d2..3a7c69b7877 100644 --- a/spec/support/snippet_visibility.rb +++ b/spec/support/snippet_visibility.rb @@ -252,6 +252,15 @@ RSpec.shared_examples 'snippet visibility' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end + + it 'returns no snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + snippets = described_class.new(user).execute + + expect(snippets).to be_empty + end end end end @@ -298,6 +307,15 @@ RSpec.shared_examples 'snippet visibility' do results = described_class.new(user).execute expect(results.include?(snippet)).to eq(outcome) end + + it 'should return personal snippets when the user cannot read cross project' do + allow(Ability).to receive(:allowed?).and_call_original + allow(Ability).to receive(:allowed?).with(user, :read_cross_project) { false } + + results = described_class.new(user).execute + + expect(results.include?(snippet)).to eq(outcome) + end end end end diff --git a/spec/support/track_untracked_uploads_helpers.rb b/spec/support/track_untracked_uploads_helpers.rb deleted file mode 100644 index a8b3ed1f41c..00000000000 --- a/spec/support/track_untracked_uploads_helpers.rb +++ /dev/null @@ -1,16 +0,0 @@ -module TrackUntrackedUploadsHelpers - def uploaded_file - fixture_path = Rails.root.join('spec/fixtures/rails_sample.jpg') - fixture_file_upload(fixture_path) - end - - def ensure_temporary_tracking_table_exists - Gitlab::BackgroundMigration::PrepareUntrackedUploads.new.send(:ensure_temporary_tracking_table_exists) - end - - def create_or_update_appearance(attrs) - a = Appearance.first_or_initialize(title: 'foo', description: 'bar') - a.update!(attrs) - a - end -end diff --git a/spec/views/shared/projects/_project.html.haml_spec.rb b/spec/views/shared/projects/_project.html.haml_spec.rb index f0a4f153699..3b14045e61f 100644 --- a/spec/views/shared/projects/_project.html.haml_spec.rb +++ b/spec/views/shared/projects/_project.html.haml_spec.rb @@ -5,6 +5,7 @@ describe 'shared/projects/_project.html.haml' do before do allow(view).to receive(:current_application_settings).and_return(Gitlab::CurrentSettings.current_application_settings) + allow(view).to receive(:can?) { true } end it 'should render creator avatar if project has a creator' do diff --git a/spec/workers/pages_domain_verification_cron_worker_spec.rb b/spec/workers/pages_domain_verification_cron_worker_spec.rb new file mode 100644 index 00000000000..8f780428c82 --- /dev/null +++ b/spec/workers/pages_domain_verification_cron_worker_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe PagesDomainVerificationCronWorker do + subject(:worker) { described_class.new } + + describe '#perform' do + it 'enqueues a PagesDomainVerificationWorker for domains needing verification' do + verified = create(:pages_domain) + reverify = create(:pages_domain, :reverify) + disabled = create(:pages_domain, :disabled) + + [reverify, disabled].each do |domain| + expect(PagesDomainVerificationWorker).to receive(:perform_async).with(domain.id) + end + + expect(PagesDomainVerificationWorker).not_to receive(:perform_async).with(verified.id) + + worker.perform + end + end +end diff --git a/spec/workers/pages_domain_verification_worker_spec.rb b/spec/workers/pages_domain_verification_worker_spec.rb new file mode 100644 index 00000000000..372fc95ab4a --- /dev/null +++ b/spec/workers/pages_domain_verification_worker_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe PagesDomainVerificationWorker do + subject(:worker) { described_class.new } + + let(:domain) { create(:pages_domain) } + + describe '#perform' do + it 'does nothing for a non-existent domain' do + domain.destroy + + expect(VerifyPagesDomainService).not_to receive(:new) + + expect { worker.perform(domain.id) }.not_to raise_error + end + + it 'delegates to VerifyPagesDomainService' do + service = double(:service) + expected_domain = satisfy { |obj| obj == domain } + + expect(VerifyPagesDomainService).to receive(:new).with(expected_domain) { service } + expect(service).to receive(:execute) + + worker.perform(domain.id) + end + end +end diff --git a/spec/workers/stuck_import_jobs_worker_spec.rb b/spec/workers/stuck_import_jobs_worker_spec.rb index a82eb54ffe4..069514552b1 100644 --- a/spec/workers/stuck_import_jobs_worker_spec.rb +++ b/spec/workers/stuck_import_jobs_worker_spec.rb @@ -2,35 +2,59 @@ require 'spec_helper' describe StuckImportJobsWorker do let(:worker) { described_class.new } - let(:exclusive_lease_uuid) { SecureRandom.uuid } - before do - allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain).and_return(exclusive_lease_uuid) - end + shared_examples 'project import job detection' do + context 'when the job has completed' do + context 'when the import status was already updated' do + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids) do + project.import_start + project.import_finish - describe 'with started import_status' do - let(:project) { create(:project, :import_started, import_jid: '123') } + [project.import_jid] + end + end + + it 'does not mark the project as failed' do + worker.perform + + expect(project.reload.import_status).to eq('finished') + end + end + + context 'when the import status was not updated' do + before do + allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([project.import_jid]) + end - describe 'long running import' do - it 'marks the project as failed' do - allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return(['123']) + it 'marks the project as failed' do + worker.perform - expect { worker.perform }.to change { project.reload.import_status }.to('failed') + expect(project.reload.import_status).to eq('failed') + end end end - describe 'running import' do - it 'does not mark the project as failed' do + context 'when the job is still in Sidekiq' do + before do allow(Gitlab::SidekiqStatus).to receive(:completed_jids).and_return([]) + end + it 'does not mark the project as failed' do expect { worker.perform }.not_to change { project.reload.import_status } end + end + end - describe 'import without import_jid' do - it 'marks the project as failed' do - expect { worker.perform }.to change { project.reload.import_status }.to('failed') - end - end + describe 'with scheduled import_status' do + it_behaves_like 'project import job detection' do + let(:project) { create(:project, :import_scheduled, import_jid: '123') } + end + end + + describe 'with started import_status' do + it_behaves_like 'project import job detection' do + let(:project) { create(:project, :import_started, import_jid: '123') } end end end |