diff options
author | Jose Vargas <jvargas@gitlab.com> | 2018-09-25 11:22:38 -0500 |
---|---|---|
committer | Jose Vargas <jvargas@gitlab.com> | 2018-09-25 11:22:38 -0500 |
commit | efedd968fb35a1113414885b48798c2723c7578d (patch) | |
tree | a417e431a24caa67ef2ac56373c3a0f869fefaa4 /spec | |
parent | d0f81b60e45a0eed8eb863aa9fc9070d1f416f14 (diff) | |
parent | 45b365b0e5abdb86ff6ac23b00578d5398134af9 (diff) | |
download | gitlab-ce-efedd968fb35a1113414885b48798c2723c7578d.tar.gz |
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into jivl-fix-monitoring-dashboard-resizing-navbar
Diffstat (limited to 'spec')
216 files changed, 6481 insertions, 3359 deletions
diff --git a/spec/config/settings_spec.rb b/spec/config/settings_spec.rb new file mode 100644 index 00000000000..83b2de47741 --- /dev/null +++ b/spec/config/settings_spec.rb @@ -0,0 +1,9 @@ +require 'spec_helper' + +describe Settings do + describe 'omniauth' do + it 'defaults to enabled' do + expect(described_class.omniauth.enabled).to be true + end + end +end diff --git a/spec/controllers/dashboard/milestones_controller_spec.rb b/spec/controllers/dashboard/milestones_controller_spec.rb index 505c040b5d5..56047c0c8d2 100644 --- a/spec/controllers/dashboard/milestones_controller_spec.rb +++ b/spec/controllers/dashboard/milestones_controller_spec.rb @@ -3,9 +3,11 @@ require 'spec_helper' describe Dashboard::MilestonesController do let(:project) { create(:project) } let(:group) { create(:group) } + let(:public_group) { create(:group, :public) } let(:user) { create(:user) } let(:project_milestone) { create(:milestone, project: project) } let(:group_milestone) { create(:milestone, group: group) } + let!(:public_milestone) { create(:milestone, group: public_group) } let(:milestone) do DashboardMilestone.build( [project], @@ -43,13 +45,13 @@ describe Dashboard::MilestonesController do end describe "#index" do - it 'should contain group and project milestones' do + it 'returns group and project milestones to which the user belongs' do get :index, format: :json expect(response).to have_gitlab_http_status(200) expect(json_response.size).to eq(2) - expect(json_response.map { |i| i["first_milestone"]["id"] }).to include(group_milestone.id, project_milestone.id) - expect(json_response.map { |i| i["group_name"] }).to include(group.name) + expect(json_response.map { |i| i["first_milestone"]["id"] }).to match_array([group_milestone.id, project_milestone.id]) + expect(json_response.map { |i| i["group_name"] }.compact).to match_array(group.name) end end end diff --git a/spec/controllers/import/gitlab_projects_controller_spec.rb b/spec/controllers/import/gitlab_projects_controller_spec.rb index d624659bce9..cbd1a112602 100644 --- a/spec/controllers/import/gitlab_projects_controller_spec.rb +++ b/spec/controllers/import/gitlab_projects_controller_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' describe Import::GitlabProjectsController do set(:namespace) { create(:namespace) } set(:user) { namespace.owner } - let(:file) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } + let(:file) { fixture_file_upload('spec/fixtures/project_export.tar.gz', 'text/plain') } before do sign_in(user) diff --git a/spec/controllers/oauth/applications_controller_spec.rb b/spec/controllers/oauth/applications_controller_spec.rb index 1195f44f37d..ace8a954e92 100644 --- a/spec/controllers/oauth/applications_controller_spec.rb +++ b/spec/controllers/oauth/applications_controller_spec.rb @@ -15,14 +15,44 @@ describe Oauth::ApplicationsController do expect(response).to have_gitlab_http_status(200) end - it 'redirects back to profile page if OAuth applications are disabled' do - allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) + it 'shows list of applications' do + disable_user_oauth get :index + expect(response).to have_gitlab_http_status(200) + end + end + + describe 'POST #create' do + it 'creates an application' do + post :create, oauth_params + + expect(response).to have_gitlab_http_status(302) + expect(response).to redirect_to(oauth_application_path(Doorkeeper::Application.last)) + end + + it 'redirects back to profile page if OAuth applications are disabled' do + disable_user_oauth + + post :create, oauth_params + expect(response).to have_gitlab_http_status(302) expect(response).to redirect_to(profile_path) end end end + + def disable_user_oauth + allow(Gitlab::CurrentSettings.current_application_settings).to receive(:user_oauth_applications?).and_return(false) + end + + def oauth_params + { + doorkeeper_application: { + name: 'foo', + redirect_uri: 'http://example.org' + } + } + end end diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb index 26a532ee01d..97ac11fd171 100644 --- a/spec/controllers/projects/clusters_controller_spec.rb +++ b/spec/controllers/projects/clusters_controller_spec.rb @@ -170,12 +170,14 @@ describe Projects::ClustersController do end describe 'POST create for new cluster' do + let(:legacy_abac_param) { 'true' } let(:params) do { cluster: { name: 'new-cluster', provider_gcp_attributes: { - gcp_project_id: 'gcp-project-12345' + gcp_project_id: 'gcp-project-12345', + legacy_abac: legacy_abac_param } } } @@ -201,6 +203,18 @@ describe Projects::ClustersController do expect(response).to redirect_to(project_cluster_path(project, project.clusters.first)) expect(project.clusters.first).to be_gcp expect(project.clusters.first).to be_kubernetes + expect(project.clusters.first.provider_gcp).to be_legacy_abac + end + + context 'when legacy_abac param is false' do + let(:legacy_abac_param) { 'false' } + + it 'creates a new cluster with legacy_abac_disabled' do + expect(ClusterProvisionWorker).to receive(:perform_async) + expect { go }.to change { Clusters::Cluster.count } + .and change { Clusters::Providers::Gcp.count } + expect(project.clusters.first.provider_gcp).not_to be_legacy_abac + end end end diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 751919f9501..c82c85970dc 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -86,7 +86,7 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do def create_job(name, status) pipeline = create(:ci_pipeline, project: project) create(:ci_build, :tags, :triggered, :artifacts, - pipeline: pipeline, name: name, status: status) + pipeline: pipeline, name: name, status: status) end end @@ -194,6 +194,194 @@ describe Projects::JobsController, :clean_gitlab_redis_shared_state do expect(json_response['terminal_path']).to match(%r{/terminal}) end end + + context 'when job passed with no trace' do + let(:job) { create(:ci_build, :success, :artifacts, pipeline: pipeline) } + + it 'exposes empty state illustrations' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['status']['illustration']).to have_key('image') + expect(json_response['status']['illustration']).to have_key('size') + expect(json_response['status']['illustration']).to have_key('title') + end + end + + context 'with no deployment' do + let(:job) { create(:ci_build, :success, pipeline: pipeline) } + + it 'does not exposes the deployment information' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['deployment_status']).to be_nil + end + end + + context 'with deployment' do + let(:merge_request) { create(:merge_request, source_project: project) } + let(:environment) { create(:environment, project: project, name: 'staging', state: :available) } + let(:job) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } + + it 'exposes the deployment information' do + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to match_schema('job/job_details') + expect(json_response['deployment_status']["status"]).to eq 'creating' + expect(json_response['deployment_status']["icon"]).to eq 'passed' + expect(json_response['deployment_status']["environment"]).not_to be_nil + end + end + + context 'when user can edit runner' do + context 'that belongs to the project' do + let(:runner) { create(:ci_runner, :project, projects: [project]) } + let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) } + + before do + project.add_maintainer(user) + sign_in(user) + + get_show(id: job.id, format: :json) + end + + it 'user can edit runner' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runner']).to have_key('edit_path') + end + end + + context 'that belongs to group' do + let(:group) { create(:group) } + let(:runner) { create(:ci_runner, :group, groups: [group]) } + let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) } + let(:user) { create(:user, :admin) } + + before do + project.add_maintainer(user) + sign_in(user) + + get_show(id: job.id, format: :json) + end + + it 'user can not edit runner' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runner']).not_to have_key('edit_path') + end + end + + context 'that belongs to instance' do + let(:runner) { create(:ci_runner, :instance) } + let(:job) { create(:ci_build, :success, pipeline: pipeline, runner: runner) } + let(:user) { create(:user, :admin) } + + before do + project.add_maintainer(user) + sign_in(user) + + get_show(id: job.id, format: :json) + end + + it 'user can not edit runner' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runner']).not_to have_key('edit_path') + end + end + end + + context 'when no runners are available' do + let(:runner) { create(:ci_runner, :instance, active: false) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runners']['online']).to be false + expect(json_response['runners']['available']).to be false + end + end + + context 'when no runner is online' do + let(:runner) { create(:ci_runner, :instance) } + let(:job) { create(:ci_build, :pending, pipeline: pipeline, runner: runner) } + + it 'exposes needed information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runners']['online']).to be false + expect(json_response['runners']['available']).to be true + end + end + + context 'settings_path' do + context 'when user is developer' do + it 'settings_path is not available' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runners']).not_to have_key('settings_path') + end + end + + context 'when user is maintainer' do + let(:user) { create(:user, :admin) } + + before do + project.add_maintainer(user) + sign_in(user) + end + + it 'settings_path is available' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['runners']['settings_path']).to match(/runners/) + end + end + end + end + + context 'when requesting JSON job is triggered' do + let!(:merge_request) { create(:merge_request, source_project: project) } + let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger_request) { create(:ci_trigger_request, pipeline: pipeline, trigger: trigger) } + let(:job) { create(:ci_build, pipeline: pipeline, trigger_request: trigger_request) } + + before do + project.add_developer(user) + sign_in(user) + + allow_any_instance_of(Ci::Build).to receive(:merge_request).and_return(merge_request) + end + + context 'with no variables' do + before do + get_show(id: job.id, format: :json) + end + + it 'exposes trigger information' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['trigger']['short_token']).to eq 'toke' + expect(json_response['trigger']['variables'].length).to eq 0 + end + end + + context 'with variables' do + before do + create(:ci_pipeline_variable, pipeline: pipeline, key: :TRIGGER_KEY_1, value: 'TRIGGER_VALUE_1') + + get_show(id: job.id, format: :json) + end + + it 'exposes trigger information and variables' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('job/job_details') + expect(json_response['trigger']['short_token']).to eq 'toke' + expect(json_response['trigger']['variables'].length).to eq 1 + expect(json_response['trigger']['variables'].first['key']).to eq "TRIGGER_KEY_1" + expect(json_response['trigger']['variables'].first['value']).to eq "TRIGGER_VALUE_1" + expect(json_response['trigger']['variables'].first['public']).to eq false + end + end end def get_show(**extra_params) diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index d9bb3981539..7446e0650f7 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -885,4 +885,18 @@ describe Projects::MergeRequestsController do end end end + + describe 'GET edit' do + it 'responds successfully' do + get :edit, namespace_id: project.namespace, project_id: project, id: merge_request + + expect(response).to have_gitlab_http_status(:success) + end + + it 'assigns the noteable to make sure autocompletes work' do + get :edit, namespace_id: project.namespace, project_id: project, id: merge_request + + expect(assigns(:noteable)).not_to be_nil + end + end end diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 81badaac76b..e48c9dea976 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -207,6 +207,14 @@ describe Projects::NotesController do expect(response).to have_gitlab_http_status(200) end + it 'returns discussion JSON when the return_discussion param is set' do + post :create, request_params.merge(format: :json, return_discussion: 'true') + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to have_key 'discussion' + expect(json_response['discussion']['notes'][0]['note']).to eq(request_params[:note][:note]) + end + context 'when merge_request_diff_head_sha present' do before do service_params = { diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb index d89716b1b50..0d49033c691 100644 --- a/spec/controllers/projects/pipelines_controller_spec.rb +++ b/spec/controllers/projects/pipelines_controller_spec.rb @@ -193,14 +193,34 @@ describe Projects::PipelinesController do context 'when accessing existing stage' do before do + create(:ci_build, :retried, :failed, pipeline: pipeline, stage: 'build') create(:ci_build, pipeline: pipeline, stage: 'build') + end + + context 'without retried' do + before do + get_stage('build') + end - get_stage('build') + it 'returns pipeline jobs without the retried builds' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline_stage') + expect(json_response['latest_statuses'].length).to eq 1 + expect(json_response).not_to have_key('retried') + end end - it 'returns html source for stage dropdown' do - expect(response).to have_gitlab_http_status(:ok) - expect(response).to match_response_schema('pipeline_stage') + context 'with retried' do + before do + get_stage('build', retried: true) + end + + it 'returns pipelines jobs with the retried builds' do + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_response_schema('pipeline_stage') + expect(json_response['latest_statuses'].length).to eq 1 + expect(json_response['retried'].length).to eq 1 + end end end @@ -214,12 +234,13 @@ describe Projects::PipelinesController do end end - def get_stage(name) - get :stage, namespace_id: project.namespace, - project_id: project, - id: pipeline.id, - stage: name, - format: :json + def get_stage(name, params = {}) + get :stage, **params.merge( + namespace_id: project.namespace, + project_id: project, + id: pipeline.id, + stage: name, + format: :json) end end diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb index 7c4a440b9a9..c13b0249d94 100644 --- a/spec/factories/clusters/applications/helm.rb +++ b/spec/factories/clusters/applications/helm.rb @@ -46,7 +46,7 @@ FactoryBot.define do factory :clusters_applications_jupyter, class: Clusters::Applications::Jupyter do oauth_application factory: :oauth_application - cluster factory: %i(cluster with_installed_helm provided_by_gcp) + cluster factory: %i(cluster with_installed_helm provided_by_gcp project) end end end diff --git a/spec/factories/emails.rb b/spec/factories/emails.rb index 4dc7961060a..d23ddf9d79b 100644 --- a/spec/factories/emails.rb +++ b/spec/factories/emails.rb @@ -4,5 +4,6 @@ FactoryBot.define do email { generate(:email_alias) } trait(:confirmed) { confirmed_at Time.now } + trait(:skip_validate) { to_create {|instance| instance.save(validate: false) } } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index 59db8cdc34b..a47bd7cafca 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -58,6 +58,14 @@ FactoryBot.define do project_view :readme end + trait :commit_email do + after(:create) do |user, evaluator| + additional = create(:email, :confirmed, user: user, email: "commit-#{user.email}") + + user.update!(commit_email: additional.email) + end + end + factory :omniauth_user do transient do extern_uid '123456' diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb index 5623e47eadf..a6ab6a5696a 100644 --- a/spec/features/admin/admin_runners_spec.rb +++ b/spec/features/admin/admin_runners_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' describe "Admin Runners" do include StubENV + include FilteredSearchHelpers + include SortingHelper before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') @@ -12,40 +14,109 @@ describe "Admin Runners" do let(:pipeline) { create(:ci_pipeline) } context "when there are runners" do - before do - runner = FactoryBot.create(:ci_runner, contacted_at: Time.now) - FactoryBot.create(:ci_build, pipeline: pipeline, runner_id: runner.id) + it 'has all necessary texts' do + runner = create(:ci_runner, contacted_at: Time.now) + create(:ci_build, pipeline: pipeline, runner_id: runner.id) visit admin_runners_path - end - it 'has all necessary texts' do - expect(page).to have_text "Setup a shared Runner manually" + expect(page).to have_text "Set up a shared Runner manually" expect(page).to have_text "Runners currently online: 1" end - describe 'search' do + describe 'search', :js do before do - FactoryBot.create :ci_runner, description: 'runner-foo' - FactoryBot.create :ci_runner, description: 'runner-bar' + create(:ci_runner, description: 'runner-foo') + create(:ci_runner, description: 'runner-bar') + + visit admin_runners_path end it 'shows correct runner when description matches' do - search_form = find('#runners-search') - search_form.fill_in 'search', with: 'runner-foo' - search_form.click_button 'Search' + input_filtered_search_keys('runner-foo') expect(page).to have_content("runner-foo") expect(page).not_to have_content("runner-bar") end it 'shows no runner when description does not match' do - search_form = find('#runners-search') - search_form.fill_in 'search', with: 'runner-baz' - search_form.click_button 'Search' + input_filtered_search_keys('runner-baz') expect(page).to have_text 'No runners found' end end + + describe 'filter by status', :js do + it 'shows correct runner when status matches' do + create(:ci_runner, description: 'runner-active', active: true) + create(:ci_runner, description: 'runner-paused', active: false) + + visit admin_runners_path + + expect(page).to have_content 'runner-active' + expect(page).to have_content 'runner-paused' + + input_filtered_search_keys('status:active') + expect(page).to have_content 'runner-active' + expect(page).not_to have_content 'runner-paused' + end + + it 'shows no runner when status does not match' do + create(:ci_runner, :online, description: 'runner-active', active: true) + create(:ci_runner, :online, description: 'runner-paused', active: false) + + visit admin_runners_path + + input_filtered_search_keys('status:offline') + + expect(page).not_to have_content 'runner-active' + expect(page).not_to have_content 'runner-paused' + + expect(page).to have_text 'No runners found' + end + end + + it 'shows correct runner when status is selected and search term is entered', :js do + create(:ci_runner, description: 'runner-a-1', active: true) + create(:ci_runner, description: 'runner-a-2', active: false) + create(:ci_runner, description: 'runner-b-1', active: true) + + visit admin_runners_path + + input_filtered_search_keys('status:active') + expect(page).to have_content 'runner-a-1' + expect(page).to have_content 'runner-b-1' + expect(page).not_to have_content 'runner-a-2' + + input_filtered_search_keys('status:active runner-a') + expect(page).to have_content 'runner-a-1' + expect(page).not_to have_content 'runner-b-1' + expect(page).not_to have_content 'runner-a-2' + end + + it 'sorts by last contact date', :js do + create(:ci_runner, description: 'runner-1', created_at: '2018-07-12 15:37', contacted_at: '2018-07-12 15:37') + create(:ci_runner, description: 'runner-2', created_at: '2018-07-12 16:37', contacted_at: '2018-07-12 16:37') + + visit admin_runners_path + + within '.runners-content .gl-responsive-table-row:nth-child(2)' do + expect(page).to have_content 'runner-2' + end + + within '.runners-content .gl-responsive-table-row:nth-child(3)' do + expect(page).to have_content 'runner-1' + end + + sorting_by 'Last Contact' + + within '.runners-content .gl-responsive-table-row:nth-child(2)' do + expect(page).to have_content 'runner-1' + end + + within '.runners-content .gl-responsive-table-row:nth-child(3)' do + expect(page).to have_content 'runner-2' + end + end end context "when there are no runners" do @@ -54,7 +125,7 @@ describe "Admin Runners" do end it 'has all necessary texts including no runner message' do - expect(page).to have_text "Setup a shared Runner manually" + expect(page).to have_text "Set up a shared Runner manually" expect(page).to have_text "Runners currently online: 0" expect(page).to have_text 'No runners found' end @@ -76,7 +147,7 @@ describe "Admin Runners" do context 'shared runner' do it 'shows the label and does not show the project count' do - runner = create :ci_runner, :instance + runner = create(:ci_runner, :instance) visit admin_runners_path @@ -89,8 +160,8 @@ describe "Admin Runners" do context 'specific runner' do it 'shows the label and the project count' do - project = create :project - runner = create :ci_runner, :project, projects: [project] + project = create(:project) + runner = create(:ci_runner, :project, projects: [project]) visit admin_runners_path @@ -103,11 +174,11 @@ describe "Admin Runners" do end describe "Runner show page" do - let(:runner) { FactoryBot.create :ci_runner } + let(:runner) { create(:ci_runner) } before do - @project1 = FactoryBot.create(:project) - @project2 = FactoryBot.create(:project) + @project1 = create(:project) + @project2 = create(:project) visit admin_runner_path(runner) end diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 3c65b5898b4..0a69a26eb3e 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -9,340 +9,365 @@ describe 'Admin updates settings' do before do stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false') sign_in(admin) - visit admin_application_settings_path end - it 'Change visibility settings' do - page.within('.as-visibility-access') do - choose "application_setting_default_project_visibility_20" - click_button 'Save changes' + context 'General page' do + before do + visit admin_application_settings_path end - expect(page).to have_content "Application settings saved successfully" - end + it 'Change visibility settings' do + page.within('.as-visibility-access') do + choose "application_setting_default_project_visibility_20" + click_button 'Save changes' + end - it 'Uncheck all restricted visibility levels' do - page.within('.as-visibility-access') do - find('#application_setting_visibility_level_0').set(false) - find('#application_setting_visibility_level_10').set(false) - find('#application_setting_visibility_level_20').set(false) - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" end - expect(page).to have_content "Application settings saved successfully" - expect(find('#application_setting_visibility_level_0')).not_to be_checked - expect(find('#application_setting_visibility_level_10')).not_to be_checked - expect(find('#application_setting_visibility_level_20')).not_to be_checked - end + it 'Uncheck all restricted visibility levels' do + page.within('.as-visibility-access') do + find('#application_setting_visibility_level_0').set(false) + find('#application_setting_visibility_level_10').set(false) + find('#application_setting_visibility_level_20').set(false) + click_button 'Save changes' + end - it 'Modify import sources' do - expect(Gitlab::CurrentSettings.import_sources).not_to be_empty + expect(page).to have_content "Application settings saved successfully" + expect(find('#application_setting_visibility_level_0')).not_to be_checked + expect(find('#application_setting_visibility_level_10')).not_to be_checked + expect(find('#application_setting_visibility_level_20')).not_to be_checked + end - page.within('.as-visibility-access') do - Gitlab::ImportSources.options.map do |name, _| - uncheck name + it 'Modify import sources' do + expect(Gitlab::CurrentSettings.import_sources).not_to be_empty + + page.within('.as-visibility-access') do + Gitlab::ImportSources.options.map do |name, _| + uncheck name + end + + click_button 'Save changes' end - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.import_sources).to be_empty + + page.within('.as-visibility-access') do + check "Repo by URL" + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.import_sources).to eq(['git']) end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.import_sources).to be_empty + it 'Change Visibility and Access Controls' do + page.within('.as-visibility-access') do + uncheck 'Project export enabled' + click_button 'Save changes' + end - page.within('.as-visibility-access') do - check "Repo by URL" - click_button 'Save changes' + expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey + expect(page).to have_content "Application settings saved successfully" end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.import_sources).to eq(['git']) - end + it 'Change Keys settings' do + page.within('.as-visibility-access') do + select 'Are forbidden', from: 'RSA SSH keys' + select 'Are allowed', from: 'DSA SSH keys' + select 'Must be at least 384 bits', from: 'ECDSA SSH keys' + select 'Are forbidden', from: 'ED25519 SSH keys' + click_on 'Save changes' + end - it 'Change Visibility and Access Controls' do - page.within('.as-visibility-access') do - uncheck 'Project export enabled' - click_button 'Save changes' + forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s + + expect(page).to have_content 'Application settings saved successfully' + expect(find_field('RSA SSH keys').value).to eq(forbidden) + expect(find_field('DSA SSH keys').value).to eq('0') + expect(find_field('ECDSA SSH keys').value).to eq('384') + expect(find_field('ED25519 SSH keys').value).to eq(forbidden) end - expect(Gitlab::CurrentSettings.project_export_enabled).to be_falsey - expect(page).to have_content "Application settings saved successfully" - end + it 'Change Account and Limit Settings' do + page.within('.as-account-limit') do + uncheck 'Gravatar enabled' + click_button 'Save changes' + end - it 'Change Account and Limit Settings' do - page.within('.as-account-limit') do - uncheck 'Gravatar enabled' - click_button 'Save changes' + expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey + expect(page).to have_content "Application settings saved successfully" end - expect(Gitlab::CurrentSettings.gravatar_enabled).to be_falsey - expect(page).to have_content "Application settings saved successfully" - end + it 'Change New users set to external', :js do + user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all) - it 'Change New users set to external', :js do - user_internal_regex = find('#application_setting_user_default_internal_regex', visible: :all) + expect(user_internal_regex).to be_readonly + expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external' - expect(user_internal_regex).to be_readonly - expect(user_internal_regex['placeholder']).to eq 'To define internal users, first enable new users set to external' + check 'application_setting_user_default_external' - check 'application_setting_user_default_external' + expect(user_internal_regex).not_to be_readonly + expect(user_internal_regex['placeholder']).to eq 'Regex pattern' + end - expect(user_internal_regex).not_to be_readonly - expect(user_internal_regex['placeholder']).to eq 'Regex pattern' - end + it 'Change Sign-in restrictions' do + page.within('.as-signin') do + fill_in 'Home page URL', with: 'https://about.gitlab.com/' + click_button 'Save changes' + end - it 'Change Sign-in restrictions' do - page.within('.as-signin') do - fill_in 'Home page URL', with: 'https://about.gitlab.com/' - click_button 'Save changes' + expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/" + expect(page).to have_content "Application settings saved successfully" end - expect(Gitlab::CurrentSettings.home_page_url).to eq "https://about.gitlab.com/" - expect(page).to have_content "Application settings saved successfully" - end + it 'Terms of Service' do + # Already have the admin accept terms, so they don't need to accept in this spec. + _existing_terms = create(:term) + accept_terms(admin) - it 'Terms of Service' do - # Already have the admin accept terms, so they don't need to accept in this spec. - _existing_terms = create(:term) - accept_terms(admin) + page.within('.as-terms') do + check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.' + fill_in 'Terms of Service Agreement', with: 'Be nice!' + click_button 'Save changes' + end - page.within('.as-terms') do - check 'Require all users to accept Terms of Service and Privacy Policy when they access GitLab.' - fill_in 'Terms of Service Agreement', with: 'Be nice!' - click_button 'Save changes' + expect(Gitlab::CurrentSettings.enforce_terms).to be(true) + expect(Gitlab::CurrentSettings.terms).to eq 'Be nice!' + expect(page).to have_content 'Application settings saved successfully' end - expect(Gitlab::CurrentSettings.enforce_terms).to be(true) - expect(Gitlab::CurrentSettings.terms).to eq 'Be nice!' - expect(page).to have_content 'Application settings saved successfully' - end + it 'Modify oauth providers' do + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty - it 'Modify oauth providers' do - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty + page.within('.as-signin') do + uncheck 'Google' + click_button 'Save changes' + end - page.within('.as-signin') do - uncheck 'Google' - click_button 'Save changes' - end + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') + page.within('.as-signin') do + check "Google" + click_button 'Save changes' + end - page.within('.as-signin') do - check "Google" - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2') end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).not_to include('google_oauth2') - end + it 'Oauth providers do not raise validation errors when saving unrelated changes' do + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty - it 'Oauth providers do not raise validation errors when saving unrelated changes' do - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to be_empty + page.within('.as-signin') do + uncheck 'Google' + click_button 'Save changes' + end - page.within('.as-signin') do - uncheck 'Google' - click_button 'Save changes' - end + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') + # Remove google_oauth2 from the Omniauth strategies + allow(Devise).to receive(:omniauth_providers).and_return([]) - # Remove google_oauth2 from the Omniauth strategies - allow(Devise).to receive(:omniauth_providers).and_return([]) + # Save an unrelated setting + page.within('.as-terms') do + click_button 'Save changes' + end - # Save an unrelated setting - page.within('.as-ci-cd') do - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.disabled_oauth_sign_in_sources).to include('google_oauth2') - end + it 'Configure web terminal' do + page.within('.as-terminal') do + fill_in 'Max session time', with: 15 + click_button 'Save changes' + end - it 'Change Help page' do - page.within('.as-help-page') do - fill_in 'Help page text', with: 'Example text' - check 'Hide marketing-related entries from help' - fill_in 'Support page URL', with: 'http://example.com/help' - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.terminal_max_session_time).to eq(15) end - - expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text" - expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy - expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help" - expect(page).to have_content "Application settings saved successfully" end - it 'Change Pages settings' do - page.within('.as-pages') do - fill_in 'Maximum size of pages (MB)', with: 15 - check 'Require users to prove ownership of custom domains' - click_button 'Save changes' + context 'Integrations page' do + before do + visit integrations_admin_application_settings_path end - expect(Gitlab::CurrentSettings.max_pages_size).to eq 15 - expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy - expect(page).to have_content "Application settings saved successfully" - end + it 'Enable hiding third party offers' do + page.within('.as-third-party-offers') do + check 'Do not display offers from third parties within GitLab' + click_button 'Save changes' + end - it 'Change CI/CD settings' do - page.within('.as-ci-cd') do - check 'Default to Auto DevOps pipeline for all projects' - fill_in 'Auto devops domain', with: 'domain.com' - click_button 'Save changes' + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.hide_third_party_offers).to be true end - expect(Gitlab::CurrentSettings.auto_devops_enabled?).to be true - expect(Gitlab::CurrentSettings.auto_devops_domain).to eq('domain.com') - expect(page).to have_content "Application settings saved successfully" - end + it 'Change Slack Notifications Service template settings' do + first(:link, 'Service Templates').click + click_link 'Slack notifications' + fill_in 'Webhook', with: 'http://localhost' + fill_in 'Username', with: 'test_user' + fill_in 'service_push_channel', with: '#test_channel' + page.check('Notify only broken pipelines') + page.check('Notify only default branch') - it 'Change Influx settings' do - page.within('.as-influx') do - check 'Enable InfluxDB Metrics' - click_button 'Save changes' - end + check_all_events + click_on 'Save' - expect(Gitlab::CurrentSettings.metrics_enabled?).to be true - expect(page).to have_content "Application settings saved successfully" - end + expect(page).to have_content 'Application settings saved successfully' - it 'Change Prometheus settings' do - page.within('.as-prometheus') do - check 'Enable Prometheus Metrics' - click_button 'Save changes' - end + click_link 'Slack notifications' - expect(Gitlab::CurrentSettings.prometheus_metrics_enabled?).to be true - expect(page).to have_content "Application settings saved successfully" + page.all('input[type=checkbox]').each do |checkbox| + expect(checkbox).to be_checked + end + expect(find_field('Webhook').value).to eq 'http://localhost' + expect(find_field('Username').value).to eq 'test_user' + expect(find('#service_push_channel').value).to eq '#test_channel' + end end - it 'Change Performance bar settings' do - group = create(:group) + context 'CI/CD page' do + it 'Change CI/CD settings' do + visit ci_cd_admin_application_settings_path + + page.within('.as-ci-cd') do + check 'Default to Auto DevOps pipeline for all projects' + fill_in 'Auto devops domain', with: 'domain.com' + click_button 'Save changes' + end - page.within('.as-performance-bar') do - check 'Enable the Performance Bar' - fill_in 'Allowed group', with: group.path - click_on 'Save changes' + expect(Gitlab::CurrentSettings.auto_devops_enabled?).to be true + expect(Gitlab::CurrentSettings.auto_devops_domain).to eq('domain.com') + expect(page).to have_content "Application settings saved successfully" end + end - expect(page).to have_content "Application settings saved successfully" - expect(find_field('Enable the Performance Bar')).to be_checked - expect(find_field('Allowed group').value).to eq group.path + context 'Reporting page' do + it 'Change Spam settings' do + visit reporting_admin_application_settings_path - page.within('.as-performance-bar') do - uncheck 'Enable the Performance Bar' - click_on 'Save changes' - end + page.within('.as-spam') do + check 'Enable reCAPTCHA' + fill_in 'reCAPTCHA Site Key', with: 'key' + fill_in 'reCAPTCHA Private Key', with: 'key' + fill_in 'IPs per user', with: 15 + click_button 'Save changes' + end - expect(page).to have_content 'Application settings saved successfully' - expect(find_field('Enable the Performance Bar')).not_to be_checked - expect(find_field('Allowed group').value).to be_nil + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.recaptcha_enabled).to be true + expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15) + end end - it 'Change Background jobs settings' do - page.within('.as-background') do - fill_in 'Throttling Factor', with: 1 - click_button 'Save changes' + context 'Metrics and profiling page' do + before do + visit metrics_and_profiling_admin_application_settings_path end - expect(Gitlab::CurrentSettings.sidekiq_throttling_factor).to eq(1) - expect(page).to have_content "Application settings saved successfully" - end + it 'Change Influx settings' do + page.within('.as-influx') do + check 'Enable InfluxDB Metrics' + click_button 'Save changes' + end - it 'Change Spam settings' do - page.within('.as-spam') do - check 'Enable reCAPTCHA' - fill_in 'reCAPTCHA Site Key', with: 'key' - fill_in 'reCAPTCHA Private Key', with: 'key' - fill_in 'IPs per user', with: 15 - click_button 'Save changes' + expect(Gitlab::CurrentSettings.metrics_enabled?).to be true + expect(page).to have_content "Application settings saved successfully" end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.recaptcha_enabled).to be true - expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15) - end + it 'Change Prometheus settings' do + page.within('.as-prometheus') do + check 'Enable Prometheus Metrics' + click_button 'Save changes' + end - it 'Configure web terminal' do - page.within('.as-terminal') do - fill_in 'Max session time', with: 15 - click_button 'Save changes' + expect(Gitlab::CurrentSettings.prometheus_metrics_enabled?).to be true + expect(page).to have_content "Application settings saved successfully" end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.terminal_max_session_time).to eq(15) - end + it 'Change Performance bar settings' do + group = create(:group) - it 'Enable outbound requests' do - page.within('.as-outbound') do - check 'Allow requests to the local network from hooks and services' - click_button 'Save changes' - end + page.within('.as-performance-bar') do + check 'Enable the Performance Bar' + fill_in 'Allowed group', with: group.path + click_on 'Save changes' + end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true - end + expect(page).to have_content "Application settings saved successfully" + expect(find_field('Enable the Performance Bar')).to be_checked + expect(find_field('Allowed group').value).to eq group.path - it 'Enable hiding third party offers' do - page.within('.as-third-party-offers') do - check 'Do not display offers from third parties within GitLab' - click_button 'Save changes' + page.within('.as-performance-bar') do + uncheck 'Enable the Performance Bar' + click_on 'Save changes' + end + + expect(page).to have_content 'Application settings saved successfully' + expect(find_field('Enable the Performance Bar')).not_to be_checked + expect(find_field('Allowed group').value).to be_nil end - expect(page).to have_content "Application settings saved successfully" - expect(Gitlab::CurrentSettings.hide_third_party_offers).to be true - end + it 'loads usage ping payload on click', :js do + expect(page).to have_button 'Preview payload' - it 'Change Slack Notifications Service template settings' do - first(:link, 'Service Templates').click - click_link 'Slack notifications' - fill_in 'Webhook', with: 'http://localhost' - fill_in 'Username', with: 'test_user' - fill_in 'service_push_channel', with: '#test_channel' - page.check('Notify only broken pipelines') - page.check('Notify only default branch') + find('.js-usage-ping-payload-trigger').click - check_all_events - click_on 'Save' + expect(page).to have_selector '.js-usage-ping-payload' + expect(page).to have_button 'Hide payload' + end + end - expect(page).to have_content 'Application settings saved successfully' + context 'Network page' do + it 'Enable outbound requests' do + visit network_admin_application_settings_path - click_link 'Slack notifications' + page.within('.as-outbound') do + check 'Allow requests to the local network from hooks and services' + click_button 'Save changes' + end - page.all('input[type=checkbox]').each do |checkbox| - expect(checkbox).to be_checked + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true end - expect(find_field('Webhook').value).to eq 'http://localhost' - expect(find_field('Username').value).to eq 'test_user' - expect(find('#service_push_channel').value).to eq '#test_channel' end - it 'Change Keys settings' do - page.within('.as-visibility-access') do - select 'Are forbidden', from: 'RSA SSH keys' - select 'Are allowed', from: 'DSA SSH keys' - select 'Must be at least 384 bits', from: 'ECDSA SSH keys' - select 'Are forbidden', from: 'ED25519 SSH keys' - click_on 'Save changes' + context 'Preferences page' do + before do + visit preferences_admin_application_settings_path end - forbidden = ApplicationSetting::FORBIDDEN_KEY_VALUE.to_s - - expect(page).to have_content 'Application settings saved successfully' - expect(find_field('RSA SSH keys').value).to eq(forbidden) - expect(find_field('DSA SSH keys').value).to eq('0') - expect(find_field('ECDSA SSH keys').value).to eq('384') - expect(find_field('ED25519 SSH keys').value).to eq(forbidden) - end + it 'Change Help page' do + page.within('.as-help-page') do + fill_in 'Help page text', with: 'Example text' + check 'Hide marketing-related entries from help' + fill_in 'Support page URL', with: 'http://example.com/help' + click_button 'Save changes' + end - it 'loads usage ping payload on click', :js do - expect(page).to have_button 'Preview payload' + expect(Gitlab::CurrentSettings.help_page_text).to eq "Example text" + expect(Gitlab::CurrentSettings.help_page_hide_commercial_content).to be_truthy + expect(Gitlab::CurrentSettings.help_page_support_url).to eq "http://example.com/help" + expect(page).to have_content "Application settings saved successfully" + end - find('.js-usage-ping-payload-trigger').click + it 'Change Pages settings' do + page.within('.as-pages') do + fill_in 'Maximum size of pages (MB)', with: 15 + check 'Require users to prove ownership of custom domains' + click_button 'Save changes' + end - expect(page).to have_selector '.js-usage-ping-payload' - expect(page).to have_button 'Hide payload' + expect(Gitlab::CurrentSettings.max_pages_size).to eq 15 + expect(Gitlab::CurrentSettings.pages_domain_verification_enabled?).to be_truthy + expect(page).to have_content "Application settings saved successfully" + end end def check_all_events diff --git a/spec/features/admin/admin_uses_repository_checks_spec.rb b/spec/features/admin/admin_uses_repository_checks_spec.rb index e658f1b6738..d04bb9acd9e 100644 --- a/spec/features/admin/admin_uses_repository_checks_spec.rb +++ b/spec/features/admin/admin_uses_repository_checks_spec.rb @@ -33,7 +33,7 @@ describe 'Admin uses repository checks' do end it 'to clear all repository checks', :js do - visit admin_application_settings_path + visit repository_admin_application_settings_path expect(RepositoryCheck::ClearWorker).to receive(:perform_async) diff --git a/spec/features/dashboard/group_spec.rb b/spec/features/dashboard/group_spec.rb index 1c7932e7964..e57fcde8b2c 100644 --- a/spec/features/dashboard/group_spec.rb +++ b/spec/features/dashboard/group_spec.rb @@ -13,7 +13,7 @@ RSpec.describe 'Dashboard Group' do it 'creates new group', :js do visit dashboard_groups_path - find('.btn-new').click + find('.btn-success').click new_path = 'Samurai' new_description = 'Tokugawa Shogunate' diff --git a/spec/features/dashboard/groups_list_spec.rb b/spec/features/dashboard/groups_list_spec.rb index eceb12e91cd..e75c43d5338 100644 --- a/spec/features/dashboard/groups_list_spec.rb +++ b/spec/features/dashboard/groups_list_spec.rb @@ -125,7 +125,7 @@ describe 'Dashboard Groups page', :js do end it 'loads results for next page' do - expect(page).to have_selector('.gl-pagination .page', count: 2) + expect(page).to have_selector('.gl-pagination .page-item a[role=menuitemradio]', count: 2) # Check first page expect(page).to have_content(group2.full_name) @@ -134,7 +134,7 @@ describe 'Dashboard Groups page', :js do expect(page).not_to have_selector("#group-#{group.id}") # Go to next page - find(".gl-pagination .page:not(.active) a").click + find('.gl-pagination .page-item:not(.active) a[role=menuitemradio]').click wait_for_requests diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb index d9d67788b38..8fb2e37e269 100644 --- a/spec/features/dashboard/milestones_spec.rb +++ b/spec/features/dashboard/milestones_spec.rb @@ -17,8 +17,9 @@ describe 'Dashboard > Milestones' do let(:project) { create(:project, namespace: user.namespace) } let!(:milestone) { create(:milestone, project: project) } let!(:milestone2) { create(:milestone, group: group) } + before do - project.add_maintainer(user) + group.add_developer(user) sign_in(user) visit dashboard_milestones_path end diff --git a/spec/features/dashboard/projects_spec.rb b/spec/features/dashboard/projects_spec.rb index 4daacc61d85..975b7944741 100644 --- a/spec/features/dashboard/projects_spec.rb +++ b/spec/features/dashboard/projects_spec.rb @@ -103,6 +103,14 @@ describe 'Dashboard Projects' do expect(page).not_to have_content(project.name) expect(page).to have_content(project3.name) end + + it 'sorts projects by most stars when sorting by most stars' do + project_with_most_stars = create(:project, namespace: user.namespace, star_count: 10) + + visit dashboard_projects_path(sort: :stars_desc) + + expect(first('.project-row')).to have_content(project_with_most_stars.title) + end end context 'when on Starred projects tab' do diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index d4949de3f27..35d57b3896d 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -265,7 +265,7 @@ describe 'Filter issues', :js do context 'issue label clicked' do it 'filters and displays in search bar' do - find('.issues-list .issue .issue-main-info .issuable-info a .badge', text: multiple_words_label.title).click + find('.issues-list .issue .issuable-main-info .issuable-info a .badge', text: multiple_words_label.title).click expect_issues_list_count(1) expect_tokens([label_token("\"#{multiple_words_label.title}\"")]) diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb index 6f917f522bc..09904cb907f 100644 --- a/spec/features/labels_hierarchy_spec.rb +++ b/spec/features/labels_hierarchy_spec.rb @@ -156,7 +156,7 @@ describe 'Labels Hierarchy', :js, :nested_groups do find('a.label-item', text: parent_group_label.title).click find('a.label-item', text: project_label_1.title).click - find('.btn-create').click + find('.btn-success').click expect(page.find('.issue-details h2.title')).to have_content('new created issue') expect(page).to have_selector('span.badge', text: grandparent_group_label.title) diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index b6ed3686de2..fa148715855 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -200,23 +200,20 @@ describe 'Merge request > User posts diff notes', :js do end context 'with a new line' do - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'allows commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]').find(:xpath, '..')) + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) end end context 'with an old line' do - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'allows commenting' do - should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]').find(:xpath, '..')) + it 'allows commenting' do + should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]')) end end context 'with an unchanged line' do - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - xit 'allows commenting' do - should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]').find(:xpath, '..')) + it 'allows commenting' do + should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]')) end end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 260c5f9c28b..ee5f5377ca6 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -111,7 +111,7 @@ describe 'Merge request > User posts notes', :js do it 'allows using markdown buttons after saving a note and then trying to edit it again' do page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'This is the new content' - find('.btn-save').click + find('.btn-success').click end find('.note').hover @@ -129,7 +129,7 @@ describe 'Merge request > User posts notes', :js do it 'appends the edited at time to the note' do page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' - find('.btn-save').click + find('.btn-success').click end page.within("#note_#{note.id}") do diff --git a/spec/features/merge_request/user_resolves_conflicts_spec.rb b/spec/features/merge_request/user_resolves_conflicts_spec.rb index 629052442b4..50c723776a3 100644 --- a/spec/features/merge_request/user_resolves_conflicts_spec.rb +++ b/spec/features/merge_request/user_resolves_conflicts_spec.rb @@ -44,9 +44,7 @@ describe 'Merge request > User resolves conflicts', :js do within find('.diff-file', text: 'files/ruby/regex.rb') do expect(page).to have_selector('.line_content.new', text: "def username_regexp") - expect(page).not_to have_selector('.line_content.new', text: "def username_regex") expect(page).to have_selector('.line_content.new', text: "def project_name_regexp") - expect(page).not_to have_selector('.line_content.new', text: "def project_name_regex") expect(page).to have_selector('.line_content.new', text: "def path_regexp") expect(page).to have_selector('.line_content.new', text: "def archive_formats_regexp") expect(page).to have_selector('.line_content.new', text: "def git_reference_regexp") @@ -110,12 +108,8 @@ describe 'Merge request > User resolves conflicts', :js do click_link('conflicts', href: %r{/conflicts\Z}) end - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - # include_examples "conflicts are resolved in Interactive mode" - # include_examples "conflicts are resolved in Edit inline mode" - - it 'prevents RSpec/EmptyExampleGroup' do - end + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" end context 'in Parallel view mode' do @@ -124,12 +118,8 @@ describe 'Merge request > User resolves conflicts', :js do click_button 'Side-by-side' end - # TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 - # include_examples "conflicts are resolved in Interactive mode" - # include_examples "conflicts are resolved in Edit inline mode" - - it 'prevents RSpec/EmptyExampleGroup' do - end + include_examples "conflicts are resolved in Interactive mode" + include_examples "conflicts are resolved in Edit inline mode" end end diff --git a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb index 2d268ecab58..8a16c011067 100644 --- a/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb +++ b/spec/features/merge_request/user_resolves_diff_notes_and_discussions_resolve_spec.rb @@ -139,44 +139,64 @@ describe 'Merge request > User resolves diff notes and discussions', :js do expect(find('.diffs .diff-file .notes_holder')).to be_visible end end - end - it 'allows user to resolve from reply form without a comment' do - page.within '.diff-content' do - click_button 'Reply...' + describe 'reply form' do + before do + click_button 'Toggle discussion' - click_button 'Resolve discussion' - end + page.within '.diff-content' do + click_button 'Reply...' + end + end - page.within '.line-resolve-all-container' do - expect(page).to have_content('1/1 discussion resolved') - expect(page).to have_selector('.line-resolve-btn.is-active') - end - end + it 'allows user to comment' do + page.within '.diff-content' do + find('.js-note-text').set 'testing' - it 'allows user to unresolve from reply form without a comment' do - page.within '.diff-content' do - click_button 'Resolve discussion' - sleep 1 + click_button 'Comment' - click_button 'Reply...' + wait_for_requests + end - click_button 'Unresolve discussion' - end + page.within '.line-resolve-all-container' do + expect(page).to have_content('1/1 discussion resolved') + end + end - page.within '.line-resolve-all-container' do - expect(page).to have_content('0/1 discussion resolved') - expect(page).not_to have_selector('.line-resolve-btn.is-active') + it 'allows user to unresolve from reply form without a comment' do + page.within '.diff-content' do + click_button 'Unresolve discussion' + + wait_for_requests + end + + page.within '.line-resolve-all-container' do + expect(page).to have_content('0/1 discussion resolved') + expect(page).not_to have_selector('.line-resolve-btn.is-active') + end + end + + it 'allows user to comment & unresolve discussion' do + page.within '.diff-content' do + find('.js-note-text').set 'testing' + + click_button 'Comment & unresolve discussion' + + wait_for_requests + end + + page.within '.line-resolve-all-container' do + expect(page).to have_content('0/1 discussion resolved') + end + end end end - it 'allows user to comment & resolve discussion' do + it 'allows user to resolve from reply form without a comment' do page.within '.diff-content' do click_button 'Reply...' - find('.js-note-text').set 'testing' - - click_button 'Comment & resolve discussion' + click_button 'Resolve discussion' end page.within '.line-resolve-all-container' do @@ -185,19 +205,18 @@ describe 'Merge request > User resolves diff notes and discussions', :js do end end - it 'allows user to comment & unresolve discussion' do + it 'allows user to comment & resolve discussion' do page.within '.diff-content' do - click_button 'Resolve discussion' - click_button 'Reply...' find('.js-note-text').set 'testing' - click_button 'Comment & unresolve discussion' + click_button 'Comment & resolve discussion' end page.within '.line-resolve-all-container' do - expect(page).to have_content('0/1 discussion resolved') + expect(page).to have_content('1/1 discussion resolved') + expect(page).to have_selector('.line-resolve-btn.is-active') end end diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb index b285cd7a7ac..a5dc9017699 100644 --- a/spec/features/merge_request/user_sees_merge_widget_spec.rb +++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb @@ -160,7 +160,7 @@ describe 'Merge request > User sees merge widget', :js do end end - context 'view merge request where project has CI setup but no CI status' do + context 'view merge request where project has CI set up but no CI status' do before do pipeline = create(:ci_pipeline, project: project, sha: merge_request.diff_head_sha, @@ -178,7 +178,7 @@ describe 'Merge request > User sees merge widget', :js do end end - context 'view merge request in project with only-mwps setting enabled but no CI is setup' do + context 'view merge request in project with only-mwps setting enabled but no CI is set up' do before do visit project_merge_request_path(project_only_mwps, merge_request_in_only_mwps_project) end diff --git a/spec/features/projects/activity/user_sees_private_activity_spec.rb b/spec/features/projects/activity/user_sees_private_activity_spec.rb new file mode 100644 index 00000000000..d7dc0a6712a --- /dev/null +++ b/spec/features/projects/activity/user_sees_private_activity_spec.rb @@ -0,0 +1,35 @@ +require 'spec_helper' + +describe 'Project > Activity > User sees private activity', :js do + let(:project) { create(:project, :public) } + let(:author) { create(:user) } + let(:user) { create(:user) } + let(:issue) { create(:issue, :confidential, project: project, author: author) } + let(:message) { "#{author.name} opened issue #{issue.to_reference}" } + + before do + project.add_developer(author) + + create(:event, :created, project: project, target: issue, author: author) + end + + it 'shows the activity to a logged-in user with permissions' do + sign_in(author) + visit activity_project_path(project) + + expect(page).to have_content(message) + end + + it 'hides the activity from a logged-in user without permissions' do + sign_in(user) + visit activity_project_path(project) + + expect(page).not_to have_content(message) + end + + it 'hides the activity from an anonymous user' do + visit activity_project_path(project) + + expect(page).not_to have_content(message) + end +end diff --git a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb index 67ed2f18d76..554f0b49052 100644 --- a/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb +++ b/spec/features/projects/artifacts/user_downloads_artifacts_spec.rb @@ -20,23 +20,13 @@ describe "User downloads artifacts" do end context "via job id" do - set(:url) { download_project_job_artifacts_path(project, job) } + let(:url) { download_project_job_artifacts_path(project, job) } it_behaves_like "downloading" end context "via branch name and job name" do - set(:url) { latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) } - - it_behaves_like "downloading" - end - - context "via clicking the `Download` button" do - set(:url) { project_job_path(project, job) } - - before do - click_link("Download") - end + let(:url) { latest_succeeded_project_artifacts_path(project, "#{pipeline.ref}/download", job: job.name) } it_behaves_like "downloading" end diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb index 2ba4d4918ff..e2f9e7e9cc5 100644 --- a/spec/features/projects/blobs/blob_show_spec.rb +++ b/spec/features/projects/blobs/blob_show_spec.rb @@ -533,7 +533,7 @@ describe 'File blob', :js do expect(page).to have_content('This project is licensed under the MIT License.') # shows a learn more link - expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/') + expect(page).to have_link('Learn more', href: 'http://choosealicense.com/licenses/mit/') end end end @@ -566,10 +566,10 @@ describe 'File blob', :js do expect(page).to have_content('This project manages its dependencies using RubyGems and defines a gem named activerecord.') # shows a link to the gem - expect(page).to have_link('activerecord', 'https://rubygems.org/gems/activerecord') + expect(page).to have_link('activerecord', href: 'https://rubygems.org/gems/activerecord') # shows a learn more link - expect(page).to have_link('Learn more', 'http://choosealicense.com/licenses/mit/') + expect(page).to have_link('Learn more', href: 'https://rubygems.org/') end end end diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb index 31e3ebf675d..edc763ad0ad 100644 --- a/spec/features/projects/clusters/gcp_spec.rb +++ b/spec/features/projects/clusters/gcp_spec.rb @@ -33,6 +33,32 @@ describe 'Gcp Cluster', :js do context 'when user filled form with valid parameters' do subject { click_button 'Create Kubernetes cluster' } + shared_examples 'valid cluster gcp form' do + it 'users sees a form with the GCP token' do + expect(page).to have_selector(:css, 'form[data-token="token"]') + end + + it 'user sees a cluster details page and creation status' do + subject + + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + + Clusters::Cluster.last.provider.make_created! + + expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') + end + + it 'user sees a error if something wrong during creation' do + subject + + expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + + Clusters::Cluster.last.provider.make_errored!('Something wrong!') + + expect(page).to have_content('Something wrong!') + end + end + before do allow_any_instance_of(GoogleApi::CloudPlatform::Client) .to receive(:projects_zones_clusters_create) do @@ -56,28 +82,16 @@ describe 'Gcp Cluster', :js do fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2' end - it 'users sees a form with the GCP token' do - expect(page).to have_selector(:css, 'form[data-token="token"]') - end - - it 'user sees a cluster details page and creation status' do - subject - - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') - - Clusters::Cluster.last.provider.make_created! - - expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine') - end - - it 'user sees a error if something wrong during creation' do - subject + it_behaves_like 'valid cluster gcp form' - expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...') + context 'rbac_clusters feature flag is enabled' do + before do + stub_feature_flags(rbac_clusters: true) - Clusters::Cluster.last.provider.make_errored!('Something wrong!') + check 'cluster_provider_gcp_attributes_legacy_abac' + end - expect(page).to have_content('Something wrong!') + it_behaves_like 'valid cluster gcp form' end end diff --git a/spec/features/projects/clusters/user_spec.rb b/spec/features/projects/clusters/user_spec.rb index ec968bfcf7d..2b4998ed5ac 100644 --- a/spec/features/projects/clusters/user_spec.rb +++ b/spec/features/projects/clusters/user_spec.rb @@ -21,42 +21,43 @@ describe 'User Cluster', :js do end context 'when user filled form with valid parameters' do + shared_examples 'valid cluster user form' do + it 'user sees a cluster details page' do + subject + + expect(page).to have_content('Kubernetes cluster integration') + expect(page.find_field('cluster[name]').value).to eq('dev-cluster') + expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) + .to have_content('http://example.com') + expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value) + .to have_content('my-token') + end + end + before do fill_in 'cluster_name', with: 'dev-cluster' fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com' fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token' - click_button 'Add Kubernetes cluster' end - it 'user sees a cluster details page' do - expect(page).to have_content('Kubernetes cluster integration') - expect(page.find_field('cluster[name]').value).to eq('dev-cluster') - expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) - .to have_content('http://example.com') - expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value) - .to have_content('my-token') - end - end + subject { click_button 'Add Kubernetes cluster' } - context 'rbac_clusters feature flag is enabled' do - before do - stub_feature_flags(rbac_clusters: true) + it_behaves_like 'valid cluster user form' - fill_in 'cluster_name', with: 'dev-cluster' - fill_in 'cluster_platform_kubernetes_attributes_api_url', with: 'http://example.com' - fill_in 'cluster_platform_kubernetes_attributes_token', with: 'my-token' - check 'cluster_platform_kubernetes_attributes_authorization_type' - click_button 'Add Kubernetes cluster' - end + context 'rbac_clusters feature flag is enabled' do + before do + stub_feature_flags(rbac_clusters: true) + + check 'cluster_platform_kubernetes_attributes_authorization_type' + end - it 'user sees a cluster details page' do - expect(page).to have_content('Kubernetes cluster integration') - expect(page.find_field('cluster[name]').value).to eq('dev-cluster') - expect(page.find_field('cluster[platform_kubernetes_attributes][api_url]').value) - .to have_content('http://example.com') - expect(page.find_field('cluster[platform_kubernetes_attributes][token]').value) - .to have_content('my-token') - expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked + it_behaves_like 'valid cluster user form' + + it 'user sees a cluster details page with RBAC enabled' do + subject + + expect(page.find_field('cluster[platform_kubernetes_attributes][authorization_type]', disabled: true)).to be_checked + end end end @@ -85,7 +86,6 @@ describe 'User Cluster', :js do context 'when user disables the cluster' do before do page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click - fill_in 'cluster_name', with: 'dev-cluster' page.within('#cluster-integration') { click_button 'Save changes' } end diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb index 8a418356541..a2b96514d64 100644 --- a/spec/features/projects/import_export/export_file_spec.rb +++ b/spec/features/projects/import_export/export_file_spec.rb @@ -49,6 +49,8 @@ describe 'Import/Export - project export integration test', :js do expect(file_permissions(project.export_path)).to eq(0700) + expect(project.export_file.path).to include('tar.gz') + in_directory_with_expanded_export(project) do |exit_status, tmpdir| expect(exit_status).to eq(0) diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb index 6cd5810325f..65c68277167 100644 --- a/spec/features/projects/import_export/import_file_spec.rb +++ b/spec/features/projects/import_export/import_file_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' describe 'Import/Export - project import integration test', :js do include Select2Helper + include GitHelpers let(:user) { create(:user) } let(:file) { File.join(Rails.root, 'spec', 'features', 'projects', 'import_export', 'test_project_export.tar.gz') } @@ -94,12 +95,6 @@ describe 'Import/Export - project import integration test', :js do wiki.repository.exists? && !wiki.repository.empty? end - def project_hook_exists?(project) - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository).exists? - end - end - def click_import_project_tab find('#import-project-tab').click end diff --git a/spec/features/projects/jobs_spec.rb b/spec/features/projects/jobs_spec.rb index 83293c0ca7d..d0bf4975b81 100644 --- a/spec/features/projects/jobs_spec.rb +++ b/spec/features/projects/jobs_spec.rb @@ -5,7 +5,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do let(:user) { create(:user) } let(:user_access_level) { :developer } let(:project) { create(:project, :repository) } - let(:pipeline) { create(:ci_pipeline, project: project) } + let(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit('HEAD').sha) } let(:job) { create(:ci_build, :trace_live, pipeline: pipeline) } let(:job2) { create(:ci_build) } @@ -20,7 +20,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end describe "GET /:project/jobs" do - let!(:job) { create(:ci_build, pipeline: pipeline) } + let!(:job) { create(:ci_build, pipeline: pipeline) } context "Pending scope" do before do @@ -115,22 +115,28 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do context "Job from project" do let(:job) { create(:ci_build, :success, :trace_live, pipeline: pipeline) } - before do + it 'shows status name', :js do visit project_job_path(project, job) - end - it 'shows status name', :js do + wait_for_requests + expect(page).to have_css('.ci-status.ci-success', text: 'passed') end - it 'shows commit`s data' do - expect(page.status_code).to eq(200) + it 'shows commit`s data', :js do + requests = inspect_requests() do + visit project_job_path(project, job) + end + + wait_for_requests + expect(requests.first.status_code).to eq(200) expect(page).to have_content pipeline.sha[0..7] - expect(page).to have_content pipeline.git_commit_message - expect(page).to have_content pipeline.git_author_name + expect(page).to have_content pipeline.commit.title end it 'shows active job' do + visit project_job_path(project, job) + expect(page).to have_selector('.build-job.active') end end @@ -199,7 +205,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it { expect(page.status_code).to eq(404) } end - context "Download artifacts" do + context "Download artifacts", :js do before do job.update(legacy_artifacts_file: artifacts_file) visit project_job_path(project, job) @@ -208,9 +214,22 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do it 'has button to download artifacts' do expect(page).to have_content 'Download' end + + it 'downloads the zip file when user clicks the download button' do + requests = inspect_requests() do + click_link 'Download' + end + + artifact_request = requests.find { |req| req.url.match(%r{artifacts/download}) } + + expect(artifact_request.response_headers["Content-Disposition"]).to eq(%Q{attachment; filename="#{job.artifacts_file.filename}"}) + expect(artifact_request.response_headers['Content-Transfer-Encoding']).to eq("binary") + expect(artifact_request.response_headers['Content-Type']).to eq("image/gif") + expect(artifact_request.body).to eq(job.artifacts_file.file.read.b) + end end - context 'Artifacts expire date' do + context 'Artifacts expire date', :js do before do job.update(legacy_artifacts_file: artifacts_file, artifacts_expire_at: expire_at) @@ -231,12 +250,12 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do context 'when user has ability to update job' do it 'keeps artifacts when keep button is clicked' do - expect(page).to have_content 'The artifacts will be removed' + expect(page).to have_content 'The artifacts will be removed in' click_link 'Keep' expect(page).to have_no_link 'Keep' - expect(page).to have_no_content 'The artifacts will be removed' + expect(page).to have_no_content 'The artifacts will be removed in' end end @@ -314,6 +333,7 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do shared_examples 'expected variables behavior' do it 'shows variable key and value after click', :js do + expect(page).to have_content('Token') expect(page).to have_css('.js-reveal-variables') expect(page).not_to have_css('.js-build-variable') expect(page).not_to have_css('.js-build-value') @@ -542,20 +562,26 @@ describe 'Jobs', :clean_gitlab_redis_shared_state do end end - describe "GET /:project/jobs/:id/download" do + describe "GET /:project/jobs/:id/download", :js do before do job.update(legacy_artifacts_file: artifacts_file) visit project_job_path(project, job) + click_link 'Download' end context "Build from other project" do before do job2.update(legacy_artifacts_file: artifacts_file) - visit download_project_job_artifacts_path(project, job2) end - it { expect(page.status_code).to eq(404) } + it do + requests = inspect_requests() do + visit download_project_job_artifacts_path(project, job2) + end + + expect(requests.first.status_code).to eq(404) + end end end diff --git a/spec/features/projects/members/invite_group_spec.rb b/spec/features/projects/members/invite_group_spec.rb index 0fb3eb20b5b..fceead0b45e 100644 --- a/spec/features/projects/members/invite_group_spec.rb +++ b/spec/features/projects/members/invite_group_spec.rb @@ -40,7 +40,7 @@ describe 'Project > Members > Invite group', :js do select2 group_to_share_with.id, from: '#link_group_id' page.find('body').click - find('.btn-create').click + find('.btn-success').click page.within('.project-members-groups') do expect(page).to have_content(group_to_share_with.name) @@ -122,7 +122,7 @@ describe 'Project > Members > Invite group', :js do fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d') click_on 'invite-group-tab' - find('.btn-create').click + find('.btn-success').click end it 'the group link shows the expiration time with a warning class' do diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb index 0acd5059385..75c72a68069 100644 --- a/spec/features/projects/new_project_spec.rb +++ b/spec/features/projects/new_project_spec.rb @@ -66,12 +66,34 @@ describe 'New project' do end context 'Readme selector' do - it 'shows the initialize with Readme checkbox' do + it 'shows the initialize with Readme checkbox on "Blank project" tab' do visit new_project_path expect(page).to have_css('input#project_initialize_with_readme') expect(page).to have_content('Initialize repository with a README') end + + it 'does not show the initialize with Readme checkbox on "Create from template" tab' do + visit new_project_path + find('#create-from-template-pane').click + first('.choose-template').click + + page.within '.project-fields-form' do + expect(page).not_to have_css('input#project_initialize_with_readme') + expect(page).not_to have_content('Initialize repository with a README') + end + end + + it 'does not show the initialize with Readme checkbox on "Import project" tab' do + visit new_project_path + find('#import-project-tab').click + first('.js-import-git-toggle-button').click + + page.within '.toggle-import-form' do + expect(page).not_to have_css('input#project_initialize_with_readme') + expect(page).not_to have_content('Initialize repository with a README') + end + end end context 'Namespace selector' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 26a92f14787..41822babbc9 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -9,6 +9,7 @@ describe 'Pipelines', :js do before do sign_in(user) project.add_developer(user) + project.update!(auto_devops_attributes: { enabled: false }) end describe 'GET /:project/pipelines' do @@ -641,6 +642,7 @@ describe 'Pipelines', :js do context 'when user is not logged in' do before do + project.update!(auto_devops_attributes: { enabled: false }) visit project_pipelines_path(project) end diff --git a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb index 25b74cc481d..70f3a812ee9 100644 --- a/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/projects/services/user_activates_mattermost_slash_command_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'Setup Mattermost slash commands', :js do +describe 'Set up Mattermost slash commands', :js do let(:user) { create(:user) } let(:project) { create(:project) } let(:service) { project.create_mattermost_slash_commands_service } diff --git a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb index b8326edd4fd..6fe21579e8e 100644 --- a/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb +++ b/spec/features/projects/show/user_sees_setup_shortcut_buttons_spec.rb @@ -28,8 +28,6 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end it '"Auto DevOps enabled" button not linked' do - project.create_auto_devops!(enabled: true) - visit project_path(project) page.within('.project-stats') do @@ -65,19 +63,23 @@ describe 'Projects > Show > User sees setup shortcut buttons' do 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: 'autodevops-settings')) + context 'when Auto DevOps is enabled' do + it '"Auto DevOps enabled" anchor linked to settings page' do + 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: 'autodevops-settings')) + end end end - it '"Auto DevOps enabled" anchor linked to settings page' do - project.create_auto_devops!(enabled: true) - - visit project_path(project) + context 'when Auto DevOps is not enabled' do + let(:project) { create(:project, :public, :empty_repo, auto_devops_attributes: { enabled: false }) } - page.within('.project-stats') do - expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + 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: 'autodevops-settings')) + end end end end @@ -113,27 +115,31 @@ describe 'Projects > Show > User sees setup shortcut buttons' do 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') + context 'when Auto DevOps is enabled' do + it '"Auto DevOps enabled" button not linked' do + visit project_path(project) + + page.within('.project-stats') do + expect(page).to have_text('Auto DevOps enabled') + end end end - it '"Auto DevOps enabled" button not linked' do - project.create_auto_devops!(enabled: true) - - visit project_path(project) + context 'when Auto DevOps is not enabled' do + let(:project) { create(:project, :public, :repository, auto_devops_attributes: { enabled: false }) } - page.within('.project-stats') do - expect(page).to have_text('Auto DevOps enabled') + 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 - 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') + 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 end @@ -222,97 +228,105 @@ describe 'Projects > Show > User sees setup shortcut buttons' do end describe 'GitLab CI configuration button' do - it '"Set up CI/CD" button linked to new file populated for a .gitlab-ci.yml' do - visit project_path(project) - - expect(project.repository.gitlab_ci_yml).to be_nil + context 'when Auto DevOps is enabled' do + it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do + visit project_path(project) - page.within('.project-stats') do - expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path) + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end 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 + context 'when Auto DevOps is not enabled' do + let(:project) { create(:project, :public, :repository, auto_devops_attributes: { enabled: false }) } - expect(project.repository.gitlab_ci_yml).not_to be_nil + it '"Set up CI/CD" button linked to new file populated for a .gitlab-ci.yml' do + visit project_path(project) - visit project_path(project) + expect(project.repository.gitlab_ci_yml).to be_nil - page.within('.project-stats') do - expect(page).not_to have_link('Set up CI/CD') + page.within('.project-stats') do + expect(page).to have_link('Set up CI/CD', href: presenter.add_ci_yml_path) + end end - end - it 'no "Set up CI/CD" button if the project has Auto DevOps enabled' do - project.create_auto_devops!(enabled: true) + 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 - visit project_path(project) + expect(project.repository.gitlab_ci_yml).not_to be_nil - page.within('.project-stats') do - expect(page).not_to have_link('Set up CI/CD') + visit project_path(project) + + page.within('.project-stats') do + expect(page).not_to have_link('Set up CI/CD') + end end end end describe 'Auto DevOps button' do - it '"Enable Auto DevOps" button linked to settings page' do - visit project_path(project) + context 'when Auto DevOps is enabled' do + it '"Auto DevOps enabled" anchor linked to settings page' do + visit project_path(project) - page.within('.project-stats') do - expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + page.within('.project-stats') do + expect(page).to have_link('Auto DevOps enabled', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + end end end - it '"Enable Auto DevOps" button linked to settings page' do - project.create_auto_devops!(enabled: true) + context 'when Auto DevOps is not enabled' do + let(:project) { create(:project, :public, :repository, auto_devops_attributes: { enabled: false }) } - visit project_path(project) + it '"Enable Auto DevOps" button linked to settings page' do + 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: 'autodevops-settings')) + page.within('.project-stats') do + expect(page).to have_link('Enable Auto DevOps', href: project_settings_ci_cd_path(project, anchor: 'autodevops-settings')) + end 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) + 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) + visit project_path(project) - expect(page).to have_selector('.js-autodevops-banner') + 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') + 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 - 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 + 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 + expect(project.repository.gitlab_ci_yml).not_to be_nil - visit project_path(project) + 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') + 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 end diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb index 9e58280b868..2cb2a23b7be 100644 --- a/spec/features/projects/tree/create_directory_spec.rb +++ b/spec/features/projects/tree/create_directory_spec.rb @@ -43,7 +43,7 @@ describe 'Multi-file editor new directory', :js do find('.js-ide-commit-mode').click find('.multi-file-commit-list-item').hover - first('.multi-file-discard-btn .btn').click + click_button 'Stage' fill_in('commit-message', with: 'commit message ide') diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb index a04d3566a7e..9f5524da8e9 100644 --- a/spec/features/projects/tree/create_file_spec.rb +++ b/spec/features/projects/tree/create_file_spec.rb @@ -35,7 +35,7 @@ describe 'Multi-file editor new file', :js do find('.js-ide-commit-mode').click find('.multi-file-commit-list-item').hover - first('.multi-file-discard-btn .btn').click + click_button 'Stage' fill_in('commit-message', with: 'commit message ide') diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb index 0c6cf3dc477..cb7a912946c 100644 --- a/spec/features/runners_spec.rb +++ b/spec/features/runners_spec.rb @@ -198,7 +198,7 @@ describe 'Runners' do expect(page).to have_content 'This group does not provide any group Runners yet' expect(page).to have_content 'Group maintainers can register group runners in the Group CI/CD settings' - expect(page).not_to have_content 'Ask your group maintainer to setup a group Runner' + expect(page).not_to have_content 'Ask your group maintainer to set up a group Runner' end end end @@ -224,7 +224,7 @@ describe 'Runners' do expect(page).to have_content 'This group does not provide any group Runners yet.' expect(page).not_to have_content 'Group maintainers can register group runners in the Group CI/CD settings' - expect(page).to have_content 'Ask your group maintainer to setup a group Runner.' + expect(page).to have_content 'Ask your group maintainer to set up a group Runner.' end end diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb index 1442e011d52..eeacaf5f72a 100644 --- a/spec/features/snippets/notes_on_personal_snippets_spec.rb +++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb @@ -103,7 +103,7 @@ describe 'Comments on personal snippets', :js do page.within('.current-note-edit-form') do fill_in 'note[note]', with: 'new content' - find('.btn-save').click + find('.btn-success').click end page.within("#notes-list li#note_#{snippet_notes[0].id}") do diff --git a/spec/features/snippets/user_sees_breadcrumb_links.rb b/spec/features/snippets/user_sees_breadcrumb_links.rb new file mode 100644 index 00000000000..696f2b93390 --- /dev/null +++ b/spec/features/snippets/user_sees_breadcrumb_links.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe 'New user snippet breadcrumbs' do + let(:user) { create(:user) } + + before do + sign_in(user) + visit new_snippet_path + end + + it 'display a link to user snippets and new user snippet pages' do + page.within '.breadcrumbs' do + expect(find_link('Snippets')[:href]).to end_with(dashboard_snippets_path) + expect(find_link('New')[:href]).to end_with(new_snippet_path) + end + end +end diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb index f245c1ebbd9..f1192f48b86 100644 --- a/spec/features/u2f_spec.rb +++ b/spec/features/u2f_spec.rb @@ -3,14 +3,14 @@ require 'spec_helper' describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do def manage_two_factor_authentication click_on 'Manage two-factor authentication' - expect(page).to have_content("Setup new U2F device") + expect(page).to have_content("Set up new U2F device") wait_for_requests end def register_u2f_device(u2f_device = nil, name: 'My device') u2f_device ||= FakeU2fDevice.new(page, name) u2f_device.respond_to_u2f_registration - click_on 'Setup new U2F device' + click_on 'Set up new U2F device' expect(page).to have_content('Your device was successfully set up') fill_in "Pick a name", with: name click_on 'Register U2F device' @@ -34,7 +34,7 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do visit profile_account_path click_on 'Enable two-factor authentication' - expect(page).to have_button('Setup new U2F device', disabled: true) + expect(page).to have_button('Set up new U2F device', disabled: true) end end @@ -109,7 +109,7 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do # Have the "u2f device" respond with bad data page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") - click_on 'Setup new U2F device' + click_on 'Set up new U2F device' expect(page).to have_content('Your device was successfully set up') click_on 'Register U2F device' @@ -124,7 +124,7 @@ describe 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do # Failed registration page.execute_script("u2f.register = function(_,_,_,callback) { callback('bad response'); };") - click_on 'Setup new U2F device' + click_on 'Set up new U2F device' expect(page).to have_content('Your device was successfully set up') click_on 'Register U2F device' expect(page).to have_content("The form contains the following error") diff --git a/spec/finders/admin/runners_finder_spec.rb b/spec/finders/admin/runners_finder_spec.rb new file mode 100644 index 00000000000..1e9793a5e0a --- /dev/null +++ b/spec/finders/admin/runners_finder_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Admin::RunnersFinder do + describe '#execute' do + context 'with empty params' do + it 'returns all runners' do + runner1 = create :ci_runner, active: true + runner2 = create :ci_runner, active: false + + expect(described_class.new(params: {}).execute).to match_array [runner1, runner2] + end + end + + context 'filter by search term' do + it 'calls Ci::Runner.search' do + expect(Ci::Runner).to receive(:search).with('term').and_call_original + + described_class.new(params: { search: 'term' }).execute + end + end + + context 'filter by status' do + it 'calls the corresponding scope on Ci::Runner' do + expect(Ci::Runner).to receive(:paused).and_call_original + + described_class.new(params: { status_status: 'paused' }).execute + end + end + + context 'sort' do + context 'without sort param' do + it 'sorts by created_at' do + runner1 = create :ci_runner, created_at: '2018-07-12 07:00' + runner2 = create :ci_runner, created_at: '2018-07-12 08:00' + runner3 = create :ci_runner, created_at: '2018-07-12 09:00' + + expect(described_class.new(params: {}).execute).to eq [runner3, runner2, runner1] + end + end + + context 'with sort param' do + it 'sorts by specified attribute' do + runner1 = create :ci_runner, contacted_at: 1.minute.ago + runner2 = create :ci_runner, contacted_at: 3.minutes.ago + runner3 = create :ci_runner, contacted_at: 2.minutes.ago + + expect(described_class.new(params: { sort: 'contacted_asc' }).execute).to eq [runner2, runner3, runner1] + end + end + end + + context 'paginate' do + it 'returns the runners for the specified page' do + stub_const('Admin::RunnersFinder::NUMBER_OF_RUNNERS_PER_PAGE', 1) + runner1 = create :ci_runner, created_at: '2018-07-12 07:00' + runner2 = create :ci_runner, created_at: '2018-07-12 08:00' + + expect(described_class.new(params: { page: 1 }).execute).to eq [runner2] + expect(described_class.new(params: { page: 2 }).execute).to eq [runner1] + end + end + end +end diff --git a/spec/finders/group_labels_finder_spec.rb b/spec/finders/group_labels_finder_spec.rb new file mode 100644 index 00000000000..ef68fc105e4 --- /dev/null +++ b/spec/finders/group_labels_finder_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe GroupLabelsFinder, '#execute' do + let!(:group) { create(:group) } + let!(:label1) { create(:group_label, title: 'Foo', description: 'Lorem ipsum', group: group) } + let!(:label2) { create(:group_label, title: 'Bar', description: 'Fusce consequat', group: group) } + + it 'returns all group labels sorted by name if no params' do + result = described_class.new(group).execute + + expect(result.to_a).to match_array([label2, label1]) + end + + it 'returns all group labels sorted by name desc' do + result = described_class.new(group, sort: 'name_desc').execute + + expect(result.to_a).to match_array([label2, label1]) + end + + it 'returns group labels that march search' do + result = described_class.new(group, search: 'Foo').execute + + expect(result.to_a).to match_array([label1]) + end + + it 'returns second page of labels' do + result = described_class.new(group, page: '2').execute + + expect(result.to_a).to match_array([]) + end +end diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb index 7931ad9b9f0..590e838f13e 100644 --- a/spec/finders/projects_finder_spec.rb +++ b/spec/finders/projects_finder_spec.rb @@ -174,6 +174,13 @@ describe ProjectsFinder do end end + describe 'filter by without_deleted' do + let(:params) { { without_deleted: true } } + let!(:pending_delete_project) { create(:project, :public, pending_delete: true) } + + it { is_expected.to match_array([public_project, internal_project]) } + end + describe 'sorting' do let(:params) { { sort: 'name_asc' } } diff --git a/spec/fixtures/api/schemas/deployment.json b/spec/fixtures/api/schemas/deployment.json index 536e6475c23..8c8cdf8bcb2 100644 --- a/spec/fixtures/api/schemas/deployment.json +++ b/spec/fixtures/api/schemas/deployment.json @@ -1,45 +1,31 @@ { - "additionalProperties": false, - "properties": { - "created_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "iid": { - "type": "integer" - }, - "last?": { - "type": "boolean" - }, - "ref": { - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - } - }, - "required": [ - "name" - ], - "type": "object" - }, - "sha": { - "type": "string" - }, - "tag": { - "type": "boolean" - } + "type": "object", + "required": [ + "sha", + "created_at", + "iid", + "tag", + "last?", + "ref", + "id" + ], + "properties": { + "created_at": { "type": "string" }, + "id": { "type": "integer" }, + "iid": { "type": "integer" }, + "last?": { "type": "boolean" }, + "ref": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { "type": "string" } + }, + "additionalProperties": false }, - "required": [ - "sha", - "created_at", - "iid", - "tag", - "last?", - "ref", - "id" - ], - "type": "object" + "sha": { "type": "string" }, + "tag": { "type": "boolean" } + }, + "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/entities/commit.json b/spec/fixtures/api/schemas/entities/commit.json new file mode 100644 index 00000000000..686d29c97d2 --- /dev/null +++ b/spec/fixtures/api/schemas/entities/commit.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "allOf": [ + { "$ref": "../public_api/v4/commit/basic.json" }, + { + "type": "object", + "required": [ + "author_gravatar_url", + "commit_url", + "commit_path", + "author" + ], + "properties": { + "author_gravatar_url": { "type": "string" }, + "commit_url": { "type": "string" }, + "commit_path": { "type": "string" }, + "author": { + "oneOf": [ + { "type": "null" }, + { "type": "user.json" } + ] + } + }, + "additionalProperties": false + } + ] +} diff --git a/spec/fixtures/api/schemas/entities/user.json b/spec/fixtures/api/schemas/entities/user.json index 6482e0eedd2..82d80b75cef 100644 --- a/spec/fixtures/api/schemas/entities/user.json +++ b/spec/fixtures/api/schemas/entities/user.json @@ -5,13 +5,19 @@ "state", "avatar_url", "web_url", - "path" + "path", + "name", + "username" ], "properties": { "id": { "type": "integer" }, "state": { "type": "string" }, "avatar_url": { "type": "string" }, "web_url": { "type": "string" }, - "path": { "type": "string" } - } + "path": { "type": "string" }, + "name": { "type": "string" }, + "username": { "type": "string" }, + "status_tooltip_html": { "$ref": "../types/nullable_string.json" } + }, + "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/environment.json b/spec/fixtures/api/schemas/environment.json new file mode 100644 index 00000000000..f1d33e3ce7b --- /dev/null +++ b/spec/fixtures/api/schemas/environment.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "required": [ + "id", + "name", + "state", + "external_url", + "environment_type", + "has_stop_action", + "environment_path", + "stop_path", + "folder_path", + "created_at", + "updated_at", + "can_stop" + ], + "properties": { + "id": { "type": "integer" }, + "name": { "type": "string" }, + "state": { "type": "string" }, + "external_url": { "$ref": "types/nullable_string.json" }, + "environment_type": { "$ref": "types/nullable_string.json" }, + "has_stop_action": { "type": "boolean" }, + "environment_path": { "type": "string" }, + "stop_path": { "type": "string" }, + "folder_path": { "type": "string" }, + "created_at": { "type": "string", "format": "date-time" }, + "updated_at": { "type": "string", "format": "date-time" }, + "can_stop": { "type": "boolean" }, + "last_deployment": { + "oneOf": [ + { "type": "null" }, + { "$ref": "deployment.json" } + ] + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/job/deployment_status.json b/spec/fixtures/api/schemas/job/deployment_status.json new file mode 100644 index 00000000000..a90b8b35654 --- /dev/null +++ b/spec/fixtures/api/schemas/job/deployment_status.json @@ -0,0 +1,27 @@ +{ + "type": "object", + "required": [ + "status", + "icon", + "environment" + ], + "properties": { + "status": { + "oneOf": [ + { + "type": "string", + "enum": [ + "last", + "creating", + "failed", + "out_of_date" + ] + }, + { "type": "null" } + ] + }, + "icon": { "type": "string" }, + "environment": { "$ref": "../environment.json" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/job/job.json b/spec/fixtures/api/schemas/job/job.json index c793d93c0f6..734c535ef70 100644 --- a/spec/fixtures/api/schemas/job/job.json +++ b/spec/fixtures/api/schemas/job/job.json @@ -25,7 +25,9 @@ "playable": { "type": "boolean" }, "created_at": { "type": "string" }, "updated_at": { "type": "string" }, - "status": { "$ref": "../ci_detailed_status.json" } + "status": { "$ref": "../status/ci_detailed_status.json" }, + "callout_message": { "type": "string" }, + "recoverable": { "type": "boolean" } }, "additionalProperties": true } diff --git a/spec/fixtures/api/schemas/job/job_details.json b/spec/fixtures/api/schemas/job/job_details.json index b8c099250be..cd67d3e4160 100644 --- a/spec/fixtures/api/schemas/job/job_details.json +++ b/spec/fixtures/api/schemas/job/job_details.json @@ -1,8 +1,14 @@ { - "allOf": [{ "$ref": "job.json" }], + "allOf": [ + { "$ref": "job.json" } + ], "description": "An extension of job.json with more detailed information", "properties": { "artifact": { "$ref": "artifact.json" }, - "terminal_path": { "type": "string" } + "terminal_path": { "type": "string" }, + "trigger": { "$ref": "trigger.json" }, + "deployment_status": { "$ref": "deployment_status.json" }, + "runner": { "$ref": "runner.json" }, + "runners": { "type": "runners.json" } } } diff --git a/spec/fixtures/api/schemas/job/runner.json b/spec/fixtures/api/schemas/job/runner.json new file mode 100644 index 00000000000..acfeeeeb808 --- /dev/null +++ b/spec/fixtures/api/schemas/job/runner.json @@ -0,0 +1,17 @@ +{ + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "required": [ + "id", + "description" + ], + "properties": { + "id": { "type": "integer" }, + "description": { "type": "string" }, + "edit_path": { "type": "string" } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/job/runners.json b/spec/fixtures/api/schemas/job/runners.json new file mode 100644 index 00000000000..bebb0c88652 --- /dev/null +++ b/spec/fixtures/api/schemas/job/runners.json @@ -0,0 +1,13 @@ +{ + "type": "object", + "required": [ + "online", + "available" + ], + "properties": { + "online": { "type": "boolean" }, + "available": { "type": "boolean" }, + "settings_path": { "type": "string" } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/job/trigger.json b/spec/fixtures/api/schemas/job/trigger.json new file mode 100644 index 00000000000..1c7e9cc7693 --- /dev/null +++ b/spec/fixtures/api/schemas/job/trigger.json @@ -0,0 +1,28 @@ +{ + "type": "object", + "required": [ + "short_token", + "variables" + ], + "properties": { + "short_token": { "type": "string" }, + "variables": { + "type": "array", + "items": { + "type": "object", + "required": [ + "key", + "value", + "public" + ], + "properties": { + "key": { "type": "string" }, + "value": { "type": "string" }, + "public": { "type": "boolean" } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/spec/fixtures/api/schemas/pipeline_stage.json b/spec/fixtures/api/schemas/pipeline_stage.json index eb2667295f0..c01a1946185 100644 --- a/spec/fixtures/api/schemas/pipeline_stage.json +++ b/spec/fixtures/api/schemas/pipeline_stage.json @@ -16,7 +16,12 @@ "items": { "$ref": "job/job.json" }, "optional": true }, - "status": { "$ref": "ci_detailed_status.json" }, + "retried": { + "type": "array", + "items": { "$ref": "job/job.json" }, + "optional": true + }, + "status": { "$ref": "status/ci_detailed_status.json" }, "path": { "type": "string" }, "dropdown_path": { "type": "string" } }, diff --git a/spec/fixtures/api/schemas/status/action.json b/spec/fixtures/api/schemas/status/action.json new file mode 100644 index 00000000000..99a576e6c5b --- /dev/null +++ b/spec/fixtures/api/schemas/status/action.json @@ -0,0 +1,22 @@ +{ + "type": "object", + "required": [ + "icon", + "title", + "path", + "method" + ], + "properties": { + "icon": { + "type": "string", + "enum": [ + "retry", + "play", + "cancel" + ] + }, + "title": { "type": "string" }, + "path": { "type": "string" }, + "method": { "$ref": "../http_method.json" } + } +} diff --git a/spec/fixtures/api/schemas/ci_detailed_status.json b/spec/fixtures/api/schemas/status/ci_detailed_status.json index d74248eabef..8d0f1e4a6af 100644 --- a/spec/fixtures/api/schemas/ci_detailed_status.json +++ b/spec/fixtures/api/schemas/status/ci_detailed_status.json @@ -1,6 +1,6 @@ { "type": "object", - "required" : [ + "required": [ "icon", "text", "label", @@ -19,28 +19,8 @@ "has_details": { "type": "boolean" }, "details_path": { "type": "string" }, "favicon": { "type": "string" }, - "action": { - "type": "object", - "required": [ - "icon", - "title", - "path", - "method" - ], - "properties": { - "icon": { - "type": "string", - "enum": [ - "retry", - "play", - "cancel" - ] - }, - "title": { "type": "string" }, - "path": { "type": "string" }, - "method": { "$ref": "http_method.json" } - } - } + "illustration": { "$ref": "illustration.json" }, + "action": { "$ref": "action.json" } }, "additionalProperties": false } diff --git a/spec/fixtures/api/schemas/status/illustration.json b/spec/fixtures/api/schemas/status/illustration.json new file mode 100644 index 00000000000..9a085f5f1ee --- /dev/null +++ b/spec/fixtures/api/schemas/status/illustration.json @@ -0,0 +1,19 @@ +{ + "oneOf": [ + { "type": "null" }, + { + "type": "object", + "required": [ + "image", + "size", + "title" + ], + "properties": { + "image": { "type": "string" }, + "size": { "type": "string" }, + "title": { "type": "string" }, + "content": { "type": "string" } + } + } + ] +} diff --git a/spec/fixtures/api/schemas/types/nullable_string.json b/spec/fixtures/api/schemas/types/nullable_string.json new file mode 100644 index 00000000000..e3b0baef849 --- /dev/null +++ b/spec/fixtures/api/schemas/types/nullable_string.json @@ -0,0 +1,6 @@ +{ + "oneOf": [ + { "type": "null" }, + { "type": "string" } + ] +} diff --git a/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml new file mode 100644 index 00000000000..0bab94a7c2e --- /dev/null +++ b/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml @@ -0,0 +1,10 @@ +before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + +rspec: + script: + - bundle exec rspec diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb index 14297a1a544..1238cfbd1e7 100644 --- a/spec/helpers/application_helper_spec.rb +++ b/spec/helpers/application_helper_spec.rb @@ -3,48 +3,66 @@ require 'spec_helper' describe ApplicationHelper do describe 'current_controller?' do - it 'returns true when controller matches argument' do + before do stub_controller_name('foo') + end - expect(helper.current_controller?(:foo)).to eq true + it 'returns true when controller matches argument' do + expect(helper.current_controller?(:foo)).to be_truthy end it 'returns false when controller does not match argument' do - stub_controller_name('foo') - - expect(helper.current_controller?(:bar)).to eq false + expect(helper.current_controller?(:bar)).to be_falsey end it 'takes any number of arguments' do - stub_controller_name('foo') + expect(helper.current_controller?(:baz, :bar)).to be_falsey + expect(helper.current_controller?(:baz, :bar, :foo)).to be_truthy + end + + context 'when namespaced' do + before do + stub_controller_path('bar/foo') + end + + it 'returns true when controller matches argument' do + expect(helper.current_controller?(:foo)).to be_truthy + end - expect(helper.current_controller?(:baz, :bar)).to eq false - expect(helper.current_controller?(:baz, :bar, :foo)).to eq true + it 'returns true when controller and namespace matches argument in path notation' do + expect(helper.current_controller?('bar/foo')).to be_truthy + end + + it 'returns false when namespace doesnt match' do + expect(helper.current_controller?('foo/foo')).to be_falsey + end end def stub_controller_name(value) allow(helper.controller).to receive(:controller_name).and_return(value) end + + def stub_controller_path(value) + allow(helper.controller).to receive(:controller_path).and_return(value) + end end describe 'current_action?' do - it 'returns true when action matches' do + before do stub_action_name('foo') + end - expect(helper.current_action?(:foo)).to eq true + it 'returns true when action matches' do + expect(helper.current_action?(:foo)).to be_truthy end it 'returns false when action does not match' do - stub_action_name('foo') - - expect(helper.current_action?(:bar)).to eq false + expect(helper.current_action?(:bar)).to be_falsey end it 'takes any number of arguments' do - stub_action_name('foo') - - expect(helper.current_action?(:baz, :bar)).to eq false - expect(helper.current_action?(:baz, :bar, :foo)).to eq true + expect(helper.current_action?(:baz, :bar)).to be_falsey + expect(helper.current_action?(:baz, :bar, :foo)).to be_truthy end def stub_action_name(value) @@ -100,8 +118,7 @@ describe ApplicationHelper do end it 'accepts a custom html_class' do - expect(element(html_class: 'custom_class').attr('class')) - .to eq 'js-timeago custom_class' + expect(element(html_class: 'custom_class').attr('class')).to eq 'js-timeago custom_class' end it 'accepts a custom tooltip placement' do @@ -114,6 +131,7 @@ describe ApplicationHelper do it 'add class for the short format' do timeago_element = element(short_format: 'short') + expect(timeago_element.attr('class')).to eq 'js-short-timeago' expect(timeago_element.next_element).to eq nil end @@ -128,11 +146,9 @@ describe ApplicationHelper do context 'when alternate support url is specified' do let(:alternate_url) { 'http://company.example.com/getting-help' } - before do + it 'returns the alternate support url' do stub_application_setting(help_page_support_url: alternate_url) - end - it 'returns the alternate support url' do expect(helper.support_url).to eq(alternate_url) end end @@ -155,9 +171,12 @@ describe ApplicationHelper do describe '#autocomplete_data_sources' do let(:project) { create(:project) } let(:noteable_type) { Issue } + it 'returns paths for autocomplete_sources_controller' do sources = helper.autocomplete_data_sources(project, noteable_type) + expect(sources.keys).to match_array([:members, :issues, :mergeRequests, :labels, :milestones, :commands]) + sources.keys.each do |key| expect(sources[key]).not_to be_nil end diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb index 1950c2b129b..75c30dbfe48 100644 --- a/spec/helpers/auto_devops_helper_spec.rb +++ b/spec/helpers/auto_devops_helper_spec.rb @@ -16,7 +16,15 @@ describe AutoDevopsHelper do subject { helper.show_auto_devops_callout?(project) } - context 'when all conditions are met' do + context 'when auto devops is implicitly enabled' do + it { is_expected.to eq(false) } + end + + context 'when auto devops is not implicitly enabled' do + before do + Gitlab::CurrentSettings.update!(auto_devops_enabled: false) + end + it { is_expected.to eq(true) } end diff --git a/spec/helpers/markup_helper_spec.rb b/spec/helpers/markup_helper_spec.rb index 9a29ac26eff..a0c0af94fa5 100644 --- a/spec/helpers/markup_helper_spec.rb +++ b/spec/helpers/markup_helper_spec.rb @@ -339,11 +339,25 @@ describe MarkupHelper do expect(first_line_in_markdown(object, attribute, 150, project: project)).to eq(expected) end - it 'preserves data-src for lazy images' do - object = create_object("![ImageTest](/uploads/test.png)") - image_url = "data-src=\".*/uploads/test.png\"" + context 'when images are allowed' do + it 'preserves data-src for lazy images' do + object = create_object("![ImageTest](/uploads/test.png)") + image_url = "data-src=\".*/uploads/test.png\"" + text = first_line_in_markdown(object, attribute, 150, project: project, allow_images: true) + + expect(text).to match(image_url) + expect(text).to match('<a') + end + end - expect(first_line_in_markdown(object, attribute, 150, project: project)).to match(image_url) + context 'when images are not allowed' do + it 'removes any images' do + object = create_object("![ImageTest](/uploads/test.png)") + text = first_line_in_markdown(object, attribute, 150, project: project) + + expect(text).not_to match('<img') + expect(text).not_to match('<a') + end end context 'labels formatting' do diff --git a/spec/helpers/tab_helper_spec.rb b/spec/helpers/tab_helper_spec.rb index b473c0a7416..9abf63d4bd4 100644 --- a/spec/helpers/tab_helper_spec.rb +++ b/spec/helpers/tab_helper_spec.rb @@ -9,31 +9,71 @@ describe TabHelper do allow(self).to receive(:action_name).and_return('foo') end - it "captures block output" do - expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/) + context 'with the content of the li' do + it "captures block output" do + expect(nav_link { "Testing Blocks" }).to match(/Testing Blocks/) + end end - it "performs checks on the current controller" do - expect(nav_link(controller: :foo)).to match(/<li class="active">/) - expect(nav_link(controller: :bar)).not_to match(/active/) - expect(nav_link(controller: [:foo, :bar])).to match(/active/) - end + context 'with controller param' do + it "performs checks on the current controller" do + expect(nav_link(controller: :foo)).to match(/<li class="active">/) + expect(nav_link(controller: :bar)).not_to match(/active/) + expect(nav_link(controller: [:foo, :bar])).to match(/active/) + end + + context 'with action param' do + it "performs checks on both controller and action when both are present" do + expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/) + expect(nav_link(controller: :foo, action: :foo)).to match(/active/) + end + end + + context 'with namespace in path notation' do + before do + allow(controller).to receive(:controller_path).and_return('bar/foo') + end - it "performs checks on the current action" do - expect(nav_link(action: :foo)).to match(/<li class="active">/) - expect(nav_link(action: :bar)).not_to match(/active/) - expect(nav_link(action: [:foo, :bar])).to match(/active/) + it 'performs checks on both controller and namespace' do + expect(nav_link(controller: 'foo/foo')).not_to match(/active/) + expect(nav_link(controller: 'bar/foo')).to match(/active/) + end + + context 'with action param' do + it "performs checks on both namespace, controller and action when they are all present" do + expect(nav_link(controller: 'foo/foo', action: :foo)).not_to match(/active/) + expect(nav_link(controller: 'bar/foo', action: :bar)).not_to match(/active/) + expect(nav_link(controller: 'bar/foo', action: :foo)).to match(/active/) + end + end + end end - it "performs checks on both controller and action when both are present" do - expect(nav_link(controller: :bar, action: :foo)).not_to match(/active/) - expect(nav_link(controller: :foo, action: :bar)).not_to match(/active/) - expect(nav_link(controller: :foo, action: :foo)).to match(/active/) + context 'with action param' do + it "performs checks on the current action" do + expect(nav_link(action: :foo)).to match(/<li class="active">/) + expect(nav_link(action: :bar)).not_to match(/active/) + expect(nav_link(action: [:foo, :bar])).to match(/active/) + end end - it "accepts a path shorthand" do - expect(nav_link(path: 'foo#bar')).not_to match(/active/) - expect(nav_link(path: 'foo#foo')).to match(/active/) + context 'with path param' do + it "accepts a path shorthand" do + expect(nav_link(path: 'foo#bar')).not_to match(/active/) + expect(nav_link(path: 'foo#foo')).to match(/active/) + end + + context 'with namespace' do + before do + allow(controller).to receive(:controller_path).and_return('bar/foo') + end + + it 'accepts a path shorthand with namespace' do + expect(nav_link(path: 'bar/foo#foo')).to match(/active/) + expect(nav_link(path: 'foo/foo#foo')).not_to match(/active/) + end + end end it "passes extra html options to the list element" do diff --git a/spec/javascripts/.eslintrc.yml b/spec/javascripts/.eslintrc.yml index 5525c9f5bd0..9b2c84ce9f5 100644 --- a/spec/javascripts/.eslintrc.yml +++ b/spec/javascripts/.eslintrc.yml @@ -35,3 +35,9 @@ rules: - error - ignore: - 'fixtures/blob' + # Temporarily disabled to facilitate an upgrade to eslint-plugin-jasmine + jasmine/new-line-before-expect: off + jasmine/new-line-between-declarations: off + jasmine/no-promise-without-done-fail: off + jasmine/prefer-jasmine-matcher: off + jasmine/prefer-toHaveBeenCalledWith: off diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js index a4753ab7cde..01b5bc112b2 100644 --- a/spec/javascripts/shortcuts_issuable_spec.js +++ b/spec/javascripts/behaviors/shortcuts/shortcuts_issuable_spec.js @@ -1,6 +1,6 @@ import $ from 'jquery'; import initCopyAsGFM from '~/behaviors/markdown/copy_as_gfm'; -import ShortcutsIssuable from '~/shortcuts_issuable'; +import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable'; initCopyAsGFM(); diff --git a/spec/javascripts/boards/board_blank_state_spec.js b/spec/javascripts/boards/board_blank_state_spec.js index 89a4fae4b59..0e4e1697fd0 100644 --- a/spec/javascripts/boards/board_blank_state_spec.js +++ b/spec/javascripts/boards/board_blank_state_spec.js @@ -64,7 +64,7 @@ describe('Boards blank state', () => { }); it('creates pre-defined labels', (done) => { - vm.$el.querySelector('.btn-create').click(); + vm.$el.querySelector('.btn-success').click(); setTimeout(() => { expect(gl.issueBoards.BoardsStore.state.lists.length).toBe(2); @@ -78,7 +78,7 @@ describe('Boards blank state', () => { it('resets the store if request fails', (done) => { fail = true; - vm.$el.querySelector('.btn-create').click(); + vm.$el.querySelector('.btn-success').click(); setTimeout(() => { expect(gl.issueBoards.BoardsStore.welcomeIsHidden()).toBeFalsy(); diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js index a234c81fadf..3257a3fb8a3 100644 --- a/spec/javascripts/boards/modal_store_spec.js +++ b/spec/javascripts/boards/modal_store_spec.js @@ -11,7 +11,7 @@ describe('Modal store', () => { let issue2; beforeEach(() => { - // Setup default state + // Set up default state Store.store.issues = []; Store.store.selectedIssues = []; diff --git a/spec/javascripts/close_reopen_report_toggle_spec.js b/spec/javascripts/close_reopen_report_toggle_spec.js index 925e959c85a..412abe2cbf8 100644 --- a/spec/javascripts/close_reopen_report_toggle_spec.js +++ b/spec/javascripts/close_reopen_report_toggle_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable jasmine/no-unsafe-spy */ + import CloseReopenReportToggle from '~/close_reopen_report_toggle'; import DropLab from '~/droplab/drop_lab'; diff --git a/spec/javascripts/deploy_keys/components/app_spec.js b/spec/javascripts/deploy_keys/components/app_spec.js index 183d7cf2d41..cd147bb2935 100644 --- a/spec/javascripts/deploy_keys/components/app_spec.js +++ b/spec/javascripts/deploy_keys/components/app_spec.js @@ -11,7 +11,7 @@ describe('Deploy keys app component', () => { let mock; beforeEach((done) => { - // setup axios mock before component + // set up axios mock before component mock = new MockAdapter(axios); mock.onGet(`${TEST_HOST}/dummy/`).replyOnce(200, data); diff --git a/spec/javascripts/diffs/components/app_spec.js b/spec/javascripts/diffs/components/app_spec.js index 7237274eb43..7be44a26ded 100644 --- a/spec/javascripts/diffs/components/app_spec.js +++ b/spec/javascripts/diffs/components/app_spec.js @@ -1 +1,71 @@ -// TODO: https://gitlab.com/gitlab-org/gitlab-ce/issues/48034 +import Vue from 'vue'; +import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import { TEST_HOST } from 'spec/test_constants'; +import App from '~/diffs/components/app.vue'; +import createDiffsStore from '../create_diffs_store'; + +describe('diffs/components/app', () => { + const oldMrTabs = window.mrTabs; + const Component = Vue.extend(App); + + let vm; + + beforeEach(() => { + // setup globals (needed for component to mount :/) + window.mrTabs = jasmine.createSpyObj('mrTabs', ['resetViewContainer']); + + // setup component + const store = createDiffsStore(); + store.state.diffs.isLoading = false; + + vm = mountComponentWithStore(Component, { + store, + props: { + endpoint: `${TEST_HOST}/diff/endpoint`, + projectPath: 'namespace/project', + currentUser: {}, + }, + }); + }); + + afterEach(() => { + // reset globals + window.mrTabs = oldMrTabs; + + // reset component + vm.$destroy(); + }); + + it('shows comments message, with commit', done => { + vm.$store.state.diffs.commit = {}; + + vm.$nextTick() + .then(() => { + expect(vm.$el).toContainText('Only comments from the following commit are shown below'); + }) + .then(done) + .catch(done.fail); + }); + + it('shows comments message, with old mergeRequestDiff', done => { + vm.$store.state.diffs.mergeRequestDiff = { latest: false }; + + vm.$nextTick() + .then(() => { + expect(vm.$el).toContainText("Not all comments are displayed because you're viewing an old version of the diff."); + }) + .then(done) + .catch(done.fail); + }); + + it('shows comments message, with startVersion', done => { + vm.$store.state.diffs.startVersion = 'test'; + + vm.$nextTick() + .then(() => { + expect(vm.$el).toContainText("Not all comments are displayed because you're comparing two versions of the diff."); + }) + .then(done) + .catch(done.fail); + }); +}); diff --git a/spec/javascripts/diffs/components/changed_files_spec.js b/spec/javascripts/diffs/components/changed_files_spec.js index f737e8fa38e..7f21273a991 100644 --- a/spec/javascripts/diffs/components/changed_files_spec.js +++ b/spec/javascripts/diffs/components/changed_files_spec.js @@ -8,7 +8,7 @@ describe('ChangedFiles', () => { const Component = Vue.extend(changedFiles); const store = new Vuex.Store({ modules: { - diffs: diffsModule, + diffs: diffsModule(), }, }); diff --git a/spec/javascripts/diffs/components/diff_file_header_spec.js b/spec/javascripts/diffs/components/diff_file_header_spec.js index 92b2004c4d7..c986ea604b2 100644 --- a/spec/javascripts/diffs/components/diff_file_header_spec.js +++ b/spec/javascripts/diffs/components/diff_file_header_spec.js @@ -16,8 +16,8 @@ describe('diff_file_header', () => { const store = new Vuex.Store({ modules: { - diffs: diffsModule, - notes: notesModule, + diffs: diffsModule(), + notes: notesModule(), }, }); @@ -450,13 +450,14 @@ describe('diff_file_header', () => { propsCopy.diffFile.deletedFile = true; const discussionGetter = () => [diffDiscussionMock]; - notesModule.getters.discussions = discussionGetter; + const notesModuleMock = notesModule(); + notesModuleMock.getters.discussions = discussionGetter; vm = mountComponentWithStore(Component, { props: propsCopy, store: new Vuex.Store({ modules: { - diffs: diffsModule, - notes: notesModule, + diffs: diffsModule(), + notes: notesModuleMock, }, }), }); diff --git a/spec/javascripts/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js index 845fef23db6..2a52cd2b179 100644 --- a/spec/javascripts/diffs/components/diff_file_spec.js +++ b/spec/javascripts/diffs/components/diff_file_spec.js @@ -63,6 +63,18 @@ describe('DiffFile', () => { }); }); + it('should have collapsed text and link even before rendered', done => { + vm.file.renderIt = false; + vm.file.collapsed = true; + + vm.$nextTick(() => { + expect(vm.$el.innerText).toContain('This diff is collapsed'); + expect(vm.$el.querySelectorAll('.js-click-to-expand').length).toEqual(1); + + done(); + }); + }); + it('should have loading icon while loading a collapsed diffs', done => { vm.file.collapsed = true; vm.isLoadingCollapsedDiff = true; diff --git a/spec/javascripts/diffs/components/diff_line_note_form_spec.js b/spec/javascripts/diffs/components/diff_line_note_form_spec.js index 6fe5fdaf7f9..f31fc1f0e2b 100644 --- a/spec/javascripts/diffs/components/diff_line_note_form_spec.js +++ b/spec/javascripts/diffs/components/diff_line_note_form_spec.js @@ -69,22 +69,21 @@ describe('DiffLineNoteForm', () => { describe('saveNoteForm', () => { it('should call saveNote action with proper params', done => { - let isPromiseCalled = false; - const formDataSpy = spyOnDependency(DiffLineNoteForm, 'getNoteFormData').and.returnValue({ - postData: 1, - }); - const saveNoteSpy = spyOn(component, 'saveNote').and.returnValue( - new Promise(() => { - isPromiseCalled = true; - done(); - }), + const saveDiffDiscussionSpy = spyOn(component, 'saveDiffDiscussion').and.returnValue( + Promise.resolve(), ); - - component.handleSaveNote('note body'); - - expect(formDataSpy).toHaveBeenCalled(); - expect(saveNoteSpy).toHaveBeenCalled(); - expect(isPromiseCalled).toEqual(true); + spyOnProperty(component, 'formData').and.returnValue('formData'); + + component + .handleSaveNote('note body') + .then(() => { + expect(saveDiffDiscussionSpy).toHaveBeenCalledWith({ + note: 'note body', + formData: 'formData', + }); + }) + .then(done) + .catch(done.fail); }); }); }); diff --git a/spec/javascripts/diffs/create_diffs_store.js b/spec/javascripts/diffs/create_diffs_store.js new file mode 100644 index 00000000000..aacde99964c --- /dev/null +++ b/spec/javascripts/diffs/create_diffs_store.js @@ -0,0 +1,15 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import diffsModule from '~/diffs/store/modules'; +import notesModule from '~/notes/stores/modules'; + +Vue.use(Vuex); + +export default function createDiffsStore() { + return new Vuex.Store({ + modules: { + diffs: diffsModule(), + notes: notesModule(), + }, + }); +} diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js index cfb8f862598..05b39bad6ea 100644 --- a/spec/javascripts/diffs/store/actions_spec.js +++ b/spec/javascripts/diffs/store/actions_spec.js @@ -5,7 +5,24 @@ import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE, } from '~/diffs/constants'; -import * as actions from '~/diffs/store/actions'; +import actions, { + setBaseConfig, + fetchDiffFiles, + assignDiscussionsToDiff, + removeDiscussionsFromDiff, + startRenderDiffsQueue, + setInlineDiffViewType, + setParallelDiffViewType, + showCommentForm, + cancelCommentForm, + loadMoreLines, + scrollToLineIfNeededInline, + scrollToLineIfNeededParallel, + loadCollapsedDiff, + expandAllFiles, + toggleFileDiscussions, + saveDiffDiscussion, +} from '~/diffs/store/actions'; import * as types from '~/diffs/store/mutation_types'; import { reduceDiscussionsToLineCodes } from '~/notes/stores/utils'; import axios from '~/lib/utils/axios_utils'; @@ -37,7 +54,7 @@ describe('DiffsStoreActions', () => { const projectPath = '/root/project'; testAction( - actions.setBaseConfig, + setBaseConfig, { endpoint, projectPath }, { endpoint: '', projectPath: '' }, [{ type: types.SET_BASE_CONFIG, payload: { endpoint, projectPath } }], @@ -55,7 +72,7 @@ describe('DiffsStoreActions', () => { mock.onGet(endpoint).reply(200, res); testAction( - actions.fetchDiffFiles, + fetchDiffFiles, {}, { endpoint }, [ @@ -139,7 +156,7 @@ describe('DiffsStoreActions', () => { const discussions = reduceDiscussionsToLineCodes([singleDiscussion]); testAction( - actions.assignDiscussionsToDiff, + assignDiscussionsToDiff, discussions, state, [ @@ -157,6 +174,7 @@ describe('DiffsStoreActions', () => { newPath: 'file1', oldLine: 5, oldPath: 'file2', + lineCode: 'ABC_1_1', }, }, }, @@ -207,7 +225,7 @@ describe('DiffsStoreActions', () => { }; testAction( - actions.removeDiscussionsFromDiff, + removeDiscussionsFromDiff, singleDiscussion, state, [ @@ -228,7 +246,7 @@ describe('DiffsStoreActions', () => { }); describe('startRenderDiffsQueue', () => { - it('should set all files to RENDER_FILE', done => { + it('should set all files to RENDER_FILE', () => { const state = { diffFiles: [ { @@ -251,24 +269,17 @@ describe('DiffsStoreActions', () => { }); }; - actions - .startRenderDiffsQueue({ state, commit: pseudoCommit }) - .then(() => { - expect(state.diffFiles[0].renderIt).toBeTruthy(); - expect(state.diffFiles[1].renderIt).toBeTruthy(); + startRenderDiffsQueue({ state, commit: pseudoCommit }); - done(); - }) - .catch(() => { - done.fail(); - }); + expect(state.diffFiles[0].renderIt).toBe(true); + expect(state.diffFiles[1].renderIt).toBe(true); }); }); describe('setInlineDiffViewType', () => { it('should set diff view type to inline and also set the cookie properly', done => { testAction( - actions.setInlineDiffViewType, + setInlineDiffViewType, null, {}, [{ type: types.SET_DIFF_VIEW_TYPE, payload: INLINE_DIFF_VIEW_TYPE }], @@ -286,7 +297,7 @@ describe('DiffsStoreActions', () => { describe('setParallelDiffViewType', () => { it('should set diff view type to parallel and also set the cookie properly', done => { testAction( - actions.setParallelDiffViewType, + setParallelDiffViewType, null, {}, [{ type: types.SET_DIFF_VIEW_TYPE, payload: PARALLEL_DIFF_VIEW_TYPE }], @@ -306,7 +317,7 @@ describe('DiffsStoreActions', () => { const payload = { lineCode: 'lineCode' }; testAction( - actions.showCommentForm, + showCommentForm, payload, {}, [{ type: types.ADD_COMMENT_FORM_LINE, payload }], @@ -321,7 +332,7 @@ describe('DiffsStoreActions', () => { const payload = { lineCode: 'lineCode' }; testAction( - actions.cancelCommentForm, + cancelCommentForm, payload, {}, [{ type: types.REMOVE_COMMENT_FORM_LINE, payload }], @@ -343,7 +354,7 @@ describe('DiffsStoreActions', () => { mock.onGet(endpoint).reply(200, contextLines); testAction( - actions.loadMoreLines, + loadMoreLines, options, {}, [ @@ -369,7 +380,7 @@ describe('DiffsStoreActions', () => { mock.onGet(file.loadCollapsedDiffUrl).reply(200, data); testAction( - actions.loadCollapsedDiff, + loadCollapsedDiff, file, {}, [ @@ -390,7 +401,7 @@ describe('DiffsStoreActions', () => { describe('expandAllFiles', () => { it('should change the collapsed prop from the diffFiles', done => { testAction( - actions.expandAllFiles, + expandAllFiles, null, {}, [ @@ -414,7 +425,7 @@ describe('DiffsStoreActions', () => { const dispatch = jasmine.createSpy('dispatch'); - actions.toggleFileDiscussions({ getters, dispatch }); + toggleFileDiscussions({ getters, dispatch }); expect(dispatch).toHaveBeenCalledWith( 'collapseDiscussion', @@ -432,7 +443,7 @@ describe('DiffsStoreActions', () => { const dispatch = jasmine.createSpy(); - actions.toggleFileDiscussions({ getters, dispatch }); + toggleFileDiscussions({ getters, dispatch }); expect(dispatch).toHaveBeenCalledWith( 'expandDiscussion', @@ -450,7 +461,7 @@ describe('DiffsStoreActions', () => { const dispatch = jasmine.createSpy(); - actions.toggleFileDiscussions({ getters, dispatch }); + toggleFileDiscussions({ getters, dispatch }); expect(dispatch).toHaveBeenCalledWith( 'expandDiscussion', @@ -459,4 +470,142 @@ describe('DiffsStoreActions', () => { ); }); }); + + describe('scrollToLineIfNeededInline', () => { + const lineMock = { + lineCode: 'ABC_123', + }; + + it('should not call handleLocationHash when there is not hash', () => { + window.location.hash = ''; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededInline({}, lineMock); + + expect(handleLocationHashSpy).not.toHaveBeenCalled(); + }); + + it('should not call handleLocationHash when the hash does not match any line', () => { + window.location.hash = 'XYZ_456'; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededInline({}, lineMock); + + expect(handleLocationHashSpy).not.toHaveBeenCalled(); + }); + + it('should call handleLocationHash only when the hash matches a line', () => { + window.location.hash = 'ABC_123'; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededInline( + {}, + { + lineCode: 'ABC_456', + }, + ); + scrollToLineIfNeededInline({}, lineMock); + scrollToLineIfNeededInline( + {}, + { + lineCode: 'XYZ_456', + }, + ); + + expect(handleLocationHashSpy).toHaveBeenCalled(); + expect(handleLocationHashSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('scrollToLineIfNeededParallel', () => { + const lineMock = { + left: null, + right: { + lineCode: 'ABC_123', + }, + }; + + it('should not call handleLocationHash when there is not hash', () => { + window.location.hash = ''; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededParallel({}, lineMock); + + expect(handleLocationHashSpy).not.toHaveBeenCalled(); + }); + + it('should not call handleLocationHash when the hash does not match any line', () => { + window.location.hash = 'XYZ_456'; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededParallel({}, lineMock); + + expect(handleLocationHashSpy).not.toHaveBeenCalled(); + }); + + it('should call handleLocationHash only when the hash matches a line', () => { + window.location.hash = 'ABC_123'; + + const handleLocationHashSpy = spyOnDependency(actions, 'handleLocationHash').and.stub(); + + scrollToLineIfNeededParallel( + {}, + { + left: null, + right: { + lineCode: 'ABC_456', + }, + }, + ); + scrollToLineIfNeededParallel({}, lineMock); + scrollToLineIfNeededParallel( + {}, + { + left: null, + right: { + lineCode: 'XYZ_456', + }, + }, + ); + + expect(handleLocationHashSpy).toHaveBeenCalled(); + expect(handleLocationHashSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('saveDiffDiscussion', () => { + beforeEach(() => { + spyOnDependency(actions, 'getNoteFormData').and.returnValue('testData'); + spyOnDependency(actions, 'reduceDiscussionsToLineCodes').and.returnValue('discussions'); + }); + + it('dispatches actions', done => { + const dispatch = jasmine.createSpy('dispatch').and.callFake(name => { + switch (name) { + case 'saveNote': + return Promise.resolve({ + discussion: 'test', + }); + case 'updateDiscussion': + return Promise.resolve('discussion'); + default: + return Promise.resolve({}); + } + }); + + saveDiffDiscussion({ dispatch }, { note: {}, formData: {} }) + .then(() => { + expect(dispatch.calls.argsFor(0)).toEqual(['saveNote', 'testData', { root: true }]); + expect(dispatch.calls.argsFor(1)).toEqual(['updateDiscussion', 'test', { root: true }]); + expect(dispatch.calls.argsFor(2)).toEqual(['assignDiscussionsToDiff', 'discussions']); + }) + .then(done) + .catch(done.fail); + }); + }); }); diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js index 7eeca6712cc..9a5d8dfbd15 100644 --- a/spec/javascripts/diffs/store/mutations_spec.js +++ b/spec/javascripts/diffs/store/mutations_spec.js @@ -162,6 +162,7 @@ describe('DiffsStoreMutations', () => { }; const state = { + latestDiff: true, diffFiles: [ { fileHash: 'ABC', @@ -229,6 +230,76 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); }); + + it('should add legacy discussions to the given line', () => { + const diffPosition = { + baseSha: 'ed13df29948c41ba367caa757ab3ec4892509910', + headSha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + newLine: null, + newPath: '500-lines-4.txt', + oldLine: 5, + oldPath: '500-lines-4.txt', + startSha: 'ed13df29948c41ba367caa757ab3ec4892509910', + lineCode: 'ABC_1', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + fileHash: 'ABC', + parallelDiffLines: [ + { + left: { + lineCode: 'ABC_1', + discussions: [], + }, + right: { + lineCode: 'ABC_1', + discussions: [], + }, + }, + ], + highlightedDiffLines: [ + { + lineCode: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussions = [ + { + id: 1, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + }, + { + id: 2, + line_code: 'ABC_1', + diff_discussion: true, + active: true, + }, + ]; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + fileHash: 'ABC', + discussions, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions.length).toEqual(2); + expect(state.diffFiles[0].parallelDiffLines[0].left.discussions[1].id).toEqual(2); + + expect(state.diffFiles[0].highlightedDiffLines[0].discussions.length).toEqual(2); + expect(state.diffFiles[0].highlightedDiffLines[0].discussions[1].id).toEqual(2); + }); }); describe('REMOVE_LINE_DISCUSSIONS', () => { diff --git a/spec/javascripts/diffs/store/utils_spec.js b/spec/javascripts/diffs/store/utils_spec.js index 4b5cf450c68..897cd1483aa 100644 --- a/spec/javascripts/diffs/store/utils_spec.js +++ b/spec/javascripts/diffs/store/utils_spec.js @@ -3,6 +3,7 @@ import { LINE_POSITION_LEFT, LINE_POSITION_RIGHT, TEXT_DIFF_POSITION_TYPE, + LEGACY_DIFF_NOTE_TYPE, DIFF_NOTE_TYPE, NEW_LINE_TYPE, OLD_LINE_TYPE, @@ -135,6 +136,7 @@ describe('DiffsStoreUtils', () => { note_project_id: '', target_type: options.noteableType, target_id: options.noteableData.id, + return_discussion: true, note: { noteable_type: options.noteableType, noteable_id: options.noteableData.id, @@ -151,6 +153,65 @@ describe('DiffsStoreUtils', () => { data: postData, }); }); + + it('should create legacy note form data', () => { + const diffFile = getDiffFileMock(); + delete diffFile.diffRefs.startSha; + delete diffFile.diffRefs.headSha; + + noteableDataMock.targetType = MERGE_REQUEST_NOTEABLE_TYPE; + + const options = { + note: 'Hello world!', + noteableData: noteableDataMock, + noteableType: MERGE_REQUEST_NOTEABLE_TYPE, + diffFile, + noteTargetLine: { + lineCode: '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_1_3', + metaData: null, + newLine: 3, + oldLine: 1, + }, + diffViewType: PARALLEL_DIFF_VIEW_TYPE, + linePosition: LINE_POSITION_LEFT, + }; + + const position = JSON.stringify({ + base_sha: diffFile.diffRefs.baseSha, + start_sha: undefined, + head_sha: undefined, + old_path: diffFile.oldPath, + new_path: diffFile.newPath, + position_type: TEXT_DIFF_POSITION_TYPE, + old_line: options.noteTargetLine.oldLine, + new_line: options.noteTargetLine.newLine, + }); + + const postData = { + view: options.diffViewType, + line_type: options.linePosition === LINE_POSITION_RIGHT ? NEW_LINE_TYPE : OLD_LINE_TYPE, + merge_request_diff_head_sha: undefined, + in_reply_to_discussion_id: '', + note_project_id: '', + target_type: options.noteableType, + target_id: options.noteableData.id, + return_discussion: true, + note: { + noteable_type: options.noteableType, + noteable_id: options.noteableData.id, + commit_id: '', + type: LEGACY_DIFF_NOTE_TYPE, + line_code: options.noteTargetLine.lineCode, + note: options.note, + position, + }, + }; + + expect(utils.getNoteFormData(options)).toEqual({ + endpoint: options.noteableData.create_note_path, + data: postData, + }); + }); }); describe('addLineReferences', () => { @@ -291,13 +352,72 @@ describe('DiffsStoreUtils', () => { it('returns true when the discussion is up to date', () => { expect( - utils.isDiscussionApplicableToLine(discussions.upToDateDiscussion1, diffPosition), + utils.isDiscussionApplicableToLine({ + discussion: discussions.upToDateDiscussion1, + diffPosition, + latestDiff: true, + }), ).toBe(true); }); it('returns false when the discussion is not up to date', () => { expect( - utils.isDiscussionApplicableToLine(discussions.outDatedDiscussion1, diffPosition), + utils.isDiscussionApplicableToLine({ + discussion: discussions.outDatedDiscussion1, + diffPosition, + latestDiff: true, + }), + ).toBe(false); + }); + + it('returns true when line codes match and discussion does not contain position and is not active', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: false }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + lineCode: 'ABC_1', + }, + latestDiff: true, + }), + ).toBe(false); + }); + + it('returns true when line codes match and discussion does not contain position and is active', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + lineCode: 'ABC_1', + }, + latestDiff: true, + }), + ).toBe(true); + }); + + it('returns false when not latest diff', () => { + const discussion = { ...discussions.outDatedDiscussion1, line_code: 'ABC_1', active: true }; + delete discussion.original_position; + delete discussion.position; + + expect( + utils.isDiscussionApplicableToLine({ + discussion, + diffPosition: { + ...diffPosition, + lineCode: 'ABC_1', + }, + latestDiff: false, + }), ).toBe(false); }); }); diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js index d926663fac0..9d670afe206 100644 --- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js +++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import eventHub from '~/filtered_search/event_hub'; import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue'; -import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; const createComponent = (propsData) => { const Component = Vue.extend(RecentSearchesDropdownContent); @@ -18,14 +18,14 @@ const trimMarkupWhitespace = text => text.replace(/(\n|\s)+/gm, ' ').trim(); describe('RecentSearchesDropdownContent', () => { const propsDataWithoutItems = { items: [], - allowedKeys: FilteredSearchTokenKeys.getKeys(), + allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), }; const propsDataWithItems = { items: [ 'foo', 'author:@root label:~foo bar', ], - allowedKeys: FilteredSearchTokenKeys.getKeys(), + allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), }; let vm; diff --git a/spec/javascripts/filtered_search/dropdown_user_spec.js b/spec/javascripts/filtered_search/dropdown_user_spec.js index c37a964975d..b48b1456eff 100644 --- a/spec/javascripts/filtered_search/dropdown_user_spec.js +++ b/spec/javascripts/filtered_search/dropdown_user_spec.js @@ -1,7 +1,7 @@ import DropdownUtils from '~/filtered_search/dropdown_utils'; import DropdownUser from '~/filtered_search/dropdown_user'; import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; -import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; +import IssuableFilteredTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; describe('Dropdown User', () => { describe('getSearchInput', () => { @@ -14,7 +14,7 @@ describe('Dropdown User', () => { spyOn(DropdownUtils, 'getSearchInput').and.callFake(() => {}); dropdownUser = new DropdownUser({ - tokenKeys: FilteredSearchTokenKeys, + tokenKeys: IssuableFilteredTokenKeys, }); }); diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js index 3d6dec19eca..8792e99d461 100644 --- a/spec/javascripts/filtered_search/dropdown_utils_spec.js +++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js @@ -1,6 +1,6 @@ import DropdownUtils from '~/filtered_search/dropdown_utils'; import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager'; -import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper'; describe('Dropdown Utils', () => { @@ -137,7 +137,7 @@ describe('Dropdown Utils', () => { `); input = document.getElementById('test'); - allowedKeys = FilteredSearchTokenKeys.getKeys(); + allowedKeys = IssuableFilteredSearchTokenKeys.getKeys(); }); function config() { diff --git a/spec/javascripts/filtered_search/filtered_search_manager_spec.js b/spec/javascripts/filtered_search/filtered_search_manager_spec.js index 8fcee36beb8..a03d5a31b41 100644 --- a/spec/javascripts/filtered_search/filtered_search_manager_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_manager_spec.js @@ -1,7 +1,7 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_service'; import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error'; import RecentSearchesRoot from '~/filtered_search/recent_searches_root'; -import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import '~/lib/utils/common_utils'; import DropdownUtils from '~/filtered_search/dropdown_utils'; import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens'; @@ -86,7 +86,7 @@ describe('Filtered Search Manager', function () { expect(RecentSearchesService.isAvailable).toHaveBeenCalled(); expect(RecentSearchesStoreSpy).toHaveBeenCalledWith({ isLocalStorageAvailable, - allowedKeys: FilteredSearchTokenKeys.getKeys(), + allowedKeys: IssuableFilteredSearchTokenKeys.getKeys(), }); }); }); diff --git a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js index 68158cf52e4..ab0ab72720e 100644 --- a/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_token_keys_spec.js @@ -1,26 +1,36 @@ import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; describe('Filtered Search Token Keys', () => { - describe('get', () => { - let tokenKeys; + const tokenKeys = [{ + key: 'author', + type: 'string', + param: 'username', + symbol: '@', + icon: 'pencil', + tag: '@author', + }]; + + const conditions = [{ + url: 'assignee_id=0', + tokenKey: 'assignee', + value: 'none', + }]; - beforeEach(() => { - tokenKeys = FilteredSearchTokenKeys.get(); - }); + describe('get', () => { it('should return tokenKeys', () => { - expect(tokenKeys !== null).toBe(true); + expect(new FilteredSearchTokenKeys().get() !== null).toBe(true); }); it('should return tokenKeys as an array', () => { - expect(tokenKeys instanceof Array).toBe(true); + expect(new FilteredSearchTokenKeys().get() instanceof Array).toBe(true); }); }); describe('getKeys', () => { it('should return keys', () => { - const getKeys = FilteredSearchTokenKeys.getKeys(); - const keys = FilteredSearchTokenKeys.get().map(i => i.key); + const getKeys = new FilteredSearchTokenKeys(tokenKeys).getKeys(); + const keys = new FilteredSearchTokenKeys(tokenKeys).get().map(i => i.key); keys.forEach((key, i) => { expect(key).toEqual(getKeys[i]); @@ -29,88 +39,78 @@ describe('Filtered Search Token Keys', () => { }); describe('getConditions', () => { - let conditions; - - beforeEach(() => { - conditions = FilteredSearchTokenKeys.getConditions(); - }); - it('should return conditions', () => { - expect(conditions !== null).toBe(true); + expect(new FilteredSearchTokenKeys().getConditions() !== null).toBe(true); }); it('should return conditions as an array', () => { - expect(conditions instanceof Array).toBe(true); + expect(new FilteredSearchTokenKeys().getConditions() instanceof Array).toBe(true); }); }); describe('searchByKey', () => { it('should return null when key not found', () => { - const tokenKey = FilteredSearchTokenKeys.searchByKey('notakey'); + const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKey('notakey'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key', () => { - const tokenKeys = FilteredSearchTokenKeys.get(); - const result = FilteredSearchTokenKeys.searchByKey(tokenKeys[0].key); + const result = new FilteredSearchTokenKeys(tokenKeys).searchByKey(tokenKeys[0].key); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchBySymbol', () => { it('should return null when symbol not found', () => { - const tokenKey = FilteredSearchTokenKeys.searchBySymbol('notasymbol'); + const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchBySymbol('notasymbol'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by symbol', () => { - const tokenKeys = FilteredSearchTokenKeys.get(); - const result = FilteredSearchTokenKeys.searchBySymbol(tokenKeys[0].symbol); + const result = new FilteredSearchTokenKeys(tokenKeys).searchBySymbol(tokenKeys[0].symbol); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByKeyParam', () => { it('should return null when key param not found', () => { - const tokenKey = FilteredSearchTokenKeys.searchByKeyParam('notakeyparam'); + const tokenKey = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam('notakeyparam'); expect(tokenKey === null).toBe(true); }); it('should return tokenKey when found by key param', () => { - const tokenKeys = FilteredSearchTokenKeys.get(); - const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const result = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); it('should return alternative tokenKey when found by key param', () => { - const tokenKeys = FilteredSearchTokenKeys.getAlternatives(); - const result = FilteredSearchTokenKeys.searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); + const result = new FilteredSearchTokenKeys(tokenKeys).searchByKeyParam(`${tokenKeys[0].key}_${tokenKeys[0].param}`); expect(result).toEqual(tokenKeys[0]); }); }); describe('searchByConditionUrl', () => { it('should return null when condition url not found', () => { - const condition = FilteredSearchTokenKeys.searchByConditionUrl(null); + const condition = new FilteredSearchTokenKeys([], [], conditions).searchByConditionUrl(null); expect(condition === null).toBe(true); }); it('should return condition when found by url', () => { - const conditions = FilteredSearchTokenKeys.getConditions(); - const result = FilteredSearchTokenKeys.searchByConditionUrl(conditions[0].url); + const result = new FilteredSearchTokenKeys([], [], conditions) + .searchByConditionUrl(conditions[0].url); expect(result).toBe(conditions[0]); }); }); describe('searchByConditionKeyValue', () => { it('should return null when condition tokenKey and value not found', () => { - const condition = FilteredSearchTokenKeys.searchByConditionKeyValue(null, null); + const condition = new FilteredSearchTokenKeys([], [], conditions) + .searchByConditionKeyValue(null, null); expect(condition === null).toBe(true); }); it('should return condition when found by tokenKey and value', () => { - const conditions = FilteredSearchTokenKeys.getConditions(); - const result = FilteredSearchTokenKeys + const result = new FilteredSearchTokenKeys([], [], conditions) .searchByConditionKeyValue(conditions[0].tokenKey, conditions[0].value); expect(result).toEqual(conditions[0]); }); diff --git a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js index 465f5f79931..4f9f546cbb5 100644 --- a/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js +++ b/spec/javascripts/filtered_search/filtered_search_tokenizer_spec.js @@ -1,8 +1,8 @@ -import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys'; +import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys'; import FilteredSearchTokenizer from '~/filtered_search/filtered_search_tokenizer'; describe('Filtered Search Tokenizer', () => { - const allowedKeys = FilteredSearchTokenKeys.getKeys(); + const allowedKeys = IssuableFilteredSearchTokenKeys.getKeys(); describe('processTokens', () => { it('returns for input containing only search value', () => { diff --git a/spec/javascripts/gfm_auto_complete_spec.js b/spec/javascripts/gfm_auto_complete_spec.js index 1cb20a1e7ff..4f9cacf2724 100644 --- a/spec/javascripts/gfm_auto_complete_spec.js +++ b/spec/javascripts/gfm_auto_complete_spec.js @@ -146,7 +146,7 @@ describe('GfmAutoComplete', function () { shouldNotBeFollowedBy.forEach((followedSymbol) => { const seq = atSign + followedSymbol; - it(`should not match "${seq}"`, () => { + it(`should not match ${JSON.stringify(seq)}`, () => { expect(defaultMatcher(atwhoInstance, atSign, seq)).toBe(null); }); }); diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js index 76933cf337b..89c07d1f06d 100644 --- a/spec/javascripts/groups/components/app_spec.js +++ b/spec/javascripts/groups/components/app_spec.js @@ -24,6 +24,8 @@ const createComponent = (hideProjects = false) => { const store = new GroupsStore(false); const service = new GroupsService(mockEndpoint); + store.state.pageInfo = mockPageInfo; + return new Component({ propsData: { store, @@ -484,7 +486,6 @@ describe('AppComponent', () => { it('should render groups tree', done => { vm.store.state.groups = [mockParentGroupItem]; vm.isLoading = false; - vm.store.state.pageInfo = mockPageInfo; Vue.nextTick(() => { expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined(); done(); diff --git a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js index b45ae5bbb0f..bf48d7bfdad 100644 --- a/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js +++ b/spec/javascripts/ide/components/commit_sidebar/list_item_spec.js @@ -33,10 +33,6 @@ describe('Multi-file editor commit sidebar list item', () => { expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent).toContain(f.path); }); - it('renders actionn button', () => { - expect(vm.$el.querySelector('.multi-file-discard-btn')).not.toBeNull(); - }); - it('opens a closed file in the editor when clicking the file path', done => { spyOn(vm, 'openPendingTab').and.callThrough(); spyOn(router, 'push'); diff --git a/spec/javascripts/ide/components/file_row_extra_spec.js b/spec/javascripts/ide/components/file_row_extra_spec.js new file mode 100644 index 00000000000..60dabe28045 --- /dev/null +++ b/spec/javascripts/ide/components/file_row_extra_spec.js @@ -0,0 +1,159 @@ +import Vue from 'vue'; +import { createStore } from '~/ide/stores'; +import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; +import FileRowExtra from '~/ide/components/file_row_extra.vue'; +import { file, resetStore } from '../helpers'; + +describe('IDE extra file row component', () => { + let Component; + let vm; + let unstagedFilesCount = 0; + let stagedFilesCount = 0; + let changesCount = 0; + + beforeAll(() => { + Component = Vue.extend(FileRowExtra); + }); + + beforeEach(() => { + vm = createComponentWithStore(Component, createStore(), { + file: { + ...file('test'), + }, + mouseOver: false, + }); + + spyOnProperty(vm, 'getUnstagedFilesCountForPath').and.returnValue(() => unstagedFilesCount); + spyOnProperty(vm, 'getStagedFilesCountForPath').and.returnValue(() => stagedFilesCount); + spyOnProperty(vm, 'getChangesInFolder').and.returnValue(() => changesCount); + + vm.$mount(); + }); + + afterEach(() => { + vm.$destroy(); + resetStore(vm.$store); + + stagedFilesCount = 0; + unstagedFilesCount = 0; + changesCount = 0; + }); + + describe('folderChangesTooltip', () => { + it('returns undefined when changes count is 0', () => { + expect(vm.folderChangesTooltip).toBe(undefined); + }); + + it('returns unstaged changes text', () => { + changesCount = 1; + unstagedFilesCount = 1; + + expect(vm.folderChangesTooltip).toBe('1 unstaged change'); + }); + + it('returns staged changes text', () => { + changesCount = 1; + stagedFilesCount = 1; + + expect(vm.folderChangesTooltip).toBe('1 staged change'); + }); + + it('returns staged and unstaged changes text', () => { + changesCount = 1; + stagedFilesCount = 1; + unstagedFilesCount = 1; + + expect(vm.folderChangesTooltip).toBe('1 unstaged and 1 staged changes'); + }); + }); + + describe('show tree changes count', () => { + it('does not show for blobs', () => { + vm.file.type = 'blob'; + + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + }); + + it('does not show when changes count is 0', () => { + vm.file.type = 'tree'; + + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + }); + + it('does not show when tree is open', done => { + vm.file.type = 'tree'; + vm.file.opened = true; + changesCount = 1; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-tree-changes')).toBe(null); + + done(); + }); + }); + + it('shows for trees with changes', done => { + vm.file.type = 'tree'; + vm.file.opened = false; + changesCount = 1; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-tree-changes')).not.toBe(null); + + done(); + }); + }); + }); + + describe('changes file icon', () => { + it('hides when file is not changed', () => { + expect(vm.$el.querySelector('.ide-file-changed-icon')).toBe(null); + }); + + it('shows when file is changed', done => { + vm.file.changed = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('shows when file is staged', done => { + vm.file.staged = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null); + + done(); + }); + }); + + it('shows when file is a tempFile', done => { + vm.file.tempFile = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ide-file-changed-icon')).not.toBe(null); + + done(); + }); + }); + }); + + describe('merge request icon', () => { + it('hides when not a merge request change', () => { + expect(vm.$el.querySelector('.ic-git-merge')).toBe(null); + }); + + it('shows when a merge request change', done => { + vm.file.mrChange = true; + + vm.$nextTick(() => { + expect(vm.$el.querySelector('.ic-git-merge')).not.toBe(null); + + done(); + }); + }); + }); +}); diff --git a/spec/javascripts/ide/components/repo_commit_section_spec.js b/spec/javascripts/ide/components/repo_commit_section_spec.js index d09ccd7ac34..6c726c1e154 100644 --- a/spec/javascripts/ide/components/repo_commit_section_spec.js +++ b/spec/javascripts/ide/components/repo_commit_section_spec.js @@ -103,65 +103,6 @@ describe('RepoCommitSection', () => { }); }); - it('adds changed files into staged files', done => { - vm.$el.querySelector('.multi-file-discard-btn .btn').click(); - vm - .$nextTick() - .then(() => vm.$el.querySelector('.multi-file-discard-btn .btn').click()) - .then(vm.$nextTick) - .then(() => { - expect(vm.$el.querySelector('.ide-commit-list-container').textContent).toContain( - 'There are no unstaged changes', - ); - }) - .then(done) - .catch(done.fail); - }); - - it('stages a single file', done => { - vm.$el.querySelector('.multi-file-discard-btn .btn').click(); - - Vue.nextTick(() => { - expect( - vm.$el - .querySelector('.ide-commit-list-container') - .querySelectorAll('.multi-file-commit-list > li').length, - ).toBe(1); - - done(); - }); - }); - - it('discards a single file', done => { - vm.$el.querySelector('.multi-file-commit-list li:first-child .js-modal-primary-action').click(); - - Vue.nextTick(() => { - expect(vm.$el.querySelector('.ide-commit-list-container').textContent).not.toContain('file1'); - expect( - vm.$el - .querySelector('.ide-commit-list-container') - .querySelectorAll('.multi-file-commit-list > li').length, - ).toBe(1); - - done(); - }); - }); - - it('unstages a single file', done => { - vm.$el - .querySelectorAll('.multi-file-discard-btn')[2] - .querySelector('.btn') - .click(); - - Vue.nextTick(() => { - expect( - vm.$el.querySelectorAll('.ide-commit-list-container')[1].querySelectorAll('li').length, - ).toBe(1); - - done(); - }); - }); - describe('mounted', () => { it('opens last opened file', () => { expect(store.state.openFiles.length).toBe(1); diff --git a/spec/javascripts/ide/components/repo_file_spec.js b/spec/javascripts/ide/components/repo_file_spec.js deleted file mode 100644 index fc639a672e2..00000000000 --- a/spec/javascripts/ide/components/repo_file_spec.js +++ /dev/null @@ -1,145 +0,0 @@ -import Vue from 'vue'; -import store from '~/ide/stores'; -import repoFile from '~/ide/components/repo_file.vue'; -import router from '~/ide/ide_router'; -import { createComponentWithStore } from '../../helpers/vue_mount_component_helper'; -import { file } from '../helpers'; - -describe('RepoFile', () => { - let vm; - - function createComponent(propsData) { - const RepoFile = Vue.extend(repoFile); - - vm = createComponentWithStore(RepoFile, store, propsData); - - vm.$mount(); - } - - afterEach(() => { - vm.$destroy(); - }); - - it('renders link, icon and name', () => { - createComponent({ - file: file('t4'), - level: 0, - }); - - const name = vm.$el.querySelector('.ide-file-name'); - - expect(name.href).toMatch(''); - expect(name.textContent.trim()).toEqual(vm.file.name); - }); - - it('fires clickFile when the link is clicked', done => { - spyOn(router, 'push'); - createComponent({ - file: file('t3'), - level: 0, - }); - - vm.$el.querySelector('.file-name').click(); - - setTimeout(() => { - expect(router.push).toHaveBeenCalledWith(`/project${vm.file.url}`); - - done(); - }); - }); - - describe('folder', () => { - it('renders changes count inside folder', () => { - const f = { - ...file('folder'), - path: 'testing', - type: 'tree', - branchId: 'master', - projectId: 'project', - }; - - store.state.changedFiles.push({ - ...file('fileName'), - path: 'testing/fileName', - }); - - createComponent({ - file: f, - level: 0, - }); - - const treeChangesEl = vm.$el.querySelector('.ide-tree-changes'); - - expect(treeChangesEl).not.toBeNull(); - expect(treeChangesEl.textContent).toContain('1'); - }); - - it('renders action dropdown', done => { - createComponent({ - file: { - ...file('t4'), - type: 'tree', - branchId: 'master', - projectId: 'project', - }, - level: 0, - }); - - setTimeout(() => { - expect(vm.$el.querySelector('.ide-new-btn')).not.toBeNull(); - - done(); - }); - }); - }); - - describe('locked file', () => { - let f; - - beforeEach(() => { - f = file('locked file'); - f.file_lock = { - user: { - name: 'testuser', - updated_at: new Date(), - }, - }; - - createComponent({ - file: f, - level: 0, - }); - }); - - it('renders lock icon', () => { - expect(vm.$el.querySelector('.file-status-icon')).not.toBeNull(); - }); - - it('renders a tooltip', () => { - expect( - vm.$el.querySelector('.ide-file-name span:nth-child(2)').dataset.originalTitle, - ).toContain('Locked by testuser'); - }); - }); - - it('calls scrollIntoView if made active', done => { - createComponent({ - file: { - ...file(), - type: 'blob', - active: false, - }, - level: 0, - }); - - spyOn(vm, 'scrollIntoView'); - - vm.file.active = true; - - vm.$nextTick(() => { - expect(vm.scrollIntoView).toHaveBeenCalled(); - - done(); - }); - }); -}); diff --git a/spec/javascripts/issue_show/components/edit_actions_spec.js b/spec/javascripts/issue_show/components/edit_actions_spec.js index d779ab7bb31..004621f488a 100644 --- a/spec/javascripts/issue_show/components/edit_actions_spec.js +++ b/spec/javascripts/issue_show/components/edit_actions_spec.js @@ -21,6 +21,7 @@ describe('Edit Actions components', () => { propsData: { canDestroy: true, formState: store.formState, + issuableType: 'issue', }, }).$mount(); @@ -54,7 +55,7 @@ describe('Edit Actions components', () => { Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn-save').getAttribute('disabled'), + vm.$el.querySelector('.btn-success').getAttribute('disabled'), ).toBe('disabled'); done(); @@ -72,7 +73,7 @@ describe('Edit Actions components', () => { describe('updateIssuable', () => { it('sends update.issauble event when clicking save button', () => { - vm.$el.querySelector('.btn-save').click(); + vm.$el.querySelector('.btn-success').click(); expect( eventHub.$emit, @@ -80,11 +81,11 @@ describe('Edit Actions components', () => { }); it('shows loading icon after clicking save button', (done) => { - vm.$el.querySelector('.btn-save').click(); + vm.$el.querySelector('.btn-success').click(); Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn-save .fa'), + vm.$el.querySelector('.btn-success .fa'), ).not.toBeNull(); done(); @@ -92,11 +93,11 @@ describe('Edit Actions components', () => { }); it('disabled button after clicking save button', (done) => { - vm.$el.querySelector('.btn-save').click(); + vm.$el.querySelector('.btn-success').click(); Vue.nextTick(() => { expect( - vm.$el.querySelector('.btn-save').getAttribute('disabled'), + vm.$el.querySelector('.btn-success').getAttribute('disabled'), ).toBe('disabled'); done(); diff --git a/spec/javascripts/issue_show/components/form_spec.js b/spec/javascripts/issue_show/components/form_spec.js index 50ce019c32a..eaac1e3536d 100644 --- a/spec/javascripts/issue_show/components/form_spec.js +++ b/spec/javascripts/issue_show/components/form_spec.js @@ -15,6 +15,7 @@ describe('Inline edit form component', () => { description: 'a', lockedWarningVisible: false, }, + issuableType: 'issue', markdownPreviewPath: '/', markdownDocsPath: '/', projectPath: '/', diff --git a/spec/javascripts/jobs/components/artifacts_block_spec.js b/spec/javascripts/jobs/components/artifacts_block_spec.js index a06d287b3fa..2fa7ff653fe 100644 --- a/spec/javascripts/jobs/components/artifacts_block_spec.js +++ b/spec/javascripts/jobs/components/artifacts_block_spec.js @@ -11,6 +11,19 @@ describe('Artifacts block', () => { const timeago = getTimeago(); const formatedDate = timeago.format(expireAt); + const expiredArtifact = { + expire_at: expireAt, + expired: true, + }; + + const nonExpiredArtifact = { + download_path: '/gitlab-org/gitlab-ce/-/jobs/98314558/artifacts/download', + browse_path: '/gitlab-org/gitlab-ce/-/jobs/98314558/artifacts/browse', + keep_path: '/gitlab-org/gitlab-ce/-/jobs/98314558/artifacts/keep', + expire_at: expireAt, + expired: false, + }; + afterEach(() => { vm.$destroy(); }); @@ -18,100 +31,87 @@ describe('Artifacts block', () => { describe('with expired artifacts', () => { it('renders expired artifact date and info', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, + artifact: expiredArtifact, }); expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull(); expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull(); expect(vm.$el.textContent).toContain(formatedDate); + expect(vm.$el.querySelector('.js-artifacts-removed').textContent.trim()).toEqual( + 'The artifacts were removed', + ); }); }); describe('with artifacts that will expire', () => { it('renders will expire artifact date and info', () => { vm = mountComponent(Component, { - haveArtifactsExpired: false, - willArtifactsExpire: true, - expireAt, + artifact: nonExpiredArtifact, }); expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull(); expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull(); expect(vm.$el.textContent).toContain(formatedDate); + expect(vm.$el.querySelector('.js-artifacts-will-be-removed').textContent.trim()).toEqual( + 'The artifacts will be removed in', + ); }); }); - describe('when the user can keep the artifacts', () => { + describe('with keep path', () => { it('renders the keep button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, - keepArtifactsPath: '/keep', + artifact: nonExpiredArtifact, }); expect(vm.$el.querySelector('.js-keep-artifacts')).not.toBeNull(); }); }); - describe('when the user can not keep the artifacts', () => { + describe('without keep path', () => { it('does not render the keep button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, + artifact: expiredArtifact, }); expect(vm.$el.querySelector('.js-keep-artifacts')).toBeNull(); }); }); - describe('when the user can download the artifacts', () => { + describe('with download path', () => { it('renders the download button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, - downloadArtifactsPath: '/download', + artifact: nonExpiredArtifact, }); expect(vm.$el.querySelector('.js-download-artifacts')).not.toBeNull(); }); }); - describe('when the user can not download the artifacts', () => { + describe('without download path', () => { it('does not render the keep button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, + artifact: expiredArtifact, }); expect(vm.$el.querySelector('.js-download-artifacts')).toBeNull(); }); }); - describe('when the user can browse the artifacts', () => { + describe('with browse path', () => { it('does not render the browse button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, - browseArtifactsPath: '/browse', + artifact: nonExpiredArtifact, }); expect(vm.$el.querySelector('.js-browse-artifacts')).not.toBeNull(); }); }); - describe('when the user can not browse the artifacts', () => { + describe('without browse path', () => { it('does not render the browse button', () => { vm = mountComponent(Component, { - haveArtifactsExpired: true, - willArtifactsExpire: false, - expireAt, + artifact: expiredArtifact, }); expect(vm.$el.querySelector('.js-browse-artifacts')).toBeNull(); diff --git a/spec/javascripts/jobs/components/commit_block_spec.js b/spec/javascripts/jobs/components/commit_block_spec.js index e21fa9c2874..61ee993f46a 100644 --- a/spec/javascripts/jobs/components/commit_block_spec.js +++ b/spec/javascripts/jobs/components/commit_block_spec.js @@ -7,11 +7,16 @@ describe('Commit block', () => { let vm; const props = { - pipelineShortSha: '1f0fb84f', - pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', - mergeRequestReference: '!21244', - mergeRequestPath: 'merge_requests/21244', - gitCommitTitlte: 'Regenerate pot files', + commit: { + short_id: '1f0fb84f', + commit_path: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c', + title: 'Update README.md', + }, + mergeRequest: { + iid: '!21244', + path: 'merge_requests/21244', + }, + isLastBlock: true, }; afterEach(() => { @@ -26,12 +31,18 @@ describe('Commit block', () => { }); it('renders pipeline short sha link', () => { - expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath); - expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha); + expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual( + props.commit.commit_path, + ); + expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual( + props.commit.short_id, + ); }); it('renders clipboard button', () => { - expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha); + expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual( + props.commit.short_id, + ); }); }); @@ -41,17 +52,19 @@ describe('Commit block', () => { ...props, }); - expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath); - expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference); - + expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual( + props.mergeRequest.path, + ); + expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual( + props.mergeRequest.iid, + ); }); }); describe('without merge request', () => { it('does not render merge request', () => { const copyProps = Object.assign({}, props); - delete copyProps.mergeRequestPath; - delete copyProps.mergeRequestReference; + delete copyProps.mergeRequest; vm = mountComponent(Component, { ...copyProps, @@ -67,7 +80,7 @@ describe('Commit block', () => { ...props, }); - expect(vm.$el.textContent).toContain(props.gitCommitTitlte); + expect(vm.$el.textContent).toContain(props.commit.title); }); }); }); diff --git a/spec/javascripts/jobs/components/trigger_value_spec.js b/spec/javascripts/jobs/components/trigger_block_spec.js index 3d41a3cfac1..e1b9898393e 100644 --- a/spec/javascripts/jobs/components/trigger_value_spec.js +++ b/spec/javascripts/jobs/components/trigger_block_spec.js @@ -13,7 +13,9 @@ describe('Trigger block', () => { describe('with short token', () => { it('renders short token', () => { vm = mountComponent(Component, { - shortToken: '0a666b2', + trigger: { + short_token: '0a666b2', + }, }); expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2'); @@ -22,7 +24,7 @@ describe('Trigger block', () => { describe('without short token', () => { it('does not render short token', () => { - vm = mountComponent(Component, {}); + vm = mountComponent(Component, { trigger: {} }); expect(vm.$el.querySelector('.js-short-token')).toBeNull(); }); @@ -32,9 +34,12 @@ describe('Trigger block', () => { describe('reveal variables', () => { it('reveals variables on click', done => { vm = mountComponent(Component, { - variables: { - key: 'value', - variable: 'foo', + trigger: { + short_token: 'bd7e', + variables: [ + { key: 'UPLOAD_TO_GCS', value: 'false', public: false }, + { key: 'UPLOAD_TO_S3', value: 'true', public: false }, + ], }, }); @@ -44,10 +49,10 @@ describe('Trigger block', () => { .$nextTick() .then(() => { expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull(); - expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('key'); - expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('value'); - expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('variable'); - expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('foo'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('UPLOAD_TO_GCS'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('false'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('UPLOAD_TO_S3'); + expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true'); }) .then(done) .catch(done.fail); @@ -57,7 +62,7 @@ describe('Trigger block', () => { describe('without variables', () => { it('does not render variables', () => { - vm = mountComponent(Component); + vm = mountComponent(Component, { trigger: {} }); expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull(); expect(vm.$el.querySelector('.js-build-variables')).toBeNull(); diff --git a/spec/javascripts/shortcuts_dashboard_navigation_spec.js b/spec/javascripts/lib/utils/navigation_utility_spec.js index 7cb201e01d8..be620e4a27c 100644 --- a/spec/javascripts/shortcuts_dashboard_navigation_spec.js +++ b/spec/javascripts/lib/utils/navigation_utility_spec.js @@ -1,4 +1,4 @@ -import findAndFollowLink from '~/shortcuts_dashboard_navigation'; +import findAndFollowLink from '~/lib/utils/navigation_utility'; describe('findAndFollowLink', () => { it('visits a link when the selector exists', () => { diff --git a/spec/javascripts/lib/utils/poll_spec.js b/spec/javascripts/lib/utils/poll_spec.js index 523f4997bc0..b28a052902e 100644 --- a/spec/javascripts/lib/utils/poll_spec.js +++ b/spec/javascripts/lib/utils/poll_spec.js @@ -1,3 +1,5 @@ +/* eslint-disable jasmine/no-unsafe-spy */ + import Poll from '~/lib/utils/poll'; import { successCodes } from '~/lib/utils/http_status'; diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js index b66e8e1ceb3..f4643fd55ed 100644 --- a/spec/javascripts/notes/stores/actions_spec.js +++ b/spec/javascripts/notes/stores/actions_spec.js @@ -3,6 +3,7 @@ import _ from 'underscore'; import { headersInterceptor } from 'spec/helpers/vue_resource_helper'; import * as actions from '~/notes/stores/actions'; import createStore from '~/notes/stores'; +import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub'; import testAction from '../../helpers/vuex_action_helper'; import { resetStore } from '../helpers'; import { @@ -317,4 +318,195 @@ describe('Actions Notes Store', () => { ); }); }); + + describe('deleteNote', () => { + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify({}), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => { + const note = { path: `${gl.TEST_HOST}`, id: 1 }; + + testAction( + actions.deleteNote, + note, + store.state, + [ + { + type: 'DELETE_NOTE', + payload: note, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + ], + done, + ); + }); + }); + + describe('createNewNote', () => { + describe('success', () => { + const res = { + id: 1, + valid: true, + }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', done => { + testAction( + actions.createNewNote, + { endpoint: `${gl.TEST_HOST}`, data: {} }, + store.state, + [ + { + type: 'ADD_NEW_NOTE', + payload: res, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + const res = { + errors: ['error'], + }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', done => { + testAction( + actions.createNewNote, + { endpoint: `${gl.TEST_HOST}`, data: {} }, + store.state, + [], + [], + done, + ); + }); + }); + }); + + describe('toggleResolveNote', () => { + const res = { + resolved: true, + }; + const interceptor = (request, next) => { + next( + request.respondWith(JSON.stringify(res), { + status: 200, + }), + ); + }; + + beforeEach(() => { + Vue.http.interceptors.push(interceptor); + }); + + afterEach(() => { + Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor); + }); + + describe('as note', () => { + it('commits UPDATE_NOTE and dispatches updateMergeRequestWidget', done => { + testAction( + actions.toggleResolveNote, + { endpoint: `${gl.TEST_HOST}`, isResolved: true, discussion: false }, + store.state, + [ + { + type: 'UPDATE_NOTE', + payload: res, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + ], + done, + ); + }); + }); + + describe('as discussion', () => { + it('commits UPDATE_DISCUSSION and dispatches updateMergeRequestWidget', done => { + testAction( + actions.toggleResolveNote, + { endpoint: `${gl.TEST_HOST}`, isResolved: true, discussion: true }, + store.state, + [ + { + type: 'UPDATE_DISCUSSION', + payload: res, + }, + ], + [ + { + type: 'updateMergeRequestWidget', + }, + ], + done, + ); + }); + }); + }); + + describe('updateMergeRequestWidget', () => { + it('calls mrWidget checkStatus', () => { + spyOn(mrWidgetEventHub, '$emit'); + + actions.updateMergeRequestWidget(); + + expect(mrWidgetEventHub.$emit).toHaveBeenCalledWith('mr.discussion.updated'); + }); + }); }); diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js index a15ff1a5888..1ecfe914859 100644 --- a/spec/javascripts/notes/stores/mutation_spec.js +++ b/spec/javascripts/notes/stores/mutation_spec.js @@ -1,3 +1,4 @@ +import Vue from 'vue'; import mutations from '~/notes/stores/mutations'; import { note, @@ -155,6 +156,41 @@ describe('Notes Store mutations', () => { expect(state.discussions[2].notes[0].note).toBe(legacyNote.notes[1].note); expect(state.discussions.length).toEqual(3); }); + + it('adds truncated_diff_lines if discussion is a diffFile', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + truncated_diff_lines: ['a'], + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual(['a']); + }); + + it('adds empty truncated_diff_lines when not in discussion', () => { + const state = { + discussions: [], + }; + + mutations.SET_INITIAL_DISCUSSIONS(state, [ + { + ...note, + diff_file: { + file_hash: 'a', + }, + }, + ]); + + expect(state.discussions[0].truncated_diff_lines).toEqual([]); + }); }); describe('SET_LAST_FETCHED_AT', () => { @@ -333,7 +369,7 @@ describe('Notes Store mutations', () => { }); }); - describe('SET_NOTES_FETCHING_STATE', () => { + describe('SET_NOTES_FETCHED_STATE', () => { it('should set the given state', () => { const state = { isNotesFetched: false, @@ -343,4 +379,37 @@ describe('Notes Store mutations', () => { expect(state.isNotesFetched).toEqual(true); }); }); + + describe('SET_DISCUSSION_DIFF_LINES', () => { + it('sets truncated_diff_lines', () => { + const state = { + discussions: [ + { + id: 1, + }, + ], + }; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { discussionId: 1, diffLines: ['test'] }); + + expect(state.discussions[0].truncated_diff_lines).toEqual(['test']); + }); + + it('keeps reactivity of discussion', () => { + const state = {}; + Vue.set(state, 'discussions', [ + { + id: 1, + expanded: false, + }, + ]); + const discussion = state.discussions[0]; + + mutations.SET_DISCUSSION_DIFF_LINES(state, { discussionId: 1, diffLines: ['test'] }); + + discussion.expanded = true; + + expect(state.discussions[0].expanded).toBe(true); + }); + }); }); diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js index 6d49536a712..c7190ea9960 100644 --- a/spec/javascripts/right_sidebar_spec.js +++ b/spec/javascripts/right_sidebar_spec.js @@ -1,4 +1,4 @@ -/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-return-assign, vars-on-top, max-len */ +/* eslint-disable no-var, one-var, one-var-declaration-per-line, no-return-assign, vars-on-top, jasmine/no-unsafe-spy, max-len */ import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js index 86c001678c5..646d843162c 100644 --- a/spec/javascripts/search_autocomplete_spec.js +++ b/spec/javascripts/search_autocomplete_spec.js @@ -2,7 +2,7 @@ import $ from 'jquery'; import '~/gl_dropdown'; -import SearchAutocomplete from '~/search_autocomplete'; +import initSearchAutocomplete from '~/search_autocomplete'; import '~/lib/utils/common_utils'; describe('Search autocomplete dropdown', () => { @@ -132,7 +132,7 @@ describe('Search autocomplete dropdown', () => { window.gon.current_user_id = userId; window.gon.current_username = userName; - return (widget = new SearchAutocomplete()); + return (widget = initSearchAutocomplete()); }); afterEach(function() { diff --git a/spec/javascripts/shortcuts_spec.js b/spec/javascripts/shortcuts_spec.js index 94cded7ee37..3ca6ecaa938 100644 --- a/spec/javascripts/shortcuts_spec.js +++ b/spec/javascripts/shortcuts_spec.js @@ -1,5 +1,5 @@ import $ from 'jquery'; -import Shortcuts from '~/shortcuts'; +import Shortcuts from '~/behaviors/shortcuts/shortcuts'; describe('Shortcuts', () => { const fixtureName = 'snippets/show.html.raw'; diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js index 9e437084224..af2fde0a5be 100644 --- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js +++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js @@ -12,7 +12,7 @@ describe('Sidebar Subscriptions', function () { beforeEach(() => { SidebarSubscriptions = Vue.extend(sidebarSubscriptions); - // Setup the stores, services, etc + // Set up the stores, services, etc // eslint-disable-next-line no-new new SidebarMediator(Mock.mediator); }); diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js index 4452c470b82..96c0844f83c 100644 --- a/spec/javascripts/test_bundle.js +++ b/spec/javascripts/test_bundle.js @@ -41,8 +41,8 @@ jasmine.getJSONFixtures().fixturesPath = FIXTURES_PATH; beforeAll(() => { jasmine.addMatchers( jasmineDiff(jasmine, { - colors: true, - inline: true, + colors: window.__karma__.config.color, + inline: window.__karma__.config.color, }), ); jasmine.addMatchers(customMatchers); diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js index d9383314891..b774627651f 100644 --- a/spec/javascripts/u2f/register_spec.js +++ b/spec/javascripts/u2f/register_spec.js @@ -16,7 +16,7 @@ describe('U2FRegister', function () { it('allows registering a U2F device', () => { const setupButton = this.container.find('#js-setup-u2f-device'); - expect(setupButton.text()).toBe('Setup new U2F device'); + expect(setupButton.text()).toBe('Set up new U2F device'); setupButton.trigger('click'); const inProgressMessage = this.container.children('p'); expect(inProgressMessage.text()).toContain('Trying to communicate with your device'); diff --git a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js index 6342ea00436..6ac7138743b 100644 --- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js +++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js @@ -27,6 +27,10 @@ describe('mrWidgetOptions', () => { }); }); + afterEach(() => { + vm.$destroy(); + }); + describe('data', () => { it('should instantiate Store and Service', () => { expect(vm.mr).toBeDefined(); diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js index 1c666fc6c55..f2a09d08829 100644 --- a/spec/javascripts/vue_shared/components/file_icon_spec.js +++ b/spec/javascripts/vue_shared/components/file_icon_spec.js @@ -62,9 +62,11 @@ describe('File Icon component', () => { loading: true, }); - expect( - vm.$el.querySelector('i').getAttribute('class'), - ).toEqual('fa fa-spin fa-spinner fa-1x'); + const { classList } = vm.$el.querySelector('i'); + expect(classList.contains('fa')).toEqual(true); + expect(classList.contains('fa-spin')).toEqual(true); + expect(classList.contains('fa-spinner')).toEqual(true); + expect(classList.contains('fa-1x')).toEqual(true); }); it('should add a special class and a size class', () => { diff --git a/spec/javascripts/vue_shared/components/file_row_spec.js b/spec/javascripts/vue_shared/components/file_row_spec.js new file mode 100644 index 00000000000..9914c0b70f3 --- /dev/null +++ b/spec/javascripts/vue_shared/components/file_row_spec.js @@ -0,0 +1,74 @@ +import Vue from 'vue'; +import FileRow from '~/vue_shared/components/file_row.vue'; +import { file } from 'spec/ide/helpers'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('RepoFile', () => { + let vm; + + function createComponent(propsData) { + const FileRowComponent = Vue.extend(FileRow); + + vm = mountComponent(FileRowComponent, propsData); + } + + afterEach(() => { + vm.$destroy(); + }); + + it('renders name', () => { + createComponent({ + file: file('t4'), + level: 0, + }); + + const name = vm.$el.querySelector('.file-row-name'); + + expect(name.textContent.trim()).toEqual(vm.file.name); + }); + + it('emits toggleTreeOpen on click', () => { + createComponent({ + file: { + ...file('t3'), + type: 'tree', + }, + level: 0, + }); + spyOn(vm, '$emit').and.stub(); + + vm.$el.querySelector('.file-row').click(); + + expect(vm.$emit).toHaveBeenCalledWith('toggleTreeOpen', vm.file.path); + }); + + it('calls scrollIntoView if made active', done => { + createComponent({ + file: { + ...file(), + type: 'blob', + active: false, + }, + level: 0, + }); + + spyOn(vm, 'scrollIntoView').and.stub(); + + vm.file.active = true; + + vm.$nextTick(() => { + expect(vm.scrollIntoView).toHaveBeenCalled(); + + done(); + }); + }); + + it('indents row based on level', () => { + createComponent({ + file: file('t4'), + level: 2, + }); + + expect(vm.$el.querySelector('.file-row-name').style.marginLeft).toBe('32px'); + }); +}); diff --git a/spec/javascripts/vue_shared/components/loading_icon_spec.js b/spec/javascripts/vue_shared/components/loading_icon_spec.js deleted file mode 100644 index 5cd3466f501..00000000000 --- a/spec/javascripts/vue_shared/components/loading_icon_spec.js +++ /dev/null @@ -1,54 +0,0 @@ -import Vue from 'vue'; -import loadingIcon from '~/vue_shared/components/loading_icon.vue'; - -describe('Loading Icon Component', () => { - let LoadingIconComponent; - - beforeEach(() => { - LoadingIconComponent = Vue.extend(loadingIcon); - }); - - it('should render a spinner font awesome icon', () => { - const component = new LoadingIconComponent().$mount(); - - expect( - component.$el.querySelector('i').getAttribute('class'), - ).toEqual('fa fa-spin fa-spinner fa-1x'); - - expect(component.$el.tagName).toEqual('DIV'); - expect(component.$el.classList).toContain('text-center'); - expect(component.$el.classList).toContain('loading-container'); - }); - - it('should render accessibility attributes', () => { - const component = new LoadingIconComponent().$mount(); - - const icon = component.$el.querySelector('i'); - expect(icon.getAttribute('aria-hidden')).toEqual('true'); - expect(icon.getAttribute('aria-label')).toEqual('Loading'); - }); - - it('should render the provided label', () => { - const component = new LoadingIconComponent({ - propsData: { - label: 'This is a loading icon', - }, - }).$mount(); - - expect( - component.$el.querySelector('i').getAttribute('aria-label'), - ).toEqual('This is a loading icon'); - }); - - it('should render the provided size', () => { - const component = new LoadingIconComponent({ - propsData: { - size: '2', - }, - }).$mount(); - - expect( - component.$el.querySelector('i').classList.contains('fa-2x'), - ).toEqual(true); - }); -}); diff --git a/spec/javascripts/vue_shared/components/pagination_links_spec.js b/spec/javascripts/vue_shared/components/pagination_links_spec.js new file mode 100644 index 00000000000..c9d183872b4 --- /dev/null +++ b/spec/javascripts/vue_shared/components/pagination_links_spec.js @@ -0,0 +1,72 @@ +import Vue from 'vue'; +import PaginationLinks from '~/vue_shared/components/pagination_links.vue'; +import { s__ } from '~/locale'; +import mountComponent from '../../helpers/vue_mount_component_helper'; + +describe('Pagination links component', () => { + const paginationLinksComponent = Vue.extend(PaginationLinks); + const change = page => page; + const pageInfo = { + page: 3, + perPage: 5, + total: 30, + }; + const translations = { + firstText: s__('Pagination|« First'), + prevText: s__('Pagination|Prev'), + nextText: s__('Pagination|Next'), + lastText: s__('Pagination|Last »'), + }; + + let paginationLinks; + let glPagination; + let destinationComponent; + + beforeEach(() => { + paginationLinks = mountComponent( + paginationLinksComponent, + { + change, + pageInfo, + }, + ); + [glPagination] = paginationLinks.$children; + [destinationComponent] = glPagination.$children; + }); + + afterEach(() => { + paginationLinks.$destroy(); + }); + + it('should provide translated text to GitLab UI pagination', () => { + Object.entries(translations).forEach(entry => + expect( + destinationComponent[entry[0]], + ).toBe(entry[1]), + ); + }); + + it('should pass change to GitLab UI pagination', () => { + expect( + Object.is(glPagination.change, change), + ).toBe(true); + }); + + it('should pass page from pageInfo to GitLab UI pagination', () => { + expect( + destinationComponent.value, + ).toBe(pageInfo.page); + }); + + it('should pass per page from pageInfo to GitLab UI pagination', () => { + expect( + destinationComponent.perPage, + ).toBe(pageInfo.perPage); + }); + + it('should pass total items from pageInfo to GitLab UI pagination', () => { + expect( + destinationComponent.totalRows, + ).toBe(pageInfo.total); + }); +}); diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb index d9018a7e4fe..0d0554a2259 100644 --- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb @@ -79,13 +79,9 @@ describe Banzai::Filter::ExternalIssueReferenceFilter do expect(link).to eq helper.url_for_issue(issue_id, project, only_path: true) end - context 'with RequestStore enabled' do + context 'with RequestStore enabled', :request_store do let(:reference_filter) { HTML::Pipeline.new([described_class]) } - before do - allow(RequestStore).to receive(:active?).and_return(true) - end - it 'queries the collection on the first call' do expect_any_instance_of(Project).to receive(:default_issues_tracker?).once.and_call_original expect_any_instance_of(Project).to receive(:external_issue_reference_pattern).once.and_call_original diff --git a/spec/lib/banzai/filter/markdown_filter_spec.rb b/spec/lib/banzai/filter/markdown_filter_spec.rb index a515d07b072..cf49249756a 100644 --- a/spec/lib/banzai/filter/markdown_filter_spec.rb +++ b/spec/lib/banzai/filter/markdown_filter_spec.rb @@ -40,6 +40,12 @@ describe Banzai::Filter::MarkdownFilter do expect(result).to start_with("<pre><code>") end + + it 'works with utf8 chars in language' do + result = filter("```日\nsome code\n```") + + expect(result).to start_with("<pre><code lang=\"日\">") + end end context 'using Redcarpet' do @@ -60,4 +66,21 @@ describe Banzai::Filter::MarkdownFilter do end end end + + describe 'footnotes in tables' do + it 'processes footnotes in table cells' do + text = <<-MD.strip_heredoc + | Column1 | + | --------- | + | foot [^1] | + + [^1]: a footnote + MD + + result = filter(text) + + expect(result).to include('<td>foot <sup') + expect(result).to include('<section class="footnotes">') + end + end end diff --git a/spec/lib/banzai/reference_parser/base_parser_spec.rb b/spec/lib/banzai/reference_parser/base_parser_spec.rb index 4e6e8eca38a..c6e9fc414a1 100644 --- a/spec/lib/banzai/reference_parser/base_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/base_parser_spec.rb @@ -263,11 +263,10 @@ describe Banzai::ReferenceParser::BaseParser do end end - context 'with RequestStore enabled' do + context 'with RequestStore enabled', :request_store do before do cache = Hash.new { |hash, key| hash[key] = {} } - allow(RequestStore).to receive(:active?).and_return(true) allow(subject).to receive(:collection_cache).and_return(cache) end diff --git a/spec/lib/feature_spec.rb b/spec/lib/feature_spec.rb index 48c0ba8a653..9d56c62ae57 100644 --- a/spec/lib/feature_spec.rb +++ b/spec/lib/feature_spec.rb @@ -91,7 +91,11 @@ describe Feature do end describe '.flipper' do - shared_examples 'a memoized Flipper instance' do + before do + described_class.instance_variable_set(:@flipper, nil) + end + + context 'when request store is inactive' do it 'memoizes the Flipper instance' do expect(Flipper).to receive(:new).once.and_call_original @@ -101,16 +105,14 @@ describe Feature do end end - context 'when request store is inactive' do - before do + context 'when request store is active', :request_store do + it 'memoizes the Flipper instance' do + expect(Flipper).to receive(:new).once.and_call_original + + described_class.flipper described_class.instance_variable_set(:@flipper, nil) + described_class.flipper end - - it_behaves_like 'a memoized Flipper instance' - end - - context 'when request store is inactive', :request_store do - it_behaves_like 'a memoized Flipper instance' end end diff --git a/spec/lib/gitlab/auth/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb index 7800c543cdb..662f899180b 100644 --- a/spec/lib/gitlab/auth/ldap/access_spec.rb +++ b/spec/lib/gitlab/auth/ldap/access_spec.rb @@ -48,7 +48,7 @@ describe Gitlab::Auth::LDAP::Access do it 'logs the reason' do expect(Gitlab::AppLogger).to receive(:info).with( "LDAP account \"123456\" does not exist anymore, " \ - "blocking Gitlab user \"#{user.name}\" (#{user.email})" + "blocking GitLab user \"#{user.name}\" (#{user.email})" ) access.allowed? @@ -79,7 +79,7 @@ describe Gitlab::Auth::LDAP::Access do it 'logs the reason' do expect(Gitlab::AppLogger).to receive(:info).with( "LDAP account \"123456\" is disabled in Active Directory, " \ - "blocking Gitlab user \"#{user.name}\" (#{user.email})" + "blocking GitLab user \"#{user.name}\" (#{user.email})" ) access.allowed? @@ -123,7 +123,7 @@ describe Gitlab::Auth::LDAP::Access do it 'logs the reason' do expect(Gitlab::AppLogger).to receive(:info).with( "LDAP account \"123456\" is not disabled anymore, " \ - "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + "unblocking GitLab user \"#{user.name}\" (#{user.email})" ) access.allowed? @@ -161,7 +161,7 @@ describe Gitlab::Auth::LDAP::Access do it 'logs the reason' do expect(Gitlab::AppLogger).to receive(:info).with( "LDAP account \"123456\" does not exist anymore, " \ - "blocking Gitlab user \"#{user.name}\" (#{user.email})" + "blocking GitLab user \"#{user.name}\" (#{user.email})" ) access.allowed? @@ -183,7 +183,7 @@ describe Gitlab::Auth::LDAP::Access do it 'logs the reason' do expect(Gitlab::AppLogger).to receive(:info).with( "LDAP account \"123456\" is available again, " \ - "unblocking Gitlab user \"#{user.name}\" (#{user.email})" + "unblocking GitLab user \"#{user.name}\" (#{user.email})" ) access.allowed? diff --git a/spec/lib/gitlab/ci/config_spec.rb b/spec/lib/gitlab/ci/config_spec.rb index 5a78ce783dd..b43aca8a354 100644 --- a/spec/lib/gitlab/ci/config_spec.rb +++ b/spec/lib/gitlab/ci/config_spec.rb @@ -124,4 +124,237 @@ describe Gitlab::Ci::Config do end end end + + context "when using 'include' directive" do + let(:project) { create(:project, :repository) } + let(:remote_location) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:local_location) { 'spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml' } + + let(:remote_file_content) do + <<~HEREDOC + variables: + AUTO_DEVOPS_DOMAIN: domain.example.com + POSTGRES_USER: user + POSTGRES_PASSWORD: testing-password + POSTGRES_ENABLED: "true" + POSTGRES_DB: $CI_ENVIRONMENT_SLUG + HEREDOC + end + + let(:local_file_content) do + File.read(Rails.root.join(local_location)) + end + + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{local_location} + - #{remote_location} + + image: ruby:2.2 + HEREDOC + end + + let(:config) do + described_class.new(gitlab_ci_yml, project: project, sha: '12345') + end + + before do + WebMock.stub_request(:get, remote_location) + .to_return(body: remote_file_content) + + allow(project.repository) + .to receive(:blob_data_at).and_return(local_file_content) + end + + context "when gitlab_ci_yml has valid 'include' defined" do + it 'should return a composed hash' do + before_script_values = [ + "apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs", "ruby -v", + "which ruby", + "gem install bundler --no-ri --no-rdoc", + "bundle install --jobs $(nproc) \"${FLAGS[@]}\"" + ] + variables = { + AUTO_DEVOPS_DOMAIN: "domain.example.com", + POSTGRES_USER: "user", + POSTGRES_PASSWORD: "testing-password", + POSTGRES_ENABLED: "true", + POSTGRES_DB: "$CI_ENVIRONMENT_SLUG" + } + composed_hash = { + before_script: before_script_values, + image: "ruby:2.2", + rspec: { script: ["bundle exec rspec"] }, + variables: variables + } + + expect(config.to_hash).to eq(composed_hash) + end + end + + context "when gitlab_ci.yml has invalid 'include' defined" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: invalid + HEREDOC + end + + it 'raises error YamlProcessor validationError' do + expect { config }.to raise_error( + ::Gitlab::Ci::YamlProcessor::ValidationError, + "Local file 'invalid' is not valid." + ) + end + end + + describe 'external file version' do + context 'when external local file SHA is defined' do + it 'is using a defined value' do + expect(project.repository).to receive(:blob_data_at) + .with('eeff1122', local_location) + + described_class.new(gitlab_ci_yml, project: project, sha: 'eeff1122') + end + end + + context 'when external local file SHA is not defined' do + it 'is using latest SHA on the default branch' do + expect(project.repository).to receive(:root_ref_sha) + + described_class.new(gitlab_ci_yml, project: project) + end + end + end + + context "when both external files and gitlab_ci.yml defined the same key" do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{remote_location} + + image: ruby:2.2 + HEREDOC + end + + let(:remote_file_content) do + <<~HEREDOC + image: php:5-fpm-alpine + HEREDOC + end + + it 'should take precedence' do + expect(config.to_hash).to eq({ image: 'ruby:2.2' }) + end + end + + context "when both external files and gitlab_ci.yml define a dictionary of distinct variables" do + let(:remote_file_content) do + <<~HEREDOC + variables: + A: 'alpha' + B: 'beta' + HEREDOC + end + + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{remote_location} + + variables: + C: 'gamma' + D: 'delta' + HEREDOC + end + + it 'should merge the variables dictionaries' do + expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } }) + end + end + + context "when both external files and gitlab_ci.yml define a dictionary of overlapping variables" do + let(:remote_file_content) do + <<~HEREDOC + variables: + A: 'alpha' + B: 'beta' + C: 'omnicron' + HEREDOC + end + + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{remote_location} + + variables: + C: 'gamma' + D: 'delta' + HEREDOC + end + + it 'later declarations should take precedence' do + expect(config.to_hash).to eq({ variables: { A: 'alpha', B: 'beta', C: 'gamma', D: 'delta' } }) + end + end + + context 'when both external files and gitlab_ci.yml define a job' do + let(:remote_file_content) do + <<~HEREDOC + job1: + script: + - echo 'hello from remote file' + HEREDOC + end + + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{remote_location} + + job1: + variables: + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + HEREDOC + end + + it 'merges the jobs' do + expect(config.to_hash).to eq({ + job1: { + script: ["echo 'hello from remote file'"], + variables: { + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + } + } + }) + end + + context 'when the script key is in both' do + let(:gitlab_ci_yml) do + <<~HEREDOC + include: + - #{remote_location} + + job1: + script: + - echo 'hello from main file' + variables: + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + HEREDOC + end + + it 'uses the script from the gitlab_ci.yml' do + expect(config.to_hash).to eq({ + job1: { + script: ["echo 'hello from main file'"], + variables: { + VARIABLE_DEFINED_IN_MAIN_FILE: 'some value' + } + } + }) + end + end + end + end end diff --git a/spec/lib/gitlab/ci/external/file/local_spec.rb b/spec/lib/gitlab/ci/external/file/local_spec.rb new file mode 100644 index 00000000000..3f32d81a827 --- /dev/null +++ b/spec/lib/gitlab/ci/external/file/local_spec.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::External::File::Local do + let(:project) { create(:project, :repository) } + let(:local_file) { described_class.new(location, { project: project, sha: '12345' }) } + + describe '#valid?' do + context 'when is a valid local path' do + let(:location) { '/vendor/gitlab-ci-yml/existent-file.yml' } + + before do + allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return("image: 'ruby2:2'") + end + + it 'should return true' do + expect(local_file.valid?).to be_truthy + end + end + + context 'when is not a valid local path' do + let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' } + + it 'should return false' do + expect(local_file.valid?).to be_falsy + end + end + + context 'when is not a yaml file' do + let(:location) { '/config/application.rb' } + + it 'should return false' do + expect(local_file.valid?).to be_falsy + end + end + end + + describe '#content' do + context 'with a a valid file' do + let(:local_file_content) do + <<~HEREDOC + before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + HEREDOC + end + let(:location) { '/vendor/gitlab-ci-yml/existent-file.yml' } + + before do + allow_any_instance_of(described_class).to receive(:fetch_local_content).and_return(local_file_content) + end + + it 'should return the content of the file' do + expect(local_file.content).to eq(local_file_content) + end + end + + context 'with an invalid file' do + let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' } + + it 'should be nil' do + expect(local_file.content).to be_nil + end + end + end + + describe '#error_message' do + let(:location) { '/vendor/gitlab-ci-yml/non-existent-file.yml' } + + it 'should return an error message' do + expect(local_file.error_message).to eq("Local file '#{location}' is not valid.") + end + end +end diff --git a/spec/lib/gitlab/ci/external/file/remote_spec.rb b/spec/lib/gitlab/ci/external/file/remote_spec.rb new file mode 100644 index 00000000000..b1819c8960b --- /dev/null +++ b/spec/lib/gitlab/ci/external/file/remote_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::External::File::Remote do + let(:remote_file) { described_class.new(location) } + let(:location) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:remote_file_content) do + <<~HEREDOC + before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + HEREDOC + end + + describe "#valid?" do + context 'when is a valid remote url' do + before do + WebMock.stub_request(:get, location).to_return(body: remote_file_content) + end + + it 'should return true' do + expect(remote_file.valid?).to be_truthy + end + end + + context 'with an irregular url' do + let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + + it 'should return false' do + expect(remote_file.valid?).to be_falsy + end + end + + context 'with a timeout' do + before do + allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) + end + + it 'should be falsy' do + expect(remote_file.valid?).to be_falsy + end + end + + context 'when is not a yaml file' do + let(:location) { 'https://asdasdasdaj48ggerexample.com' } + + it 'should be falsy' do + expect(remote_file.valid?).to be_falsy + end + end + + context 'with an internal url' do + let(:location) { 'http://localhost:8080' } + + it 'should be falsy' do + expect(remote_file.valid?).to be_falsy + end + end + end + + describe "#content" do + context 'with a valid remote file' do + before do + WebMock.stub_request(:get, location).to_return(body: remote_file_content) + end + + it 'should return the content of the file' do + expect(remote_file.content).to eql(remote_file_content) + end + end + + context 'with a timeout' do + before do + allow(Gitlab::HTTP).to receive(:get).and_raise(Timeout::Error) + end + + it 'should be falsy' do + expect(remote_file.content).to be_falsy + end + end + + context 'with an invalid remote url' do + let(:location) { 'https://asdasdasdaj48ggerexample.com' } + + before do + WebMock.stub_request(:get, location).to_raise(SocketError.new('Some HTTP error')) + end + + it 'should be nil' do + expect(remote_file.content).to be_nil + end + end + + context 'with an internal url' do + let(:location) { 'http://localhost:8080' } + + it 'should be nil' do + expect(remote_file.content).to be_nil + end + end + end + + describe "#error_message" do + let(:location) { 'not-valid://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + + it 'should return an error message' do + expect(remote_file.error_message).to eq("Remote file '#{location}' is not valid.") + end + end +end diff --git a/spec/lib/gitlab/ci/external/mapper_spec.rb b/spec/lib/gitlab/ci/external/mapper_spec.rb new file mode 100644 index 00000000000..6270d27a36d --- /dev/null +++ b/spec/lib/gitlab/ci/external/mapper_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::External::Mapper do + let(:project) { create(:project, :repository) } + let(:file_content) do + <<~HEREDOC + image: 'ruby:2.2' + HEREDOC + end + + describe '#process' do + subject { described_class.new(values, project, '123456').process } + + context "when 'include' keyword is defined as string" do + context 'when the string is a local file' do + let(:values) do + { + include: '/vendor/gitlab-ci-yml/non-existent-file.yml', + image: 'ruby:2.2' + } + end + + it 'returns an array' do + expect(subject).to be_an(Array) + end + + it 'returns File instances' do + expect(subject.first).to be_an_instance_of(Gitlab::Ci::External::File::Local) + end + end + + context 'when the string is a remote file' do + let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:values) do + { + include: remote_url, + image: 'ruby:2.2' + } + end + + before do + WebMock.stub_request(:get, remote_url).to_return(body: file_content) + end + + it 'returns an array' do + expect(subject).to be_an(Array) + end + + it 'returns File instances' do + expect(subject.first).to be_an_instance_of(Gitlab::Ci::External::File::Remote) + end + end + end + + context "when 'include' is defined as an array" do + let(:remote_url) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:values) do + { + include: + [ + remote_url, + '/vendor/gitlab-ci-yml/template.yml' + ], + image: 'ruby:2.2' + } + end + + before do + WebMock.stub_request(:get, remote_url).to_return(body: file_content) + end + + it 'returns an array' do + expect(subject).to be_an(Array) + end + + it 'returns Files instances' do + expect(subject).to all(respond_to(:valid?)) + expect(subject).to all(respond_to(:content)) + end + end + + context "when 'include' is not defined" do + let(:values) do + { + image: 'ruby:2.2' + } + end + + it 'returns an empty array' do + expect(subject).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/ci/external/processor_spec.rb b/spec/lib/gitlab/ci/external/processor_spec.rb new file mode 100644 index 00000000000..688c2b3c8aa --- /dev/null +++ b/spec/lib/gitlab/ci/external/processor_spec.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Ci::External::Processor do + let(:project) { create(:project, :repository) } + let(:processor) { described_class.new(values, project, '12345') } + + describe "#perform" do + context 'when no external files defined' do + let(:values) { { image: 'ruby:2.2' } } + + it 'should return the same values' do + expect(processor.perform).to eq(values) + end + end + + context 'when an invalid local file is defined' do + let(:values) { { include: '/vendor/gitlab-ci-yml/non-existent-file.yml', image: 'ruby:2.2' } } + + it 'should raise an error' do + expect { processor.perform }.to raise_error( + described_class::FileError, + "Local file '/vendor/gitlab-ci-yml/non-existent-file.yml' is not valid." + ) + end + end + + context 'when an invalid remote file is defined' do + let(:remote_file) { 'http://doesntexist.com/.gitlab-ci-1.yml' } + let(:values) { { include: remote_file, image: 'ruby:2.2' } } + + before do + WebMock.stub_request(:get, remote_file).to_raise(SocketError.new('Some HTTP error')) + end + + it 'should raise an error' do + expect { processor.perform }.to raise_error( + described_class::FileError, + "Remote file '#{remote_file}' is not valid." + ) + end + end + + context 'with a valid remote external file is defined' do + let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:values) { { include: remote_file, image: 'ruby:2.2' } } + let(:external_file_content) do + <<-HEREDOC + before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + + rspec: + script: + - bundle exec rspec + + rubocop: + script: + - bundle exec rubocop + HEREDOC + end + + before do + WebMock.stub_request(:get, remote_file).to_return(body: external_file_content) + end + + it 'should append the file to the values' do + output = processor.perform + expect(output.keys).to match_array([:image, :before_script, :rspec, :rubocop]) + end + + it "should remove the 'include' keyword" do + expect(processor.perform[:include]).to be_nil + end + end + + context 'with a valid local external file is defined' do + let(:values) { { include: '/vendor/gitlab-ci-yml/template.yml', image: 'ruby:2.2' } } + let(:local_file_content) do + <<-HEREDOC + before_script: + - apt-get update -qq && apt-get install -y -qq sqlite3 libsqlite3-dev nodejs + - ruby -v + - which ruby + - gem install bundler --no-ri --no-rdoc + - bundle install --jobs $(nproc) "${FLAGS[@]}" + HEREDOC + end + + before do + allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + end + + it 'should append the file to the values' do + output = processor.perform + expect(output.keys).to match_array([:image, :before_script]) + end + + it "should remove the 'include' keyword" do + expect(processor.perform[:include]).to be_nil + end + end + + context 'with multiple external files are defined' do + let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:external_files) do + [ + '/spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml', + remote_file + ] + end + let(:values) do + { + include: external_files, + image: 'ruby:2.2' + } + end + + let(:remote_file_content) do + <<-HEREDOC + stages: + - build + - review + - cleanup + HEREDOC + end + + before do + local_file_content = File.read(Rails.root.join('spec/fixtures/gitlab/ci/external_files/.gitlab-ci-template-1.yml')) + allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) + end + + it 'should append the files to the values' do + expect(processor.perform.keys).to match_array([:image, :stages, :before_script, :rspec]) + end + + it "should remove the 'include' keyword" do + expect(processor.perform[:include]).to be_nil + end + end + + context 'when external files are defined but not valid' do + let(:values) { { include: '/vendor/gitlab-ci-yml/template.yml', image: 'ruby:2.2' } } + + let(:local_file_content) { 'invalid content file ////' } + + before do + allow_any_instance_of(Gitlab::Ci::External::File::Local).to receive(:fetch_local_content).and_return(local_file_content) + end + + it 'should raise an error' do + expect { processor.perform }.to raise_error(Gitlab::Ci::Config::Loader::FormatError) + end + end + + context "when both external files and values defined the same key" do + let(:remote_file) { 'https://gitlab.com/gitlab-org/gitlab-ce/blob/1234/.gitlab-ci-1.yml' } + let(:values) do + { + include: remote_file, + image: 'ruby:2.2' + } + end + + let(:remote_file_content) do + <<~HEREDOC + image: php:5-fpm-alpine + HEREDOC + end + + it 'should take precedence' do + WebMock.stub_request(:get, remote_file).to_return(body: remote_file_content) + expect(processor.perform[:image]).to eq('ruby:2.2') + end + end + end +end diff --git a/spec/lib/gitlab/database/subquery_spec.rb b/spec/lib/gitlab/database/subquery_spec.rb new file mode 100644 index 00000000000..70380e02f16 --- /dev/null +++ b/spec/lib/gitlab/database/subquery_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Database::Subquery do + describe '.self_join' do + set(:project) { create(:project) } + + it 'allows you to delete_all rows with WHERE and LIMIT' do + events = create_list(:event, 8, project: project) + + expect do + described_class.self_join(Event.where('id < ?', events[5]).recent.limit(2)).delete_all + end.to change { Event.count }.by(-2) + end + end +end diff --git a/spec/lib/gitlab/diff/file_collection/commit_spec.rb b/spec/lib/gitlab/diff/file_collection/commit_spec.rb new file mode 100644 index 00000000000..6d1b66deb6a --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection/commit_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::FileCollection::Commit do + let(:project) { create(:project, :repository) } + + it_behaves_like 'diff statistics' do + let(:collection_default_args) do + { diff_options: {} } + end + let(:diffable) { project.commit } + let(:stub_path) { 'bar/branch-test.txt' } + end +end diff --git a/spec/lib/gitlab/diff/file_collection/compare_spec.rb b/spec/lib/gitlab/diff/file_collection/compare_spec.rb new file mode 100644 index 00000000000..f330f299ac1 --- /dev/null +++ b/spec/lib/gitlab/diff/file_collection/compare_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Diff::FileCollection::Compare do + include RepoHelpers + + let(:project) { create(:project, :repository) } + let(:commit) { project.commit } + let(:start_commit) { sample_image_commit } + let(:head_commit) { sample_commit } + let(:raw_compare) do + Gitlab::Git::Compare.new(project.repository.raw_repository, + start_commit.id, + head_commit.id) + end + + it_behaves_like 'diff statistics' do + let(:collection_default_args) do + { + project: diffable.project, + diff_options: {}, + diff_refs: diffable.diff_refs + } + end + let(:diffable) { Compare.new(raw_compare, project) } + let(:stub_path) { '.gitignore' } + end +end diff --git a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb index 79287021981..4578da70bfc 100644 --- a/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb +++ b/spec/lib/gitlab/diff/file_collection/merge_request_diff_spec.rb @@ -29,6 +29,14 @@ describe Gitlab::Diff::FileCollection::MergeRequestDiff do expect(mr_diff.cache_key).not_to eq(key) end + it_behaves_like 'diff statistics' do + let(:collection_default_args) do + { diff_options: {} } + end + let(:diffable) { merge_request.merge_request_diff } + let(:stub_path) { '.gitignore' } + end + shared_examples 'initializes a DiffCollection' do it 'returns a valid instance of a DiffCollection' do expect(diff_files).to be_a(Gitlab::Git::DiffCollection) diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb index ebeb05d6e02..2f51642b58e 100644 --- a/spec/lib/gitlab/diff/file_spec.rb +++ b/spec/lib/gitlab/diff/file_spec.rb @@ -186,6 +186,70 @@ describe Gitlab::Diff::File do end end + context 'diff file stats' do + let(:diff_file) do + described_class.new(diff, + diff_refs: commit.diff_refs, + repository: project.repository, + stats: stats) + end + + let(:raw_diff) do + <<~EOS + --- a/files/ruby/popen.rb + +++ b/files/ruby/popen.rb + @@ -6,12 +6,18 @@ module Popen + + def popen(cmd, path=nil) + unless cmd.is_a?(Array) + - raise "System commands must be given as an array of strings" + + raise RuntimeError, "System commands must be given as an array of strings" + + # foobar + end + EOS + end + + describe '#added_lines' do + context 'when stats argument given' do + let(:stats) { double(Gitaly::DiffStats, additions: 10, deletions: 15) } + + it 'returns added lines from stats' do + expect(diff_file.added_lines).to eq(stats.additions) + end + end + + context 'when stats argument not given' do + let(:stats) { nil } + + it 'returns added lines by parsing raw diff' do + allow(diff_file).to receive(:raw_diff) { raw_diff } + + expect(diff_file.added_lines).to eq(2) + end + end + end + + describe '#removed_lines' do + context 'when stats argument given' do + let(:stats) { double(Gitaly::DiffStats, additions: 10, deletions: 15) } + + it 'returns removed lines from stats' do + expect(diff_file.removed_lines).to eq(stats.deletions) + end + end + + context 'when stats argument not given' do + let(:stats) { nil } + + it 'returns removed lines by parsing raw diff' do + allow(diff_file).to receive(:raw_diff) { raw_diff } + + expect(diff_file.removed_lines).to eq(1) + end + end + end + end + describe '#simple_viewer' do context 'when the file is not diffable' do before do diff --git a/spec/lib/gitlab/git/committer_with_hooks_spec.rb b/spec/lib/gitlab/git/committer_with_hooks_spec.rb deleted file mode 100644 index c7626058acd..00000000000 --- a/spec/lib/gitlab/git/committer_with_hooks_spec.rb +++ /dev/null @@ -1,156 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::CommitterWithHooks, :seed_helper do - # TODO https://gitlab.com/gitlab-org/gitaly/issues/1234 - skip 'needs to be moved to gitaly-ruby test suite' do - shared_examples 'calling wiki hooks' do - let(:project) { create(:project) } - let(:user) { project.owner } - let(:project_wiki) { ProjectWiki.new(project, user) } - let(:wiki) { project_wiki.wiki } - let(:options) do - { - id: user.id, - username: user.username, - name: user.name, - email: user.email, - message: 'commit message' - } - end - - subject { described_class.new(wiki, options) } - - before do - project_wiki.create_page('home', 'test content') - end - - shared_examples 'failing pre-receive hook' do - before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([false, '']) - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('update') - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') - end - - it 'raises exception' do - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - end - - it 'does not create a new commit inside the repository' do - current_rev = find_current_rev - - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - - expect(current_rev).to eq find_current_rev - end - end - - shared_examples 'failing update hook' do - before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([false, '']) - expect_any_instance_of(Gitlab::Git::HooksService).not_to receive(:run_hook).with('post-receive') - end - - it 'raises exception' do - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - end - - it 'does not create a new commit inside the repository' do - current_rev = find_current_rev - - expect { subject.commit }.to raise_error(Gitlab::Git::Wiki::OperationError) - - expect(current_rev).to eq find_current_rev - end - end - - shared_examples 'failing post-receive hook' do - before do - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('pre-receive').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('update').and_return([true, '']) - expect_any_instance_of(Gitlab::Git::HooksService).to receive(:run_hook).with('post-receive').and_return([false, '']) - end - - it 'does not raise exception' do - expect { subject.commit }.not_to raise_error - end - - it 'creates the commit' do - current_rev = find_current_rev - - subject.commit - - expect(current_rev).not_to eq find_current_rev - end - end - - shared_examples 'when hooks call succceeds' do - let(:hook) { double(:hook) } - - it 'calls the three hooks' do - expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(hook).to receive(:trigger).exactly(3).times.and_return([true, nil]) - - subject.commit - end - - it 'creates the commit' do - current_rev = find_current_rev - - subject.commit - - expect(current_rev).not_to eq find_current_rev - end - end - - context 'when creating a page' do - before do - project_wiki.create_page('index', 'test content') - end - - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end - - context 'when updating a page' do - before do - project_wiki.update_page(find_page('home'), content: 'some other content', format: :markdown) - end - - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end - - context 'when deleting a page' do - before do - project_wiki.delete_page(find_page('home')) - end - - it_behaves_like 'failing pre-receive hook' - it_behaves_like 'failing update hook' - it_behaves_like 'failing post-receive hook' - it_behaves_like 'when hooks call succceeds' - end - - def find_current_rev - wiki.gollum_wiki.repo.commits.first&.sha - end - - def find_page(name) - wiki.page(title: name) - end - end - - context 'when Gitaly is enabled' do - it_behaves_like 'calling wiki hooks' - end - - context 'when Gitaly is disabled', :disable_gitaly do - it_behaves_like 'calling wiki hooks' - end - end -end diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb index 87d9fcee39e..27d803e0117 100644 --- a/spec/lib/gitlab/git/diff_spec.rb +++ b/spec/lib/gitlab/git/diff_spec.rb @@ -2,12 +2,24 @@ require "spec_helper" describe Gitlab::Git::Diff, :seed_helper do let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } + let(:gitaly_diff) do + Gitlab::GitalyClient::Diff.new( + from_path: '.gitmodules', + to_path: '.gitmodules', + old_mode: 0100644, + new_mode: 0100644, + from_id: '0792c58905eff3432b721f8c4a64363d8e28d9ae', + to_id: 'efd587ccb47caf5f31fc954edb21f0a713d9ecc3', + overflow_marker: false, + collapsed: false, + too_large: false, + patch: "@@ -4,3 +4,6 @@\n [submodule \"gitlab-shell\"]\n \tpath = gitlab-shell\n \turl = https://github.com/gitlabhq/gitlab-shell.git\n+[submodule \"gitlab-grack\"]\n+\tpath = gitlab-grack\n+\turl = https://gitlab.com/gitlab-org/gitlab-grack.git\n" + ) + end before do @raw_diff_hash = { diff: <<EOT.gsub(/^ {8}/, "").sub(/\n$/, ""), - --- a/.gitmodules - +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "gitlab-shell"] \tpath = gitlab-shell @@ -26,12 +38,6 @@ EOT deleted_file: false, too_large: false } - - # TODO use a Gitaly diff object instead - @rugged_diff = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repository.rugged.diff("5937ac0a7beb003549fc5fd26fc247adbce4a52e^", "5937ac0a7beb003549fc5fd26fc247adbce4a52e", paths: - [".gitmodules"]).patches.first - end end describe '.new' do @@ -60,7 +66,7 @@ EOT context 'using a Rugged::Patch' do context 'with a small diff' do - let(:diff) { described_class.new(@rugged_diff) } + let(:diff) { described_class.new(gitaly_diff) } it 'initializes the diff' do expect(diff.to_hash).to eq(@raw_diff_hash) @@ -73,10 +79,8 @@ EOT context 'using a diff that is too large' do it 'prunes the diff' do - expect_any_instance_of(String).to receive(:bytesize) - .and_return(1024 * 1024 * 1024) - - diff = described_class.new(@rugged_diff) + gitaly_diff.too_large = true + diff = described_class.new(gitaly_diff) expect(diff.diff).to be_empty expect(diff).to be_too_large @@ -84,33 +88,15 @@ EOT end context 'using a collapsable diff that is too large' do - before do - # The patch total size is 200, with lines between 21 and 54. - # This is a quick-and-dirty way to test this. Ideally, a new patch is - # added to the test repo with a size that falls between the real limits. - stub_const("#{described_class}::SIZE_LIMIT", 150) - stub_const("#{described_class}::COLLAPSE_LIMIT", 100) - end - it 'prunes the diff as a large diff instead of as a collapsed diff' do - diff = described_class.new(@rugged_diff, expanded: false) + gitaly_diff.too_large = true + diff = described_class.new(gitaly_diff, expanded: false) expect(diff.diff).to be_empty expect(diff).to be_too_large expect(diff).not_to be_collapsed end end - - context 'using a large binary diff' do - it 'does not prune the diff' do - expect_any_instance_of(Rugged::Diff::Delta).to receive(:binary?) - .and_return(true) - - diff = described_class.new(@rugged_diff) - - expect(diff.diff).not_to be_empty - end - end end context 'using a GitalyClient::Diff' do @@ -259,31 +245,37 @@ EOT end it 'leave non-binary diffs as-is' do - diff = described_class.new(@rugged_diff) + diff = described_class.new(gitaly_diff) expect(diff.json_safe_diff).to eq(diff.diff) end end describe '#submodule?' do - before do - # TODO use a Gitaly diff object instead - rugged_commit = Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repository.rugged.rev_parse('5937ac0a7beb003549fc5fd26fc247adbce4a52e') - end - - @diffs = rugged_commit.parents[0].diff(rugged_commit).patches + let(:gitaly_submodule_diff) do + Gitlab::GitalyClient::Diff.new( + from_path: 'gitlab-grack', + to_path: 'gitlab-grack', + old_mode: 0, + new_mode: 57344, + from_id: '0000000000000000000000000000000000000000', + to_id: '645f6c4c82fd3f5e06f67134450a570b795e55a6', + overflow_marker: false, + collapsed: false, + too_large: false, + patch: "@@ -0,0 +1 @@\n+Subproject commit 645f6c4c82fd3f5e06f67134450a570b795e55a6\n" + ) end - it { expect(described_class.new(@diffs[0]).submodule?).to eq(false) } - it { expect(described_class.new(@diffs[1]).submodule?).to eq(true) } + it { expect(described_class.new(gitaly_diff).submodule?).to eq(false) } + it { expect(described_class.new(gitaly_submodule_diff).submodule?).to eq(true) } end describe '#line_count' do it 'returns the correct number of lines' do - diff = described_class.new(@rugged_diff) + diff = described_class.new(gitaly_diff) - expect(diff.line_count).to eq(9) + expect(diff.line_count).to eq(7) end end diff --git a/spec/lib/gitlab/git/diff_stats_collection_spec.rb b/spec/lib/gitlab/git/diff_stats_collection_spec.rb new file mode 100644 index 00000000000..89927cbb3a6 --- /dev/null +++ b/spec/lib/gitlab/git/diff_stats_collection_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +require "spec_helper" + +describe Gitlab::Git::DiffStatsCollection do + let(:stats_a) do + double(Gitaly::DiffStats, additions: 10, deletions: 15, path: 'foo') + end + + let(:stats_b) do + double(Gitaly::DiffStats, additions: 5, deletions: 1, path: 'bar') + end + + let(:diff_stats) { [stats_a, stats_b] } + let(:collection) { described_class.new(diff_stats) } + + describe '.find_by_path' do + it 'returns stats by path when found' do + expect(collection.find_by_path('foo')).to eq(stats_a) + end + + it 'returns nil when stats is not found by path' do + expect(collection.find_by_path('no-file')).to be_nil + end + end +end diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb deleted file mode 100644 index f5d8503c30c..00000000000 --- a/spec/lib/gitlab/git/gitlab_projects_spec.rb +++ /dev/null @@ -1,321 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::GitlabProjects do - after do - TestEnv.clean_test_path - end - - around do |example| - # TODO move this spec to gitaly-ruby. GitlabProjects is not used in gitlab-ce - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - let(:project) { create(:project, :repository) } - - if $VERBOSE - let(:logger) { Logger.new(STDOUT) } - else - let(:logger) { double('logger').as_null_object } - end - - let(:tmp_repos_path) { TestEnv.repos_path } - let(:repo_name) { project.disk_path + '.git' } - let(:tmp_repo_path) { File.join(tmp_repos_path, repo_name) } - let(:gl_projects) { build_gitlab_projects(TestEnv::REPOS_STORAGE, repo_name) } - - describe '#initialize' do - it { expect(gl_projects.shard_path).to eq(tmp_repos_path) } - it { expect(gl_projects.repository_relative_path).to eq(repo_name) } - it { expect(gl_projects.repository_absolute_path).to eq(File.join(tmp_repos_path, repo_name)) } - it { expect(gl_projects.logger).to eq(logger) } - end - - describe '#push_branches' do - let(:remote_name) { 'remote-name' } - let(:branch_name) { 'master' } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} push -- #{remote_name} #{branch_name}) } - let(:force) { false } - - subject { gl_projects.push_branches(remote_name, 600, force, [branch_name]) } - - it 'executes the command' do - stub_spawn(cmd, 600, tmp_repo_path, success: true) - - is_expected.to be_truthy - end - - it 'fails' do - stub_spawn(cmd, 600, tmp_repo_path, success: false) - - is_expected.to be_falsy - end - - context 'with --force' do - let(:cmd) { %W(#{Gitlab.config.git.bin_path} push --force -- #{remote_name} #{branch_name}) } - let(:force) { true } - - it 'executes the command' do - stub_spawn(cmd, 600, tmp_repo_path, success: true) - - is_expected.to be_truthy - end - end - end - - describe '#fetch_remote' do - let(:remote_name) { 'remote-name' } - let(:branch_name) { 'master' } - let(:force) { false } - let(:prune) { true } - let(:tags) { true } - let(:args) { { force: force, tags: tags, prune: prune }.merge(extra_args) } - let(:extra_args) { {} } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --tags) } - - subject { gl_projects.fetch_remote(remote_name, 600, args) } - - def stub_tempfile(name, filename, opts = {}) - chmod = opts.delete(:chmod) - file = StringIO.new - - allow(file).to receive(:close!) - allow(file).to receive(:path).and_return(name) - - expect(Tempfile).to receive(:new).with(filename).and_return(file) - expect(file).to receive(:chmod).with(chmod) if chmod - - file - end - - context 'with default args' do - it 'executes the command' do - stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) - - is_expected.to be_truthy - end - - it 'fails' do - stub_spawn(cmd, 600, tmp_repo_path, {}, success: false) - - is_expected.to be_falsy - end - end - - context 'with --force' do - let(:force) { true } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --force --tags) } - - it 'executes the command with forced option' do - stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) - - is_expected.to be_truthy - end - end - - context 'with --no-tags' do - let(:tags) { false } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --prune --no-tags) } - - it 'executes the command' do - stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) - - is_expected.to be_truthy - end - end - - context 'with no prune' do - let(:prune) { false } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} fetch #{remote_name} --quiet --tags) } - - it 'executes the command' do - stub_spawn(cmd, 600, tmp_repo_path, {}, success: true) - - is_expected.to be_truthy - end - end - - describe 'with an SSH key' do - let(:extra_args) { { ssh_key: 'SSH KEY' } } - - it 'sets GIT_SSH to a custom script' do - script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755) - key = stub_tempfile('/tmp files/keyFile', 'gitlab-shell-key-file', chmod: 0o400) - - stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true) - - is_expected.to be_truthy - - expect(script.string).to eq("#!/bin/sh\nexec ssh '-oIdentityFile=\"/tmp files/keyFile\"' '-oIdentitiesOnly=\"yes\"' \"$@\"") - expect(key.string).to eq('SSH KEY') - end - end - - describe 'with known_hosts data' do - let(:extra_args) { { known_hosts: 'KNOWN HOSTS' } } - - it 'sets GIT_SSH to a custom script' do - script = stub_tempfile('scriptFile', 'gitlab-shell-ssh-wrapper', chmod: 0o755) - key = stub_tempfile('/tmp files/knownHosts', 'gitlab-shell-known-hosts', chmod: 0o400) - - stub_spawn(cmd, 600, tmp_repo_path, { 'GIT_SSH' => 'scriptFile' }, success: true) - - is_expected.to be_truthy - - expect(script.string).to eq("#!/bin/sh\nexec ssh '-oStrictHostKeyChecking=\"yes\"' '-oUserKnownHostsFile=\"/tmp files/knownHosts\"' \"$@\"") - expect(key.string).to eq('KNOWN HOSTS') - end - end - end - - describe '#import_project' do - let(:project) { create(:project) } - let(:import_url) { TestEnv.factory_repo_path_bare } - let(:cmd) { %W(#{Gitlab.config.git.bin_path} clone --bare -- #{import_url} #{tmp_repo_path}) } - let(:timeout) { 600 } - - subject { gl_projects.import_project(import_url, timeout) } - - shared_examples 'importing repository' do - context 'success import' do - it 'imports a repo' do - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy - - is_expected.to be_truthy - - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_truthy - end - end - - context 'already exists' do - it "doesn't import" do - FileUtils.mkdir_p(tmp_repo_path) - - is_expected.to be_falsy - end - end - end - - describe 'logging' do - it 'imports a repo' do - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}>." - expect(logger).to receive(:info).with(message) - - subject - end - end - - context 'timeout' do - it 'does not import a repo' do - stub_spawn_timeout(cmd, timeout, nil) - - message = "Importing project from <#{import_url}> to <#{tmp_repo_path}> failed." - expect(logger).to receive(:error).with(message) - - is_expected.to be_falsy - - expect(gl_projects.output).to eq("Timed out\n") - expect(File.exist?(File.join(tmp_repo_path, 'HEAD'))).to be_falsy - end - end - - it_behaves_like 'importing repository' - end - - describe '#fork_repository' do - let(:dest_repos) { TestEnv::REPOS_STORAGE } - let(:dest_repos_path) { tmp_repos_path } - let(:dest_repo_name) { File.join('@hashed', 'aa', 'bb', 'xyz.git') } - let(:dest_repo) { File.join(dest_repos_path, dest_repo_name) } - - subject { gl_projects.fork_repository(dest_repos, dest_repo_name) } - - before do - FileUtils.mkdir_p(dest_repos_path) - end - - after do - FileUtils.rm_rf(dest_repos_path) - end - - shared_examples 'forking a repository' do - it 'forks the repository' do - is_expected.to be_truthy - - expect(File.exist?(dest_repo)).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy - end - - it 'does not fork if a project of the same name already exists' do - # create a fake project at the intended destination - FileUtils.mkdir_p(dest_repo) - - is_expected.to be_falsy - end - end - - it_behaves_like 'forking a repository' - - # We seem to be stuck to having only one working Gitaly storage in tests, changing - # that is not very straight-forward so I'm leaving this test here for now till - # https://gitlab.com/gitlab-org/gitlab-ce/issues/41393 is fixed. - context 'different storages' do - let(:dest_repos) { 'alternative' } - let(:dest_repos_path) { File.join(File.dirname(tmp_repos_path), dest_repos) } - - before do - stub_storage_settings(dest_repos => { 'path' => dest_repos_path }) - end - - it 'forks the repo' do - is_expected.to be_truthy - - expect(File.exist?(dest_repo)).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'pre-receive'))).to be_truthy - expect(File.exist?(File.join(dest_repo, 'hooks', 'post-receive'))).to be_truthy - end - end - - describe 'log messages' do - describe 'successful fork' do - it do - message = "Forking repository from <#{tmp_repo_path}> to <#{dest_repo}>." - expect(logger).to receive(:info).with(message) - - subject - end - end - - describe 'failed fork due existing destination' do - it do - FileUtils.mkdir_p(dest_repo) - message = "fork-repository failed: destination repository <#{dest_repo}> already exists." - expect(logger).to receive(:error).with(message) - - subject - end - end - end - end - - def build_gitlab_projects(*args) - described_class.new( - *args, - global_hooks_path: Gitlab.config.gitlab_shell.hooks_path, - logger: logger - ) - end - - def stub_spawn(*args, success: true) - exitstatus = success ? 0 : nil - expect(gl_projects).to receive(:popen_with_timeout).with(*args) - .and_return(["output", exitstatus]) - end - - def stub_spawn_timeout(*args) - expect(gl_projects).to receive(:popen_with_timeout).with(*args) - .and_raise(Timeout::Error) - end -end diff --git a/spec/lib/gitlab/git/hook_env_spec.rb b/spec/lib/gitlab/git/hook_env_spec.rb index e6aa5ad8c90..5e49ea6da7a 100644 --- a/spec/lib/gitlab/git/hook_env_spec.rb +++ b/spec/lib/gitlab/git/hook_env_spec.rb @@ -4,11 +4,7 @@ describe Gitlab::Git::HookEnv do let(:gl_repository) { 'project-123' } describe ".set" do - context 'with RequestStore.store disabled' do - before do - allow(RequestStore).to receive(:active?).and_return(false) - end - + context 'with RequestStore disabled' do it 'does not store anything' do described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo') @@ -16,11 +12,7 @@ describe Gitlab::Git::HookEnv do end end - context 'with RequestStore.store enabled' do - before do - allow(RequestStore).to receive(:active?).and_return(true) - end - + context 'with RequestStore enabled', :request_store do it 'whitelist some `GIT_*` variables and stores them using RequestStore' do described_class.set( gl_repository, @@ -41,9 +33,8 @@ describe Gitlab::Git::HookEnv do end describe ".all" do - context 'with RequestStore.store enabled' do + context 'with RequestStore enabled', :request_store do before do - allow(RequestStore).to receive(:active?).and_return(true) described_class.set( gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo', @@ -60,7 +51,7 @@ describe Gitlab::Git::HookEnv do end describe ".to_env_hash" do - context 'with RequestStore.store enabled' do + context 'with RequestStore enabled', :request_store do using RSpec::Parameterized::TableSyntax let(:key) { 'GIT_OBJECT_DIRECTORY_RELATIVE' } @@ -76,7 +67,6 @@ describe Gitlab::Git::HookEnv do with_them do before do - allow(RequestStore).to receive(:active?).and_return(true) described_class.set(gl_repository, key.to_sym => input) end @@ -92,7 +82,7 @@ describe Gitlab::Git::HookEnv do end describe 'thread-safety' do - context 'with RequestStore.store enabled' do + context 'with RequestStore enabled', :request_store do before do allow(RequestStore).to receive(:active?).and_return(true) described_class.set(gl_repository, GIT_OBJECT_DIRECTORY_RELATIVE: 'foo') diff --git a/spec/lib/gitlab/git/hook_spec.rb b/spec/lib/gitlab/git/hook_spec.rb deleted file mode 100644 index a45c8510b15..00000000000 --- a/spec/lib/gitlab/git/hook_spec.rb +++ /dev/null @@ -1,111 +0,0 @@ -require 'spec_helper' -require 'fileutils' - -describe Gitlab::Git::Hook do - before do - # We need this because in the spec/spec_helper.rb we define it like this: - # allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - allow_any_instance_of(described_class).to receive(:trigger).and_call_original - end - - around do |example| - # TODO move hook tests to gitaly-ruby. Hook will disappear from gitlab-ce - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - describe "#trigger" do - set(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw_repository } - let(:repo_path) { repository.path } - let(:hooks_dir) { File.join(repo_path, 'hooks') } - let(:user) { create(:user) } - let(:gl_id) { Gitlab::GlId.gl_id(user) } - let(:gl_username) { user.username } - - def create_hook(name) - FileUtils.mkdir_p(hooks_dir) - hook_path = File.join(hooks_dir, name) - File.open(hook_path, 'w', 0755) do |f| - f.write(<<~HOOK) - #!/bin/sh - exit 0 - HOOK - end - end - - def create_failing_hook(name) - FileUtils.mkdir_p(hooks_dir) - hook_path = File.join(hooks_dir, name) - File.open(hook_path, 'w', 0755) do |f| - f.write(<<~HOOK) - #!/bin/sh - echo 'regular message from the hook' - echo 'error message from the hook' 1>&2 - echo 'error message from the hook line 2' 1>&2 - exit 1 - HOOK - end - end - - ['pre-receive', 'post-receive', 'update'].each do |hook_name| - context "when triggering a #{hook_name} hook" do - context "when the hook is successful" do - let(:hook_path) { File.join(hooks_dir, hook_name) } - let(:gl_repository) { Gitlab::GlRepository.gl_repository(project, false) } - let(:env) do - { - 'GL_ID' => gl_id, - 'GL_USERNAME' => gl_username, - 'PWD' => repo_path, - 'GL_PROTOCOL' => 'web', - 'GL_REPOSITORY' => gl_repository - } - end - - it "returns success with no errors" do - create_hook(hook_name) - hook = described_class.new(hook_name, repository) - blank = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - - if hook_name != 'update' - expect(Open3).to receive(:popen3) - .with(env, hook_path, chdir: repo_path).and_call_original - end - - status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) - expect(status).to be true - expect(errors).to be_blank - end - end - - context "when the hook is unsuccessful" do - it "returns failure with errors" do - create_failing_hook(hook_name) - hook = described_class.new(hook_name, repository) - blank = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - - status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) - expect(status).to be false - expect(errors).to eq("error message from the hook\nerror message from the hook line 2\n") - end - end - end - end - - context "when the hook doesn't exist" do - it "returns success with no errors" do - hook = described_class.new('unknown_hook', repository) - blank = Gitlab::Git::BLANK_SHA - ref = Gitlab::Git::BRANCH_REF_PREFIX + 'new_branch' - - status, errors = hook.trigger(gl_id, gl_username, blank, blank, ref) - expect(status).to be true - expect(errors).to be_nil - end - end - end -end diff --git a/spec/lib/gitlab/git/hooks_service_spec.rb b/spec/lib/gitlab/git/hooks_service_spec.rb deleted file mode 100644 index 55ffced36ac..00000000000 --- a/spec/lib/gitlab/git/hooks_service_spec.rb +++ /dev/null @@ -1,50 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::HooksService, :seed_helper do - let(:gl_id) { 'user-456' } - let(:gl_username) { 'janedoe' } - let(:user) { Gitlab::Git::User.new(gl_username, 'Jane Doe', 'janedoe@example.com', gl_id) } - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, 'project-123') } - let(:service) { described_class.new } - let(:blankrev) { Gitlab::Git::BLANK_SHA } - let(:oldrev) { SeedRepo::Commit::PARENT_ID } - let(:newrev) { SeedRepo::Commit::ID } - let(:ref) { 'refs/heads/feature' } - - describe '#execute' do - context 'when receive hooks were successful' do - let(:hook) { double(:hook) } - - it 'calls all three hooks' do - expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - expect(hook).to receive(:trigger).with(gl_id, gl_username, blankrev, newrev, ref) - .exactly(3).times.and_return([true, nil]) - - service.execute(user, repository, blankrev, newrev, ref) { } - end - end - - context 'when pre-receive hook failed' do - it 'does not call post-receive hook' do - expect(service).to receive(:run_hook).with('pre-receive').and_return([false, 'hello world']) - expect(service).not_to receive(:run_hook).with('post-receive') - - expect do - service.execute(user, repository, blankrev, newrev, ref) - end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world') - end - end - - context 'when update hook failed' do - it 'does not call post-receive hook' do - expect(service).to receive(:run_hook).with('pre-receive').and_return([true, nil]) - expect(service).to receive(:run_hook).with('update').and_return([false, 'hello world']) - expect(service).not_to receive(:run_hook).with('post-receive') - - expect do - service.execute(user, repository, blankrev, newrev, ref) - end.to raise_error(Gitlab::Git::PreReceiveError, 'hello world') - end - end - end -end diff --git a/spec/lib/gitlab/git/index_spec.rb b/spec/lib/gitlab/git/index_spec.rb deleted file mode 100644 index c4edd6961e1..00000000000 --- a/spec/lib/gitlab/git/index_spec.rb +++ /dev/null @@ -1,239 +0,0 @@ -require 'spec_helper' - -describe Gitlab::Git::Index, :seed_helper do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } - let(:index) { described_class.new(repository) } - - before do - index.read_tree(lookup('master').tree) - end - - around do |example| - # TODO move these specs to gitaly-ruby. The Index class will disappear from gitlab-ce - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - describe '#create' do - let(:options) do - { - content: 'Lorem ipsum...', - file_path: 'documents/story.txt' - } - end - - context 'when no file at that path exists' do - it 'creates the file in the index' do - index.create(options) - - entry = index.get(options[:file_path]) - - expect(entry).not_to be_nil - expect(lookup(entry[:oid]).content).to eq(options[:content]) - end - end - - context 'when a file at that path exists' do - before do - options[:file_path] = 'files/executables/ls' - end - - it 'raises an error' do - expect { index.create(options) }.to raise_error('A file with this name already exists') - end - end - - context 'when content is in base64' do - before do - options[:content] = Base64.encode64(options[:content]) - options[:encoding] = 'base64' - end - - it 'decodes base64' do - index.create(options) - - entry = index.get(options[:file_path]) - expect(lookup(entry[:oid]).content).to eq(Base64.decode64(options[:content])) - end - end - - context 'when content contains CRLF' do - before do - repository.autocrlf = :input - options[:content] = "Hello,\r\nWorld" - end - - it 'converts to LF' do - index.create(options) - - entry = index.get(options[:file_path]) - expect(lookup(entry[:oid]).content).to eq("Hello,\nWorld") - end - end - end - - describe '#create_dir' do - let(:options) do - { - file_path: 'newdir' - } - end - - context 'when no file or dir at that path exists' do - it 'creates the dir in the index' do - index.create_dir(options) - - entry = index.get(options[:file_path] + '/.gitkeep') - - expect(entry).not_to be_nil - end - end - - context 'when a file at that path exists' do - before do - options[:file_path] = 'files/executables/ls' - end - - it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('A file with this name already exists') - end - end - - context 'when a directory at that path exists' do - before do - options[:file_path] = 'files/executables' - end - - it 'raises an error' do - expect { index.create_dir(options) }.to raise_error('A directory with this name already exists') - end - end - end - - describe '#update' do - let(:options) do - { - content: 'Lorem ipsum...', - file_path: 'README.md' - } - end - - context 'when no file at that path exists' do - before do - options[:file_path] = 'documents/story.txt' - end - - it 'raises an error' do - expect { index.update(options) }.to raise_error("A file with this name doesn't exist") - end - end - - context 'when a file at that path exists' do - it 'updates the file in the index' do - index.update(options) - - entry = index.get(options[:file_path]) - - expect(lookup(entry[:oid]).content).to eq(options[:content]) - end - - it 'preserves file mode' do - options[:file_path] = 'files/executables/ls' - - index.update(options) - - entry = index.get(options[:file_path]) - - expect(entry[:mode]).to eq(0100755) - end - end - end - - describe '#move' do - let(:options) do - { - content: 'Lorem ipsum...', - previous_path: 'README.md', - file_path: 'NEWREADME.md' - } - end - - context 'when no file at that path exists' do - it 'raises an error' do - options[:previous_path] = 'documents/story.txt' - - expect { index.move(options) }.to raise_error("A file with this name doesn't exist") - end - end - - context 'when a file at the new path already exists' do - it 'raises an error' do - options[:file_path] = 'CHANGELOG' - - expect { index.move(options) }.to raise_error("A file with this name already exists") - end - end - - context 'when a file at that path exists' do - it 'removes the old file in the index' do - index.move(options) - - entry = index.get(options[:previous_path]) - - expect(entry).to be_nil - end - - it 'creates the new file in the index' do - index.move(options) - - entry = index.get(options[:file_path]) - - expect(entry).not_to be_nil - expect(lookup(entry[:oid]).content).to eq(options[:content]) - end - - it 'preserves file mode' do - options[:previous_path] = 'files/executables/ls' - - index.move(options) - - entry = index.get(options[:file_path]) - - expect(entry[:mode]).to eq(0100755) - end - end - end - - describe '#delete' do - let(:options) do - { - file_path: 'README.md' - } - end - - context 'when no file at that path exists' do - before do - options[:file_path] = 'documents/story.txt' - end - - it 'raises an error' do - expect { index.delete(options) }.to raise_error("A file with this name doesn't exist") - end - end - - context 'when a file at that path exists' do - it 'removes the file in the index' do - index.delete(options) - - entry = index.get(options[:file_path]) - - expect(entry).to be_nil - end - end - end - - def lookup(revision) - repository.rugged.rev_parse(revision) - end -end diff --git a/spec/lib/gitlab/git/popen_spec.rb b/spec/lib/gitlab/git/popen_spec.rb deleted file mode 100644 index 074e66d2a5d..00000000000 --- a/spec/lib/gitlab/git/popen_spec.rb +++ /dev/null @@ -1,179 +0,0 @@ -require 'spec_helper' - -describe 'Gitlab::Git::Popen' do - let(:path) { Rails.root.join('tmp').to_s } - let(:test_string) { 'The quick brown fox jumped over the lazy dog' } - # The pipe buffer is typically 64K. This string is about 440K. - let(:spew_command) { ['bash', '-c', "for i in {1..10000}; do echo '#{test_string}' 1>&2; done"] } - - let(:klass) do - Class.new(Object) do - include Gitlab::Git::Popen - end - end - - context 'popen' do - context 'zero status' do - let(:result) { klass.new.popen(%w(ls), path) } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to be_zero } - it { expect(output).to include('tests') } - end - - context 'non-zero status' do - let(:result) { klass.new.popen(%w(cat NOTHING), path) } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to eq(1) } - it { expect(output).to include('No such file or directory') } - end - - context 'unsafe string command' do - it 'raises an error when it gets called with a string argument' do - expect { klass.new.popen('ls', path) }.to raise_error(RuntimeError) - end - end - - context 'with custom options' do - let(:vars) { { 'foobar' => 123, 'PWD' => path } } - let(:options) { { chdir: path } } - - it 'calls popen3 with the provided environment variables' do - expect(Open3).to receive(:popen3).with(vars, 'ls', options) - - klass.new.popen(%w(ls), path, { 'foobar' => 123 }) - end - end - - context 'use stdin' do - let(:result) { klass.new.popen(%w[cat], path) { |stdin| stdin.write 'hello' } } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to be_zero } - it { expect(output).to eq('hello') } - end - - context 'with lazy block' do - it 'yields a lazy io' do - expect_lazy_io = lambda do |io| - expect(io).to be_a Enumerator::Lazy - expect(io.inspect).to include('#<IO:fd') - end - - klass.new.popen(%w[ls], path, lazy_block: expect_lazy_io) - end - - it "doesn't wait for process exit" do - Timeout.timeout(2) do - klass.new.popen(%w[yes], path, lazy_block: ->(io) {}) - end - end - end - - context 'with a process that writes a lot of data to stderr' do - it 'returns zero' do - output, status = klass.new.popen(spew_command, path) - - expect(output).to include(test_string) - expect(status).to eq(0) - end - end - end - - context 'popen_with_timeout' do - let(:timeout) { 1.second } - - context 'no timeout' do - context 'zero status' do - let(:result) { klass.new.popen_with_timeout(%w(ls), timeout, path) } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to be_zero } - it { expect(output).to include('tests') } - end - - context 'multi-line string' do - let(:test_string) { "this is 1 line\n2nd line\n3rd line\n" } - let(:result) { klass.new.popen_with_timeout(['echo', test_string], timeout, path) } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to be_zero } - # echo adds its own line - it { expect(output).to eq(test_string + "\n") } - end - - context 'non-zero status' do - let(:result) { klass.new.popen_with_timeout(%w(cat NOTHING), timeout, path) } - let(:output) { result.first } - let(:status) { result.last } - - it { expect(status).to eq(1) } - it { expect(output).to include('No such file or directory') } - end - - context 'unsafe string command' do - it 'raises an error when it gets called with a string argument' do - expect { klass.new.popen_with_timeout('ls', timeout, path) }.to raise_error(RuntimeError) - end - end - end - - context 'timeout' do - context 'timeout' do - it "raises a Timeout::Error" do - expect { klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) }.to raise_error(Timeout::Error) - end - - it "handles processes that do not shutdown correctly" do - expect { klass.new.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) - end - - it 'handles process that writes a lot of data to stderr' do - output, status = klass.new.popen_with_timeout(spew_command, timeout, path) - - expect(output).to include(test_string) - expect(status).to eq(0) - end - end - - context 'timeout period' do - let(:time_taken) do - begin - start = Time.now - klass.new.popen_with_timeout(%w(sleep 1000), timeout, path) - rescue - Time.now - start - end - end - - it { expect(time_taken).to be >= timeout } - end - - context 'clean up' do - let(:instance) { klass.new } - - it 'kills the child process' do - expect(instance).to receive(:kill_process_group_for_pid).and_wrap_original do |m, *args| - # is the PID, and it should not be running at this point - m.call(*args) - - pid = args.first - begin - Process.getpgid(pid) - raise "The child process should have been killed" - rescue Errno::ESRCH - end - end - - expect { instance.popen_with_timeout(['bash', '-c', "trap -- '' SIGTERM; sleep 1000"], timeout, path) }.to raise_error(Timeout::Error) - end - end - end - end -end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 28c34e234f7..d02536a2fb4 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -322,21 +322,12 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#commit_count' do - shared_examples 'simple commit counting' do - it { expect(repository.commit_count("master")).to eq(25) } - it { expect(repository.commit_count("feature")).to eq(9) } - it { expect(repository.commit_count("does-not-exist")).to eq(0) } - end - - context 'when Gitaly commit_count feature is enabled' do - it_behaves_like 'simple commit counting' - it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do - subject { repository.commit_count('master') } - end - end + it { expect(repository.commit_count("master")).to eq(25) } + it { expect(repository.commit_count("feature")).to eq(9) } + it { expect(repository.commit_count("does-not-exist")).to eq(0) } - context 'when Gitaly commit_count feature is disabled', :skip_gitaly_mock do - it_behaves_like 'simple commit counting' + it_behaves_like 'wrapping gRPC errors', Gitlab::GitalyClient::CommitService, :commit_count do + subject { repository.commit_count('master') } end end @@ -378,118 +369,88 @@ describe Gitlab::Git::Repository, :seed_helper do end describe "#delete_branch" do - shared_examples "deleting a branch" do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - after do - ensure_seeds - end - - it "removes the branch from the repo" do - branch_name = "to-be-deleted-soon" + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - repository.create_branch(branch_name) - expect(repository_rugged.branches[branch_name]).not_to be_nil + after do + ensure_seeds + end - repository.delete_branch(branch_name) - expect(repository_rugged.branches[branch_name]).to be_nil - end + it "removes the branch from the repo" do + branch_name = "to-be-deleted-soon" - context "when branch does not exist" do - it "raises a DeleteBranchError exception" do - expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError) - end - end - end + repository.create_branch(branch_name) + expect(repository_rugged.branches[branch_name]).not_to be_nil - context "when Gitaly delete_branch is enabled" do - it_behaves_like "deleting a branch" + repository.delete_branch(branch_name) + expect(repository_rugged.branches[branch_name]).to be_nil end - context "when Gitaly delete_branch is disabled", :skip_gitaly_mock do - it_behaves_like "deleting a branch" + context "when branch does not exist" do + it "raises a DeleteBranchError exception" do + expect { repository.delete_branch("this-branch-does-not-exist") }.to raise_error(Gitlab::Git::Repository::DeleteBranchError) + end end end describe "#create_branch" do - shared_examples 'creating a branch' do - let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - after do - ensure_seeds - end - - it "should create a new branch" do - expect(repository.create_branch('new_branch', 'master')).not_to be_nil - end + let(:repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - it "should create a new branch with the right name" do - expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') - end + after do + ensure_seeds + end - it "should fail if we create an existing branch" do - repository.create_branch('duplicated_branch', 'master') - expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") - end + it "should create a new branch" do + expect(repository.create_branch('new_branch', 'master')).not_to be_nil + end - it "should fail if we create a branch from a non existing ref" do - expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") - end + it "should create a new branch with the right name" do + expect(repository.create_branch('another_branch', 'master').name).to eq('another_branch') end - context 'when Gitaly create_branch feature is enabled' do - it_behaves_like 'creating a branch' + it "should fail if we create an existing branch" do + repository.create_branch('duplicated_branch', 'master') + expect {repository.create_branch('duplicated_branch', 'master')}.to raise_error("Branch duplicated_branch already exists") end - context 'when Gitaly create_branch feature is disabled', :skip_gitaly_mock do - it_behaves_like 'creating a branch' + it "should fail if we create a branch from a non existing ref" do + expect {repository.create_branch('branch_based_in_wrong_ref', 'master_2_the_revenge')}.to raise_error("Invalid reference master_2_the_revenge") end end describe '#delete_refs' do - shared_examples 'deleting refs' do - let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - def repo_rugged - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - repo.rugged - end - end + let(:repo) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - after do - ensure_seeds + def repo_rugged + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + repo.rugged end + end - it 'deletes the ref' do - repo.delete_refs('refs/heads/feature') - - expect(repo_rugged.references['refs/heads/feature']).to be_nil - end + after do + ensure_seeds + end - it 'deletes all refs' do - refs = %w[refs/heads/wip refs/tags/v1.1.0] - repo.delete_refs(*refs) + it 'deletes the ref' do + repo.delete_refs('refs/heads/feature') - refs.each do |ref| - expect(repo_rugged.references[ref]).to be_nil - end - end + expect(repo_rugged.references['refs/heads/feature']).to be_nil + end - it 'does not fail when deleting an empty list of refs' do - expect { repo.delete_refs(*[]) }.not_to raise_error - end + it 'deletes all refs' do + refs = %w[refs/heads/wip refs/tags/v1.1.0] + repo.delete_refs(*refs) - it 'raises an error if it failed' do - expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) + refs.each do |ref| + expect(repo_rugged.references[ref]).to be_nil end end - context 'when Gitaly delete_refs feature is enabled' do - it_behaves_like 'deleting refs' + it 'does not fail when deleting an empty list of refs' do + expect { repo.delete_refs(*[]) }.not_to raise_error end - context 'when Gitaly delete_refs feature is disabled', :disable_gitaly do - it_behaves_like 'deleting refs' + it 'raises an error if it failed' do + expect { repo.delete_refs('refs\heads\fix') }.to raise_error(Gitlab::Git::Repository::GitError) end end @@ -542,38 +503,28 @@ describe Gitlab::Git::Repository, :seed_helper do Gitlab::Shell.new.remove_repository('default', 'my_project') end - shared_examples 'repository mirror fetching' do - it 'fetches a repository as a mirror remote' do - subject - - expect(refs(new_repository_path)).to eq(refs(repository_path)) - end - - context 'with keep-around refs' do - let(:sha) { SeedRepo::Commit::ID } - let(:keep_around_ref) { "refs/keep-around/#{sha}" } - let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } + it 'fetches a repository as a mirror remote' do + subject - before do - repository_rugged.references.create(keep_around_ref, sha, force: true) - repository_rugged.references.create(tmp_ref, sha, force: true) - end + expect(refs(new_repository_path)).to eq(refs(repository_path)) + end - it 'includes the temporary and keep-around refs' do - subject + context 'with keep-around refs' do + let(:sha) { SeedRepo::Commit::ID } + let(:keep_around_ref) { "refs/keep-around/#{sha}" } + let(:tmp_ref) { "refs/tmp/#{SecureRandom.hex}" } - expect(refs(new_repository_path)).to include(keep_around_ref) - expect(refs(new_repository_path)).to include(tmp_ref) - end + before do + repository_rugged.references.create(keep_around_ref, sha, force: true) + repository_rugged.references.create(tmp_ref, sha, force: true) end - end - context 'with gitaly enabled' do - it_behaves_like 'repository mirror fetching' - end + it 'includes the temporary and keep-around refs' do + subject - context 'with gitaly enabled', :skip_gitaly_mock do - it_behaves_like 'repository mirror fetching' + expect(refs(new_repository_path)).to include(keep_around_ref) + expect(refs(new_repository_path)).to include(tmp_ref) + end end def new_repository_path @@ -918,25 +869,15 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#merge_base' do - shared_examples '#merge_base' do - where(:from, :to, :result) do - '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' - '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' - '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil - 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil - end - - with_them do - it { expect(repository.merge_base(from, to)).to eq(result) } - end + where(:from, :to, :result) do + '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' + '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' | '570e7b2abdd848b95f2f578043fc23bd6f6fd24d' + '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | 'foobar' | nil + 'foobar' | '40f4a7a617393735a95a0bb67b08385bc1e7c66d' | nil end - context 'with gitaly' do - it_behaves_like '#merge_base' - end - - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#merge_base' + with_them do + it { expect(repository.merge_base(from, to)).to eq(result) } end end @@ -1028,54 +969,6 @@ describe Gitlab::Git::Repository, :seed_helper do end end - describe '#autocrlf' do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.rugged.config['core.autocrlf'] = true - end - - around do |example| - # OK because autocrlf is only used in gitaly-ruby - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - it 'return the value of the autocrlf option' do - expect(@repo.autocrlf).to be(true) - end - - after(:all) do - @repo.rugged.config.delete('core.autocrlf') - end - end - - describe '#autocrlf=' do - before(:all) do - @repo = Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') - @repo.rugged.config['core.autocrlf'] = false - end - - around do |example| - # OK because autocrlf= is only used in gitaly-ruby - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - it 'should set the autocrlf option to the provided option' do - @repo.autocrlf = :input - - File.open(File.join(SEED_STORAGE_PATH, TEST_MUTABLE_REPO_PATH, 'config')) do |config_file| - expect(config_file.read).to match('autocrlf = input') - end - end - - after(:all) do - @repo.rugged.config.delete('core.autocrlf') - end - end - describe '#find_branch' do it 'should return a Branch for master' do branch = repository.find_branch('master') @@ -1175,57 +1068,81 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#merged_branch_names' do - shared_examples 'finding merged branch names' do - context 'when branch names are passed' do - it 'only returns the names we are asking' do - names = repository.merged_branch_names(%w[merge-test]) + context 'when branch names are passed' do + it 'only returns the names we are asking' do + names = repository.merged_branch_names(%w[merge-test]) - expect(names).to contain_exactly('merge-test') - end + expect(names).to contain_exactly('merge-test') + end - it 'does not return unmerged branch names' do - names = repository.merged_branch_names(%w[feature]) + it 'does not return unmerged branch names' do + names = repository.merged_branch_names(%w[feature]) - expect(names).to be_empty - end + expect(names).to be_empty end + end - context 'when no root ref is available' do - it 'returns empty list' do - project = create(:project, :empty_repo) + context 'when no root ref is available' do + it 'returns empty list' do + project = create(:project, :empty_repo) - names = project.repository.merged_branch_names(%w[feature]) + names = project.repository.merged_branch_names(%w[feature]) - expect(names).to be_empty - end + expect(names).to be_empty end + end - context 'when no branch names are specified' do - before do - repository.create_branch('identical', 'master') - end + context 'when no branch names are specified' do + before do + repository.create_branch('identical', 'master') + end - after do - ensure_seeds - end + after do + ensure_seeds + end - it 'returns all merged branch names except for identical one' do - names = repository.merged_branch_names + it 'returns all merged branch names except for identical one' do + names = repository.merged_branch_names - expect(names).to include('merge-test') - expect(names).to include('fix-mode') - expect(names).not_to include('feature') - expect(names).not_to include('identical') - end + expect(names).to include('merge-test') + expect(names).to include('fix-mode') + expect(names).not_to include('feature') + expect(names).not_to include('identical') end end + end + + describe '#diff_stats' do + let(:left_commit_id) { 'feature' } + let(:right_commit_id) { 'master' } + + it 'returns a DiffStatsCollection' do + collection = repository.diff_stats(left_commit_id, right_commit_id) + + expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) + expect(collection).to be_a(Enumerable) + end + + it 'yields Gitaly::DiffStats objects' do + collection = repository.diff_stats(left_commit_id, right_commit_id) + + expect(collection.to_a).to all(be_a(Gitaly::DiffStats)) + end - context 'when Gitaly merged_branch_names feature is enabled' do - it_behaves_like 'finding merged branch names' + it 'returns no Gitaly::DiffStats when SHAs are invalid' do + collection = repository.diff_stats('foo', 'bar') + + expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to be_empty end - context 'when Gitaly merged_branch_names feature is disabled', :disable_gitaly do - it_behaves_like 'finding merged branch names' + it 'returns no Gitaly::DiffStats when there is a nil SHA' do + collection = repository.diff_stats(nil, 'master') + + expect(collection).to be_a(Gitlab::Git::DiffStatsCollection) + expect(collection).to be_a(Enumerable) + expect(collection.to_a).to be_empty end end @@ -1342,76 +1259,46 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#ref_exists?' do - shared_examples 'checks the existence of refs' do - it 'returns true for an existing tag' do - expect(repository.ref_exists?('refs/heads/master')).to eq(true) - end - - it 'returns false for a non-existing tag' do - expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false) - end - - it 'raises an ArgumentError for an empty string' do - expect { repository.ref_exists?('') }.to raise_error(ArgumentError) - end + it 'returns true for an existing tag' do + expect(repository.ref_exists?('refs/heads/master')).to eq(true) + end - it 'raises an ArgumentError for an invalid ref' do - expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError) - end + it 'returns false for a non-existing tag' do + expect(repository.ref_exists?('refs/tags/THIS_TAG_DOES_NOT_EXIST')).to eq(false) end - context 'when Gitaly ref_exists feature is enabled' do - it_behaves_like 'checks the existence of refs' + it 'raises an ArgumentError for an empty string' do + expect { repository.ref_exists?('') }.to raise_error(ArgumentError) end - context 'when Gitaly ref_exists feature is disabled', :skip_gitaly_mock do - it_behaves_like 'checks the existence of refs' + it 'raises an ArgumentError for an invalid ref' do + expect { repository.ref_exists?('INVALID') }.to raise_error(ArgumentError) end end describe '#tag_exists?' do - shared_examples 'checks the existence of tags' do - it 'returns true for an existing tag' do - tag = repository.tag_names.first - - expect(repository.tag_exists?(tag)).to eq(true) - end + it 'returns true for an existing tag' do + tag = repository.tag_names.first - it 'returns false for a non-existing tag' do - expect(repository.tag_exists?('v9000')).to eq(false) - end - end - - context 'when Gitaly ref_exists_tags feature is enabled' do - it_behaves_like 'checks the existence of tags' + expect(repository.tag_exists?(tag)).to eq(true) end - context 'when Gitaly ref_exists_tags feature is disabled', :skip_gitaly_mock do - it_behaves_like 'checks the existence of tags' + it 'returns false for a non-existing tag' do + expect(repository.tag_exists?('v9000')).to eq(false) end end describe '#branch_exists?' do - shared_examples 'checks the existence of branches' do - it 'returns true for an existing branch' do - expect(repository.branch_exists?('master')).to eq(true) - end - - it 'returns false for a non-existing branch' do - expect(repository.branch_exists?('kittens')).to eq(false) - end - - it 'returns false when using an invalid branch name' do - expect(repository.branch_exists?('.bla')).to eq(false) - end + it 'returns true for an existing branch' do + expect(repository.branch_exists?('master')).to eq(true) end - context 'when Gitaly ref_exists_branches feature is enabled' do - it_behaves_like 'checks the existence of branches' + it 'returns false for a non-existing branch' do + expect(repository.branch_exists?('kittens')).to eq(false) end - context 'when Gitaly ref_exists_branches feature is disabled', :skip_gitaly_mock do - it_behaves_like 'checks the existence of branches' + it 'returns false when using an invalid branch name' do + expect(repository.branch_exists?('.bla')).to eq(false) end end @@ -1502,52 +1389,6 @@ describe Gitlab::Git::Repository, :seed_helper do end end - describe '#with_repo_branch_commit' do - context 'when comparing with the same repository' do - let(:start_repository) { repository } - - context 'when the branch exists' do - let(:start_branch_name) { 'master' } - - it 'yields the commit' do - expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } - .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) - end - end - - context 'when the branch does not exist' do - let(:start_branch_name) { 'definitely-not-master' } - - it 'yields nil' do - expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } - .to yield_with_args(nil) - end - end - end - - context 'when comparing with another repository' do - let(:start_repository) { Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '') } - - context 'when the branch exists' do - let(:start_branch_name) { 'master' } - - it 'yields the commit' do - expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } - .to yield_with_args(an_instance_of(Gitlab::Git::Commit)) - end - end - - context 'when the branch does not exist' do - let(:start_branch_name) { 'definitely-not-master' } - - it 'yields nil' do - expect { |b| repository.with_repo_branch_commit(start_repository, start_branch_name, &b) } - .to yield_with_args(nil) - end - end - end - end - describe '#fetch_source_branch!' do let(:local_ref) { 'refs/merge-requests/1/head' } let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '') } @@ -1598,29 +1439,19 @@ describe Gitlab::Git::Repository, :seed_helper do end describe '#rm_branch' do - shared_examples "user deleting a branch" do - let(:project) { create(:project, :repository) } - let(:repository) { project.repository.raw } - let(:branch_name) { "to-be-deleted-soon" } + let(:project) { create(:project, :repository) } + let(:repository) { project.repository.raw } + let(:branch_name) { "to-be-deleted-soon" } - before do - project.add_developer(user) - repository.create_branch(branch_name) - end - - it "removes the branch from the repo" do - repository.rm_branch(branch_name, user: user) - - expect(repository_rugged.branches[branch_name]).to be_nil - end + before do + project.add_developer(user) + repository.create_branch(branch_name) end - context "when Gitaly user_delete_branch is enabled" do - it_behaves_like "user deleting a branch" - end + it "removes the branch from the repo" do + repository.rm_branch(branch_name, user: user) - context "when Gitaly user_delete_branch is disabled", :skip_gitaly_mock do - it_behaves_like "user deleting a branch" + expect(repository_rugged.branches[branch_name]).to be_nil end end @@ -1744,39 +1575,29 @@ describe Gitlab::Git::Repository, :seed_helper do ensure_seeds end - shared_examples '#merge' do - it 'can perform a merge' do - merge_commit_id = nil - result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| - merge_commit_id = commit_id - end - - expect(result.newrev).to eq(merge_commit_id) - expect(result.repo_created).to eq(false) - expect(result.branch_created).to eq(false) + it 'can perform a merge' do + merge_commit_id = nil + result = repository.merge(user, source_sha, target_branch, 'Test merge') do |commit_id| + merge_commit_id = commit_id end - it 'returns nil if there was a concurrent branch update' do - concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' - result = repository.merge(user, source_sha, target_branch, 'Test merge') do - # This ref update should make the merge fail - repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) - end - - # This 'nil' signals that the merge was not applied - expect(result).to be_nil + expect(result.newrev).to eq(merge_commit_id) + expect(result.repo_created).to eq(false) + expect(result.branch_created).to eq(false) + end - # Our concurrent ref update should not have been undone - expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id) + it 'returns nil if there was a concurrent branch update' do + concurrent_update_id = '33f3729a45c02fc67d00adb1b8bca394b0e761d9' + result = repository.merge(user, source_sha, target_branch, 'Test merge') do + # This ref update should make the merge fail + repository.write_ref(Gitlab::Git::BRANCH_REF_PREFIX + target_branch, concurrent_update_id) end - end - context 'with gitaly' do - it_behaves_like '#merge' - end + # This 'nil' signals that the merge was not applied + expect(result).to be_nil - context 'without gitaly', :skip_gitaly_mock do - it_behaves_like '#merge' + # Our concurrent ref update should not have been undone + expect(repository.find_branch(target_branch).target).to eq(concurrent_update_id) end end @@ -1888,88 +1709,47 @@ describe Gitlab::Git::Repository, :seed_helper do describe '#add_remote' do let(:mirror_refmap) { '+refs/*:refs/*' } - shared_examples 'add_remote' do - it 'added the remote' do - begin - rugged.remotes.delete(remote_name) - rescue Rugged::ConfigError - end - - repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - - expect(rugged.remotes[remote_name]).not_to be_nil - expect(rugged.config["remote.#{remote_name}.mirror"]).to eq('true') - expect(rugged.config["remote.#{remote_name}.prune"]).to eq('true') - expect(rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap) + it 'added the remote' do + begin + rugged.remotes.delete(remote_name) + rescue Rugged::ConfigError end - end - context 'using Gitaly' do - it_behaves_like 'add_remote' - end + repository.add_remote(remote_name, url, mirror_refmap: mirror_refmap) - context 'with Gitaly disabled', :disable_gitaly do - it_behaves_like 'add_remote' + expect(rugged.remotes[remote_name]).not_to be_nil + expect(rugged.config["remote.#{remote_name}.mirror"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.prune"]).to eq('true') + expect(rugged.config["remote.#{remote_name}.fetch"]).to eq(mirror_refmap) end end describe '#remove_remote' do - shared_examples 'remove_remote' do - it 'removes the remote' do - rugged.remotes.create(remote_name, url) + it 'removes the remote' do + rugged.remotes.create(remote_name, url) - repository.remove_remote(remote_name) + repository.remove_remote(remote_name) - expect(rugged.remotes[remote_name]).to be_nil - end - end - - context 'using Gitaly' do - it_behaves_like 'remove_remote' - end - - context 'with Gitaly disabled', :disable_gitaly do - it_behaves_like 'remove_remote' - end - end - end - - describe '#gitlab_projects' do - subject { repository.gitlab_projects } - - it do - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - expect(subject.shard_path).to eq(storage_path) + expect(rugged.remotes[remote_name]).to be_nil end end - it { expect(subject.repository_relative_path).to eq(repository.relative_path) } end describe '#bundle_to_disk' do - shared_examples 'bundling to disk' do - let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } + let(:save_path) { File.join(Dir.tmpdir, "repo-#{SecureRandom.hex}.bundle") } - after do - FileUtils.rm_rf(save_path) - end - - it 'saves a bundle to disk' do - repository.bundle_to_disk(save_path) - - success = system( - *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}), - [:out, :err] => '/dev/null' - ) - expect(success).to be true - end + after do + FileUtils.rm_rf(save_path) end - context 'when Gitaly bundle_to_disk feature is enabled' do - it_behaves_like 'bundling to disk' - end + it 'saves a bundle to disk' do + repository.bundle_to_disk(save_path) - context 'when Gitaly bundle_to_disk feature is disabled', :disable_gitaly do - it_behaves_like 'bundling to disk' + success = system( + *%W(#{Gitlab.config.git.bin_path} -C #{repository_path} bundle verify #{save_path}), + [:out, :err] => '/dev/null' + ) + expect(success).to be true end end @@ -2006,7 +1786,7 @@ describe Gitlab::Git::Repository, :seed_helper do describe '#checksum' do it 'calculates the checksum for non-empty repo' do - expect(repository.checksum).to eq '4be7d24ce7e8d845502d599b72d567d23e6a40c0' + expect(repository.checksum).to eq '51d0a9662681f93e1fee547a6b7ba2bcaf716059' end it 'returns 0000000000000000000000000000000000000000 for an empty repo' do @@ -2044,138 +1824,41 @@ describe Gitlab::Git::Repository, :seed_helper do end end - context 'gitlab_projects commands' do - let(:gitlab_projects) { repository.gitlab_projects } - let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } - - describe '#push_remote_branches' do - subject do - repository.push_remote_branches('downstream-remote', ['master']) - end - - it 'executes the command' do - expect(gitlab_projects).to receive(:push_branches) - .with('downstream-remote', timeout, true, ['master']) - .and_return(true) - - is_expected.to be_truthy - end - - it 'raises an error if the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:push_branches) - .with('downstream-remote', timeout, true, ['master']) - .and_return(false) - - expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') - end - end - - describe '#delete_remote_branches' do - subject do - repository.delete_remote_branches('downstream-remote', ['master']) - end - - it 'executes the command' do - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(true) + describe '#clean_stale_repository_files' do + let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') } - is_expected.to be_truthy - end + it 'cleans up the files' do + create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master] + raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') - it 'raises an error if the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(false) + FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) + # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, + # but the HEAD must be 40 characters long or git will ignore it. + File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) - expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') - end - end + # git 2.16 fails with "fatal: bad object HEAD" + expect(rev_list_all).to be false - describe '#delete_remote_branches' do - subject do - repository.delete_remote_branches('downstream-remote', ['master']) - end + repository.clean_stale_repository_files - it 'executes the command' do - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(true) - - is_expected.to be_truthy - end - - it 'raises an error if the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(false) - - expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') - end + expect(rev_list_all).to be true + expect(File.exist?(worktree_path)).to be_falsey end - describe '#clean_stale_repository_files' do - let(:worktree_path) { File.join(repository_path, 'worktrees', 'delete-me') } - - it 'cleans up the files' do - create_worktree = %W[git -C #{repository_path} worktree add --detach #{worktree_path} master] - raise 'preparation failed' unless system(*create_worktree, err: '/dev/null') - - FileUtils.touch(worktree_path, mtime: Time.now - 8.hours) - # git rev-list --all will fail in git 2.16 if HEAD is pointing to a non-existent object, - # but the HEAD must be 40 characters long or git will ignore it. - File.write(File.join(worktree_path, 'HEAD'), Gitlab::Git::BLANK_SHA) - - # git 2.16 fails with "fatal: bad object HEAD" - expect(rev_list_all).to be false - - repository.clean_stale_repository_files - - expect(rev_list_all).to be true - expect(File.exist?(worktree_path)).to be_falsey - end - - def rev_list_all - system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null') - end - - it 'increments a counter upon an error' do - expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError) - - counter = double(:counter) - - expect(counter).to receive(:increment) - expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total, - 'Number of failed repository cleanup events').and_return(counter) - - repository.clean_stale_repository_files - end + def rev_list_all + system(*%W[git -C #{repository_path} rev-list --all], out: '/dev/null', err: '/dev/null') end - describe '#delete_remote_branches' do - subject do - repository.delete_remote_branches('downstream-remote', ['master']) - end + it 'increments a counter upon an error' do + expect(repository.gitaly_repository_client).to receive(:cleanup).and_raise(Gitlab::Git::CommandError) - it 'executes the command' do - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(true) + counter = double(:counter) - is_expected.to be_truthy - end - - it 'raises an error if the command fails' do - allow(gitlab_projects).to receive(:output) { 'error' } - expect(gitlab_projects).to receive(:delete_remote_branches) - .with('downstream-remote', ['master']) - .and_return(false) + expect(counter).to receive(:increment) + expect(Gitlab::Metrics).to receive(:counter).with(:failed_repository_cleanup_total, + 'Number of failed repository cleanup events').and_return(counter) - expect { subject }.to raise_error(Gitlab::Git::CommandError, 'error') - end + repository.clean_stale_repository_files end end diff --git a/spec/lib/gitlab/git/user_spec.rb b/spec/lib/gitlab/git/user_spec.rb index 99d850e1df9..d9d338206f8 100644 --- a/spec/lib/gitlab/git/user_spec.rb +++ b/spec/lib/gitlab/git/user_spec.rb @@ -22,10 +22,19 @@ describe Gitlab::Git::User do end describe '.from_gitlab' do - let(:user) { build(:user) } - subject { described_class.from_gitlab(user) } + context 'when no commit_email has been set' do + let(:user) { build(:user, email: 'alice@example.com', commit_email: nil) } + subject { described_class.from_gitlab(user) } - it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) } + it { expect(subject).to eq(described_class.new(user.username, user.name, user.email, 'user-')) } + end + + context 'when commit_email has been set' do + let(:user) { build(:user, email: 'alice@example.com', commit_email: 'bob@example.com') } + subject { described_class.from_gitlab(user) } + + it { expect(subject).to eq(described_class.new(user.username, user.name, user.commit_email, 'user-')) } + end end describe '#==' do diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb index bcdf12a00a0..d7bd757149d 100644 --- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb @@ -118,6 +118,22 @@ describe Gitlab::GitalyClient::CommitService do end end + describe '#diff_stats' do + let(:left_commit_id) { 'master' } + let(:right_commit_id) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } + + it 'sends an RPC request' do + request = Gitaly::DiffStatsRequest.new(repository: repository_message, + left_commit_id: left_commit_id, + right_commit_id: right_commit_id) + + expect_any_instance_of(Gitaly::DiffService::Stub).to receive(:diff_stats) + .with(request, kind_of(Hash)).and_return([]) + + described_class.new(repository).diff_stats(left_commit_id, right_commit_id) + end + end + describe '#tree_entries' do let(:path) { '/' } diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index eefd00e7383..3f2281f213f 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -6143,7 +6143,7 @@ "id": 36, "project_id": 5, "ref": "master", - "sha": "be93687618e4b132087f430a4d8fc3a609c9b77c", + "sha": "sha-notes", "before_sha": null, "push_data": null, "created_at": "2016-03-22T15:20:35.755Z", @@ -6154,6 +6154,7 @@ "status": "failed", "started_at": null, "finished_at": null, + "user_id": 9999, "duration": null, "notes": [ { @@ -6353,6 +6354,7 @@ }, { "id": 38, + "iid": 1, "project_id": 5, "ref": "master", "sha": "5f923865dde3436854e9ceb9cdb7815618d4e849", diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb index 3ff6be595a8..7ebfc61f5e7 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -59,7 +59,11 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end it 'creates a valid pipeline note' do - expect(Ci::Pipeline.first.notes).not_to be_empty + expect(Ci::Pipeline.find_by_sha('sha-notes').notes).not_to be_empty + end + + it 'pipeline has the correct user ID' do + expect(Ci::Pipeline.find_by_sha('sha-notes').user_id).to eq(@user.id) end it 'restores pipelines with missing ref' do diff --git a/spec/lib/gitlab/import_export/repo_restorer_spec.rb b/spec/lib/gitlab/import_export/repo_restorer_spec.rb index 7ffa84f906d..8a699eb1461 100644 --- a/spec/lib/gitlab/import_export/repo_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/repo_restorer_spec.rb @@ -1,6 +1,8 @@ require 'spec_helper' describe Gitlab::ImportExport::RepoRestorer do + include GitHelpers + describe 'bundle a project Git repo' do let(:user) { create(:user) } let!(:project_with_repo) { create(:project, :repository, name: 'test-repo-restorer', path: 'test-repo-restorer') } @@ -36,9 +38,7 @@ describe Gitlab::ImportExport::RepoRestorer do it 'has the webhooks' do restorer.restore - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - expect(Gitlab::Git::Hook.new('post-receive', project.repository.raw_repository)).to exist - end + expect(project_hook_exists?(project)).to be true end end end diff --git a/spec/lib/gitlab/kubernetes/kube_client_spec.rb b/spec/lib/gitlab/kubernetes/kube_client_spec.rb index 9146729d139..53c5a4e7c94 100644 --- a/spec/lib/gitlab/kubernetes/kube_client_spec.rb +++ b/spec/lib/gitlab/kubernetes/kube_client_spec.rb @@ -116,12 +116,14 @@ describe Gitlab::Kubernetes::KubeClient do :get_config_map, :get_pod, :get_namespace, + :get_secret, :get_service, :get_service_account, :delete_pod, :create_config_map, :create_namespace, :create_pod, + :create_secret, :create_service_account, :update_config_map, :update_service_account diff --git a/spec/lib/gitlab/kubernetes/service_account_token_spec.rb b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb new file mode 100644 index 00000000000..0773d3d9aec --- /dev/null +++ b/spec/lib/gitlab/kubernetes/service_account_token_spec.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::Kubernetes::ServiceAccountToken do + let(:name) { 'token-name' } + let(:service_account_name) { 'a_service_account' } + let(:namespace_name) { 'a_namespace' } + let(:service_account_token) { described_class.new(name, service_account_name, namespace_name) } + + it { expect(service_account_token.name).to eq(name) } + it { expect(service_account_token.service_account_name).to eq(service_account_name) } + it { expect(service_account_token.namespace_name).to eq(namespace_name) } + + describe '#generate' do + let(:resource) do + ::Kubeclient::Resource.new( + metadata: { + name: name, + namespace: namespace_name, + annotations: { + 'kubernetes.io/service-account.name': service_account_name + } + }, + type: 'kubernetes.io/service-account-token' + ) + end + + subject { service_account_token.generate } + + it 'should build a Kubeclient Resource' do + is_expected.to eq(resource) + end + end +end diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb index 8fbeaa065fa..ac3bc6b2dfe 100644 --- a/spec/lib/gitlab/middleware/read_only_spec.rb +++ b/spec/lib/gitlab/middleware/read_only_spec.rb @@ -34,7 +34,7 @@ describe Gitlab::Middleware::ReadOnly do end end - context 'normal requests to a read-only Gitlab instance' do + context 'normal requests to a read-only GitLab instance' do let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } } before do diff --git a/spec/lib/gitlab/null_request_store_spec.rb b/spec/lib/gitlab/null_request_store_spec.rb new file mode 100644 index 00000000000..c023dac53ad --- /dev/null +++ b/spec/lib/gitlab/null_request_store_spec.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::NullRequestStore do + let(:null_store) { described_class.new } + + describe '#store' do + it 'returns an empty hash' do + expect(null_store.store).to eq({}) + end + end + + describe '#active?' do + it 'returns falsey' do + expect(null_store.active?).to be_falsey + end + end + + describe '#read' do + it 'returns nil' do + expect(null_store.read('foo')).to be nil + end + end + + describe '#[]' do + it 'returns nil' do + expect(null_store['foo']).to be nil + end + end + + describe '#write' do + it 'returns the same value' do + expect(null_store.write('key', 'value')).to eq('value') + end + end + + describe '#[]=' do + it 'returns the same value' do + expect(null_store['key'] = 'value').to eq('value') + end + end + + describe '#exist?' do + it 'returns falsey' do + expect(null_store.exist?('foo')).to be_falsey + end + end + + describe '#fetch' do + it 'returns the block result' do + expect(null_store.fetch('key') { 'block result' }).to eq('block result') + end + end + + describe '#delete' do + context 'when a block is given' do + it 'yields the key to the block' do + expect do |b| + null_store.delete('foo', &b) + end.to yield_with_args('foo') + end + + it 'returns the block result' do + expect(null_store.delete('foo') { |key| 'block result' }).to eq('block result') + end + end + + context 'when a block is not given' do + it 'returns nil' do + expect(null_store.delete('foo')).to be nil + end + end + end +end diff --git a/spec/lib/gitlab/patch/prependable_spec.rb b/spec/lib/gitlab/patch/prependable_spec.rb new file mode 100644 index 00000000000..725d733d176 --- /dev/null +++ b/spec/lib/gitlab/patch/prependable_spec.rb @@ -0,0 +1,234 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +# Patching ActiveSupport::Concern +require_relative '../../../../config/initializers/0_as_concern' + +describe Gitlab::Patch::Prependable do + before do + @prepended_modules = [] + end + + let(:ee) do + # So that block in Module.new could see them + prepended_modules = @prepended_modules + + Module.new do + extend ActiveSupport::Concern + + class_methods do + def class_name + super.tr('C', 'E') + end + end + + this = self + prepended do + prepended_modules << [self, this] + end + + def name + super.tr('c', 'e') + end + end + end + + let(:ce) do + # So that block in Module.new could see them + prepended_modules = @prepended_modules + ee_ = ee + + Module.new do + extend ActiveSupport::Concern + prepend ee_ + + class_methods do + def class_name + 'CE' + end + end + + this = self + prepended do + prepended_modules << [self, this] + end + + def name + 'ce' + end + end + end + + describe 'a class including a concern prepending a concern' do + subject { Class.new.include(ce) } + + it 'returns values from prepended module ee' do + expect(subject.new.name).to eq('ee') + expect(subject.class_name).to eq('EE') + end + + it 'has the expected ancestors' do + expect(subject.ancestors.take(3)).to eq([subject, ee, ce]) + expect(subject.singleton_class.ancestors.take(3)) + .to eq([subject.singleton_class, + ee.const_get(:ClassMethods), + ce.const_get(:ClassMethods)]) + end + + it 'prepends only once even if called twice' do + 2.times { ce.prepend(ee) } + + subject + + expect(@prepended_modules).to eq([[ce, ee]]) + end + + context 'overriding methods' do + before do + subject.module_eval do + def self.class_name + 'Custom' + end + + def name + 'custom' + end + end + end + + it 'returns values from the class' do + expect(subject.new.name).to eq('custom') + expect(subject.class_name).to eq('Custom') + end + end + end + + describe 'a class prepending a concern prepending a concern' do + subject { Class.new.prepend(ce) } + + it 'returns values from prepended module ee' do + expect(subject.new.name).to eq('ee') + expect(subject.class_name).to eq('EE') + end + + it 'has the expected ancestors' do + expect(subject.ancestors.take(3)).to eq([ee, ce, subject]) + expect(subject.singleton_class.ancestors.take(3)) + .to eq([ee.const_get(:ClassMethods), + ce.const_get(:ClassMethods), + subject.singleton_class]) + end + + it 'prepends only once' do + subject.prepend(ce) + + expect(@prepended_modules).to eq([[ce, ee], [subject, ce]]) + end + end + + describe 'a class prepending a concern' do + subject do + ee_ = ee + + Class.new do + prepend ee_ + + def self.class_name + 'CE' + end + + def name + 'ce' + end + end + end + + it 'returns values from prepended module ee' do + expect(subject.new.name).to eq('ee') + expect(subject.class_name).to eq('EE') + end + + it 'has the expected ancestors' do + expect(subject.ancestors.take(2)).to eq([ee, subject]) + expect(subject.singleton_class.ancestors.take(2)) + .to eq([ee.const_get(:ClassMethods), + subject.singleton_class]) + end + + it 'prepends only once' do + subject.prepend(ee) + + expect(@prepended_modules).to eq([[subject, ee]]) + end + end + + describe 'simple case' do + subject do + foo_ = foo + + Class.new do + prepend foo_ + + def value + 10 + end + end + end + + let(:foo) do + Module.new do + extend ActiveSupport::Concern + + prepended do + def self.class_value + 20 + end + end + + def value + super * 10 + end + end + end + + context 'class methods' do + it "has a method" do + expect(subject).to respond_to(:class_value) + end + + it 'can execute a method' do + expect(subject.class_value).to eq(20) + end + end + + context 'instance methods' do + it "has a method" do + expect(subject.new).to respond_to(:value) + end + + it 'chains a method execution' do + expect(subject.new.value).to eq(100) + end + end + end + + context 'having two prepended blocks' do + subject do + Module.new do + extend ActiveSupport::Concern + + prepended do + end + + prepended do + end + end + end + + it "raises an error" do + expect { subject } + .to raise_error(described_class::MultiplePrependedBlocks) + end + end +end diff --git a/spec/lib/gitlab/safe_request_store_spec.rb b/spec/lib/gitlab/safe_request_store_spec.rb new file mode 100644 index 00000000000..27766fa0eda --- /dev/null +++ b/spec/lib/gitlab/safe_request_store_spec.rb @@ -0,0 +1,245 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Gitlab::SafeRequestStore do + describe '.store' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect(described_class.store).to eq(RequestStore) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect(described_class.store).to be_a(Gitlab::NullRequestStore) + end + end + end + + describe '.begin!' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect(RequestStore).to receive(:begin!) + + described_class.begin! + end + end + + context 'when RequestStore is NOT active' do + it 'uses RequestStore' do + expect(RequestStore).to receive(:begin!) + + described_class.begin! + end + end + end + + describe '.clear!' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect(RequestStore).to receive(:clear!).twice.and_call_original + + described_class.clear! + end + end + + context 'when RequestStore is NOT active' do + it 'uses RequestStore' do + expect(RequestStore).to receive(:clear!).and_call_original + + described_class.clear! + end + end + end + + describe '.end!' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect(RequestStore).to receive(:end!).twice.and_call_original + + described_class.end! + end + end + + context 'when RequestStore is NOT active' do + it 'uses RequestStore' do + expect(RequestStore).to receive(:end!).and_call_original + + described_class.end! + end + end + end + + describe '.write' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + described_class.write('foo', true) + end.to change { described_class.read('foo') }.from(nil).to(true) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect do + described_class.write('foo', true) + end.not_to change { described_class.read('foo') }.from(nil) + end + end + end + + describe '.[]=' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + described_class['foo'] = true + end.to change { described_class.read('foo') }.from(nil).to(true) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect do + described_class['foo'] = true + end.not_to change { described_class.read('foo') }.from(nil) + end + end + end + + describe '.read' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + RequestStore.write('foo', true) + end.to change { described_class.read('foo') }.from(nil).to(true) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect do + RequestStore.write('foo', true) + end.not_to change { described_class.read('foo') }.from(nil) + + RequestStore.clear! # Clean up + end + end + end + + describe '.[]' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + RequestStore.write('foo', true) + end.to change { described_class['foo'] }.from(nil).to(true) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect do + RequestStore.write('foo', true) + end.not_to change { described_class['foo'] }.from(nil) + + RequestStore.clear! # Clean up + end + end + end + + describe '.exist?' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + RequestStore.write('foo', 'not nil') + end.to change { described_class.exist?('foo') }.from(false).to(true) + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + expect do + RequestStore.write('foo', 'not nil') + end.not_to change { described_class.exist?('foo') }.from(false) + + RequestStore.clear! # Clean up + end + end + end + + describe '.fetch' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + expect do + described_class.fetch('foo') { 'block result' } + end.to change { described_class.read('foo') }.from(nil).to('block result') + end + end + + context 'when RequestStore is NOT active' do + it 'does not use RequestStore' do + RequestStore.clear! # Ensure clean + + expect do + described_class.fetch('foo') { 'block result' } + end.not_to change { described_class.read('foo') }.from(nil) + + RequestStore.clear! # Clean up + end + end + end + + describe '.delete' do + context 'when RequestStore is active', :request_store do + it 'uses RequestStore' do + described_class.write('foo', true) + + expect do + described_class.delete('foo') + end.to change { described_class.read('foo') }.from(true).to(nil) + end + + context 'when given a block and the key exists' do + it 'does not execute the block' do + described_class.write('foo', true) + + expect do |b| + described_class.delete('foo', &b) + end.not_to yield_control + end + end + + context 'when given a block and the key does not exist' do + it 'yields the key and returns the block result' do + result = described_class.delete('foo') { |key| "#{key} block result" } + + expect(result).to eq('foo block result') + end + end + end + + context 'when RequestStore is NOT active' do + around do |example| + RequestStore.write('foo', true) + + example.run + + RequestStore.clear! # Clean up + end + + it 'does not use RequestStore' do + expect do + described_class.delete('foo') + end.not_to change { RequestStore.read('foo') }.from(true) + end + + context 'when given a block' do + it 'yields the key and returns the block result' do + result = described_class.delete('foo') { |key| "#{key} block result" } + + expect(result).to eq('foo block result') + end + end + end + end +end diff --git a/spec/lib/gitlab/shell_spec.rb b/spec/lib/gitlab/shell_spec.rb index f8bf896950e..b1b7c427313 100644 --- a/spec/lib/gitlab/shell_spec.rb +++ b/spec/lib/gitlab/shell_spec.rb @@ -7,15 +7,10 @@ describe Gitlab::Shell do let(:repository) { project.repository } let(:gitlab_shell) { described_class.new } let(:popen_vars) { { 'GIT_TERMINAL_PROMPT' => ENV['GIT_TERMINAL_PROMPT'] } } - let(:gitlab_projects) { double('gitlab_projects') } let(:timeout) { Gitlab.config.gitlab_shell.git_timeout } before do allow(Project).to receive(:find).and_return(project) - - allow(gitlab_shell).to receive(:gitlab_projects) - .with(project.repository_storage, project.disk_path + '.git') - .and_return(gitlab_projects) end it { is_expected.to respond_to :add_key } diff --git a/spec/lib/gitlab/sidekiq_throttler_spec.rb b/spec/lib/gitlab/sidekiq_throttler_spec.rb deleted file mode 100644 index 2dbb7bb7c34..00000000000 --- a/spec/lib/gitlab/sidekiq_throttler_spec.rb +++ /dev/null @@ -1,44 +0,0 @@ -require 'spec_helper' - -describe Gitlab::SidekiqThrottler do - describe '#execute!' do - context 'when job throttling is enabled' do - before do - Sidekiq.options[:concurrency] = 35 - - stub_application_setting( - sidekiq_throttling_enabled: true, - sidekiq_throttling_factor: 0.1, - sidekiq_throttling_queues: %w[build project_cache] - ) - end - - it 'requires sidekiq-limit_fetch' do - expect(described_class).to receive(:require).with('sidekiq-limit_fetch').and_call_original - - described_class.execute! - end - - it 'sets limits on the selected queues' do - described_class.execute! - - expect(Sidekiq::Queue['build'].limit).to eq 4 - expect(Sidekiq::Queue['project_cache'].limit).to eq 4 - end - - it 'does not set limits on other queues' do - described_class.execute! - - expect(Sidekiq::Queue['merge'].limit).to be_nil - end - end - - context 'when job throttling is disabled' do - it 'does not require sidekiq-limit_fetch' do - expect(described_class).not_to receive(:require).with('sidekiq-limit_fetch') - - described_class.execute! - end - end - end -end diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb index 27cb3198e5b..e2134dc279c 100644 --- a/spec/lib/google_api/cloud_platform/client_spec.rb +++ b/spec/lib/google_api/cloud_platform/client_spec.rb @@ -66,25 +66,30 @@ describe GoogleApi::CloudPlatform::Client do describe '#projects_zones_clusters_create' do subject do client.projects_zones_clusters_create( - spy, spy, cluster_name, cluster_size, machine_type: machine_type) + project_id, zone, cluster_name, cluster_size, machine_type: machine_type, legacy_abac: legacy_abac) end + let(:project_id) { 'project-123' } + let(:zone) { 'us-central1-a' } let(:cluster_name) { 'test-cluster' } let(:cluster_size) { 1 } let(:machine_type) { 'n1-standard-2' } + let(:legacy_abac) { true } + let(:create_cluster_request_body) { double('Google::Apis::ContainerV1::CreateClusterRequest') } let(:operation) { double } before do allow_any_instance_of(Google::Apis::ContainerV1::ContainerService) - .to receive(:create_cluster).with(any_args, options: user_agent_options) + .to receive(:create_cluster).with(any_args) .and_return(operation) end - it { is_expected.to eq(operation) } - it 'sets corresponded parameters' do - expect_any_instance_of(Google::Apis::ContainerV1::CreateClusterRequest) - .to receive(:initialize).with( + expect_any_instance_of(Google::Apis::ContainerV1::ContainerService) + .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options) + + expect(Google::Apis::ContainerV1::CreateClusterRequest) + .to receive(:new).with( { "cluster": { "name": cluster_name, @@ -96,9 +101,35 @@ describe GoogleApi::CloudPlatform::Client do "enabled": true } } - } ) + } ).and_return(create_cluster_request_body) + + expect(subject).to eq operation + end + + context 'create without legacy_abac' do + let(:legacy_abac) { false } + + it 'sets corresponded parameters' do + expect_any_instance_of(Google::Apis::ContainerV1::ContainerService) + .to receive(:create_cluster).with(project_id, zone, create_cluster_request_body, options: user_agent_options) + + expect(Google::Apis::ContainerV1::CreateClusterRequest) + .to receive(:new).with( + { + "cluster": { + "name": cluster_name, + "initial_node_count": cluster_size, + "node_config": { + "machine_type": machine_type + }, + "legacy_abac": { + "enabled": false + } + } + } ).and_return(create_cluster_request_body) - subject + expect(subject).to eq operation + end end end diff --git a/spec/lib/mattermost/session_spec.rb b/spec/lib/mattermost/session_spec.rb index b7687d48c68..f18f97a9c6a 100644 --- a/spec/lib/mattermost/session_spec.rb +++ b/spec/lib/mattermost/session_spec.rb @@ -82,7 +82,7 @@ describe Mattermost::Session, type: :request do .to_return(headers: { Authorization: 'token thisworksnow' }, status: 200) end - it 'can setup a session' do + it 'can set up a session' do subject.with_session do |session| end @@ -106,7 +106,7 @@ describe Mattermost::Session, type: :request do expect_to_obtain_exclusive_lease(lease_key, 'uuid') expect_to_cancel_exclusive_lease(lease_key, 'uuid') - # Cannot setup a session, but we should still cancel the lease + # Cannot set up a session, but we should still cancel the lease expect { subject.with_session }.to raise_error(Mattermost::NoSessionError) end diff --git a/spec/lib/object_storage/direct_upload_spec.rb b/spec/lib/object_storage/direct_upload_spec.rb index 9c308cc1be9..1024e1a25ea 100644 --- a/spec/lib/object_storage/direct_upload_spec.rb +++ b/spec/lib/object_storage/direct_upload_spec.rb @@ -83,6 +83,16 @@ describe ObjectStorage::DirectUpload do expect(subject[:MultipartUpload][:AbortURL]).to start_with(storage_url) expect(subject[:MultipartUpload][:AbortURL]).to include('uploadId=myUpload') end + + it 'uses only strings in query parameters' do + expect(direct_upload.send(:connection)).to receive(:signed_url).at_least(:once) do |params| + if params[:query] + expect(params[:query].keys.all? { |key| key.is_a?(String) }).to be_truthy + end + end + + subject + end end shared_examples 'a valid upload without multipart data' do diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index 483cc546423..9647c1b9f63 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -305,6 +305,36 @@ describe ApplicationSetting do end end + describe 'setting Sentry DSNs' do + context 'server DSN' do + it 'strips leading and trailing whitespace' do + subject.update(sentry_dsn: ' http://test ') + + expect(subject.sentry_dsn).to eq('http://test') + end + + it 'handles nil values' do + subject.update(sentry_dsn: nil) + + expect(subject.sentry_dsn).to be_nil + end + end + + context 'client-side DSN' do + it 'strips leading and trailing whitespace' do + subject.update(clientside_sentry_dsn: ' http://test ') + + expect(subject.clientside_sentry_dsn).to eq('http://test') + end + + it 'handles nil values' do + subject.update(clientside_sentry_dsn: nil) + + expect(subject.clientside_sentry_dsn).to be_nil + end + end + end + describe '#disabled_oauth_sign_in_sources=' do before do allow(Devise).to receive(:omniauth_providers).and_return([:github]) diff --git a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb index bed364a8c14..01c555a7a90 100644 --- a/spec/models/blob_viewer/gitlab_ci_yml_spec.rb +++ b/spec/models/blob_viewer/gitlab_ci_yml_spec.rb @@ -2,22 +2,24 @@ require 'spec_helper' describe BlobViewer::GitlabCiYml do include FakeBlobHelpers + include RepoHelpers - let(:project) { build_stubbed(:project) } + let(:project) { create(:project, :repository) } let(:data) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) } let(:blob) { fake_blob(path: '.gitlab-ci.yml', data: data) } + let(:sha) { sample_commit.id } subject { described_class.new(blob) } describe '#validation_message' do it 'calls prepare! on the viewer' do expect(subject).to receive(:prepare!) - subject.validation_message + subject.validation_message(project, sha) end context 'when the configuration is valid' do it 'returns nil' do - expect(subject.validation_message).to be_nil + expect(subject.validation_message(project, sha)).to be_nil end end @@ -25,7 +27,7 @@ describe BlobViewer::GitlabCiYml do let(:data) { 'oof' } it 'returns the error message' do - expect(subject.validation_message).to eq('Invalid configuration format') + expect(subject.validation_message(project, sha)).to eq('Invalid configuration format') end end end diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index 42b627b6823..dbebda20ce0 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -2981,4 +2981,46 @@ describe Ci::Build do end end end + + describe '#deployment_status' do + context 'when build is a last deployment' do + let(:build) { create(:ci_build, :success, environment: 'production') } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:last) } + end + + context 'when there is a newer build with deployment' do + let(:build) { create(:ci_build, :success, environment: 'production') } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + let!(:last_deployment) { create(:deployment, environment: environment, project: environment.project) } + + it { expect(build.deployment_status).to eq(:out_of_date) } + end + + context 'when build with deployment has failed' do + let(:build) { create(:ci_build, :failed, environment: 'production') } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:failed) } + end + + context 'when build with deployment is running' do + let(:build) { create(:ci_build, environment: 'production') } + let(:environment) { create(:environment, name: 'production', project: build.project) } + let!(:deployment) { create(:deployment, environment: environment, project: environment.project, deployable: build) } + + it { expect(build.deployment_status).to eq(:creating) } + end + + context 'when build is successful but deployment is not ready yet' do + let(:build) { create(:ci_build, :success, environment: 'production') } + let(:environment) { create(:environment, name: 'production', project: build.project) } + + it { expect(build.deployment_status).to eq(:creating) } + end + end end diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb index 14ccc2960bb..4755702c0e9 100644 --- a/spec/models/ci/pipeline_spec.rb +++ b/spec/models/ci/pipeline_spec.rb @@ -1151,7 +1151,11 @@ describe Ci::Pipeline, :mailer do end describe '#set_config_source' do - context 'when pipelines does not contain needed data' do + context 'when pipelines does not contain needed data and auto devops is disabled' do + before do + stub_application_setting(auto_devops_enabled: false) + end + it 'defines source to be unknown' do pipeline.set_config_source @@ -1196,7 +1200,6 @@ describe Ci::Pipeline, :mailer do context 'auto devops enabled' do before do - stub_application_setting(auto_devops_enabled: true) allow(project).to receive(:ci_config_path) { 'custom' } end @@ -1743,7 +1746,7 @@ describe Ci::Pipeline, :mailer do create(:ci_pipeline, config: { rspec: { script: 'rake test' } }) end - it 'does not containyaml errors' do + it 'does not contain yaml errors' do expect(pipeline).not_to have_yaml_errors end end diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb index 953af2c4710..b545e036aa1 100644 --- a/spec/models/ci/runner_spec.rb +++ b/spec/models/ci/runner_spec.rb @@ -223,7 +223,7 @@ describe Ci::Runner do subject { described_class.online } before do - @runner1 = create(:ci_runner, :instance, contacted_at: 1.year.ago) + @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) end @@ -300,6 +300,17 @@ describe Ci::Runner do end end + describe '.offline' do + subject { described_class.offline } + + before do + @runner1 = create(:ci_runner, :instance, contacted_at: 1.hour.ago) + @runner2 = create(:ci_runner, :instance, contacted_at: 1.second.ago) + end + + it { is_expected.to eq([@runner1])} + end + describe '#can_pick?' do set(:pipeline) { create(:ci_pipeline) } let(:build) { create(:ci_build, pipeline: pipeline) } @@ -786,4 +797,22 @@ describe Ci::Runner do expect { subject.destroy }.to change { described_class.count }.by(-1) end end + + describe '.order_by' do + it 'supports ordering by the contact date' do + runner1 = create(:ci_runner, contacted_at: 1.year.ago) + runner2 = create(:ci_runner, contacted_at: 1.month.ago) + runners = described_class.order_by('contacted_asc') + + expect(runners).to eq([runner1, runner2]) + end + + it 'supports ordering by the creation date' do + runner1 = create(:ci_runner, created_at: 1.year.ago) + runner2 = create(:ci_runner, created_at: 1.month.ago) + runners = described_class.order_by('created_asc') + + expect(runners).to eq([runner2, runner1]) + end + end end diff --git a/spec/models/clusters/providers/gcp_spec.rb b/spec/models/clusters/providers/gcp_spec.rb index b38b5e6bcad..d134608b538 100644 --- a/spec/models/clusters/providers/gcp_spec.rb +++ b/spec/models/clusters/providers/gcp_spec.rb @@ -74,6 +74,24 @@ describe Clusters::Providers::Gcp do end end + describe '#legacy_abac?' do + let(:gcp) { build(:cluster_provider_gcp) } + + subject { gcp } + + it 'should default to true' do + is_expected.to be_legacy_abac + end + + context 'legacy_abac is set to false' do + let(:gcp) { build(:cluster_provider_gcp, legacy_abac: false) } + + it 'is false' do + is_expected.not_to be_legacy_abac + end + end + end + describe '#state_machine' do context 'when any => [:created]' do let(:gcp) { build(:cluster_provider_gcp, :creating) } diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb index d5f88e930d4..a6957095166 100644 --- a/spec/models/commit_spec.rb +++ b/spec/models/commit_spec.rb @@ -65,7 +65,7 @@ describe Commit do key = "Commit:author:#{commit.author_email.downcase}" - expect(RequestStore.store[key]).to eq(user) + expect(Gitlab::SafeRequestStore[key]).to eq(user) expect(commit.author).to eq(user) end diff --git a/spec/models/concerns/from_union_spec.rb b/spec/models/concerns/from_union_spec.rb new file mode 100644 index 00000000000..ee427a667c6 --- /dev/null +++ b/spec/models/concerns/from_union_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe FromUnion do + describe '.from_union' do + let(:model) do + Class.new(ActiveRecord::Base) do + self.table_name = 'users' + + include FromUnion + end + end + + it 'selects from the results of the UNION' do + query = model.from_union([model.where(id: 1), model.where(id: 2)]) + + expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) users/m) + end + + it 'supports the use of a custom alias for the sub query' do + query = model.from_union( + [model.where(id: 1), model.where(id: 2)], + alias_as: 'kittens' + ) + + expect(query.to_sql).to match(/FROM \(SELECT.+UNION.+SELECT.+\) kittens/m) + end + + it 'supports keeping duplicate rows' do + query = model.from_union( + [model.where(id: 1), model.where(id: 2)], + remove_duplicates: false + ) + + expect(query.to_sql) + .to match(/FROM \(SELECT.+UNION ALL.+SELECT.+\) users/m) + end + end +end diff --git a/spec/models/instance_configuration_spec.rb b/spec/models/instance_configuration_spec.rb index 8548fff5c76..34db94920f3 100644 --- a/spec/models/instance_configuration_spec.rb +++ b/spec/models/instance_configuration_spec.rb @@ -52,7 +52,7 @@ RSpec.describe InstanceConfiguration do expect(gitlab_pages).to eq(Settings.pages.symbolize_keys) end - it 'returns the Gitlab\'s pages host ip address' do + it 'returns the GitLab\'s pages host ip address' do expect(gitlab_pages.keys).to include(:ip_address) end diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb index c21d85fb2a4..19bc2713ef5 100644 --- a/spec/models/issue_spec.rb +++ b/spec/models/issue_spec.rb @@ -84,15 +84,32 @@ describe Issue do end end - describe '#closed_at' do - it 'sets closed_at to Time.now when issue is closed' do - issue = create(:issue, state: 'opened') + describe '#close' do + subject(:issue) { create(:issue, state: 'opened') } - expect(issue.closed_at).to be_nil + it 'sets closed_at to Time.now when an issue is closed' do + expect { issue.close }.to change { issue.closed_at }.from(nil) + end - issue.close + it 'changes the state to closed' do + expect { issue.close }.to change { issue.state }.from('opened').to('closed') + end + end + + describe '#reopen' do + let(:user) { create(:user) } + let(:issue) { create(:issue, state: 'closed', closed_at: Time.now, closed_by: user) } + + it 'sets closed_at to nil when an issue is reopend' do + expect { issue.reopen }.to change { issue.closed_at }.to(nil) + end + + it 'sets closed_by to nil when an issue is reopend' do + expect { issue.reopen }.to change { issue.closed_by }.from(user).to(nil) + end - expect(issue.closed_at).to be_present + it 'changes the state to opened' do + expect { issue.reopen }.to change { issue.state }.from('closed').to('opened') end end diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb index 55b984faecf..27d4e622710 100644 --- a/spec/models/milestone_spec.rb +++ b/spec/models/milestone_spec.rb @@ -95,6 +95,24 @@ describe Milestone do end end + describe '.order_by_name_asc' do + it 'sorts by name ascending' do + milestone1 = create(:milestone, title: 'Foo') + milestone2 = create(:milestone, title: 'Bar') + + expect(described_class.order_by_name_asc).to eq([milestone2, milestone1]) + end + end + + describe '.reorder_by_due_date_asc' do + it 'reorders the input relation' do + milestone1 = create(:milestone, due_date: Date.new(2018, 9, 30)) + milestone2 = create(:milestone, due_date: Date.new(2018, 10, 20)) + + expect(described_class.reorder_by_due_date_asc).to eq([milestone1, milestone2]) + end + end + describe "#percent_complete" do it "does not count open issues" do milestone.issues << issue diff --git a/spec/models/project_services/chat_message/merge_message_spec.rb b/spec/models/project_services/chat_message/merge_message_spec.rb index 184a07ae0f9..7997b5bb6b9 100644 --- a/spec/models/project_services/chat_message/merge_message_spec.rb +++ b/spec/models/project_services/chat_message/merge_message_spec.rb @@ -27,13 +27,30 @@ describe ChatMessage::MergeMessage do } end + # Integration point in EE + context 'when state is overridden' do + it 'respects the overridden state' do + allow(subject).to receive(:state_or_action_text) { 'devoured' } + + aggregate_failures do + expect(subject.summary).not_to include('opened') + expect(subject.summary).to include('devoured') + + activity_title = subject.activity[:title] + + expect(activity_title).not_to include('opened') + expect(activity_title).to include('devoured') + end + end + end + context 'without markdown' do let(:color) { '#345' } context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*') + 'Test User (test.user) opened <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') expect(subject.attachments).to be_empty end end @@ -44,7 +61,7 @@ describe ChatMessage::MergeMessage do end it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>: *Merge Request title*') + 'Test User (test.user) closed <http://somewhere.com/merge_requests/100|!100 *Merge Request title*> in <http://somewhere.com|project_name>') expect(subject.attachments).to be_empty end end @@ -58,7 +75,7 @@ describe ChatMessage::MergeMessage do context 'open' do it 'returns a message regarding opening of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*') + 'Test User (test.user) opened [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'Merge Request opened by Test User (test.user)', @@ -76,7 +93,7 @@ describe ChatMessage::MergeMessage do it 'returns a message regarding closing of merge requests' do expect(subject.pretext).to eq( - 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com): *Merge Request title*') + 'Test User (test.user) closed [!100 *Merge Request title*](http://somewhere.com/merge_requests/100) in [project_name](http://somewhere.com)') expect(subject.attachments).to be_empty expect(subject.activity).to eq({ title: 'Merge Request closed by Test User (test.user)', diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index dfe2de71a76..afc9ea1917e 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1072,6 +1072,18 @@ describe Project do it { expect(project.builds_enabled?).to be_truthy } end + describe '.sort_by_attribute' do + it 'reorders the input relation by start count desc' do + project1 = create(:project, star_count: 2) + project2 = create(:project, star_count: 1) + project3 = create(:project) + + projects = described_class.sort_by_attribute(:stars_desc) + + expect(projects).to eq([project1, project2, project3]) + end + end + describe '.with_shared_runners' do subject { described_class.with_shared_runners } @@ -3229,17 +3241,17 @@ describe Project do expect(repository).to receive(:gitlab_ci_yml) { nil } end - it "CI is not available" do - expect(project).not_to have_ci + it "CI is available" do + expect(project).to have_ci end - context 'when auto devops is enabled' do + context 'when auto devops is disabled' do before do - stub_application_setting(auto_devops_enabled: true) + stub_application_setting(auto_devops_enabled: false) end - it "CI is available" do - expect(project).to have_ci + it "CI is not available" do + expect(project).not_to have_ci end end end @@ -3983,40 +3995,6 @@ describe Project do end end - describe '#update_root_ref' do - let(:project) { create(:project, :repository) } - - it 'updates the default branch when HEAD has changed' do - stub_find_remote_root_ref(project, ref: 'feature') - - expect { project.update_root_ref('origin') } - .to change { project.default_branch } - .from('master') - .to('feature') - end - - it 'does not update the default branch when HEAD does not change' do - stub_find_remote_root_ref(project, ref: 'master') - - expect { project.update_root_ref('origin') } - .not_to change { project.default_branch } - end - - it 'does not update the default branch when HEAD does not exist' do - stub_find_remote_root_ref(project, ref: 'foo') - - expect { project.update_root_ref('origin') } - .not_to change { project.default_branch } - end - - def stub_find_remote_root_ref(project, ref:) - allow(project.repository) - .to receive(:find_remote_root_ref) - .with('origin') - .and_return(ref) - end - end - def rugged_config Gitlab::GitalyClient::StorageSettings.allow_disk_access do project.repository.rugged.config diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb index 528f5b610d7..f38fc191943 100644 --- a/spec/models/project_wiki_spec.rb +++ b/spec/models/project_wiki_spec.rb @@ -2,12 +2,13 @@ require "spec_helper" describe ProjectWiki do - let(:project) { create(:project, :wiki_repo) } + let(:user) { create(:user, :commit_email) } + let(:project) { create(:project, :wiki_repo, namespace: user.namespace) } let(:repository) { project.repository } - let(:user) { project.owner } let(:gitlab_shell) { Gitlab::Shell.new } let(:project_wiki) { described_class.new(project, user) } let(:raw_repository) { Gitlab::Git::Repository.new(project.repository_storage, subject.disk_path + '.git', 'foo') } + let(:commit) { project_wiki.repository.head_commit } subject { project_wiki } @@ -276,6 +277,14 @@ describe ProjectWiki do expect(subject.pages.first.page.version.message).to eq("commit message") end + it 'sets the correct commit email' do + subject.create_page('test page', 'content') + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + it 'updates project activity' do subject.create_page('Test Page', 'This is content') @@ -320,6 +329,12 @@ describe ProjectWiki do expect(@page.version.message).to eq("updated page") end + it 'sets the correct commit email' do + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + it 'updates project activity' do subject.update_page( @gitlab_git_wiki_page, @@ -347,6 +362,14 @@ describe ProjectWiki do expect(subject.pages.count).to eq(0) end + it 'sets the correct commit email' do + subject.delete_page(@page) + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + it 'updates project activity' do subject.delete_page(@page) @@ -420,7 +443,7 @@ describe ProjectWiki do end def commit_details - Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.email, "test commit") + Gitlab::Git::Wiki::CommitDetails.new(user.id, user.username, user.name, user.commit_email, "test commit") end def create_page(name, content) diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb index 93898012d34..dffac05152b 100644 --- a/spec/models/repository_spec.rb +++ b/spec/models/repository_spec.rb @@ -1028,194 +1028,6 @@ describe Repository do end end - describe '#update_branch_with_hooks' do - let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature - let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev - let(:updating_ref) { 'refs/heads/feature' } - let(:target_project) { project } - let(:target_repository) { target_project.repository } - - around do |example| - # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - context 'when pre hooks were successful' do - before do - service = Gitlab::Git::HooksService.new - expect(Gitlab::Git::HooksService).to receive(:new).and_return(service) - expect(service).to receive(:execute) - .with(git_user, target_repository.raw_repository, old_rev, new_rev, updating_ref) - .and_yield(service).and_return(true) - end - - it 'runs without errors' do - expect do - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do - new_rev - end - end.not_to raise_error - end - - it 'ensures the autocrlf Git option is set to :input' do - service = Gitlab::Git::OperationService.new(git_user, repository.raw_repository) - - expect(service).to receive(:update_autocrlf_option) - - service.with_branch('feature') { new_rev } - end - - context "when the branch wasn't empty" do - it 'updates the head' do - expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev) - - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do - new_rev - end - - expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev) - end - end - - context 'when target project does not have the commit' do - let(:target_project) { create(:project, :empty_repo) } - let(:old_rev) { Gitlab::Git::BLANK_SHA } - let(:new_rev) { project.commit('feature').sha } - let(:updating_ref) { 'refs/heads/master' } - - it 'fetch_ref and create the branch' do - expect(target_project.repository.raw_repository).to receive(:fetch_ref) - .and_call_original - - Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) - .with_branch( - 'master', - start_repository: project.repository.raw_repository, - start_branch_name: 'feature') { new_rev } - - expect(target_repository.branch_names).to contain_exactly('master') - end - end - - context 'when target project already has the commit' do - let(:target_project) { create(:project, :repository) } - - it 'does not fetch_ref and just pass the commit' do - expect(target_repository).not_to receive(:fetch_ref) - - Gitlab::Git::OperationService.new(git_user, target_repository.raw_repository) - .with_branch('feature', start_repository: project.repository.raw_repository) { new_rev } - end - end - end - - context 'when temporary ref failed to be created from other project' do - let(:target_project) { create(:project, :empty_repo) } - - before do - expect(target_project.repository.raw_repository).to receive(:run_git) - end - - it 'raises Rugged::ReferenceError' do - expect do - Gitlab::Git::OperationService.new(git_user, target_project.repository.raw_repository) - .with_branch('feature', - start_repository: project.repository.raw_repository, - &:itself) - end.to raise_error(Gitlab::Git::CommandError) - end - end - - context 'when the update adds more than one commit' do - let(:old_rev) { '33f3729a45c02fc67d00adb1b8bca394b0e761d9' } - - it 'runs without errors' do - # old_rev is an ancestor of new_rev - expect(repository.merge_base(old_rev, new_rev)).to eq(old_rev) - - # old_rev is not a direct ancestor (parent) of new_rev - expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev) - - branch = 'feature-ff-target' - repository.add_branch(user, branch, old_rev) - - expect do - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do - new_rev - end - end.not_to raise_error - end - end - - context 'when the update would remove commits from the target branch' do - let(:branch) { 'master' } - let(:old_rev) { repository.find_branch(branch).dereferenced_target.sha } - - it 'raises an exception' do - # The 'master' branch is NOT an ancestor of new_rev. - expect(repository.merge_base(old_rev, new_rev)).not_to eq(old_rev) - - # Updating 'master' to new_rev would lose the commits on 'master' that - # are not contained in new_rev. This should not be allowed. - expect do - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch(branch) do - new_rev - end - end.to raise_error(Gitlab::Git::CommitError) - end - end - - context 'when pre hooks failed' do - it 'gets an error' do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, '']) - - expect do - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('feature') do - new_rev - end - end.to raise_error(Gitlab::Git::PreReceiveError) - end - end - - context 'when target branch is different from source branch' do - before do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) - end - - subject do - Gitlab::Git::OperationService.new(git_user, repository.raw_repository).with_branch('new-feature') do - new_rev - end - end - - it 'returns branch_created as true' do - expect(subject).not_to be_repo_created - expect(subject).to be_branch_created - end - end - - context 'when repository is empty' do - before do - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, '']) - end - - it 'expires creation and branch cache' do - empty_repository = create(:project, :empty_repo).repository - - expect(empty_repository).to receive(:expire_exists_cache) - expect(empty_repository).to receive(:expire_root_ref_cache) - expect(empty_repository).to receive(:expire_emptiness_caches) - expect(empty_repository).to receive(:expire_branches_cache) - - empty_repository.create_file(user, 'CHANGELOG', 'Changelog!', - message: 'Updates file content', - branch_name: 'master') - end - end - end - describe '#exists?' do it 'returns true when a repository exists' do expect(repository.exists?).to be(true) @@ -1298,40 +1110,6 @@ describe Repository do end end - describe '#update_autocrlf_option' do - around do |example| - # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - describe 'when autocrlf is not already set to :input' do - before do - repository.raw_repository.autocrlf = true - end - - it 'sets autocrlf to :input' do - Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) - - expect(repository.raw_repository.autocrlf).to eq(:input) - end - end - - describe 'when autocrlf is already set to :input' do - before do - repository.raw_repository.autocrlf = :input - end - - it 'does nothing' do - expect(repository.raw_repository).not_to receive(:autocrlf=) - .with(:input) - - Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_autocrlf_option) - end - end - end - describe '#empty?' do let(:empty_repository) { create(:project_empty_repo).repository } @@ -2025,27 +1803,6 @@ describe Repository do end end - describe '#update_ref' do - around do |example| - # TODO Gitlab::Git::OperationService will be moved to gitaly-ruby and disappear from this repo - Gitlab::GitalyClient::StorageSettings.allow_disk_access do - example.run - end - end - - it 'can create a ref' do - Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA) - - expect(repository.find_branch('foobar')).not_to be_nil - end - - it 'raises CommitError when the ref update fails' do - expect do - Gitlab::Git::OperationService.new(nil, repository.raw_repository).send(:update_ref, 'refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA) - end.to raise_error(Gitlab::Git::CommitError) - end - end - describe '#contribution_guide', :use_clean_rails_memory_store_caching do it 'returns and caches the output' do expect(repository).to receive(:file_on_head) diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index bee4a3d24a7..99d17f563d9 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -167,6 +167,55 @@ describe User do subject { build(:user).tap { |user| user.emails << build(:email, email: email_value) } } end + describe '#commit_email' do + subject(:user) { create(:user) } + + it 'defaults to the primary email' do + expect(user.email).to be_present + expect(user.commit_email).to eq(user.email) + end + + it 'defaults to the primary email when the column in the database is null' do + user.update_column(:commit_email, nil) + + found_user = described_class.find_by(id: user.id) + + expect(found_user.commit_email).to eq(user.email) + end + + it 'can be set to a confirmed email' do + confirmed = create(:email, :confirmed, user: user) + user.commit_email = confirmed.email + + expect(user).to be_valid + expect(user.commit_email).to eq(confirmed.email) + end + + it 'can not be set to an unconfirmed email' do + unconfirmed = create(:email, user: user) + user.commit_email = unconfirmed.email + + # This should set the commit_email attribute to the primary email + expect(user).to be_valid + expect(user.commit_email).to eq(user.email) + end + + it 'can not be set to a non-existent email' do + user.commit_email = 'non-existent-email@nonexistent.nonexistent' + + # This should set the commit_email attribute to the primary email + expect(user).to be_valid + expect(user.commit_email).to eq(user.email) + end + + it 'can not be set to an invalid email, even if confirmed' do + confirmed = create(:email, :confirmed, :skip_validate, user: user, email: 'invalid') + user.commit_email = confirmed.email + + expect(user).not_to be_valid + end + end + describe 'email' do context 'when no signup domains whitelisted' do before do @@ -1390,7 +1439,6 @@ describe User do it 'returns only confirmed emails' do email_confirmed = create :email, user: user, confirmed_at: Time.now create :email, user: user - user.reload expect(user.verified_emails).to match_array([user.email, email_confirmed.email]) end @@ -2495,6 +2543,34 @@ describe User do end end + describe '#assigned_open_merge_requests_count' do + it 'returns number of open merge requests from non-archived projects' do + user = create(:user) + project = create(:project, :public) + archived_project = create(:project, :public, :archived) + + create(:merge_request, source_project: project, author: user, assignee: user) + create(:merge_request, :closed, source_project: project, author: user, assignee: user) + create(:merge_request, source_project: archived_project, author: user, assignee: user) + + expect(user.assigned_open_merge_requests_count(force: true)).to eq 1 + end + end + + describe '#assigned_open_issues_count' do + it 'returns number of open issues from non-archived projects' do + user = create(:user) + project = create(:project, :public) + archived_project = create(:project, :public, :archived) + + create(:issue, project: project, author: user, assignees: [user]) + create(:issue, :closed, project: project, author: user, assignees: [user]) + create(:issue, project: archived_project, author: user, assignees: [user]) + + expect(user.assigned_open_issues_count(force: true)).to eq 1 + end + end + describe '#personal_projects_count' do it 'returns the number of personal projects using a single query' do user = build(:user) diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index d9fb27e101e..96193784072 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -412,7 +412,7 @@ describe ProjectPresenter do end describe '#koding_anchor_data' do - it 'returns link to setup Koding if user can push and no koding YML exists' do + it 'returns link to set up 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) diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb index 246947e58a8..d5b31610dad 100644 --- a/spec/requests/api/commits_spec.rb +++ b/spec/requests/api/commits_spec.rb @@ -1040,6 +1040,14 @@ describe API::Commits do end end + context 'when branch is empty' do + ['', ' '].each do |branch| + it_behaves_like '400 response' do + let(:request) { post api(route, current_user), branch: branch } + end + end + end + context 'when branch does not exist' do it_behaves_like '404 response' do let(:request) { post api(route, current_user), branch: 'foo' } diff --git a/spec/requests/api/pipelines_spec.rb b/spec/requests/api/pipelines_spec.rb index 342a97b6a69..f0e1992bccd 100644 --- a/spec/requests/api/pipelines_spec.rb +++ b/spec/requests/api/pipelines_spec.rb @@ -370,12 +370,18 @@ describe API::Pipelines do end context 'without gitlab-ci.yml' do - it 'fails to create pipeline' do - post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + context 'without auto devops enabled' do + before do + project.update!(auto_devops_attributes: { enabled: false }) + end - expect(response).to have_gitlab_http_status(400) - expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' - expect(json_response).not_to be_an Array + it 'fails to create pipeline' do + post api("/projects/#{project.id}/pipeline", user), ref: project.default_branch + + expect(response).to have_gitlab_http_status(400) + expect(json_response['message']['base'].first).to eq 'Missing .gitlab-ci.yml file' + expect(json_response).not_to be_an Array + end end end end diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index b6f92042ecc..c8e98e6024c 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -148,6 +148,16 @@ describe API::Projects do expect(json_response.first.keys).to include('open_issues_count') end + it 'does not include projects marked for deletion' do + project.update(pending_delete: true) + + get api('/projects', user) + + expect(response).to have_gitlab_http_status(200) + expect(json_response).to be_an Array + expect(json_response.map { |p| p['id'] }).not_to include(project.id) + end + it 'does not include open_issues_count if issues are disabled' do project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED) @@ -557,6 +567,14 @@ describe API::Projects do expect(json_response['visibility']).to eq('private') end + it 'creates a new project initialized with a README.md' do + project = attributes_for(:project, initialize_with_readme: 1, name: 'somewhere') + + post api('/projects', user), project + + expect(json_response['readme_url']).to eql("#{Gitlab.config.gitlab.url}/#{json_response['namespace']['full_path']}/somewhere/blob/master/README.md") + end + it 'sets tag list to a project' do project = attributes_for(:project, tag_list: %w[tagFirst tagSecond]) @@ -1004,6 +1022,15 @@ describe API::Projects do expect(json_response).not_to include("import_error") end + it 'returns 404 when project is marked for deletion' do + project.update(pending_delete: true) + + get api("/projects/#{project.id}", user) + + expect(response).to have_gitlab_http_status(404) + expect(json_response['message']).to eq('404 Project Not Found') + end + context 'links exposure' do it 'exposes related resources full URIs' do get api("/projects/#{project.id}", user) diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb index b14d4b8fb6e..b1cf7a531f4 100644 --- a/spec/requests/openid_connect_spec.rb +++ b/spec/requests/openid_connect_spec.rb @@ -121,7 +121,7 @@ describe 'OpenID Connect requests' do expect(@payload).to match(a_hash_including(id_token_claims)) end - it 'includes the Gitlab root URL' do + it 'includes the GitLab root URL' do expect(@payload['iss']).to eq Gitlab.config.gitlab.url end diff --git a/spec/rubocop/code_reuse_helpers_spec.rb b/spec/rubocop/code_reuse_helpers_spec.rb new file mode 100644 index 00000000000..2720141aad2 --- /dev/null +++ b/spec/rubocop/code_reuse_helpers_spec.rb @@ -0,0 +1,249 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'parser/current' +require_relative '../../rubocop/code_reuse_helpers' + +describe RuboCop::CodeReuseHelpers do + def parse_source(source, path = 'foo.rb') + buffer = Parser::Source::Buffer.new(path) + buffer.source = source + + builder = RuboCop::AST::Builder.new + parser = Parser::CurrentRuby.new(builder) + + parser.parse(buffer) + end + + let(:cop) do + Class.new do + include RuboCop::CodeReuseHelpers + end.new + end + + describe '#send_to_constant?' do + it 'returns true when sending to a constant' do + node = parse_source('Foo.bar') + + expect(cop.send_to_constant?(node)).to eq(true) + end + + it 'returns false when sending to something other than a constant' do + node = parse_source('10') + + expect(cop.send_to_constant?(node)).to eq(false) + end + end + + describe '#send_receiver_name_ends_with?' do + it 'returns true when the receiver ends with a suffix' do + node = parse_source('FooFinder.new') + + expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(true) + end + + it 'returns false when the receiver is the same as a suffix' do + node = parse_source('Finder.new') + + expect(cop.send_receiver_name_ends_with?(node, 'Finder')).to eq(false) + end + end + + describe '#file_path_for_node' do + it 'returns the file path of a node' do + node = parse_source('10') + path = cop.file_path_for_node(node) + + expect(path).to eq('foo.rb') + end + end + + describe '#name_of_constant' do + it 'returns the name of a constant' do + node = parse_source('Foo') + + expect(cop.name_of_constant(node)).to eq(:Foo) + end + end + + describe '#in_finder?' do + it 'returns true for a node in the finders directory' do + node = parse_source('10', Rails.root.join('app', 'finders', 'foo.rb')) + + expect(cop.in_finder?(node)).to eq(true) + end + + it 'returns false for a node outside the finders directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_finder?(node)).to eq(false) + end + end + + describe '#in_model?' do + it 'returns true for a node in the models directory' do + node = parse_source('10', Rails.root.join('app', 'models', 'foo.rb')) + + expect(cop.in_model?(node)).to eq(true) + end + + it 'returns false for a node outside the models directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_model?(node)).to eq(false) + end + end + + describe '#in_service_class?' do + it 'returns true for a node in the services directory' do + node = parse_source('10', Rails.root.join('app', 'services', 'foo.rb')) + + expect(cop.in_service_class?(node)).to eq(true) + end + + it 'returns false for a node outside the services directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_service_class?(node)).to eq(false) + end + end + + describe '#in_presenter?' do + it 'returns true for a node in the presenters directory' do + node = parse_source('10', Rails.root.join('app', 'presenters', 'foo.rb')) + + expect(cop.in_presenter?(node)).to eq(true) + end + + it 'returns false for a node outside the presenters directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_presenter?(node)).to eq(false) + end + end + + describe '#in_serializer?' do + it 'returns true for a node in the serializers directory' do + node = parse_source('10', Rails.root.join('app', 'serializers', 'foo.rb')) + + expect(cop.in_serializer?(node)).to eq(true) + end + + it 'returns false for a node outside the serializers directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_serializer?(node)).to eq(false) + end + end + + describe '#in_worker?' do + it 'returns true for a node in the workers directory' do + node = parse_source('10', Rails.root.join('app', 'workers', 'foo.rb')) + + expect(cop.in_worker?(node)).to eq(true) + end + + it 'returns false for a node outside the workers directory' do + node = parse_source('10', Rails.root.join('app', 'foo', 'foo.rb')) + + expect(cop.in_worker?(node)).to eq(false) + end + end + + describe '#in_api?' do + it 'returns true for a node in the API directory' do + node = parse_source('10', Rails.root.join('lib', 'api', 'foo.rb')) + + expect(cop.in_api?(node)).to eq(true) + end + + it 'returns false for a node outside the API directory' do + node = parse_source('10', Rails.root.join('lib', 'foo', 'foo.rb')) + + expect(cop.in_api?(node)).to eq(false) + end + end + + describe '#in_directory?' do + it 'returns true for a directory in the CE app/ directory' do + node = parse_source('10', Rails.root.join('app', 'models', 'foo.rb')) + + expect(cop.in_directory?(node, 'models')).to eq(true) + end + + it 'returns true for a directory in the EE app/ directory' do + node = + parse_source('10', Rails.root.join('ee', 'app', 'models', 'foo.rb')) + + expect(cop.in_directory?(node, 'models')).to eq(true) + end + + it 'returns false for a directory in the lib/ directory' do + node = + parse_source('10', Rails.root.join('lib', 'models', 'foo.rb')) + + expect(cop.in_directory?(node, 'models')).to eq(false) + end + end + + describe '#name_of_receiver' do + it 'returns the name of a send receiver' do + node = parse_source('Foo.bar') + + expect(cop.name_of_receiver(node)).to eq('Foo') + end + end + + describe '#each_class_method' do + it 'yields every class method to the supplied block' do + node = parse_source(<<~RUBY) + class Foo + class << self + def first + end + end + + def self.second + end + end + RUBY + + nodes = cop.each_class_method(node).to_a + + expect(nodes.length).to eq(2) + + expect(nodes[0].children[0]).to eq(:first) + expect(nodes[1].children[1]).to eq(:second) + end + end + + describe '#each_send_node' do + it 'yields every send node to the supplied block' do + node = parse_source("foo\nbar") + nodes = cop.each_send_node(node).to_a + + expect(nodes.length).to eq(2) + expect(nodes[0].children[1]).to eq(:foo) + expect(nodes[1].children[1]).to eq(:bar) + end + end + + describe '#disallow_send_to' do + it 'disallows sending a message to a constant' do + def_node = parse_source(<<~RUBY) + def foo + FooFinder.new + end + RUBY + + send_node = def_node.each_child_node(:send).first + + expect(cop) + .to receive(:add_offense) + .with(send_node, location: :expression, message: 'oops') + + cop.disallow_send_to(def_node, 'Finder', 'oops') + end + end +end diff --git a/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb b/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb new file mode 100644 index 00000000000..c9eb61ccc72 --- /dev/null +++ b/spec/rubocop/cop/avoid_route_redirect_leading_slash_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require_relative '../../../rubocop/cop/avoid_route_redirect_leading_slash' + +describe RuboCop::Cop::AvoidRouteRedirectLeadingSlash do + include CopHelper + + subject(:cop) { described_class.new } + + before do + allow(cop).to receive(:in_routes?).and_return(true) + end + + it 'registers an offense when redirect has a leading slash' do + expect_offense(<<~PATTERN.strip_indent) + root to: redirect("/-/route") + ^^^^^^^^^^^^^^^^^^^^ Do not use a leading "/" in route redirects + PATTERN + end + + it 'does not register an offense when redirect does not have a leading slash' do + expect_no_offenses(<<~PATTERN.strip_indent) + root to: redirect("-/route") + PATTERN + end + + it 'autocorrect `/-/route` to `-/route`' do + expect(autocorrect_source('redirect("/-/route")')).to eq('redirect("-/route")') + end +end diff --git a/spec/rubocop/cop/code_reuse/active_record_spec.rb b/spec/rubocop/cop/code_reuse/active_record_spec.rb new file mode 100644 index 00000000000..a30fc52d26f --- /dev/null +++ b/spec/rubocop/cop/code_reuse/active_record_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/active_record' + +describe RuboCop::Cop::CodeReuse::ActiveRecord do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of "where" without any arguments' do + expect_offense(<<~SOURCE) + def foo + User.where + ^^^^^ This method can only be used inside an ActiveRecord model + end + SOURCE + end + + it 'flags the use of "where" with arguments' do + expect_offense(<<~SOURCE) + def foo + User.where(id: 10) + ^^^^^ This method can only be used inside an ActiveRecord model + end + SOURCE + end + + it 'does not flag the use of "group" without any arguments' do + expect_no_offenses(<<~SOURCE) + def foo + project.group + end + SOURCE + end + + it 'flags the use of "group" with arguments' do + expect_offense(<<~SOURCE) + def foo + project.group(:name) + ^^^^^ This method can only be used inside an ActiveRecord model + end + SOURCE + end + + it 'does not flag the use of ActiveRecord models in a model' do + path = Rails.root.join('app', 'models', 'foo.rb').to_s + + expect_no_offenses(<<~SOURCE, path) + def foo + project.group(:name) + end + SOURCE + end + + it 'does not flag the use of ActiveRecord models in a spec' do + path = Rails.root.join('spec', 'foo_spec.rb').to_s + + expect_no_offenses(<<~SOURCE, path) + def foo + project.group(:name) + end + SOURCE + end + + it 'does not flag the use of ActiveRecord models in a background migration' do + path = Rails + .root + .join('lib', 'gitlab', 'background_migration', 'foo.rb') + .to_s + + expect_no_offenses(<<~SOURCE, path) + def foo + project.group(:name) + end + SOURCE + end + + it 'does not flag the use of ActiveRecord models in lib/gitlab/database' do + path = Rails.root.join('lib', 'gitlab', 'database', 'foo.rb').to_s + + expect_no_offenses(<<~SOURCE, path) + def foo + project.group(:name) + end + SOURCE + end + + it 'autocorrects offenses in instance methods by whitelisting them' do + corrected = autocorrect_source(<<~SOURCE) + def foo + User.where + end + SOURCE + + expect(corrected).to eq(<<~SOURCE) + # rubocop: disable CodeReuse/ActiveRecord + def foo + User.where + end + # rubocop: enable CodeReuse/ActiveRecord + SOURCE + end + + it 'autocorrects offenses in class methods by whitelisting them' do + corrected = autocorrect_source(<<~SOURCE) + def self.foo + User.where + end + SOURCE + + expect(corrected).to eq(<<~SOURCE) + # rubocop: disable CodeReuse/ActiveRecord + def self.foo + User.where + end + # rubocop: enable CodeReuse/ActiveRecord + SOURCE + end + + it 'autocorrects offenses in blocks by whitelisting them' do + corrected = autocorrect_source(<<~SOURCE) + get '/' do + User.where + end + SOURCE + + expect(corrected).to eq(<<~SOURCE) + # rubocop: disable CodeReuse/ActiveRecord + get '/' do + User.where + end + # rubocop: enable CodeReuse/ActiveRecord + SOURCE + end +end diff --git a/spec/rubocop/cop/code_reuse/finder_spec.rb b/spec/rubocop/cop/code_reuse/finder_spec.rb new file mode 100644 index 00000000000..b04e053a4c3 --- /dev/null +++ b/spec/rubocop/cop/code_reuse/finder_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/finder' + +describe RuboCop::Cop::CodeReuse::Finder do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of a Finder inside another Finder' do + allow(cop) + .to receive(:in_finder?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooFinder + def execute + BarFinder.new.execute + ^^^^^^^^^^^^^ Finders can not be used inside a Finder. + end + end + SOURCE + + expect(cop.offenses.size).to eq(1) + end + + it 'flags the use of a Finder inside a model class method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User + class << self + def second_method + BarFinder.new + ^^^^^^^^^^^^^ Finders can not be used inside model class methods. + end + end + + def self.second_method + FooFinder.new + ^^^^^^^^^^^^^ Finders can not be used inside model class methods. + end + end + SOURCE + end + + it 'does not flag the use of a Finder in a non Finder file' do + expect_no_offenses(<<~SOURCE) + class FooFinder + def execute + BarFinder.new.execute + end + end + SOURCE + end + + it 'does not flag the use of a Finder in a regular class method' do + expect_no_offenses(<<~SOURCE) + class User + class << self + def second_method + BarFinder.new + end + end + + def self.second_method + FooFinder.new + end + end + SOURCE + end +end diff --git a/spec/rubocop/cop/code_reuse/presenter_spec.rb b/spec/rubocop/cop/code_reuse/presenter_spec.rb new file mode 100644 index 00000000000..4fe72619273 --- /dev/null +++ b/spec/rubocop/cop/code_reuse/presenter_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/presenter' + +describe RuboCop::Cop::CodeReuse::Presenter do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of a Presenter in a Service class' do + allow(cop) + .to receive(:in_service_class?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooService + def execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a Service class. + end + end + SOURCE + end + + it 'flags the use of a Presenter in a Finder' do + allow(cop) + .to receive(:in_finder?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooFinder + def execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a Finder. + end + end + SOURCE + end + + it 'flags the use of a Service class in a Presenter' do + allow(cop) + .to receive(:in_presenter?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooPresenter + def execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a Presenter. + end + end + SOURCE + end + + it 'flags the use of a Presenter in a Serializer' do + allow(cop) + .to receive(:in_serializer?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooSerializer + def execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a Serializer. + end + end + SOURCE + end + + it 'flags the use of a Presenter in a model instance method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Base + def execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a model. + end + end + SOURCE + end + + it 'flags the use of a Presenter in a model class method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Base + def self.execute + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a model. + end + end + SOURCE + end + + it 'flags the use of a Presenter in a worker' do + allow(cop) + .to receive(:in_worker?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooWorker + def perform + FooPresenter.new.execute + ^^^^^^^^^^^^^^^^ Presenters can not be used in a worker. + end + end + SOURCE + end +end diff --git a/spec/rubocop/cop/code_reuse/serializer_spec.rb b/spec/rubocop/cop/code_reuse/serializer_spec.rb new file mode 100644 index 00000000000..4530b15eed7 --- /dev/null +++ b/spec/rubocop/cop/code_reuse/serializer_spec.rb @@ -0,0 +1,117 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/serializer' + +describe RuboCop::Cop::CodeReuse::Serializer do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of a Serializer in a Service class' do + allow(cop) + .to receive(:in_service_class?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooService + def execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a Service class. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a Finder' do + allow(cop) + .to receive(:in_finder?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooFinder + def execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a Finder. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a Presenter' do + allow(cop) + .to receive(:in_presenter?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooPresenter + def execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a Presenter. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a Serializer' do + allow(cop) + .to receive(:in_serializer?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooSerializer + def execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a Serializer. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a model instance method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Base + def execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a model. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a model class method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Base + def self.execute + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a model. + end + end + SOURCE + end + + it 'flags the use of a Serializer in a worker' do + allow(cop) + .to receive(:in_worker?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooWorker + def perform + FooSerializer.new.execute + ^^^^^^^^^^^^^^^^^ Serializers can not be used in a worker. + end + end + SOURCE + end +end diff --git a/spec/rubocop/cop/code_reuse/service_class_spec.rb b/spec/rubocop/cop/code_reuse/service_class_spec.rb new file mode 100644 index 00000000000..7b8d82f332e --- /dev/null +++ b/spec/rubocop/cop/code_reuse/service_class_spec.rb @@ -0,0 +1,89 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/service_class' + +describe RuboCop::Cop::CodeReuse::ServiceClass do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of a Service class in a Finder' do + allow(cop) + .to receive(:in_finder?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooFinder + def execute + FooService.new.execute + ^^^^^^^^^^^^^^ Service classes can not be used in a Finder. + end + end + SOURCE + end + + it 'flags the use of a Service class in a Presenter' do + allow(cop) + .to receive(:in_presenter?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooPresenter + def execute + FooService.new.execute + ^^^^^^^^^^^^^^ Service classes can not be used in a Presenter. + end + end + SOURCE + end + + it 'flags the use of a Service class in a Serializer' do + allow(cop) + .to receive(:in_serializer?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooSerializer + def execute + FooService.new.execute + ^^^^^^^^^^^^^^ Service classes can not be used in a Serializer. + end + end + SOURCE + end + + it 'flags the use of a Service class in a model' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Model + class << self + def first + FooService.new.execute + ^^^^^^^^^^^^^^ Service classes can not be used in a model. + end + end + + def second + FooService.new.execute + ^^^^^^^^^^^^^^ Service classes can not be used in a model. + end + end + SOURCE + end + + it 'does not flag the use of a Service class in a regular class' do + expect_no_offenses(<<~SOURCE) + class Foo + def execute + FooService.new.execute + end + end + SOURCE + end +end diff --git a/spec/rubocop/cop/code_reuse/worker_spec.rb b/spec/rubocop/cop/code_reuse/worker_spec.rb new file mode 100644 index 00000000000..97acaeb7643 --- /dev/null +++ b/spec/rubocop/cop/code_reuse/worker_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/code_reuse/worker' + +describe RuboCop::Cop::CodeReuse::Worker do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of a worker in a controller' do + allow(cop) + .to receive(:in_controller?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooController + def index + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in a controller. + end + end + SOURCE + end + + it 'flags the use of a worker in an API' do + allow(cop) + .to receive(:in_api?) + .and_return(true) + + expect_offense(<<~SOURCE) + class Foo < Grape::API + resource :projects do + get '/' do + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in a Grape API. + end + end + end + SOURCE + end + + it 'flags the use of a worker in a Finder' do + allow(cop) + .to receive(:in_finder?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooFinder + def execute + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in a Finder. + end + end + SOURCE + end + + it 'flags the use of a worker in a Presenter' do + allow(cop) + .to receive(:in_presenter?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooPresenter + def execute + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in a Presenter. + end + end + SOURCE + end + + it 'flags the use of a worker in a Serializer' do + allow(cop) + .to receive(:in_serializer?) + .and_return(true) + + expect_offense(<<~SOURCE) + class FooSerializer + def execute + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in a Serializer. + end + end + SOURCE + end + + it 'flags the use of a worker in a model class method' do + allow(cop) + .to receive(:in_model?) + .and_return(true) + + expect_offense(<<~SOURCE) + class User < ActiveRecord::Base + def self.execute + FooWorker.perform_async + ^^^^^^^^^^^^^^^^^^^^^^^ Workers can not be used in model class methods. + end + end + SOURCE + end +end diff --git a/spec/rubocop/cop/gitlab/union_spec.rb b/spec/rubocop/cop/gitlab/union_spec.rb new file mode 100644 index 00000000000..5b06f30b25f --- /dev/null +++ b/spec/rubocop/cop/gitlab/union_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'spec_helper' +require 'rubocop' +require 'rubocop/rspec/support' +require_relative '../../../../rubocop/cop/gitlab/union' + +describe RuboCop::Cop::Gitlab::Union do + include CopHelper + + subject(:cop) { described_class.new } + + it 'flags the use of Gitlab::SQL::Union.new' do + expect_offense(<<~SOURCE) + Gitlab::SQL::Union.new([foo]) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Use the `FromUnion` concern, instead of using `Gitlab::SQL::Union` directly + SOURCE + end + + it 'does not flag the use of Gitlab::SQL::Union in a spec' do + allow(cop).to receive(:in_spec?).and_return(true) + + expect_no_offenses('Gitlab::SQL::Union.new([foo])') + end +end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/detailed_status_entity_spec.rb index 0b010ebd507..62f57ca8689 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/detailed_status_entity_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe StatusEntity do +describe DetailedStatusEntity do let(:entity) { described_class.new(status) } let(:status) do diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index c7f88e45c84..f2e9799452a 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -145,7 +145,7 @@ describe Auth::ContainerRegistryAuthenticationService do { scopes: ["registry:catalog:*"] } end - context 'disallow browsing for users without Gitlab admin rights' do + context 'disallow browsing for users without GitLab admin rights' do it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' end diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 27a7bf0e605..010679b5360 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -24,7 +24,7 @@ describe Boards::Issues::ListService do let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) } let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2]) } - let!(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Issue 3' ) } + let!(:reopened_issue1) { create(:issue, :opened, project: project, title: 'Reopened Issue 1' ) } let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) } @@ -44,12 +44,19 @@ describe Boards::Issues::ListService do end it_behaves_like 'issues list service' + + context 'when project is archived' do + let(:project) { create(:project, :archived) } + + it_behaves_like 'issues list service' + end end context 'when parent is a group' do let(:user) { create(:user) } let(:project) { create(:project, :empty_repo, namespace: group) } let(:project1) { create(:project, :empty_repo, namespace: group) } + let(:project_archived) { create(:project, :empty_repo, :archived, namespace: group) } let(:m1) { create(:milestone, group: group) } let(:m2) { create(:milestone, group: group) } @@ -77,7 +84,8 @@ describe Boards::Issues::ListService do let!(:opened_issue1) { create(:labeled_issue, project: project, milestone: m1, title: 'Issue 1', labels: [bug]) } let!(:opened_issue2) { create(:labeled_issue, project: project, milestone: m2, title: 'Issue 2', labels: [p2, p2_project]) } - let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Issue 3', closed_at: Time.now ) } + let!(:opened_issue3) { create(:labeled_issue, project: project_archived, milestone: m1, title: 'Issue 3', labels: [bug]) } + let!(:reopened_issue1) { create(:issue, state: 'opened', project: project, title: 'Reopened Issue 1', closed_at: Time.now ) } let!(:list1_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [p2, p2_project, development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, milestone: m2, labels: [development]) } diff --git a/spec/services/ci/fetch_kubernetes_token_service_spec.rb b/spec/services/ci/fetch_kubernetes_token_service_spec.rb deleted file mode 100644 index 1d05c9671a9..00000000000 --- a/spec/services/ci/fetch_kubernetes_token_service_spec.rb +++ /dev/null @@ -1,64 +0,0 @@ -require 'spec_helper' - -describe Ci::FetchKubernetesTokenService do - describe '#execute' do - subject { described_class.new(api_url, ca_pem, username, password).execute } - - let(:api_url) { 'http://111.111.111.111' } - let(:ca_pem) { '' } - let(:username) { 'admin' } - let(:password) { 'xxx' } - - context 'when params correct' do - let(:token) { 'xxx.token.xxx' } - - let(:secrets_json) do - [ - { - 'metadata': { - name: metadata_name - }, - 'data': { - 'token': Base64.encode64(token) - } - } - ] - end - - before do - allow_any_instance_of(Kubeclient::Client) - .to receive(:get_secrets).and_return(secrets_json) - end - - context 'when default-token exists' do - let(:metadata_name) { 'default-token-123' } - - it { is_expected.to eq(token) } - end - - context 'when default-token does not exist' do - let(:metadata_name) { 'another-token-123' } - - it { is_expected.to be_nil } - end - end - - context 'when api_url is nil' do - let(:api_url) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - - context 'when username is nil' do - let(:username) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - - context 'when password is nil' do - let(:password) { nil } - - it { expect { subject }.to raise_error("Incomplete settings") } - end - end -end diff --git a/spec/services/clusters/gcp/finalize_creation_service_spec.rb b/spec/services/clusters/gcp/finalize_creation_service_spec.rb index 0cf91307589..0f484222228 100644 --- a/spec/services/clusters/gcp/finalize_creation_service_spec.rb +++ b/spec/services/clusters/gcp/finalize_creation_service_spec.rb @@ -12,9 +12,11 @@ describe Clusters::Gcp::FinalizeCreationService do let(:zone) { provider.zone } let(:cluster_name) { cluster.name } + subject { described_class.new.execute(provider) } + shared_examples 'success' do it 'configures provider and kubernetes' do - described_class.new.execute(provider) + subject expect(provider).to be_created end @@ -22,7 +24,7 @@ describe Clusters::Gcp::FinalizeCreationService do shared_examples 'error' do it 'sets an error to provider object' do - described_class.new.execute(provider) + subject expect(provider.reload).to be_errored end @@ -33,6 +35,7 @@ describe Clusters::Gcp::FinalizeCreationService do let(:api_url) { 'https://' + endpoint } let(:username) { 'sample-username' } let(:password) { 'sample-password' } + let(:secret_name) { 'gitlab-token' } before do stub_cloud_platform_get_zone_cluster( @@ -43,60 +46,102 @@ describe Clusters::Gcp::FinalizeCreationService do password: password } ) - - stub_kubeclient_discover(api_url) end - context 'when suceeded to fetch kuberenetes token' do - let(:token) { 'sample-token' } - + context 'service account and token created' do before do - stub_kubeclient_get_secrets( - api_url, - { - token: Base64.encode64(token) - } ) + stub_kubeclient_discover(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) end - it_behaves_like 'success' + shared_context 'kubernetes token successfully fetched' do + let(:token) { 'sample-token' } + + before do + stub_kubeclient_get_secret( + api_url, + { + metadata_name: secret_name, + token: Base64.encode64(token) + } ) + end + end + + context 'provider legacy_abac is enabled' do + include_context 'kubernetes token successfully fetched' + + it_behaves_like 'success' - it 'has corresponded data' do - described_class.new.execute(provider) - cluster.reload - provider.reload - platform.reload + it 'properly configures database models' do + subject - expect(provider.endpoint).to eq(endpoint) - expect(platform.api_url).to eq(api_url) - expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) - expect(platform.username).to eq(username) - expect(platform.password).to eq(password) - expect(platform.token).to eq(token) + cluster.reload + + expect(provider.endpoint).to eq(endpoint) + expect(platform.api_url).to eq(api_url) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.username).to eq(username) + expect(platform.password).to eq(password) + expect(platform).to be_abac + expect(platform.authorization_type).to eq('abac') + expect(platform.token).to eq(token) + end end - end - context 'when default-token is not found' do - before do - stub_kubeclient_get_secrets(api_url, metadata_name: 'aaaa') + context 'provider legacy_abac is disabled' do + before do + provider.legacy_abac = false + end + + include_context 'kubernetes token successfully fetched' + + context 'cluster role binding created' do + before do + stub_kubeclient_create_cluster_role_binding(api_url) + end + + it_behaves_like 'success' + + it 'properly configures database models' do + subject + + cluster.reload + + expect(provider.endpoint).to eq(endpoint) + expect(platform.api_url).to eq(api_url) + expect(platform.ca_cert).to eq(Base64.decode64(load_sample_cert)) + expect(platform.username).to eq(username) + expect(platform.password).to eq(password) + expect(platform).to be_rbac + expect(platform.token).to eq(token) + end + end end - it_behaves_like 'error' - end + context 'when token is empty' do + before do + stub_kubeclient_get_secret(api_url, token: '', metadata_name: secret_name) + end - context 'when token is empty' do - before do - stub_kubeclient_get_secrets(api_url, token: '') + it_behaves_like 'error' end - it_behaves_like 'error' - end + context 'when failed to fetch kubernetes token' do + before do + stub_kubeclient_get_secret_error(api_url, secret_name) + end - context 'when failed to fetch kuberenetes token' do - before do - stub_kubeclient_get_secrets_error(api_url) + it_behaves_like 'error' end - it_behaves_like 'error' + context 'when service account fails to create' do + before do + stub_kubeclient_create_service_account_error(api_url) + end + + it_behaves_like 'error' + end end end diff --git a/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb new file mode 100644 index 00000000000..065d021db5e --- /dev/null +++ b/spec/services/clusters/gcp/kubernetes/create_service_account_service_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Clusters::Gcp::Kubernetes::CreateServiceAccountService do + include KubernetesHelpers + + let(:service) { described_class.new(kubeclient, rbac: rbac) } + + describe '#execute' do + let(:rbac) { false } + let(:api_url) { 'http://111.111.111.111' } + let(:username) { 'admin' } + let(:password) { 'xxx' } + + let(:kubeclient) do + Gitlab::Kubernetes::KubeClient.new( + api_url, + ['api', 'apis/rbac.authorization.k8s.io'], + auth_options: { username: username, password: password } + ) + end + + subject { service.execute } + + context 'when params are correct' do + before do + stub_kubeclient_discover(api_url) + stub_kubeclient_create_service_account(api_url) + stub_kubeclient_create_secret(api_url) + end + + shared_examples 'creates service account and token' do + it 'creates a kubernetes service account' do + subject + + expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/serviceaccounts').with( + body: hash_including( + kind: 'ServiceAccount', + metadata: { name: 'gitlab', namespace: 'default' } + ) + ) + end + + it 'creates a kubernetes secret of type ServiceAccountToken' do + subject + + expect(WebMock).to have_requested(:post, api_url + '/api/v1/namespaces/default/secrets').with( + body: hash_including( + kind: 'Secret', + metadata: { + name: 'gitlab-token', + namespace: 'default', + annotations: { + 'kubernetes.io/service-account.name': 'gitlab' + } + }, + type: 'kubernetes.io/service-account-token' + ) + ) + end + end + + context 'abac enabled cluster' do + it_behaves_like 'creates service account and token' + end + + context 'rbac enabled cluster' do + let(:rbac) { true } + + before do + stub_kubeclient_create_cluster_role_binding(api_url) + end + + it_behaves_like 'creates service account and token' + + it 'creates a kubernetes cluster role binding' do + subject + + expect(WebMock).to have_requested(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings').with( + body: hash_including( + kind: 'ClusterRoleBinding', + metadata: { name: 'gitlab-admin' }, + roleRef: { + apiGroup: 'rbac.authorization.k8s.io', + kind: 'ClusterRole', + name: 'cluster-admin' + }, + subjects: [{ kind: 'ServiceAccount', namespace: 'default', name: 'gitlab' }] + ) + ) + end + end + end + end +end diff --git a/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb new file mode 100644 index 00000000000..c543de21d5b --- /dev/null +++ b/spec/services/clusters/gcp/kubernetes/fetch_kubernetes_token_service_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'fast_spec_helper' + +describe Clusters::Gcp::Kubernetes::FetchKubernetesTokenService do + describe '#execute' do + let(:api_url) { 'http://111.111.111.111' } + let(:username) { 'admin' } + let(:password) { 'xxx' } + + let(:kubeclient) do + Gitlab::Kubernetes::KubeClient.new( + api_url, + ['api', 'apis/rbac.authorization.k8s.io'], + auth_options: { username: username, password: password } + ) + end + + subject { described_class.new(kubeclient).execute } + + context 'when params correct' do + let(:decoded_token) { 'xxx.token.xxx' } + let(:token) { Base64.encode64(decoded_token) } + + let(:secret_json) do + { + 'metadata': { + name: 'gitlab-token' + }, + 'data': { + 'token': token + } + } + end + + before do + allow_any_instance_of(Kubeclient::Client) + .to receive(:get_secret).and_return(secret_json) + end + + context 'when gitlab-token exists' do + let(:metadata_name) { 'gitlab-token' } + + it { is_expected.to eq(decoded_token) } + end + + context 'when gitlab-token does not exist' do + let(:secret_json) { {} } + + it { is_expected.to be_nil } + end + + context 'when token is nil' do + let(:token) { nil } + + it { is_expected.to be_nil } + end + end + end +end diff --git a/spec/services/files/create_service_spec.rb b/spec/services/files/create_service_spec.rb index 30d94e4318d..751b7160276 100644 --- a/spec/services/files/create_service_spec.rb +++ b/spec/services/files/create_service_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" describe Files::CreateService do let(:project) { create(:project, :repository) } let(:repository) { project.repository } - let(:user) { create(:user) } + let(:user) { create(:user, :commit_email) } let(:file_content) { 'Test file content' } let(:branch_name) { project.default_branch } let(:start_branch) { branch_name } @@ -20,6 +20,8 @@ describe Files::CreateService do } end + let(:commit) { repository.head_commit } + subject { described_class.new(project, user, commit_params) } before do @@ -75,4 +77,16 @@ describe Files::CreateService do end end end + + context 'commit attribute' do + let(:file_path) { 'test-commit-attributes.txt' } + + it 'uses the commit email' do + subject.execute + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + end end diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb index 73566afe8c8..309802ce733 100644 --- a/spec/services/files/delete_service_spec.rb +++ b/spec/services/files/delete_service_spec.rb @@ -4,10 +4,11 @@ describe Files::DeleteService do subject { described_class.new(project, user, commit_params) } let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + let(:user) { create(:user, :commit_email) } let(:file_path) { 'files/ruby/popen.rb' } let(:branch_name) { project.default_branch } let(:last_commit_sha) { nil } + let(:commit) { project.repository.head_commit } let(:commit_params) do { @@ -34,6 +35,14 @@ describe Files::DeleteService do expect(blob).to be_nil end + + it 'uses the commit email' do + subject.execute + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end end before do diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index e01fe487ffa..23db35c2418 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -4,11 +4,12 @@ describe Files::UpdateService do subject { described_class.new(project, user, commit_params) } let(:project) { create(:project, :repository) } - let(:user) { create(:user) } + let(:user) { create(:user, :commit_email) } let(:file_path) { 'files/ruby/popen.rb' } let(:new_contents) { 'New Content' } let(:branch_name) { project.default_branch } let(:last_commit_sha) { nil } + let(:commit) { project.repository.commit } let(:commit_params) do { @@ -54,6 +55,14 @@ describe Files::UpdateService do expect(results.data).to eq(new_contents) end + + it 'uses the commit email' do + subject.execute + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end end context "when the last_commit_sha is not supplied" do diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index d4528256640..45ef26aebbd 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -246,13 +246,15 @@ describe GitPushService, services: true do describe 'system hooks' do let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } - let(:system_hooks_service) { SystemHooksService.new } + let!(:system_hooks_service) { SystemHooksService.new } it "sends a system hook after pushing a branch" do - expect(SystemHooksService).to receive(:new).and_return(system_hooks_service) - expect(system_hooks_service).to receive(:execute_hooks).with(push_data, :push_hooks) + allow(SystemHooksService).to receive(:new).and_return(system_hooks_service) + allow(system_hooks_service).to receive(:execute_hooks) execute_service(project, user, oldrev, newrev, ref) + + expect(system_hooks_service).to have_received(:execute_hooks).with(push_data, :push_hooks) end end end diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb index 999677cfaaa..d71ccfb4334 100644 --- a/spec/services/groups/transfer_service_spec.rb +++ b/spec/services/groups/transfer_service_spec.rb @@ -22,7 +22,7 @@ describe Groups::TransferService, :postgresql do end end - context "when there's an exception on Gitlab shell directories" do + context "when there's an exception on GitLab shell directories" do let(:new_parent_group) { create(:group, :public) } before do diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index 6e1c1fe6c02..ff85c261cd4 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -4,6 +4,8 @@ describe Notes::BuildService do let(:note) { create(:discussion_note_on_issue) } let(:project) { note.project } let(:author) { note.author } + let(:merge_request) { create(:merge_request, source_project: project) } + let(:mr_note) { create(:discussion_note_on_merge_request, noteable: merge_request, project: project, author: author) } describe '#execute' do context 'when in_reply_to_discussion_id is specified' do @@ -12,6 +14,19 @@ describe Notes::BuildService do new_note = described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: note.discussion_id).execute expect(new_note).to be_valid expect(new_note.in_reply_to?(note)).to be_truthy + expect(new_note.resolved?).to be_falsey + end + + context 'when discussion is resolved' do + before do + mr_note.resolve!(author) + end + + it 'resolves the note' do + new_note = described_class.new(project, author, note: 'Test', in_reply_to_discussion_id: mr_note.discussion_id).execute + expect(new_note).to be_valid + expect(new_note.resolved?).to be_truthy + end end end diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb index 307ccc88865..affcc66d2bb 100644 --- a/spec/services/projects/container_repository/destroy_service_spec.rb +++ b/spec/services/projects/container_repository/destroy_service_spec.rb @@ -33,6 +33,7 @@ describe Projects::ContainerRepository::DestroyService do end it 'deletes the repository' do + expect(repository).to receive(:delete_tags!).and_call_original expect { described_class.new(project, user).execute(repository) }.to change { ContainerRepository.all.count }.by(-1) end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index e428808ab68..beff499f2be 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -204,7 +204,7 @@ describe Projects::DestroyService do context 'when image repository deletion fails' do it 'raises an exception' do expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(false) + .to receive(:delete_tags!).and_raise(RuntimeError) expect(destroy_project(project, user)).to be false end diff --git a/spec/services/projects/update_remote_mirror_service_spec.rb b/spec/services/projects/update_remote_mirror_service_spec.rb index 56a36432462..cd903bfe8a5 100644 --- a/spec/services/projects/update_remote_mirror_service_spec.rb +++ b/spec/services/projects/update_remote_mirror_service_spec.rb @@ -17,7 +17,6 @@ describe Projects::UpdateRemoteMirrorService do it "ensures the remote exists" do stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name) expect(remote_mirror).to receive(:ensure_remote!) @@ -25,8 +24,6 @@ describe Projects::UpdateRemoteMirrorService do end it "fetches the remote repository" do - stub_find_remote_root_ref(project, remote_name: remote_name) - expect(project.repository) .to receive(:fetch_remote) .with(remote_mirror.remote_name, no_tags: true) @@ -34,26 +31,8 @@ describe Projects::UpdateRemoteMirrorService do service.execute(remote_mirror) end - it "updates the default branch when HEAD has changed" do - stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name, ref: "existing-branch") - - expect { service.execute(remote_mirror) } - .to change { project.default_branch } - .from("master") - .to("existing-branch") - end - - it "does not update the default branch when HEAD does not change" do - stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name, ref: "master") - - expect { service.execute(remote_mirror) }.not_to change { project.default_branch } - end - it "returns success when updated succeeds" do stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name) result = service.execute(remote_mirror) @@ -63,7 +42,6 @@ describe Projects::UpdateRemoteMirrorService do context 'when syncing all branches' do it "push all the branches the first time" do stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name) expect(remote_mirror).to receive(:update_repository).with({}) @@ -74,7 +52,6 @@ describe Projects::UpdateRemoteMirrorService do context 'when only syncing protected branches' do it "sync updated protected branches" do stub_fetch_remote(project, remote_name: remote_name) - stub_find_remote_root_ref(project, remote_name: remote_name) protected_branch = create_protected_branch(project) remote_mirror.only_protected_branches = true @@ -92,13 +69,6 @@ describe Projects::UpdateRemoteMirrorService do end end - def stub_find_remote_root_ref(project, ref: 'master', remote_name:) - allow(project.repository) - .to receive(:find_remote_root_ref) - .with(remote_name) - .and_return(ref) - end - def stub_fetch_remote(project, remote_name:) allow(project.repository) .to receive(:fetch_remote) diff --git a/spec/support/helpers/git_helpers.rb b/spec/support/helpers/git_helpers.rb new file mode 100644 index 00000000000..fc92bc38561 --- /dev/null +++ b/spec/support/helpers/git_helpers.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module GitHelpers + def project_hook_exists?(project) + Gitlab::GitalyClient::StorageSettings.allow_disk_access do + project_path = project.repository.raw_repository.path + + File.exist?(File.join(project_path, 'hooks', 'post-receive')) + end + end +end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 994a2aaef90..c077ca9f15b 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -33,31 +33,49 @@ module KubernetesHelpers WebMock.stub_request(:get, deployments_url).to_return(response || kube_deployments_response) end - def stub_kubeclient_get_secrets(api_url, **options) - WebMock.stub_request(:get, api_url + '/api/v1/secrets') - .to_return(kube_response(kube_v1_secrets_body(options))) + def stub_kubeclient_get_secret(api_url, namespace: 'default', **options) + options[:metadata_name] ||= "default-token-1" + + WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{options[:metadata_name]}") + .to_return(kube_response(kube_v1_secret_body(options))) end - def stub_kubeclient_get_secrets_error(api_url) - WebMock.stub_request(:get, api_url + '/api/v1/secrets') + def stub_kubeclient_get_secret_error(api_url, name, namespace: 'default') + WebMock.stub_request(:get, api_url + "/api/v1/namespaces/#{namespace}/secrets/#{name}") .to_return(status: [404, "Internal Server Error"]) end - def kube_v1_secrets_body(**options) + def stub_kubeclient_create_service_account(api_url, namespace: 'default') + WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts") + .to_return(kube_response({})) + end + + def stub_kubeclient_create_service_account_error(api_url, namespace: 'default') + WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/serviceaccounts") + .to_return(status: [500, "Internal Server Error"]) + end + + def stub_kubeclient_create_secret(api_url, namespace: 'default') + WebMock.stub_request(:post, api_url + "/api/v1/namespaces/#{namespace}/secrets") + .to_return(kube_response({})) + end + + def stub_kubeclient_create_cluster_role_binding(api_url) + WebMock.stub_request(:post, api_url + '/apis/rbac.authorization.k8s.io/v1/clusterrolebindings') + .to_return(kube_response({})) + end + + def kube_v1_secret_body(**options) { "kind" => "SecretList", "apiVersion": "v1", - "items" => [ - { - "metadata": { - "name": options[:metadata_name] || "default-token-1", - "namespace": "kube-system" - }, - "data": { - "token": options[:token] || Base64.encode64('token-sample-123') - } - } - ] + "metadata": { + "name": options[:metadata_name] || "default-token-1", + "namespace": "kube-system" + }, + "data": { + "token": options[:token] || Base64.encode64('token-sample-123') + } } end @@ -68,6 +86,7 @@ module KubernetesHelpers { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" }, { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } @@ -80,6 +99,7 @@ module KubernetesHelpers { "name" => "pods", "namespaced" => true, "kind" => "Pod" }, { "name" => "deployments", "namespaced" => true, "kind" => "Deployment" }, { "name" => "secrets", "namespaced" => true, "kind" => "Secret" }, + { "name" => "serviceaccounts", "namespaced" => true, "kind" => "ServiceAccount" }, { "name" => "services", "namespaced" => true, "kind" => "Service" } ] } diff --git a/spec/support/helpers/markdown_feature.rb b/spec/support/helpers/markdown_feature.rb index 346f5b1cc4d..96401379cf0 100644 --- a/spec/support/helpers/markdown_feature.rb +++ b/spec/support/helpers/markdown_feature.rb @@ -10,6 +10,12 @@ class MarkdownFeature include FactoryBot::Syntax::Methods + attr_reader :fixture_path + + def initialize(fixture_path = Rails.root.join('spec/fixtures/markdown.md.erb')) + @fixture_path = fixture_path + end + def user @user ||= create(:user) end @@ -122,7 +128,7 @@ class MarkdownFeature end def raw_markdown - markdown = File.read(Rails.root.join('spec/fixtures/markdown.md.erb')) + markdown = File.read(fixture_path) ERB.new(markdown).result(binding) end end diff --git a/spec/support/helpers/migrations_helpers.rb b/spec/support/helpers/migrations_helpers.rb index 0bc235701eb..0c35764ed9a 100644 --- a/spec/support/helpers/migrations_helpers.rb +++ b/spec/support/helpers/migrations_helpers.rb @@ -3,6 +3,10 @@ module MigrationsHelpers Class.new(ActiveRecord::Base) do self.table_name = name self.inheritance_column = :_type_disabled + + def self.name + table_name.singularize.camelcase + end end end diff --git a/spec/support/helpers/stub_configuration.rb b/spec/support/helpers/stub_configuration.rb index 8475f91799b..776119564ec 100644 --- a/spec/support/helpers/stub_configuration.rb +++ b/spec/support/helpers/stub_configuration.rb @@ -1,5 +1,8 @@ require 'active_support/core_ext/hash/transform_values' require 'active_support/hash_with_indifferent_access' +require 'active_support/dependencies' + +require_dependency 'gitlab' module StubConfiguration def stub_application_setting(messages) diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 3f8e3ae5190..97875669d0e 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -85,7 +85,7 @@ module TestEnv clean_test_path - # Setup GitLab shell for test instance + # Set up GitLab shell for test instance setup_gitlab_shell setup_gitaly @@ -107,10 +107,6 @@ module TestEnv .and_call_original end - def disable_pre_receive - allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, nil]) - end - # Clean /tmp/tests # # Keeps gitlab-shell and gitlab-test @@ -371,7 +367,7 @@ module TestEnv FileUtils.rm_rf(install_dir) exit 1 ensure - puts " #{component} setup in #{Time.now - start} seconds...\n" + puts " #{component} set up in #{Time.now - start} seconds...\n" end def ensure_component_dir_name_is_correct!(component, path) diff --git a/spec/support/services/clusters/create_service_shared.rb b/spec/support/services/clusters/create_service_shared.rb index 43a2fd05498..22f712f3fcf 100644 --- a/spec/support/services/clusters/create_service_shared.rb +++ b/spec/support/services/clusters/create_service_shared.rb @@ -7,7 +7,8 @@ shared_context 'valid cluster create params' do gcp_project_id: 'gcp-project', zone: 'us-central1-a', num_nodes: 1, - machine_type: 'machine_type-a' + machine_type: 'machine_type-a', + legacy_abac: 'true' } } end @@ -29,6 +30,10 @@ shared_context 'invalid cluster create params' do end shared_examples 'create cluster service success' do + before do + stub_feature_flags(rbac_clusters: false) + end + it 'creates a cluster object and performs a worker' do expect(ClusterProvisionWorker).to receive(:perform_async) @@ -44,6 +49,7 @@ shared_examples 'create cluster service success' do expect(subject.provider.num_nodes).to eq(1) expect(subject.provider.machine_type).to eq('machine_type-a') expect(subject.provider.access_token).to eq(access_token) + expect(subject.provider).to be_legacy_abac expect(subject.platform).to be_nil end end diff --git a/spec/support/shared_examples/diff_file_collections.rb b/spec/support/shared_examples/diff_file_collections.rb new file mode 100644 index 00000000000..55ce160add0 --- /dev/null +++ b/spec/support/shared_examples/diff_file_collections.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +shared_examples 'diff statistics' do |test_include_stats_flag: true| + def stub_stats_find_by_path(path, stats_mock) + expect_next_instance_of(Gitlab::Git::DiffStatsCollection) do |collection| + allow(collection).to receive(:find_by_path).and_call_original + expect(collection).to receive(:find_by_path).with(path).and_return(stats_mock) + end + end + + context 'when should request diff stats' do + it 'Repository#diff_stats is called' do + subject = described_class.new(diffable, collection_default_args) + + expect(diffable.project.repository) + .to receive(:diff_stats) + .with(diffable.diff_refs.base_sha, diffable.diff_refs.head_sha) + .and_call_original + + subject.diff_files + end + + it 'Gitlab::Diff::File is initialized with diff stats' do + subject = described_class.new(diffable, collection_default_args) + + stats_mock = double(Gitaly::DiffStats, path: '.gitignore', additions: 758, deletions: 120) + stub_stats_find_by_path(stub_path, stats_mock) + + diff_file = subject.diff_files.find { |file| file.new_path == stub_path } + + expect(diff_file.added_lines).to eq(stats_mock.additions) + expect(diff_file.removed_lines).to eq(stats_mock.deletions) + end + end + + context 'when should not request diff stats' do + it 'Repository#diff_stats is not called' do + collection_default_args[:diff_options][:include_stats] = false + + subject = described_class.new(diffable, collection_default_args) + + expect(diffable.project.repository).not_to receive(:diff_stats) + + subject.diff_files + end + end +end diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index 3057845061b..a096627ee62 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -73,9 +73,13 @@ RSpec.shared_examples 'an editable merge request' do it 'description has autocomplete', :js do find('#merge_request_description').native.send_keys('') - fill_in 'merge_request_description', with: '@' + fill_in 'merge_request_description', with: user.to_reference[0..4] - expect(page).to have_selector('.atwho-view') + wait_for_requests + + page.within('.atwho-view') do + expect(page).to have_content(user2.name) + end end it 'has class js-quick-submit in form' do diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb index b81aea23306..5818892d56a 100644 --- a/spec/tasks/gitlab/db_rake_spec.rb +++ b/spec/tasks/gitlab/db_rake_spec.rb @@ -61,6 +61,39 @@ describe 'gitlab:db namespace rake task' do expect(Rake::Task['db:migrate']).not_to receive(:invoke) expect { run_rake_task('gitlab:db:configure') }.to raise_error(RuntimeError, 'error') end + + context 'SKIP_POST_DEPLOYMENT_MIGRATIONS environment variable set' do + let(:rails_paths) { { 'db' => ['db'], 'db/migrate' => ['db/migrate'] } } + + before do + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:[]).with('SKIP_POST_DEPLOYMENT_MIGRATIONS').and_return true + + # Our environment has already been loaded, so we need to pretend like post_migrations were not + allow(Rails.application.config).to receive(:paths).and_return(rails_paths) + allow(ActiveRecord::Migrator).to receive(:migrations_paths).and_return(rails_paths['db/migrate'].dup) + end + + it 'adds post deployment migrations before schema load if the schema is not already loaded' do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return([]) + expect(Gitlab::Database).to receive(:add_post_migrate_path_to_rails).and_call_original + expect(Rake::Task['db:schema:load']).to receive(:invoke) + expect(Rake::Task['db:seed_fu']).to receive(:invoke) + expect(Rake::Task['db:migrate']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.not_to raise_error + expect(rails_paths['db/migrate'].include?(File.join(Rails.root, 'db', 'post_migrate'))).to be(true) + end + + it 'ignores post deployment migrations when schema has already been loaded' do + allow(ActiveRecord::Base.connection).to receive(:tables).and_return(%w[table1 table2]) + expect(Rake::Task['db:migrate']).to receive(:invoke) + expect(Gitlab::Database).not_to receive(:add_post_migrate_path_to_rails) + expect(Rake::Task['db:schema:load']).not_to receive(:invoke) + expect(Rake::Task['db:seed_fu']).not_to receive(:invoke) + expect { run_rake_task('gitlab:db:configure') }.not_to raise_error + expect(rails_paths['db/migrate'].include?(File.join(Rails.root, 'db', 'post_migrate'))).to be(false) + end + end end def run_rake_task(task_name) diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb index 93fe013d11c..ab6100509a6 100644 --- a/spec/validators/url_validator_spec.rb +++ b/spec/validators/url_validator_spec.rb @@ -24,6 +24,21 @@ describe UrlValidator do expect(badge.errors.empty?).to be true end + + it 'strips urls' do + badge.link_url = "\n\r\n\nhttps://127.0.0.1\r\n\r\n\n\n\n" + + # It's unusual for a validator to modify its arguments. Some extensions, + # such as attr_encrypted, freeze the string to signal that modifications + # will not be persisted, so freeze this string to ensure the scheme is + # compatible with them. + badge.link_url.freeze + + subject + + expect(badge.errors).to be_empty + expect(badge.link_url).to eq('https://127.0.0.1') + end end context 'when allow_localhost is set to false' do diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb index 836d452304c..4b4de540d9e 100644 --- a/spec/views/help/index.html.haml_spec.rb +++ b/spec/views/help/index.html.haml_spec.rb @@ -21,7 +21,7 @@ describe 'help/index' do render expect(rendered).to match '8.0.2' - expect(rendered).to have_link('abcdefg', 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg') + expect(rendered).to have_link('abcdefg', href: 'https://gitlab.com/gitlab-org/gitlab-ce/commits/abcdefg') end end @@ -29,7 +29,7 @@ describe 'help/index' do it 'is visible to guests' do render - expect(rendered).to have_link(nil, help_instance_configuration_url) + expect(rendered).to have_link(nil, href: help_instance_configuration_url) end end diff --git a/spec/views/projects/jobs/show.html.haml_spec.rb b/spec/views/projects/jobs/show.html.haml_spec.rb index c93152b88e3..496646dc623 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -188,40 +188,4 @@ describe 'projects/jobs/show' do expect(rendered).not_to have_link('New issue') end end - - context 'when incomplete trigger_request is used' do - before do - build.trigger_request = FactoryBot.build(:ci_trigger_request, trigger: nil) - end - - it 'test should not render token block' do - render - - expect(rendered).not_to have_content('Token') - end - end - - context 'when complete trigger_request is used' do - before do - build.trigger_request = FactoryBot.build(:ci_trigger_request) - end - - it 'should render token' do - render - - expect(rendered).to have_content('Token') - expect(rendered).to have_content(build.trigger_request.trigger.short_token) - end - end - - describe 'commit title in sidebar' do - let(:commit_title) { project.commit.title } - - it 'shows commit title and not show commit message' do - render - - expect(rendered).to have_css('p.build-light-text.append-bottom-0', - text: /\A\n#{Regexp.escape(commit_title)}\n\Z/) - end - end end diff --git a/spec/workers/project_service_worker_spec.rb b/spec/workers/project_service_worker_spec.rb new file mode 100644 index 00000000000..56934f122e4 --- /dev/null +++ b/spec/workers/project_service_worker_spec.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true +require 'spec_helper' + +describe ProjectServiceWorker, '#perform' do + let(:worker) { described_class.new } + let(:service) { JiraService.new } + + before do + allow(Service).to receive(:find).and_return(service) + end + + it 'executes service with given data' do + data = { test: 'test' } + expect(service).to receive(:execute).with(data) + + worker.perform(1, data) + end + + it 'logs error messages' do + allow(service).to receive(:execute).and_raise(StandardError, 'invalid URL') + expect(Sidekiq.logger).to receive(:error).with({ class: described_class.name, service_class: service.class.name, message: "invalid URL" }) + + worker.perform(1, {}) + end +end |