diff options
author | Kamil Trzciński <ayufan@ayufan.eu> | 2018-04-06 09:16:40 +0000 |
---|---|---|
committer | Kamil Trzciński <ayufan@ayufan.eu> | 2018-04-06 09:16:40 +0000 |
commit | 2bf8345542dbdae17c3755ca7157cbaf70ffde41 (patch) | |
tree | 48b9b82e6ae9ed2e12649b4ea4f95b6833948d3a /spec | |
parent | 99edc15127d9d23475c94079d53e2893f58c042a (diff) | |
parent | 20e9b32c96a5b08e3f61c8974b43977f98d8666e (diff) | |
download | gitlab-ce-2bf8345542dbdae17c3755ca7157cbaf70ffde41.tar.gz |
Merge branch 'master' into '42568-pipeline-empty-state'
# Conflicts:
# app/views/projects/jobs/show.html.haml
# lib/gitlab/ci/status/core.rb
Diffstat (limited to 'spec')
102 files changed, 3177 insertions, 439 deletions
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb index fcb0c2f28c8..53647749a60 100644 --- a/spec/controllers/projects/discussions_controller_spec.rb +++ b/spec/controllers/projects/discussions_controller_spec.rb @@ -16,6 +16,53 @@ describe Projects::DiscussionsController do } end + describe 'GET show' do + before do + sign_in user + end + + context 'when user is not authorized to read the MR' do + it 'returns 404' do + get :show, request_params, format: :json + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when user is authorized to read the MR' do + before do + project.add_reporter(user) + end + + it 'returns status 200' do + get :show, request_params, format: :json + + expect(response).to have_gitlab_http_status(200) + end + + it 'returns status 404 if MR does not exists' do + merge_request.destroy! + + get :show, request_params, format: :json + + expect(response).to have_gitlab_http_status(404) + end + end + + context 'when user is authorized but note is LegacyDiffNote' do + before do + project.add_developer(user) + note.update!(type: 'LegacyDiffNote') + end + + it 'returns status 200' do + get :show, request_params, format: :json + + expect(response).to have_gitlab_http_status(200) + end + end + end + describe 'POST resolve' do before do sign_in user diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb index 31046c202e6..b9a979044fe 100644 --- a/spec/controllers/projects/jobs_controller_spec.rb +++ b/spec/controllers/projects/jobs_controller_spec.rb @@ -513,13 +513,30 @@ describe Projects::JobsController do end end + context 'when job has a trace in database' do + let(:job) { create(:ci_build, pipeline: pipeline) } + + before do + job.update_column(:trace, 'Sample trace') + end + + it 'send a trace file' do + response = subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response.content_type).to eq 'text/plain; charset=utf-8' + expect(response.body).to eq 'Sample trace' + end + end + context 'when job does not have a trace file' do let(:job) { create(:ci_build, pipeline: pipeline) } it 'returns not_found' do response = subject - expect(response).to have_gitlab_http_status(:not_found) + expect(response).to have_gitlab_http_status(:ok) + expect(response.body).to eq '' end end diff --git a/spec/controllers/projects/pipelines_settings_controller_spec.rb b/spec/controllers/projects/pipelines_settings_controller_spec.rb index 913b9bd804a..694896b6bcf 100644 --- a/spec/controllers/projects/pipelines_settings_controller_spec.rb +++ b/spec/controllers/projects/pipelines_settings_controller_spec.rb @@ -11,82 +11,11 @@ describe Projects::PipelinesSettingsController do sign_in(user) end - describe 'PATCH update' do - subject do - patch :update, - namespace_id: project.namespace.to_param, - project_id: project, - project: { - auto_devops_attributes: params - } - end - - context 'when updating the auto_devops settings' do - let(:params) { { enabled: '', domain: 'mepmep.md' } } - - it 'redirects to the settings page' do - subject - - expect(response).to have_gitlab_http_status(302) - expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.") - end - - context 'following the instance default' do - let(:params) { { enabled: '' } } - - it 'allows enabled to be set to nil' do - subject - project_auto_devops.reload - - expect(project_auto_devops.enabled).to be_nil - end - end - - context 'when run_auto_devops_pipeline is true' do - before do - expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true) - end - - context 'when the project repository is empty' do - it 'sets a warning flash' do - expect(subject).to set_flash[:warning] - end - - it 'does not queue a CreatePipelineWorker' do - expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) - - subject - end - end - - context 'when the project repository is not empty' do - let(:project) { create(:project, :repository) } - - it 'sets a success flash' do - allow(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) - - expect(subject).to set_flash[:success] - end - - it 'queues a CreatePipelineWorker' do - expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) - - subject - end - end - end - - context 'when run_auto_devops_pipeline is not true' do - before do - expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false) - end - - it 'does not queue a CreatePipelineWorker' do - expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args) + describe 'GET show' do + it 'redirects with 302 status code' do + get :show, namespace_id: project.namespace, project_id: project - subject - end - end + expect(response).to have_gitlab_http_status(302) end end end diff --git a/spec/controllers/projects/settings/ci_cd_controller_spec.rb b/spec/controllers/projects/settings/ci_cd_controller_spec.rb index 293e76798ae..7dae9b85d78 100644 --- a/spec/controllers/projects/settings/ci_cd_controller_spec.rb +++ b/spec/controllers/projects/settings/ci_cd_controller_spec.rb @@ -1,8 +1,9 @@ require('spec_helper') describe Projects::Settings::CiCdController do - let(:project) { create(:project, :public, :access_requestable) } - let(:user) { create(:user) } + set(:user) { create(:user) } + set(:project_auto_devops) { create(:project_auto_devops) } + let(:project) { project_auto_devops.project } before do project.add_master(user) @@ -55,4 +56,107 @@ describe Projects::Settings::CiCdController do end end end + + describe 'PATCH update' do + let(:params) { { ci_config_path: '' } } + + subject do + patch :update, + namespace_id: project.namespace.to_param, + project_id: project, + project: params + end + + it 'redirects to the settings page' do + subject + + expect(response).to have_gitlab_http_status(302) + expect(flash[:notice]).to eq("Pipelines settings for '#{project.name}' were successfully updated.") + end + + context 'when updating the auto_devops settings' do + let(:params) { { auto_devops_attributes: { enabled: '', domain: 'mepmep.md' } } } + + context 'following the instance default' do + let(:params) { { auto_devops_attributes: { enabled: '' } } } + + it 'allows enabled to be set to nil' do + subject + project_auto_devops.reload + + expect(project_auto_devops.enabled).to be_nil + end + end + + context 'when run_auto_devops_pipeline is true' do + before do + expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(true) + end + + context 'when the project repository is empty' do + it 'sets a warning flash' do + expect(subject).to set_flash[:warning] + end + + it 'does not queue a CreatePipelineWorker' do + expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) + + subject + end + end + + context 'when the project repository is not empty' do + let(:project) { create(:project, :repository) } + + it 'sets a success flash' do + allow(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) + + expect(subject).to set_flash[:success] + end + + it 'queues a CreatePipelineWorker' do + expect(CreatePipelineWorker).to receive(:perform_async).with(project.id, user.id, project.default_branch, :web, any_args) + + subject + end + end + end + + context 'when run_auto_devops_pipeline is not true' do + before do + expect_any_instance_of(Projects::UpdateService).to receive(:run_auto_devops_pipeline?).and_return(false) + end + + it 'does not queue a CreatePipelineWorker' do + expect(CreatePipelineWorker).not_to receive(:perform_async).with(project.id, user.id, :web, any_args) + + subject + end + end + end + + context 'when updating general settings' do + context 'when build_timeout_human_readable is not specified' do + let(:params) { { build_timeout_human_readable: '' } } + + it 'set default timeout' do + subject + + project.reload + expect(project.build_timeout).to eq(3600) + end + end + + context 'when build_timeout_human_readable is specified' do + let(:params) { { build_timeout_human_readable: '1h 30m' } } + + it 'set specified timeout' do + subject + + project.reload + expect(project.build_timeout).to eq(5400) + end + end + end + end end diff --git a/spec/factories/ci/build_metadata.rb b/spec/factories/ci/build_metadata.rb deleted file mode 100644 index 66bbd977b88..00000000000 --- a/spec/factories/ci/build_metadata.rb +++ /dev/null @@ -1,9 +0,0 @@ -FactoryBot.define do - factory :ci_build_metadata, class: Ci::BuildMetadata do - build factory: :ci_build - - after(:build) do |build_metadata, _| - build_metadata.project ||= build_metadata.build.project - end - end -end diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb index f6ba3a581ca..fdacbe6c3f1 100644 --- a/spec/factories/ci/builds.rb +++ b/spec/factories/ci/builds.rb @@ -62,6 +62,7 @@ FactoryBot.define do end trait :pending do + queued_at 'Di 29. Okt 09:50:59 CET 2013' status 'pending' end @@ -237,5 +238,10 @@ FactoryBot.define do trait :protected do protected true end + + trait :script_failure do + failed + failure_reason 1 + end end end diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb index 493b7bc021c..a448d565e4b 100644 --- a/spec/factories/project_hooks.rb +++ b/spec/factories/project_hooks.rb @@ -15,6 +15,7 @@ FactoryBot.define do issues_events true confidential_issues_events true note_events true + confidential_note_events true job_events true pipeline_events true wiki_page_events true diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index 3005d74c3cf..846b8040be6 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -123,7 +123,7 @@ feature 'Admin updates settings' do scenario 'Change Performance bar settings' do group = create(:group) - page.within('.as-performance') do + page.within('.as-performance-bar') do check 'Enable the Performance Bar' fill_in 'Allowed group', with: group.path click_on 'Save changes' @@ -133,7 +133,7 @@ feature 'Admin updates settings' do expect(find_field('Enable the Performance Bar')).to be_checked expect(find_field('Allowed group').value).to eq group.path - page.within('.as-performance') do + page.within('.as-performance-bar') do uncheck 'Enable the Performance Bar' click_on 'Save changes' end @@ -167,6 +167,26 @@ feature 'Admin updates settings' do expect(Gitlab::CurrentSettings.unique_ips_limit_per_user).to eq(15) end + scenario 'Configure web terminal' do + page.within('.as-terminal') do + fill_in 'Max session time', with: 15 + click_button 'Save changes' + end + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.terminal_max_session_time).to eq(15) + end + + scenario '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 + + expect(page).to have_content "Application settings saved successfully" + expect(Gitlab::CurrentSettings.allow_local_requests_from_hooks_and_services).to be true + end + scenario 'Change Slack Notifications Service template settings' do first(:link, 'Service Templates').click click_link 'Slack notifications' diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb index b3c50964810..08ba91a2682 100644 --- a/spec/features/issues/filtered_search/filter_issues_spec.rb +++ b/spec/features/issues/filtered_search/filter_issues_spec.rb @@ -22,15 +22,6 @@ describe 'Filter issues', :js do end end - def expect_issues_list_count(open_count, closed_count = 0) - all_count = open_count + closed_count - - expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) - page.within '.issues-list' do - expect(page).to have_selector('.issue', count: open_count) - end - end - before do project.add_master(user) diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb index 38c618d300e..4625a50b8d9 100644 --- a/spec/features/issues/form_spec.rb +++ b/spec/features/issues/form_spec.rb @@ -226,6 +226,23 @@ describe 'New/edit issue', :js do expect(page).to have_selector('.atwho-view') end + + describe 'milestone' do + let!(:milestone) { create(:milestone, title: '"><img src=x onerror=alert(document.domain)>', project: project) } + + it 'escapes milestone' do + click_button 'Milestone' + + page.within '.issue-milestone' do + click_link milestone.title + end + + page.within '.js-milestone-select' do + expect(page).to have_content milestone.title + expect(page).not_to have_selector 'img' + end + end + end end context 'edit issue' do diff --git a/spec/features/labels_hierarchy_spec.rb b/spec/features/labels_hierarchy_spec.rb new file mode 100644 index 00000000000..99e1fb30d5b --- /dev/null +++ b/spec/features/labels_hierarchy_spec.rb @@ -0,0 +1,305 @@ +require 'spec_helper' + +feature 'Labels Hierarchy', :js, :nested_groups do + include FilteredSearchHelpers + + let!(:user) { create(:user) } + let!(:grandparent) { create(:group) } + let!(:parent) { create(:group, parent: grandparent) } + let!(:child) { create(:group, parent: parent) } + let!(:project_1) { create(:project, namespace: parent) } + + let!(:grandparent_group_label) { create(:group_label, group: grandparent, title: 'Label_1') } + let!(:parent_group_label) { create(:group_label, group: parent, title: 'Label_2') } + let!(:child_group_label) { create(:group_label, group: child, title: 'Label_3') } + let!(:project_label_1) { create(:label, project: project_1, title: 'Label_4') } + + before do + grandparent.add_owner(user) + + sign_in(user) + end + + shared_examples 'assigning labels from sidebar' do + it 'can assign all ancestors labels' do + [grandparent_group_label, parent_group_label, project_label_1].each do |label| + page.within('.block.labels') do + find('.edit-link').click + end + + wait_for_requests + + find('a.label-item', text: label.title).click + find('.dropdown-menu-close-icon').click + + wait_for_requests + + expect(page).to have_selector('span.label', text: label.title) + end + end + + it 'does not find child group labels on dropdown' do + page.within('.block.labels') do + find('.edit-link').click + end + + wait_for_requests + + expect(page).not_to have_selector('span.label', text: child_group_label.title) + end + end + + shared_examples 'filtering by ancestor labels for projects' do |board = false| + it 'filters by ancestor labels' do + [grandparent_group_label, parent_group_label, project_label_1].each do |label| + select_label_on_dropdown(label.title) + + wait_for_requests + + if board + expect(page).to have_selector('.card-title') do |card| + expect(card).to have_selector('a', text: labeled_issue.title) + end + else + expect_issues_list_count(1) + expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title) + end + end + end + + it 'does not filter by descendant group labels' do + filtered_search.set("label:") + + wait_for_requests + + expect(page).not_to have_selector('.btn-link', text: child_group_label.title) + end + end + + shared_examples 'filtering by ancestor labels for groups' do |board = false| + let(:project_2) { create(:project, namespace: parent) } + let!(:project_label_2) { create(:label, project: project_2, title: 'Label_4') } + + let(:project_3) { create(:project, namespace: child) } + let!(:group_label_3) { create(:group_label, group: child, title: 'Label_5') } + let!(:project_label_3) { create(:label, project: project_3, title: 'Label_6') } + + let!(:labeled_issue_2) { create(:labeled_issue, project: project_2, labels: [grandparent_group_label, parent_group_label, project_label_2]) } + let!(:labeled_issue_3) { create(:labeled_issue, project: project_3, labels: [grandparent_group_label, parent_group_label, group_label_3]) } + + let!(:issue_2) { create(:issue, project: project_2) } + + it 'filters by ancestors and current group labels' do + [grandparent_group_label, parent_group_label].each do |label| + select_label_on_dropdown(label.title) + + wait_for_requests + + if board + expect(page).to have_selector('.card-title') do |card| + expect(card).to have_selector('a', text: labeled_issue.title) + end + + expect(page).to have_selector('.card-title') do |card| + expect(card).to have_selector('a', text: labeled_issue_2.title) + end + else + expect_issues_list_count(3) + expect(page).to have_selector('span.issue-title-text', text: labeled_issue.title) + expect(page).to have_selector('span.issue-title-text', text: labeled_issue_2.title) + expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title) + end + end + end + + it 'filters by descendant group labels' do + wait_for_requests + + if board + pending("Waiting for https://gitlab.com/gitlab-org/gitlab-ce/issues/44270") + + select_label_on_dropdown(group_label_3.title) + + expect(page).to have_selector('.card-title') do |card| + expect(card).to have_selector('a', text: labeled_issue_3.title) + end + else + select_label_on_dropdown(group_label_3.title) + + expect_issues_list_count(1) + expect(page).to have_selector('span.issue-title-text', text: labeled_issue_3.title) + end + end + + it 'does not filter by descendant group project labels' do + filtered_search.set("label:") + + wait_for_requests + + expect(page).not_to have_selector('.btn-link', text: project_label_3.title) + end + end + + context 'when creating new issuable' do + before do + visit new_project_issue_path(project_1) + end + + it 'should be able to assign ancestor group labels' do + fill_in 'issue_title', with: 'new created issue' + fill_in 'issue_description', with: 'new issue description' + + find(".js-label-select").click + wait_for_requests + + find('a.label-item', text: grandparent_group_label.title).click + find('a.label-item', text: parent_group_label.title).click + find('a.label-item', text: project_label_1.title).click + + find('.btn-create').click + + expect(page.find('.issue-details h2.title')).to have_content('new created issue') + expect(page).to have_selector('span.label', text: grandparent_group_label.title) + expect(page).to have_selector('span.label', text: parent_group_label.title) + expect(page).to have_selector('span.label', text: project_label_1.title) + end + end + + context 'issuable sidebar' do + let!(:issue) { create(:issue, project: project_1) } + + context 'on issue sidebar' do + before do + visit project_issue_path(project_1, issue) + end + + it_behaves_like 'assigning labels from sidebar' + end + + context 'on project board issue sidebar' do + let(:board) { create(:board, project: project_1) } + + before do + visit project_board_path(project_1, board) + + wait_for_requests + + find('.card').click + end + + it_behaves_like 'assigning labels from sidebar' + end + + context 'on group board issue sidebar' do + let(:board) { create(:board, group: parent) } + + before do + visit group_board_path(parent, board) + + wait_for_requests + + find('.card').click + end + + it_behaves_like 'assigning labels from sidebar' + end + end + + context 'issuable filtering' do + let!(:labeled_issue) { create(:labeled_issue, project: project_1, labels: [grandparent_group_label, parent_group_label, project_label_1]) } + let!(:issue) { create(:issue, project: project_1) } + + context 'on project issuable list' do + before do + visit project_issues_path(project_1) + end + + it_behaves_like 'filtering by ancestor labels for projects' + + it 'does not filter by descendant group labels' do + filtered_search.set("label:") + + wait_for_requests + + expect(page).not_to have_selector('.btn-link', text: child_group_label.title) + end + end + + context 'on group issuable list' do + before do + visit issues_group_path(parent) + end + + it_behaves_like 'filtering by ancestor labels for groups' + end + + context 'on project boards filter' do + let(:board) { create(:board, project: project_1) } + + before do + visit project_board_path(project_1, board) + end + + it_behaves_like 'filtering by ancestor labels for projects', true + end + + context 'on group boards filter' do + let(:board) { create(:board, group: parent) } + + before do + visit group_board_path(parent, board) + end + + it_behaves_like 'filtering by ancestor labels for groups', true + end + end + + context 'creating boards lists' do + context 'on project boards' do + let(:board) { create(:board, project: project_1) } + + before do + visit project_board_path(project_1, board) + find('.js-new-board-list').click + wait_for_requests + end + + it 'creates lists from all ancestor labels' do + [grandparent_group_label, parent_group_label, project_label_1].each do |label| + find('a', text: label.title).click + end + + wait_for_requests + + expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title) + expect(page).to have_selector('.board-title-text', text: parent_group_label.title) + expect(page).to have_selector('.board-title-text', text: project_label_1.title) + end + end + + context 'on group boards' do + let(:board) { create(:board, group: parent) } + + before do + visit group_board_path(parent, board) + find('.js-new-board-list').click + wait_for_requests + end + + it 'creates lists from all ancestor group labels' do + [grandparent_group_label, parent_group_label].each do |label| + find('a', text: label.title).click + end + + wait_for_requests + + expect(page).to have_selector('.board-title-text', text: grandparent_group_label.title) + expect(page).to have_selector('.board-title-text', text: parent_group_label.title) + end + + it 'does not create lists from descendant groups' do + expect(page).not_to have_selector('a', text: child_group_label.title) + end + end + end +end diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb index c705e479690..0abef4bc447 100644 --- a/spec/features/projects/badges/list_spec.rb +++ b/spec/features/projects/badges/list_spec.rb @@ -6,7 +6,7 @@ feature 'list of badges' do project = create(:project, :repository) project.add_master(user) sign_in(user) - visit project_pipelines_settings_path(project) + visit project_settings_ci_cd_path(project) end scenario 'user wants to see build status badge' do diff --git a/spec/features/projects/issues/user_sorts_issues_spec.rb b/spec/features/projects/issues/user_sorts_issues_spec.rb index 34148ae0116..c3d63000dac 100644 --- a/spec/features/projects/issues/user_sorts_issues_spec.rb +++ b/spec/features/projects/issues/user_sorts_issues_spec.rb @@ -25,17 +25,14 @@ describe "User sorts issues" do page.within(".issues-list") do page.within("li.issue:nth-child(1)") do expect(page).to have_content(issue1.title) - expect(page).to have_content("2 1") end page.within("li.issue:nth-child(2)") do expect(page).to have_content(issue2.title) - expect(page).to have_content("1 2") end page.within("li.issue:nth-child(3)") do expect(page).to have_content(issue3.title) - expect(page).not_to have_content("0 0") end end end diff --git a/spec/features/projects/jobs/user_browses_job_spec.rb b/spec/features/projects/jobs/user_browses_job_spec.rb index a728e6eb996..3c8b28e8408 100644 --- a/spec/features/projects/jobs/user_browses_job_spec.rb +++ b/spec/features/projects/jobs/user_browses_job_spec.rb @@ -35,4 +35,26 @@ describe 'User browses a job', :js do expect(build.project.running_or_pending_build_count).to eq(build.project.builds.running_or_pending.count(:all)) end + + context 'with a failed job' do + let!(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + it 'displays the failure reason' do + within('.builds-container') do + build_link = first('.build-job > a') + expect(build_link['data-title']).to eq('test - failed <br> (unknown failure)') + end + end + end + + context 'when a failed job has been retried' do + let!(:build) { create(:ci_build, :failed, :retried, pipeline: pipeline) } + + it 'displays the failure reason and retried label' do + within('.builds-container') do + build_link = first('.build-job > a') + expect(build_link['data-title']).to eq('test - failed <br> (unknown failure) (retried)') + end + end + end end diff --git a/spec/features/projects/jobs/user_browses_jobs_spec.rb b/spec/features/projects/jobs/user_browses_jobs_spec.rb index 767777f3bf9..36ebbeadd4a 100644 --- a/spec/features/projects/jobs/user_browses_jobs_spec.rb +++ b/spec/features/projects/jobs/user_browses_jobs_spec.rb @@ -29,4 +29,15 @@ describe 'User browses jobs' do expect(ci_lint_tool_link[:href]).to end_with(ci_lint_path) end end + + context 'with a failed job' do + let!(:build) { create(:ci_build, :coverage, :failed, pipeline: pipeline) } + + it 'displays a tooltip with the failure reason' do + page.within('.ci-table') do + failed_job_link = page.find('.ci-failed') + expect(failed_job_link[:title]).to eq('Failed <br> (unknown failure)') + end + end + end end diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb index 266ef693d0b..990e5c4d9df 100644 --- a/spec/features/projects/pipelines/pipeline_spec.rb +++ b/spec/features/projects/pipelines/pipeline_spec.rb @@ -115,6 +115,13 @@ describe 'Pipeline', :js do expect(page).not_to have_content('Retry job') end + + it 'should include the failure reason' do + page.within('#ci-badge-test') do + build_link = page.find('.js-pipeline-graph-job-link') + expect(build_link['data-original-title']).to eq('test - failed <br> (unknown failure)') + end + end end context 'when pipeline has manual jobs' do @@ -289,6 +296,15 @@ describe 'Pipeline', :js do it { expect(build_manual.reload).to be_pending } end + + context 'failed jobs' do + it 'displays a tooltip with the failure reason' do + page.within('.ci-table') do + failed_job_link = page.find('.ci-failed') + expect(failed_job_link[:title]).to eq('Failed <br> (unknown failure)') + end + end + end end describe 'GET /:project/pipelines/:id/failures' do diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb index 0e81c6c629a..6e63e0f0b49 100644 --- a/spec/features/projects/pipelines/pipelines_spec.rb +++ b/spec/features/projects/pipelines/pipelines_spec.rb @@ -394,6 +394,23 @@ describe 'Pipelines', :js do expect(build.reload).to be_canceled end end + + context 'for a failed pipeline' do + let!(:build) do + create(:ci_build, :failed, pipeline: pipeline, + stage: 'build', + name: 'build') + end + + it 'should display the failure reason' do + find('.js-builds-dropdown-button').click + + within('.js-builds-dropdown-list') do + build_element = page.find('.mini-pipeline-graph-dropdown-item') + expect(build_element['data-title']).to eq('build - failed <br> (unknown failure)') + end + end + end end context 'with pagination' do diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb index d434c501110..899d0d22819 100644 --- a/spec/finders/labels_finder_spec.rb +++ b/spec/finders/labels_finder_spec.rb @@ -71,6 +71,24 @@ describe LabelsFinder do end end + context 'when group has no projects' do + let(:empty_group) { create(:group) } + let!(:empty_group_label_1) { create(:group_label, group: empty_group, title: 'Label 1 (empty group)') } + let!(:empty_group_label_2) { create(:group_label, group: empty_group, title: 'Label 2 (empty group)') } + + before do + empty_group.add_developer(user) + end + + context 'when only group labels is false' do + it 'returns group labels' do + finder = described_class.new(user, group_id: empty_group.id) + + expect(finder.execute).to eq [empty_group_label_1, empty_group_label_2] + end + end + end + context 'when including labels from group ancestors', :nested_groups do it 'returns labels from group and its ancestors' do private_group_1.add_developer(user) @@ -110,7 +128,21 @@ describe LabelsFinder do end end - context 'filtering by project_id' do + context 'filtering by project_id', :nested_groups do + context 'when include_ancestor_groups is true' do + let!(:sub_project) { create(:project, namespace: private_subgroup_1 ) } + let!(:project_label) { create(:label, project: sub_project, title: 'Label 5') } + let(:finder) { described_class.new(user, project_id: sub_project.id, include_ancestor_groups: true) } + + before do + private_group_1.add_developer(user) + end + + it 'returns all ancestor labels' do + expect(finder.execute).to match_array([private_subgroup_label_1, private_group_label_1, project_label]) + end + end + it 'returns labels available for the project' do finder = described_class.new(user, project_id: project_1.id) diff --git a/spec/fixtures/exported-project.gz b/spec/fixtures/exported-project.gz Binary files differnew file mode 100644 index 00000000000..352384f16c8 --- /dev/null +++ b/spec/fixtures/exported-project.gz diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb index 15cbe36ae76..53c010fa0db 100644 --- a/spec/helpers/diff_helper_spec.rb +++ b/spec/helpers/diff_helper_spec.rb @@ -135,11 +135,37 @@ describe DiffHelper do it "returns strings with marked inline diffs" do marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) - expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>}) + expect(marked_old_line).to eq(%q{abc <span class="idiff left right deletion">'def'</span>}) expect(marked_old_line).to be_html_safe - expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>}) + expect(marked_new_line).to eq(%q{abc <span class="idiff left right addition">"def"</span>}) expect(marked_new_line).to be_html_safe end + + context 'when given HTML' do + it 'sanitizes it' do + old_line = %{test.txt} + new_line = %{<img src=x onerror=alert(document.domain)>} + + marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) + + expect(marked_old_line).to eq(%q{<span class="idiff left right deletion">test.txt</span>}) + expect(marked_old_line).to be_html_safe + expect(marked_new_line).to eq(%q{<span class="idiff left right addition"><img src=x onerror=alert(document.domain)></span>}) + expect(marked_new_line).to be_html_safe + end + + it 'sanitizes the entire line, not just the changes' do + old_line = %{<img src=x onerror=alert(document.domain)>} + new_line = %{<img src=y onerror=alert(document.domain)>} + + marked_old_line, marked_new_line = mark_inline_diffs(old_line, new_line) + + expect(marked_old_line).to eq(%q{<img src=<span class="idiff left right deletion">x</span> onerror=alert(document.domain)>}) + expect(marked_old_line).to be_html_safe + expect(marked_new_line).to eq(%q{<img src=<span class="idiff left right addition">y</span> onerror=alert(document.domain)>}) + expect(marked_new_line).to be_html_safe + end + end end describe '#parallel_diff_discussions' do diff --git a/spec/helpers/tree_helper_spec.rb b/spec/helpers/tree_helper_spec.rb index ccac6e29447..ffdf6561a53 100644 --- a/spec/helpers/tree_helper_spec.rb +++ b/spec/helpers/tree_helper_spec.rb @@ -8,6 +8,7 @@ describe TreeHelper do describe '.render_tree' do before do @id = sha + @path = "" @project = project @lfs_blob_ids = [] end @@ -61,6 +62,15 @@ describe TreeHelper do end end end + + context 'when the root path contains a plus character' do + let(:root_path) { 'gtk/C++' } + let(:tree_item) { double(flat_path: 'gtk/C++/glade') } + + it 'returns the flattened path' do + expect(subject).to eq('glade') + end + end end describe '#commit_in_single_accessible_branch' do diff --git a/spec/initializers/artifacts_direct_upload_support_spec.rb b/spec/initializers/artifacts_direct_upload_support_spec.rb new file mode 100644 index 00000000000..bfb71da3388 --- /dev/null +++ b/spec/initializers/artifacts_direct_upload_support_spec.rb @@ -0,0 +1,71 @@ +require 'spec_helper' + +describe 'Artifacts direct upload support' do + subject do + load Rails.root.join('config/initializers/artifacts_direct_upload_support.rb') + end + + let(:connection) do + { provider: provider } + end + + before do + stub_artifacts_setting( + object_store: { + enabled: enabled, + direct_upload: direct_upload, + connection: connection + }) + end + + context 'when object storage is enabled' do + let(:enabled) { true } + + context 'when direct upload is enabled' do + let(:direct_upload) { true } + + context 'when provider is Google' do + let(:provider) { 'Google' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + + context 'when connection is empty' do + let(:connection) { nil } + + it 'raises an error' do + expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/ + end + end + + context 'when other provider is used' do + let(:provider) { 'AWS' } + + it 'raises an error' do + expect { subject }.to raise_error /object storage provider when 'direct_upload' of artifacts is used/ + end + end + end + + context 'when direct upload is disabled' do + let(:direct_upload) { false } + let(:provider) { 'AWS' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end + end + + context 'when object storage is disabled' do + let(:enabled) { false } + let(:direct_upload) { false } + let(:provider) { 'AWS' } + + it 'succeeds' do + expect { subject }.not_to raise_error + end + end +end diff --git a/spec/javascripts/ide/components/repo_file_buttons_spec.js b/spec/javascripts/ide/components/ide_file_buttons_spec.js index c86bdb132b4..8ac8d1b2acf 100644 --- a/spec/javascripts/ide/components/repo_file_buttons_spec.js +++ b/spec/javascripts/ide/components/ide_file_buttons_spec.js @@ -1,5 +1,5 @@ import Vue from 'vue'; -import repoFileButtons from '~/ide/components/repo_file_buttons.vue'; +import repoFileButtons from '~/ide/components/ide_file_buttons.vue'; import createVueComponent from '../../helpers/vue_mount_component_helper'; import { file } from '../helpers'; @@ -23,7 +23,7 @@ describe('RepoFileButtons', () => { vm.$destroy(); }); - it('renders Raw, Blame, History, Permalink and Preview toggle', done => { + it('renders Raw, Blame, History and Permalink', done => { vm = createComponent(); vm.$nextTick(() => { @@ -32,16 +32,30 @@ describe('RepoFileButtons', () => { const history = vm.$el.querySelector('.history'); expect(raw.href).toMatch(`/${activeFile.rawPath}`); - expect(raw.textContent.trim()).toEqual('Raw'); + expect(raw.getAttribute('data-original-title')).toEqual('Raw'); expect(blame.href).toMatch(`/${activeFile.blamePath}`); - expect(blame.textContent.trim()).toEqual('Blame'); + expect(blame.getAttribute('data-original-title')).toEqual('Blame'); expect(history.href).toMatch(`/${activeFile.commitsPath}`); - expect(history.textContent.trim()).toEqual('History'); - expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual( + expect(history.getAttribute('data-original-title')).toEqual('History'); + expect(vm.$el.querySelector('.permalink').getAttribute('data-original-title')).toEqual( 'Permalink', ); done(); }); }); + + it('renders Download', done => { + activeFile.binary = true; + vm = createComponent(); + + vm.$nextTick(() => { + const raw = vm.$el.querySelector('.raw'); + + expect(raw.href).toMatch(`/${activeFile.rawPath}`); + expect(raw.getAttribute('data-original-title')).toEqual('Download'); + + done(); + }); + }); }); diff --git a/spec/javascripts/ide/components/repo_editor_spec.js b/spec/javascripts/ide/components/repo_editor_spec.js index 9d3fa1280f4..63a3d2c6cd5 100644 --- a/spec/javascripts/ide/components/repo_editor_spec.js +++ b/spec/javascripts/ide/components/repo_editor_spec.js @@ -19,7 +19,6 @@ describe('RepoEditor', () => { f.active = true; f.tempFile = true; - f.html = 'testing'; vm.$store.state.openFiles.push(f); vm.$store.state.entries[f.path] = f; vm.monaco = true; @@ -47,6 +46,61 @@ describe('RepoEditor', () => { }); }); + it('renders only an edit tab', done => { + Vue.nextTick(() => { + const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li'); + expect(tabs.length).toBe(1); + expect(tabs[0].textContent.trim()).toBe('Edit'); + + done(); + }); + }); + + describe('when file is markdown', () => { + beforeEach(done => { + vm.file.previewMode = { + id: 'markdown', + previewTitle: 'Preview Markdown', + }; + + vm.$nextTick(done); + }); + + it('renders an Edit and a Preview Tab', done => { + Vue.nextTick(() => { + const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li'); + expect(tabs.length).toBe(2); + expect(tabs[0].textContent.trim()).toBe('Edit'); + expect(tabs[1].textContent.trim()).toBe('Preview Markdown'); + + done(); + }); + }); + }); + + describe('when file is markdown and viewer mode is review', () => { + beforeEach(done => { + vm.file.previewMode = { + id: 'markdown', + previewTitle: 'Preview Markdown', + }; + vm.$store.state.viewer = 'diff'; + + vm.$nextTick(done); + }); + + it('renders an Edit and a Preview Tab', done => { + Vue.nextTick(() => { + const tabs = vm.$el.querySelectorAll('.ide-mode-tabs .nav-links li'); + expect(tabs.length).toBe(2); + expect(tabs[0].textContent.trim()).toBe('Review'); + expect(tabs[1].textContent.trim()).toBe('Preview Markdown'); + + done(); + }); + }); + }); + describe('when open file is binary and not raw', () => { beforeEach(done => { vm.file.binary = true; @@ -57,10 +111,6 @@ describe('RepoEditor', () => { it('does not render the IDE', () => { expect(vm.shouldHideEditor).toBeTruthy(); }); - - it('shows activeFile html', () => { - expect(vm.$el.textContent).toContain('testing'); - }); }); describe('createEditorInstance', () => { @@ -149,47 +199,49 @@ describe('RepoEditor', () => { }); }); - describe('setup editor for merge request viewing', () => { - beforeEach(done => { - // Resetting as the main test setup has already done it - vm.$destroy(); - resetStore(vm.$store); - Editor.editorInstance.modelManager.dispose(); - - const f = { - ...file(), - active: true, - tempFile: true, - html: 'testing', - mrChange: { diff: 'ABC' }, - baseRaw: 'testing', - content: 'test', - }; - const RepoEditor = Vue.extend(repoEditor); - vm = createComponentWithStore(RepoEditor, store, { - file: f, - }); - - vm.$store.state.openFiles.push(f); - vm.$store.state.entries[f.path] = f; - - vm.$store.state.viewer = 'mrdiff'; + describe('editor updateDimensions', () => { + beforeEach(() => { + spyOn(vm.editor, 'updateDimensions').and.callThrough(); + spyOn(vm.editor, 'updateDiffView'); + }); - vm.monaco = true; + it('calls updateDimensions when rightPanelCollapsed is changed', done => { + vm.$store.state.rightPanelCollapsed = true; - vm.$mount(); + vm.$nextTick(() => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + expect(vm.editor.updateDiffView).toHaveBeenCalled(); - monacoLoader(['vs/editor/editor.main'], () => { - setTimeout(done, 0); + done(); }); }); - it('attaches merge request model to editor when merge request diff', () => { - spyOn(vm.editor, 'attachMergeRequestModel').and.callThrough(); + it('calls updateDimensions when panelResizing is false', done => { + vm.$store.state.panelResizing = true; + + vm + .$nextTick() + .then(() => { + vm.$store.state.panelResizing = false; + }) + .then(vm.$nextTick) + .then(() => { + expect(vm.editor.updateDimensions).toHaveBeenCalled(); + expect(vm.editor.updateDiffView).toHaveBeenCalled(); + }) + .then(done) + .catch(done.fail); + }); - vm.setupEditor(); + it('does not call updateDimensions when panelResizing is true', done => { + vm.$store.state.panelResizing = true; + + vm.$nextTick(() => { + expect(vm.editor.updateDimensions).not.toHaveBeenCalled(); + expect(vm.editor.updateDiffView).not.toHaveBeenCalled(); - expect(vm.editor.attachMergeRequestModel).toHaveBeenCalledWith(vm.model); + done(); + }); }); }); }); diff --git a/spec/javascripts/ide/lib/editor_spec.js b/spec/javascripts/ide/lib/editor_spec.js index ec56ebc0341..75e6f0f54ec 100644 --- a/spec/javascripts/ide/lib/editor_spec.js +++ b/spec/javascripts/ide/lib/editor_spec.js @@ -76,7 +76,8 @@ describe('Multi-file editor library', () => { occurrencesHighlight: false, renderLineHighlight: 'none', hideCursorInOverviewRuler: true, - wordWrap: 'bounded', + wordWrap: 'on', + renderSideBySide: true, }); }); }); @@ -215,4 +216,56 @@ describe('Multi-file editor library', () => { expect(instance.decorationsController.dispose).not.toHaveBeenCalled(); }); }); + + describe('updateDiffView', () => { + describe('edit mode', () => { + it('does not update options', () => { + instance.createInstance(holder); + + spyOn(instance.instance, 'updateOptions'); + + instance.updateDiffView(); + + expect(instance.instance.updateOptions).not.toHaveBeenCalled(); + }); + }); + + describe('diff mode', () => { + beforeEach(() => { + instance.createDiffInstance(holder); + + spyOn(instance.instance, 'updateOptions').and.callThrough(); + }); + + it('sets renderSideBySide to false if el is less than 700 pixels', () => { + spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(600); + + expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({ + renderSideBySide: false, + }); + }); + + it('sets renderSideBySide to false if el is more than 700 pixels', () => { + spyOnProperty(instance.instance.getDomNode(), 'offsetWidth').and.returnValue(800); + + expect(instance.instance.updateOptions).not.toHaveBeenCalledWith({ + renderSideBySide: true, + }); + }); + }); + }); + + describe('isDiffEditorType', () => { + it('returns true when diff editor', () => { + instance.createDiffInstance(holder); + + expect(instance.isDiffEditorType).toBe(true); + }); + + it('returns false when not diff editor', () => { + instance.createInstance(holder); + + expect(instance.isDiffEditorType).toBe(false); + }); + }); }); diff --git a/spec/javascripts/ide/stores/mutations/file_spec.js b/spec/javascripts/ide/stores/mutations/file_spec.js index 88285ee409f..bf9d5166d0a 100644 --- a/spec/javascripts/ide/stores/mutations/file_spec.js +++ b/spec/javascripts/ide/stores/mutations/file_spec.js @@ -194,6 +194,17 @@ describe('IDE store file mutations', () => { }); }); + describe('SET_FILE_VIEWMODE', () => { + it('updates file view mode', () => { + mutations.SET_FILE_VIEWMODE(localState, { + file: localFile, + viewMode: 'preview', + }); + + expect(localFile.viewMode).toBe('preview'); + }); + }); + describe('ADD_PENDING_TAB', () => { beforeEach(() => { const f = { diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js index ce181a1e515..c9677ae209a 100644 --- a/spec/javascripts/pipelines/graph/job_component_spec.js +++ b/spec/javascripts/pipelines/graph/job_component_spec.js @@ -13,6 +13,7 @@ describe('pipeline graph job component', () => { icon: 'icon_status_success', text: 'passed', label: 'passed', + tooltip: 'passed', group: 'success', details_path: '/root/ci-mock/builds/4256', has_details: true, @@ -137,6 +138,7 @@ describe('pipeline graph job component', () => { status: { icon: 'icon_status_success', label: 'success', + tooltip: 'success', }, }, }); diff --git a/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js new file mode 100644 index 00000000000..c7c454a0b45 --- /dev/null +++ b/spec/javascripts/vue_shared/components/content_viewer/content_viewer_spec.js @@ -0,0 +1,41 @@ +import Vue from 'vue'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import contentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue'; +import mountComponent from 'spec/helpers/vue_mount_component_helper'; + +describe('ContentViewer', () => { + let vm; + let mock; + + function createComponent(props) { + const ContentViewer = Vue.extend(contentViewer); + vm = mountComponent(ContentViewer, props); + } + + afterEach(() => { + vm.$destroy(); + if (mock) mock.restore(); + }); + + it('markdown preview renders + loads rendered markdown from server', done => { + mock = new MockAdapter(axios); + mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).reply(200, { + body: '<b>testing</b>', + }); + + createComponent({ + path: 'test.md', + content: '* Test', + projectPath: 'testproject', + }); + + const previewContainer = vm.$el.querySelector('.md-previewer'); + + setTimeout(() => { + expect(previewContainer.textContent).toContain('testing'); + + done(); + }); + }); +}); diff --git a/spec/lib/banzai/cross_project_reference_spec.rb b/spec/lib/banzai/cross_project_reference_spec.rb index 68ca960caab..aadfe7637dd 100644 --- a/spec/lib/banzai/cross_project_reference_spec.rb +++ b/spec/lib/banzai/cross_project_reference_spec.rb @@ -14,6 +14,16 @@ describe Banzai::CrossProjectReference do end end + context 'when no project was referenced in group context' do + it 'returns the group from context' do + group = double + + allow(self).to receive(:context).and_return({ group: group }) + + expect(parent_from_ref(nil)).to eq group + end + end + context 'when referenced project does not exist' do it 'returns nil' do expect(parent_from_ref('invalid/reference')).to be_nil diff --git a/spec/lib/banzai/filter/commit_trailers_filter_spec.rb b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb new file mode 100644 index 00000000000..1fd145116df --- /dev/null +++ b/spec/lib/banzai/filter/commit_trailers_filter_spec.rb @@ -0,0 +1,171 @@ +require 'spec_helper' +require 'ffaker' + +describe Banzai::Filter::CommitTrailersFilter do + include FilterSpecHelper + include CommitTrailersSpecHelper + + let(:secondary_email) { create(:email, :confirmed) } + let(:user) { create(:user) } + + let(:trailer) { "#{FFaker::Lorem.word}-by:"} + + let(:commit_message) { trailer_line(trailer, user.name, user.email) } + let(:commit_message_html) { commit_html(commit_message) } + + context 'detects' do + let(:email) { FFaker::Internet.email } + + it 'trailers in the form of *-by and replace users with links' do + doc = filter(commit_message_html) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + end + + it 'trailers prefixed with whitespaces' do + message_html = commit_html("\n\r #{commit_message}") + + doc = filter(message_html) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + end + + it 'GitLab users via a secondary email' do + _, message_html = build_commit_message( + trailer: trailer, + name: secondary_email.user.name, + email: secondary_email.email + ) + + doc = filter(message_html) + + expect_to_have_user_link_with_avatar( + doc, + user: secondary_email.user, + trailer: trailer, + email: secondary_email.email + ) + end + + it 'non GitLab users and replaces them with mailto links' do + _, message_html = build_commit_message( + trailer: trailer, + name: FFaker::Name.name, + email: email + ) + + doc = filter(message_html) + + expect_to_have_mailto_link(doc, email: email, trailer: trailer) + end + + it 'multiple trailers in the same message' do + different_trailer = "#{FFaker::Lorem.word}-by:" + message = commit_html %( + #{commit_message} + #{trailer_line(different_trailer, FFaker::Name.name, email)} + ) + + doc = filter(message) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + expect_to_have_mailto_link(doc, email: email, trailer: different_trailer) + end + + context 'special names' do + where(:name) do + [ + 'John S. Doe', + 'L33t H@x0r' + ] + end + + with_them do + it do + message, message_html = build_commit_message( + trailer: trailer, + name: name, + email: email + ) + + doc = filter(message_html) + + expect_to_have_mailto_link(doc, email: email, trailer: trailer) + expect(doc.text).to match Regexp.escape(message) + end + end + end + end + + context "ignores" do + it 'commit messages without trailers' do + exp = message = commit_html(FFaker::Lorem.sentence) + doc = filter(message) + + expect(doc.to_html).to match Regexp.escape(exp) + end + + it 'trailers that are inline the commit message body' do + message = commit_html %( + #{FFaker::Lorem.sentence} #{commit_message} #{FFaker::Lorem.sentence} + ) + + doc = filter(message) + + expect(doc.css('a').size).to eq 0 + end + end + + context "structure" do + it 'preserves the commit trailer structure' do + doc = filter(commit_message_html) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + expect(doc.text).to match Regexp.escape(commit_message) + end + + it 'preserves the original name used in the commit message' do + message, message_html = build_commit_message( + trailer: trailer, + name: FFaker::Name.name, + email: user.email + ) + + doc = filter(message_html) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + expect(doc.text).to match Regexp.escape(message) + end + + it 'preserves the original email used in the commit message' do + message, message_html = build_commit_message( + trailer: trailer, + name: secondary_email.user.name, + email: secondary_email.email + ) + + doc = filter(message_html) + + expect_to_have_user_link_with_avatar( + doc, + user: secondary_email.user, + trailer: trailer, + email: secondary_email.email + ) + expect(doc.text).to match Regexp.escape(message) + end + + it 'only replaces trailer lines not the full commit message' do + commit_body = FFaker::Lorem.paragraph + message = commit_html %( + #{commit_body} + #{commit_message} + ) + + doc = filter(message) + + expect_to_have_user_link_with_avatar(doc, user: user, trailer: trailer) + expect(doc.text).to include(commit_body) + end + end +end diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb index 0c524a1551f..392905076dc 100644 --- a/spec/lib/banzai/filter/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb @@ -596,6 +596,27 @@ describe Banzai::Filter::LabelReferenceFilter do end describe 'group context' do + it 'points to the page defined in label_url_method' do + group = create(:group) + label = create(:group_label, group: group) + reference = "~#{label.name}" + + result = reference_filter("See #{reference}", { project: nil, group: group, label_url_method: :group_url } ) + + expect(result.css('a').first.attr('href')).to eq(urls.group_url(group, label_name: label.name)) + end + + it 'finds labels also in ancestor groups' do + group = create(:group) + label = create(:group_label, group: group) + subgroup = create(:group, parent: group) + reference = "~#{label.name}" + + result = reference_filter("See #{reference}", { project: nil, group: subgroup, label_url_method: :group_url } ) + + expect(result.css('a').first.attr('href')).to eq(urls.group_url(subgroup, label_name: label.name)) + end + it 'points to referenced project issues page' do project = create(:project) label = create(:label, project: project) @@ -604,6 +625,7 @@ describe Banzai::Filter::LabelReferenceFilter do result = reference_filter("See #{reference}", { project: nil, group: create(:group) } ) expect(result.css('a').first.attr('href')).to eq(urls.project_issues_url(project, label_name: label.name)) + expect(result.css('a').first.text).to eq "#{label.name} in #{project.full_name}" end end end diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb index eeb82822f68..a1dd72c498f 100644 --- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb @@ -196,6 +196,41 @@ describe Banzai::Filter::MergeRequestReferenceFilter do end end + context 'URL reference for a commit' do + let(:mr) { create(:merge_request, :with_diffs) } + let(:reference) do + urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=#{mr.diff_head_sha}" + end + let(:commit) { mr.commits.find { |commit| commit.sha == mr.diff_head_sha } } + + it 'links to a valid reference' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('href')) + .to eq reference + end + + it 'has valid text' do + doc = reference_filter("See #{reference}") + + expect(doc.text).to eq("See #{mr.to_reference(full: true)} (#{commit.short_id})") + end + + it 'has valid title attribute' do + doc = reference_filter("See #{reference}") + + expect(doc.css('a').first.attr('title')).to eq(commit.title) + end + + it 'ignores invalid commit short_ids on link text' do + invalidate_commit_reference = + urls.project_merge_request_url(mr.project, mr) + "/diffs?commit_id=12345678" + doc = reference_filter("See #{invalidate_commit_reference}") + + expect(doc.text).to eq("See #{mr.to_reference(full: true)} (diffs)") + end + end + context 'cross-project URL reference' do let(:namespace) { create(:namespace, name: 'cross-reference') } let(:project2) { create(:project, :public, namespace: namespace) } diff --git a/spec/lib/banzai/reference_parser/issue_parser_spec.rb b/spec/lib/banzai/reference_parser/issue_parser_spec.rb index 0a63567ee40..cb7f8b20dda 100644 --- a/spec/lib/banzai/reference_parser/issue_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/issue_parser_spec.rb @@ -117,4 +117,27 @@ describe Banzai::ReferenceParser::IssueParser do expect(subject.records_for_nodes(nodes)).to eq({ link => issue }) end end + + context 'when checking multiple merge requests on another project' do + let(:other_project) { create(:project, :public) } + let(:other_issue) { create(:issue, project: other_project) } + + let(:control_links) do + [issue_link(other_issue)] + end + + let(:actual_links) do + control_links + [issue_link(create(:issue, project: other_project))] + end + + def issue_link(issue) + Nokogiri::HTML.fragment(%Q{<a data-issue="#{issue.id}"></a>}).children[0] + end + + before do + project.add_developer(user) + end + + it_behaves_like 'no N+1 queries' + end end diff --git a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb index 775749ae3a7..14542342cf6 100644 --- a/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb +++ b/spec/lib/banzai/reference_parser/merge_request_parser_spec.rb @@ -4,14 +4,13 @@ describe Banzai::ReferenceParser::MergeRequestParser do include ReferenceParserHelpers let(:user) { create(:user) } - let(:merge_request) { create(:merge_request) } - subject { described_class.new(merge_request.target_project, user) } + let(:project) { create(:project, :public) } + let(:merge_request) { create(:merge_request, source_project: project) } + subject { described_class.new(project, user) } let(:link) { empty_html_link } describe '#nodes_visible_to_user' do context 'when the link has a data-issue attribute' do - let(:project) { merge_request.target_project } - before do project.update_attribute(:visibility_level, Gitlab::VisibilityLevel::PUBLIC) link['data-merge-request'] = merge_request.id.to_s @@ -40,4 +39,27 @@ describe Banzai::ReferenceParser::MergeRequestParser do end end end + + context 'when checking multiple merge requests on another project' do + let(:other_project) { create(:project, :public) } + let(:other_merge_request) { create(:merge_request, source_project: other_project) } + + let(:control_links) do + [merge_request_link(other_merge_request)] + end + + let(:actual_links) do + control_links + [merge_request_link(create(:merge_request, :conflict, source_project: other_project))] + end + + def merge_request_link(merge_request) + Nokogiri::HTML.fragment(%Q{<a data-merge-request="#{merge_request.id}"></a>}).children[0] + end + + before do + project.add_developer(user) + end + + it_behaves_like 'no N+1 queries' + end end diff --git a/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb new file mode 100644 index 00000000000..6f3fb994f17 --- /dev/null +++ b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_services_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices, :migration, schema: 20180122154930 do + let(:services) { table(:services) } + + describe '#perform' do + it 'migrates services where note_events is true' do + service = services.create(confidential_note_events: nil, note_events: true) + + subject.perform(service.id, service.id) + + expect(service.reload.confidential_note_events).to eq(true) + end + + it 'ignores services where note_events is false' do + service = services.create(confidential_note_events: nil, note_events: false) + + subject.perform(service.id, service.id) + + expect(service.reload.confidential_note_events).to eq(nil) + end + + it 'ignores services where confidential_note_events has already been set' do + service = services.create(confidential_note_events: false, note_events: true) + + subject.perform(service.id, service.id) + + expect(service.reload.confidential_note_events).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb new file mode 100644 index 00000000000..82b484b7d5b --- /dev/null +++ b/spec/lib/gitlab/background_migration/set_confidential_note_events_on_webhooks_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks, :migration, schema: 20180104131052 do + let(:web_hooks) { table(:web_hooks) } + + describe '#perform' do + it 'migrates hooks where note_events is true' do + hook = web_hooks.create(confidential_note_events: nil, note_events: true) + + subject.perform(hook.id, hook.id) + + expect(hook.reload.confidential_note_events).to eq(true) + end + + it 'ignores hooks where note_events is false' do + hook = web_hooks.create(confidential_note_events: nil, note_events: false) + + subject.perform(hook.id, hook.id) + + expect(hook.reload.confidential_note_events).to eq(nil) + end + + it 'ignores hooks where confidential_note_events has already been set' do + hook = web_hooks.create(confidential_note_events: false, note_events: true) + + subject.perform(hook.id, hook.id) + + expect(hook.reload.confidential_note_events).to eq(false) + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/action_spec.rb b/spec/lib/gitlab/ci/status/build/action_spec.rb index d612d29e3e0..bdec582b57b 100644 --- a/spec/lib/gitlab/ci/status/build/action_spec.rb +++ b/spec/lib/gitlab/ci/status/build/action_spec.rb @@ -53,4 +53,14 @@ describe Gitlab::Ci::Status::Build::Action do end end end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:build) { create(:ci_build, :non_playable) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + it 'returns the status' do + expect(subject.badge_tooltip).to eq('created') + end + end end diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb index 0b88e0c7c9c..78d6fa65b5a 100644 --- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb @@ -40,6 +40,24 @@ describe Gitlab::Ci::Status::Build::Cancelable do end end + describe '#status_tooltip' do + it 'does not override status status_tooltip' do + expect(status).to receive(:status_tooltip) + + subject.status_tooltip + end + end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + it 'returns the status' do + expect(subject.badge_tooltip).to eq('pending') + end + end + describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb index 94eedc50bb2..6d5b73bb01b 100644 --- a/spec/lib/gitlab/ci/status/build/factory_spec.rb +++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb @@ -75,11 +75,11 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) - .to eq [Gitlab::Ci::Status::Build::Retryable] + .to eq [Gitlab::Ci::Status::Build::Retryable, Gitlab::Ci::Status::Build::Failed] end - it 'fabricates a retryable build status' do - expect(status).to be_a Gitlab::Ci::Status::Build::Retryable + it 'fabricates a failed build status' do + expect(status).to be_a Gitlab::Ci::Status::Build::Failed end it 'fabricates status with correct details' do @@ -87,6 +87,7 @@ describe Gitlab::Ci::Status::Build::Factory do expect(status.icon).to eq 'status_failed' expect(status.favicon).to eq 'favicon_status_failed' expect(status.label).to eq 'failed' + expect(status.status_tooltip).to eq 'failed <br> (unknown failure)' expect(status).to have_details expect(status).to have_action end @@ -102,6 +103,7 @@ describe Gitlab::Ci::Status::Build::Factory do it 'matches correct extended statuses' do expect(factory.extended_statuses) .to eq [Gitlab::Ci::Status::Build::Retryable, + Gitlab::Ci::Status::Build::Failed, Gitlab::Ci::Status::Build::FailedAllowed] end diff --git a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb index 99a5a7e4aca..bfaa508785e 100644 --- a/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb +++ b/spec/lib/gitlab/ci/status/build/failed_allowed_spec.rb @@ -3,6 +3,7 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::FailedAllowed do let(:status) { double('core status') } let(:user) { double('user') } + let(:build) { create(:ci_build, :failed, :allowed_to_fail) } subject do described_class.new(status) @@ -68,6 +69,28 @@ describe Gitlab::Ci::Status::Build::FailedAllowed do end end + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:failed_status) { Gitlab::Ci::Status::Failed.new(build, user) } + let(:build_status) { Gitlab::Ci::Status::Build::Failed.new(failed_status) } + let(:status) { described_class.new(build_status) } + + it 'does override badge_tooltip' do + expect(status.badge_tooltip).to eq('failed <br> (unknown failure)') + end + end + + describe '#status_tooltip' do + let(:user) { create(:user) } + let(:failed_status) { Gitlab::Ci::Status::Failed.new(build, user) } + let(:build_status) { Gitlab::Ci::Status::Build::Failed.new(failed_status) } + let(:status) { described_class.new(build_status) } + + it 'does override status_tooltip' do + expect(status.status_tooltip).to eq 'failed <br> (unknown failure) (allowed to fail)' + end + end + describe '.matches?' do subject { described_class.matches?(build, user) } diff --git a/spec/lib/gitlab/ci/status/build/failed_spec.rb b/spec/lib/gitlab/ci/status/build/failed_spec.rb new file mode 100644 index 00000000000..cadb424ea2c --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/failed_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Failed do + let(:build) { create(:ci_build, :script_failure) } + let(:status) { double('core status') } + let(:user) { double('user') } + + subject { described_class.new(status) } + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe '#favicon' do + it 'does not override status label' do + expect(status).to receive(:favicon) + + subject.favicon + end + end + + describe '#label' do + it 'does not override label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:status) { Gitlab::Ci::Status::Failed.new(build, user) } + + it 'does override badge_tooltip' do + expect(subject.badge_tooltip).to eq 'failed <br> (script failure)' + end + end + + describe '#status_tooltip' do + let(:user) { create(:user) } + let(:status) { Gitlab::Ci::Status::Failed.new(build, user) } + + it 'does override status_tooltip' do + expect(subject.status_tooltip).to eq 'failed <br> (script failure)' + end + end + + describe '.matches?' do + context 'with a failed build' do + it 'returns true' do + expect(described_class.matches?(build, user)).to be_truthy + end + end + + context 'with any other type of build' do + let(:build) { create(:ci_build, :success) } + + it 'returns false' do + expect(described_class.matches?(build, user)).to be_falsy + end + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index b0c3a8bb398..f128c1d4ca4 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -14,6 +14,22 @@ describe Gitlab::Ci::Status::Build::Play do end end + describe '#status_tooltip' do + it 'does not override status status_tooltip' do + expect(status).to receive(:status_tooltip) + + subject.status_tooltip + end + end + + describe '#badge_tooltip' do + it 'does not override status badge_tooltip' do + expect(status).to receive(:badge_tooltip) + + subject.badge_tooltip + end + end + describe '#has_action?' do context 'when user is allowed to update build' do context 'when user is allowed to trigger protected action' do diff --git a/spec/lib/gitlab/ci/status/build/retried_spec.rb b/spec/lib/gitlab/ci/status/build/retried_spec.rb new file mode 100644 index 00000000000..ee9acaf1c21 --- /dev/null +++ b/spec/lib/gitlab/ci/status/build/retried_spec.rb @@ -0,0 +1,96 @@ +require 'spec_helper' + +describe Gitlab::Ci::Status::Build::Retried do + let(:build) { create(:ci_build, :retried) } + let(:status) { double('core status') } + let(:user) { double('user') } + + subject { described_class.new(status) } + + describe '#text' do + it 'does not override status text' do + expect(status).to receive(:text) + + subject.text + end + end + + describe '#icon' do + it 'does not override status icon' do + expect(status).to receive(:icon) + + subject.icon + end + end + + describe '#group' do + it 'does not override status group' do + expect(status).to receive(:group) + + subject.group + end + end + + describe '#favicon' do + it 'does not override status label' do + expect(status).to receive(:favicon) + + subject.favicon + end + end + + describe '#label' do + it 'does not override status label' do + expect(status).to receive(:label) + + subject.label + end + end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:build) { create(:ci_build, :retried) } + let(:status) { Gitlab::Ci::Status::Success.new(build, user) } + + it 'returns status' do + expect(status.badge_tooltip).to eq('pending') + end + end + + describe '#status_tooltip' do + let(:user) { create(:user) } + + context 'with a failed build' do + let(:build) { create(:ci_build, :failed, :retried) } + let(:failed_status) { Gitlab::Ci::Status::Failed.new(build, user) } + let(:status) { Gitlab::Ci::Status::Build::Failed.new(failed_status) } + + it 'does override status_tooltip' do + expect(subject.status_tooltip).to eq 'failed <br> (unknown failure) (retried)' + end + end + + context 'with another build' do + let(:build) { create(:ci_build, :retried) } + let(:status) { Gitlab::Ci::Status::Success.new(build, user) } + + it 'does override status_tooltip' do + expect(subject.status_tooltip).to eq 'passed (retried)' + end + end + end + + describe '.matches?' do + subject { described_class.matches?(build, user) } + + context 'with a retried build' do + it { is_expected.to be_truthy } + end + + context 'with a build that has not been retried' do + let(:build) { create(:ci_build, :success) } + + it { is_expected.to be_falsy } + end + end +end diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb index d6e909f6268..84d98588f2d 100644 --- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb +++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb @@ -40,6 +40,24 @@ describe Gitlab::Ci::Status::Build::Retryable do end end + describe '#status_tooltip' do + it 'does not override status status_tooltip' do + expect(status).to receive(:status_tooltip) + + subject.status_tooltip + end + end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:build) { create(:ci_build) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + it 'does return status' do + expect(status.badge_tooltip).to eq('pending') + end + end + describe 'action details' do let(:user) { create(:user) } let(:build) { create(:ci_build) } diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb index 7a72ca2c14d..5b7534c96c1 100644 --- a/spec/lib/gitlab/ci/status/build/stop_spec.rb +++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb @@ -81,4 +81,24 @@ describe Gitlab::Ci::Status::Build::Stop do end end end + + describe '#status_tooltip' do + it 'does not override status status_tooltip' do + expect(status).to receive(:status_tooltip) + + subject.status_tooltip + end + end + + describe '#badge_tooltip' do + let(:user) { create(:user) } + let(:build) { create(:ci_build, :playable) } + let(:status) { Gitlab::Ci::Status::Core.new(build, user) } + + it 'does not override status badge_tooltip' do + expect(status).to receive(:badge_tooltip) + + subject.badge_tooltip + end + end end diff --git a/spec/lib/gitlab/ci/status/success_warning_spec.rb b/spec/lib/gitlab/ci/status/success_warning_spec.rb index 4582354e739..6d05545d1d8 100644 --- a/spec/lib/gitlab/ci/status/success_warning_spec.rb +++ b/spec/lib/gitlab/ci/status/success_warning_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe Gitlab::Ci::Status::SuccessWarning do + let(:status) { double('status') } + subject do - described_class.new(double('status')) + described_class.new(status) end describe '#test' do diff --git a/spec/lib/gitlab/data_builder/note_spec.rb b/spec/lib/gitlab/data_builder/note_spec.rb index aaa42566a4d..4f8412108ba 100644 --- a/spec/lib/gitlab/data_builder/note_spec.rb +++ b/spec/lib/gitlab/data_builder/note_spec.rb @@ -55,6 +55,14 @@ describe Gitlab::DataBuilder::Note do .to be > issue.hook_attrs['updated_at'] end + context 'with confidential issue' do + let(:issue) { create(:issue, project: project, confidential: true) } + + it 'sets event_type to confidential_note' do + expect(data[:event_type]).to eq('confidential_note') + end + end + include_examples 'project hook data' include_examples 'deprecated repository hook data' end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index f8f09d29c73..b845abab5ef 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -10,12 +10,13 @@ describe Gitlab::GitAccess do let(:protocol) { 'ssh' } let(:authentication_abilities) { %i[read_project download_code push_code] } let(:redirected_path) { nil } + let(:auth_result_type) { nil } let(:access) do described_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, namespace_path: namespace_path, project_path: project_path, - redirected_path: redirected_path) + redirected_path: redirected_path, auth_result_type: auth_result_type) end let(:changes) { '_any' } @@ -45,6 +46,7 @@ describe Gitlab::GitAccess do before do disable_protocol('http') + project.add_master(user) end it 'blocks http push and pull' do @@ -53,6 +55,26 @@ describe Gitlab::GitAccess do expect { pull_access_check }.to raise_unauthorized('Git access over HTTP is not allowed') end end + + context 'when request is made from CI' do + let(:auth_result_type) { :build } + + it "doesn't block http pull" do + aggregate_failures do + expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed') + end + end + + context 'when legacy CI credentials are used' do + let(:auth_result_type) { :ci } + + it "doesn't block http pull" do + aggregate_failures do + expect { pull_access_check }.not_to raise_unauthorized('Git access over HTTP is not allowed') + end + end + end + end end end diff --git a/spec/lib/gitlab/import_export/importer_spec.rb b/spec/lib/gitlab/import_export/importer_spec.rb new file mode 100644 index 00000000000..d75416f2a62 --- /dev/null +++ b/spec/lib/gitlab/import_export/importer_spec.rb @@ -0,0 +1,64 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::Importer do + let(:test_path) { "#{Dir.tmpdir}/importer_spec" } + let(:shared) { project.import_export_shared } + let(:project) { create(:project, import_source: File.join(test_path, 'exported-project.gz')) } + + subject(:importer) { described_class.new(project) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(test_path) + FileUtils.mkdir_p(shared.export_path) + FileUtils.cp(Rails.root.join('spec', 'fixtures', 'exported-project.gz'), test_path) + end + + after do + FileUtils.rm_rf(test_path) + end + + describe '#execute' do + it 'succeeds' do + importer.execute + + expect(shared.errors).to be_empty + end + + it 'extracts the archive' do + expect(Gitlab::ImportExport::FileImporter).to receive(:import).and_call_original + + importer.execute + end + + it 'checks the version' do + expect(Gitlab::ImportExport::VersionChecker).to receive(:check!).and_call_original + + importer.execute + end + + context 'all restores are executed' do + [ + Gitlab::ImportExport::AvatarRestorer, + Gitlab::ImportExport::RepoRestorer, + Gitlab::ImportExport::WikiRestorer, + Gitlab::ImportExport::UploadsRestorer, + Gitlab::ImportExport::LfsRestorer + ].each do |restorer| + it "calls the #{restorer}" do + fake_restorer = double(restorer.to_s) + + expect(fake_restorer).to receive(:restore).and_return(true).at_least(1) + expect(restorer).to receive(:new).and_return(fake_restorer).at_least(1) + + importer.execute + end + end + + it 'restores the ProjectTree' do + expect(Gitlab::ImportExport::ProjectTreeRestorer).to receive(:new).and_call_original + + importer.execute + end + end + end +end diff --git a/spec/lib/gitlab/import_export/lfs_restorer_spec.rb b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb new file mode 100644 index 00000000000..70eeb9ee66b --- /dev/null +++ b/spec/lib/gitlab/import_export/lfs_restorer_spec.rb @@ -0,0 +1,75 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::LfsRestorer do + include UploadHelpers + + let(:export_path) { "#{Dir.tmpdir}/lfs_object_restorer_spec" } + let(:project) { create(:project) } + let(:shared) { project.import_export_shared } + subject(:restorer) { described_class.new(project: project, shared: shared) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + FileUtils.mkdir_p(shared.export_path) + end + + after do + FileUtils.rm_rf(shared.export_path) + end + + describe '#restore' do + context 'when the archive contains lfs files' do + let(:dummy_lfs_file_path) { File.join(shared.export_path, 'lfs-objects', 'dummy') } + + def create_lfs_object_with_content(content) + dummy_lfs_file = Tempfile.new('existing') + File.write(dummy_lfs_file.path, content) + size = dummy_lfs_file.size + oid = LfsObject.calculate_oid(dummy_lfs_file.path) + LfsObject.create!(oid: oid, size: size, file: dummy_lfs_file) + end + + before do + FileUtils.mkdir_p(File.dirname(dummy_lfs_file_path)) + File.write(dummy_lfs_file_path, 'not very large') + allow(restorer).to receive(:lfs_file_paths).and_return([dummy_lfs_file_path]) + end + + it 'creates an lfs object for the project' do + expect { restorer.restore }.to change { project.reload.lfs_objects.size }.by(1) + end + + it 'assigns the file correctly' do + restorer.restore + + expect(project.lfs_objects.first.file.read).to eq('not very large') + end + + it 'links an existing LFS object if it existed' do + lfs_object = create_lfs_object_with_content('not very large') + + restorer.restore + + expect(project.lfs_objects).to include(lfs_object) + end + + it 'succeeds' do + expect(restorer.restore).to be_truthy + expect(shared.errors).to be_empty + end + + it 'stores the upload' do + expect_any_instance_of(LfsObjectUploader).to receive(:store!) + + restorer.restore + end + end + + context 'without any LFS-objects' do + it 'succeeds' do + expect(restorer.restore).to be_truthy + expect(shared.errors).to be_empty + end + end + end +end diff --git a/spec/lib/gitlab/import_export/lfs_saver_spec.rb b/spec/lib/gitlab/import_export/lfs_saver_spec.rb new file mode 100644 index 00000000000..9b0e21deb2e --- /dev/null +++ b/spec/lib/gitlab/import_export/lfs_saver_spec.rb @@ -0,0 +1,62 @@ +require 'spec_helper' + +describe Gitlab::ImportExport::LfsSaver do + let(:shared) { project.import_export_shared } + let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" } + let(:project) { create(:project) } + + subject(:saver) { described_class.new(project: project, shared: shared) } + + before do + allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path) + FileUtils.mkdir_p(shared.export_path) + end + + after do + FileUtils.rm_rf(shared.export_path) + end + + describe '#save' do + context 'when the project has LFS objects locally stored' do + let(:lfs_object) { create(:lfs_object, :with_file) } + + before do + project.lfs_objects << lfs_object + end + + it 'does not cause errors' do + saver.save + + expect(shared.errors).to be_empty + end + + it 'copies the file in the correct location when there is an lfs object' do + saver.save + + expect(File).to exist("#{shared.export_path}/lfs-objects/#{lfs_object.oid}") + end + end + + context 'when the LFS objects are stored in object storage' do + let(:lfs_object) { create(:lfs_object, :object_storage) } + + before do + allow(LfsObjectUploader).to receive(:object_store_enabled?).and_return(true) + allow(lfs_object.file).to receive(:url).and_return('http://my-object-storage.local') + project.lfs_objects << lfs_object + end + + it 'downloads the file to include in an archive' do + fake_uri = double + exported_file_path = "#{shared.export_path}/lfs-objects/#{lfs_object.oid}" + + expect(fake_uri).to receive(:open).and_return(StringIO.new('LFS file content')) + expect(URI).to receive(:parse).with('http://my-object-storage.local').and_return(fake_uri) + + saver.save + + expect(File.read(exported_file_path)).to eq('LFS file content') + end + end + end +end diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json index 4a51777ba9b..6d63749296e 100644 --- a/spec/lib/gitlab/import_export/project.json +++ b/spec/lib/gitlab/import_export/project.json @@ -2,7 +2,6 @@ "description": "Nisi et repellendus ut enim quo accusamus vel magnam.", "visibility_level": 10, "archived": false, - "description_html": "description", "labels": [ { "id": 2, @@ -6181,12 +6180,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, "erased_by_id": null, "erased_at": null, "type": "Ci::Build", @@ -6219,12 +6212,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/72/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null } @@ -6293,12 +6280,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/74/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null }, @@ -6328,12 +6309,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, "erased_by_id": null, "erased_at": null } @@ -6393,12 +6368,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/76/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null }, @@ -6428,12 +6397,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/75/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null } @@ -6493,12 +6456,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/78/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null }, @@ -6528,12 +6485,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/77/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null } @@ -6593,12 +6544,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": null - }, - "artifacts_metadata": { - "url": null - }, "erased_by_id": null, "erased_at": null }, @@ -6628,12 +6573,6 @@ "user_id": null, "target_url": null, "description": null, - "artifacts_file": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts.zip" - }, - "artifacts_metadata": { - "url": "/Users/Test/Test/gitlab-development-kit/gitlab/shared/artifacts/2016_03/5/80/p5_build_artifacts_metadata.gz" - }, "erased_by_id": null, "erased_at": null } 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 8e25cd26c2f..13a8c9adcee 100644 --- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb @@ -46,10 +46,6 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do expect(Project.find_by_path('project').description).to eq('Nisi et repellendus ut enim quo accusamus vel magnam.') end - it 'has the project html description' do - expect(Project.find_by_path('project').description_html).to eq('description') - end - it 'has the same label associated to two issues' do expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) end @@ -317,6 +313,24 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do end end + context 'when the project has overriden params in import data' do + it 'overwrites the params stored in the JSON' do + project.create_import_data(data: { override_params: { description: "Overridden" } }) + + restored_project_json + + expect(project.description).to eq("Overridden") + end + + it 'does not allow setting params that are excluded from import_export settings' do + project.create_import_data(data: { override_params: { lfs_enabled: true } }) + + restored_project_json + + expect(project.lfs_enabled).to be_nil + end + end + context 'with a project that has a group' do let!(:project) do create(:project, diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb index 0d20a551e2a..2b8a11ce8f9 100644 --- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb +++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb @@ -245,10 +245,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do end context 'project attributes' do - it 'contains the html description' do - expect(saved_project_json).to include("description_html" => 'description') - end - it 'does not contain the runners token' do expect(saved_project_json).not_to include("runners_token" => 'token') end @@ -274,7 +270,6 @@ describe Gitlab::ImportExport::ProjectTreeSaver do releases: [release], group: group ) - project.update_column(:description_html, 'description') project_label = create(:label, project: project) group_label = create(:group_label, group: group) create(:label_link, label: project_label, target: issue) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index f949a23ffbb..f84a777a27f 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -390,6 +390,7 @@ Service: - default - wiki_page_events - confidential_issues_events +- confidential_note_events ProjectHook: - id - url @@ -410,6 +411,7 @@ ProjectHook: - token - group_id - confidential_issues_events +- confidential_note_events - repository_update_events ProtectedBranch: - id diff --git a/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb new file mode 100644 index 00000000000..fed9aeba30c --- /dev/null +++ b/spec/lib/gitlab/sidekiq_logging/json_formatter_spec.rb @@ -0,0 +1,31 @@ +require 'spec_helper' + +describe Gitlab::SidekiqLogging::JSONFormatter do + let(:hash_input) { { foo: 1, bar: 'test' } } + let(:message) { 'This is a test' } + let(:timestamp) { Time.now } + + it 'wraps a Hash' do + result = subject.call('INFO', timestamp, 'my program', hash_input) + + data = JSON.parse(result) + expected_output = hash_input.stringify_keys + expected_output['severity'] = 'INFO' + expected_output['time'] = timestamp.utc.iso8601(3) + + expect(data).to eq(expected_output) + end + + it 'wraps a String' do + result = subject.call('DEBUG', timestamp, 'my string', message) + + data = JSON.parse(result) + expected_output = { + severity: 'DEBUG', + time: timestamp.utc.iso8601(3), + message: message + } + + expect(data).to eq(expected_output.stringify_keys) + end +end diff --git a/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb new file mode 100644 index 00000000000..2421b1e5a1a --- /dev/null +++ b/spec/lib/gitlab/sidekiq_logging/structured_logger_spec.rb @@ -0,0 +1,101 @@ +require 'spec_helper' + +describe Gitlab::SidekiqLogging::StructuredLogger do + describe '#call' do + let(:timestamp) { Time.new('2018-01-01 12:00:00').utc } + let(:job) do + { + "class" => "TestWorker", + "args" => [1234, 'hello'], + "retry" => false, + "queue" => "cronjob:test_queue", + "queue_namespace" => "cronjob", + "jid" => "da883554ee4fe414012f5f42", + "created_at" => timestamp.to_f, + "enqueued_at" => timestamp.to_f + } + end + let(:logger) { double() } + let(:start_payload) do + job.merge( + 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', + 'job_status' => 'start', + 'pid' => Process.pid, + 'created_at' => timestamp.iso8601(3), + 'enqueued_at' => timestamp.iso8601(3) + ) + end + let(:end_payload) do + start_payload.merge( + 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: done: 0.0 sec', + 'job_status' => 'done', + 'duration' => 0.0, + "completed_at" => timestamp.iso8601(3) + ) + end + let(:exception_payload) do + end_payload.merge( + 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', + 'job_status' => 'fail', + 'error' => ArgumentError, + 'error_message' => 'some exception' + ) + end + + before do + allow(Sidekiq).to receive(:logger).and_return(logger) + + allow(subject).to receive(:current_time).and_return(timestamp.to_f) + end + + subject { described_class.new } + + context 'with SIDEKIQ_LOG_ARGUMENTS enabled' do + before do + stub_env('SIDEKIQ_LOG_ARGUMENTS', '1') + end + + it 'logs start and end of job' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload).ordered + expect(logger).to receive(:info).with(end_payload).ordered + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + subject.call(job, 'test_queue') { } + end + end + + it 'logs an exception in job' do + Timecop.freeze(timestamp) do + expect(logger).to receive(:info).with(start_payload) + # This excludes the exception_backtrace + expect(logger).to receive(:warn).with(hash_including(exception_payload)) + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + expect do + subject.call(job, 'test_queue') do + raise ArgumentError, 'some exception' + end + end.to raise_error(ArgumentError) + end + end + end + + context 'with SIDEKIQ_LOG_ARGUMENTS disabled' do + it 'logs start and end of job' do + Timecop.freeze(timestamp) do + start_payload.delete('args') + + expect(logger).to receive(:info).with(start_payload).ordered + expect(logger).to receive(:info).with(end_payload).ordered + expect(subject).to receive(:log_job_start).and_call_original + expect(subject).to receive(:log_job_done).and_call_original + + subject.call(job, 'test_queue') { } + end + end + end + end +end diff --git a/spec/lib/uploaded_file_spec.rb b/spec/lib/uploaded_file_spec.rb new file mode 100644 index 00000000000..cc99e7e8911 --- /dev/null +++ b/spec/lib/uploaded_file_spec.rb @@ -0,0 +1,116 @@ +require 'spec_helper' + +describe UploadedFile do + describe ".from_params" do + let(:temp_dir) { Dir.tmpdir } + let(:temp_file) { Tempfile.new("test", temp_dir) } + let(:upload_path) { nil } + + subject do + described_class.from_params(params, :file, upload_path) + end + + before do + FileUtils.touch(temp_file) + end + + after do + FileUtils.rm_f(temp_file) + FileUtils.rm_r(upload_path) if upload_path + end + + context 'when valid file is specified' do + context 'only local path is specified' do + let(:params) do + { 'file.path' => temp_file.path } + end + + it "succeeds" do + is_expected.not_to be_nil + end + + it "generates filename from path" do + expect(subject.original_filename).to eq(::File.basename(temp_file.path)) + end + end + + context 'all parameters are specified' do + let(:params) do + { 'file.path' => temp_file.path, + 'file.name' => 'my_file.txt', + 'file.type' => 'my/type', + 'file.sha256' => 'sha256', + 'file.remote_id' => 'remote_id' } + end + + it "succeeds" do + is_expected.not_to be_nil + end + + it "generates filename from path" do + expect(subject.original_filename).to eq('my_file.txt') + expect(subject.content_type).to eq('my/type') + expect(subject.sha256).to eq('sha256') + expect(subject.remote_id).to eq('remote_id') + end + end + end + + context 'when no params are specified' do + let(:params) do + {} + end + + it "does not return an object" do + is_expected.to be_nil + end + end + + context 'when only remote id is specified' do + let(:params) do + { 'file.remote_id' => 'remote_id' } + end + + it "raises an error" do + expect { subject }.to raise_error(UploadedFile::InvalidPathError, /file is invalid/) + end + end + + context 'when verifying allowed paths' do + let(:params) do + { 'file.path' => temp_file.path } + end + + context 'when file is stored in system temporary folder' do + let(:temp_dir) { Dir.tmpdir } + + it "succeeds" do + is_expected.not_to be_nil + end + end + + context 'when file is stored in user provided upload path' do + let(:upload_path) { Dir.mktmpdir } + let(:temp_dir) { upload_path } + + it "succeeds" do + is_expected.not_to be_nil + end + end + + context 'when file is stored outside of user provided upload path' do + let!(:generated_dir) { Dir.mktmpdir } + let!(:temp_dir) { Dir.mktmpdir } + + before do + # We overwrite default temporary path + allow(Dir).to receive(:tmpdir).and_return(generated_dir) + end + + it "raises an error" do + expect { subject }.to raise_error(UploadedFile::InvalidPathError, /insecure path used/) + end + end + end + end +end diff --git a/spec/mailers/previews/email_rejection_mailer_preview.rb b/spec/mailers/previews/email_rejection_mailer_preview.rb new file mode 100644 index 00000000000..639e8471232 --- /dev/null +++ b/spec/mailers/previews/email_rejection_mailer_preview.rb @@ -0,0 +1,5 @@ +class EmailRejectionMailerPreview < ActionMailer::Preview + def rejection + EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message + end +end diff --git a/spec/mailers/previews/notify_preview.rb b/spec/mailers/previews/notify_preview.rb index 43c3c89f140..e32fd0bd120 100644 --- a/spec/mailers/previews/notify_preview.rb +++ b/spec/mailers/previews/notify_preview.rb @@ -58,16 +58,89 @@ class NotifyPreview < ActionMailer::Preview end end + def closed_issue_email + Notify.closed_issue_email(user.id, issue.id, user.id).message + end + + def issue_status_changed_email + Notify.issue_status_changed_email(user.id, issue.id, 'closed', user.id).message + end + + def closed_merge_request_email + Notify.closed_merge_request_email(user.id, issue.id, user.id).message + end + + def merge_request_status_email + Notify.merge_request_status_email(user.id, merge_request.id, 'closed', user.id).message + end + + def merged_merge_request_email + Notify.merged_merge_request_email(user.id, merge_request.id, user.id).message + end + + def member_access_denied_email + Notify.member_access_denied_email('project', project.id, user.id).message + end + + def member_access_granted_email + Notify.member_access_granted_email('project', user.id).message + end + + def member_access_requested_email + Notify.member_access_requested_email('group', user.id, 'some@example.com').message + end + + def member_invite_accepted_email + Notify.member_invite_accepted_email('project', user.id).message + end + + def member_invite_declined_email + Notify.member_invite_declined_email( + 'project', + project.id, + 'invite@example.com', + user.id + ).message + end + + def member_invited_email + Notify.member_invited_email('project', user.id, '1234').message + end + + def pages_domain_enabled_email + cleanup do + pages_domain = PagesDomain.new(domain: 'my.example.com', project: project, verified_at: Time.now, enabled_until: 1.week.from_now) + + Notify.pages_domain_enabled_email(pages_domain, user).message + end + end + + def pipeline_success_email + Notify.pipeline_success_email(pipeline, pipeline.user.try(:email)) + end + + def pipeline_failed_email + Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) + end + private def project @project ||= Project.find_by_full_path('gitlab-org/gitlab-test') end + def issue + @merge_request ||= project.issues.first + end + def merge_request @merge_request ||= project.merge_requests.first end + def pipeline + @pipeline = Ci::Pipeline.last + end + def user @user ||= User.last end @@ -94,14 +167,4 @@ class NotifyPreview < ActionMailer::Preview email end - - def pipeline_success_email - pipeline = Ci::Pipeline.last - Notify.pipeline_success_email(pipeline, pipeline.user.try(:email)) - end - - def pipeline_failed_email - pipeline = Ci::Pipeline.last - Notify.pipeline_failed_email(pipeline, pipeline.user.try(:email)) - end end diff --git a/spec/mailers/previews/repository_check_mailer_preview.rb b/spec/mailers/previews/repository_check_mailer_preview.rb new file mode 100644 index 00000000000..19d4eab1805 --- /dev/null +++ b/spec/mailers/previews/repository_check_mailer_preview.rb @@ -0,0 +1,5 @@ +class RepositoryCheckMailerPreview < ActionMailer::Preview + def notify + RepositoryCheckMailer.notify(3).message + end +end diff --git a/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb new file mode 100644 index 00000000000..4395e2f8264 --- /dev/null +++ b/spec/migrations/active_record/schedule_set_confidential_note_events_on_services_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180122154930_schedule_set_confidential_note_events_on_services.rb') + +describe ScheduleSetConfidentialNoteEventsOnServices, :migration, :sidekiq do + let(:services_table) { table(:services) } + let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnServices } + let(:migration_name) { migration_class.to_s.demodulize } + + let!(:service_1) { services_table.create!(confidential_note_events: nil, note_events: true) } + let!(:service_2) { services_table.create!(confidential_note_events: nil, note_events: true) } + let!(:service_migrated) { services_table.create!(confidential_note_events: true, note_events: true) } + let!(:service_skip) { services_table.create!(confidential_note_events: nil, note_events: false) } + let!(:service_new) { services_table.create!(confidential_note_events: false, note_events: true) } + let!(:service_4) { services_table.create!(confidential_note_events: nil, note_events: true) } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + it 'schedules background migrations at correct time' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(20.minutes, service_1.id, service_1.id) + expect(migration_name).to be_scheduled_delayed_migration(40.minutes, service_2.id, service_2.id) + expect(migration_name).to be_scheduled_delayed_migration(60.minutes, service_4.id, service_4.id) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'correctly processes services' do + Sidekiq::Testing.inline! do + expect(services_table.where(confidential_note_events: nil).count).to eq 4 + expect(services_table.where(confidential_note_events: true).count).to eq 1 + + migrate! + + expect(services_table.where(confidential_note_events: nil).count).to eq 1 + expect(services_table.where(confidential_note_events: true).count).to eq 4 + end + end +end diff --git a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb index c81ec887ded..df009cec25c 100644 --- a/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb +++ b/spec/migrations/migrate_gcp_clusters_to_new_clusters_architectures_spec.rb @@ -4,8 +4,24 @@ require Rails.root.join('db', 'post_migrate', '20171013104327_migrate_gcp_cluste describe MigrateGcpClustersToNewClustersArchitectures, :migration do let(:projects) { table(:projects) } let(:project) { projects.create } - let(:user) { create(:user) } - let(:service) { create(:kubernetes_service, project_id: project.id) } + let(:users) { table(:users) } + let(:user) { users.create! } + let(:service) { GcpMigrationSpec::KubernetesService.create!(project_id: project.id) } + + module GcpMigrationSpec + class KubernetesService < ActiveRecord::Base + self.table_name = 'services' + + serialize :properties, JSON # rubocop:disable Cop/ActiveRecordSerialize + + default_value_for :active, true + default_value_for :type, 'KubernetesService' + default_value_for :properties, { + api_url: 'https://kubernetes.example.com', + token: 'a' * 40 + } + end + end context 'when cluster is being created' do let(:project_id) { project.id } diff --git a/spec/migrations/rename_users_with_renamed_namespace_spec.rb b/spec/migrations/rename_users_with_renamed_namespace_spec.rb index cbc0ebeb44d..e2994103ed7 100644 --- a/spec/migrations/rename_users_with_renamed_namespace_spec.rb +++ b/spec/migrations/rename_users_with_renamed_namespace_spec.rb @@ -7,9 +7,9 @@ describe RenameUsersWithRenamedNamespace, :delete do other_user1 = create(:user, username: 'api0') user = create(:user, username: "Users0") - user.update_attribute(:username, 'Users') + user.update_column(:username, 'Users') user1 = create(:user, username: "import0") - user1.update_attribute(:username, 'import') + user1.update_column(:username, 'import') described_class.new.up diff --git a/spec/migrations/schedule_build_stage_migration_spec.rb b/spec/migrations/reschedule_builds_stages_migration_spec.rb index e2ca35447fb..3bfd9dd9f6b 100644 --- a/spec/migrations/schedule_build_stage_migration_spec.rb +++ b/spec/migrations/reschedule_builds_stages_migration_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' -require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration') +require Rails.root.join('db', 'post_migrate', '20180405101928_reschedule_builds_stages_migration') -describe ScheduleBuildStageMigration, :sidekiq, :migration do +describe RescheduleBuildsStagesMigration, :sidekiq, :migration do + let(:namespaces) { table(:namespaces) } let(:projects) { table(:projects) } let(:pipelines) { table(:ci_pipelines) } let(:stages) { table(:ci_stages) } @@ -10,7 +11,8 @@ describe ScheduleBuildStageMigration, :sidekiq, :migration do before do stub_const("#{described_class}::BATCH_SIZE", 1) - projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce') + namespaces.create(id: 12, name: 'gitlab-org', path: 'gitlab-org') + projects.create!(id: 123, namespace_id: 12, name: 'gitlab', path: 'gitlab') pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a') stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test') diff --git a/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb new file mode 100644 index 00000000000..027f4a91c90 --- /dev/null +++ b/spec/migrations/schedule_set_confidential_note_events_on_webhooks_spec.rb @@ -0,0 +1,44 @@ +require 'spec_helper' +require Rails.root.join('db', 'post_migrate', '20180104131052_schedule_set_confidential_note_events_on_webhooks.rb') + +describe ScheduleSetConfidentialNoteEventsOnWebhooks, :migration, :sidekiq do + let(:web_hooks_table) { table(:web_hooks) } + let(:migration_class) { Gitlab::BackgroundMigration::SetConfidentialNoteEventsOnWebhooks } + let(:migration_name) { migration_class.to_s.demodulize } + + let!(:web_hook_1) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) } + let!(:web_hook_2) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) } + let!(:web_hook_migrated) { web_hooks_table.create!(confidential_note_events: true, note_events: true) } + let!(:web_hook_skip) { web_hooks_table.create!(confidential_note_events: nil, note_events: false) } + let!(:web_hook_new) { web_hooks_table.create!(confidential_note_events: false, note_events: true) } + let!(:web_hook_4) { web_hooks_table.create!(confidential_note_events: nil, note_events: true) } + + before do + stub_const("#{described_class}::BATCH_SIZE", 1) + end + + it 'schedules background migrations at correct time' do + Sidekiq::Testing.fake! do + Timecop.freeze do + migrate! + + expect(migration_name).to be_scheduled_delayed_migration(5.minutes, web_hook_1.id, web_hook_1.id) + expect(migration_name).to be_scheduled_delayed_migration(10.minutes, web_hook_2.id, web_hook_2.id) + expect(migration_name).to be_scheduled_delayed_migration(15.minutes, web_hook_4.id, web_hook_4.id) + expect(BackgroundMigrationWorker.jobs.size).to eq 3 + end + end + end + + it 'correctly processes web hooks' do + Sidekiq::Testing.inline! do + expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 4 + expect(web_hooks_table.where(confidential_note_events: true).count).to eq 1 + + migrate! + + expect(web_hooks_table.where(confidential_note_events: nil).count).to eq 1 + expect(web_hooks_table.where(confidential_note_events: true).count).to eq 4 + end + end +end diff --git a/spec/models/ci/build_metadata_spec.rb b/spec/models/ci/build_metadata_spec.rb index 268561ee941..7e75d5a5411 100644 --- a/spec/models/ci/build_metadata_spec.rb +++ b/spec/models/ci/build_metadata_spec.rb @@ -13,7 +13,7 @@ describe Ci::BuildMetadata do end let(:build) { create(:ci_build, pipeline: pipeline) } - let(:build_metadata) { create(:ci_build_metadata, build: build) } + let(:build_metadata) { build.metadata } describe '#update_timeout_state' do subject { build_metadata } diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb index 27c86e60e60..8847623f705 100644 --- a/spec/models/concerns/chronic_duration_attribute_spec.rb +++ b/spec/models/concerns/chronic_duration_attribute_spec.rb @@ -63,8 +63,8 @@ shared_examples 'ChronicDurationAttribute writer' do subject.send("#{virtual_field}=", '') end - it 'writes nil' do - expect(subject.send(source_field)).to be_nil + it 'writes default value' do + expect(subject.send(source_field)).to eq(default_value) end it 'passes validation' do @@ -77,8 +77,8 @@ shared_examples 'ChronicDurationAttribute writer' do subject.send("#{virtual_field}=", nil) end - it 'writes nil' do - expect(subject.send(source_field)).to be_nil + it 'writes default value' do + expect(subject.send(source_field)).to eq(default_value) end it 'passes validation' do @@ -92,20 +92,34 @@ shared_examples 'ChronicDurationAttribute writer' do end describe 'ChronicDurationAttribute' do - let(:source_field) {:maximum_timeout} - let(:virtual_field) {:maximum_timeout_human_readable} + context 'when default value is not set' do + let(:source_field) {:maximum_timeout} + let(:virtual_field) {:maximum_timeout_human_readable} + let(:default_value) { nil } - subject { Ci::Runner.new } + subject { create(:ci_runner) } - it_behaves_like 'ChronicDurationAttribute reader' - it_behaves_like 'ChronicDurationAttribute writer' + it_behaves_like 'ChronicDurationAttribute reader' + it_behaves_like 'ChronicDurationAttribute writer' + end + + context 'when default value is set' do + let(:source_field) {:build_timeout} + let(:virtual_field) {:build_timeout_human_readable} + let(:default_value) { 3600 } + + subject { create(:project) } + + it_behaves_like 'ChronicDurationAttribute reader' + it_behaves_like 'ChronicDurationAttribute writer' + end end describe 'ChronicDurationAttribute - reader' do let(:source_field) {:timeout} let(:virtual_field) {:timeout_human_readable} - subject {Ci::BuildMetadata.new} + subject { create(:ci_build).ensure_metadata } it "doesn't contain dynamically created writer method" do expect(subject.class).not_to be_public_method_defined("#{virtual_field}=") diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index c853f707e6d..86962cd8d61 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -191,6 +191,21 @@ describe Note do end end + describe "confidential?" do + it "delegates to noteable" do + issue_note = build(:note, :on_issue) + confidential_note = build(:note, noteable: create(:issue, confidential: true)) + + expect(issue_note.confidential?).to be_falsy + expect(confidential_note.confidential?).to be_truthy + end + + it "is falsey when noteable can't be confidential" do + commit_note = build(:note_on_commit) + expect(commit_note.confidential?).to be_falsy + end + end + describe "cross_reference_not_visible_for?" do let(:private_user) { create(:user) } let(:private_project) { create(:project, namespace: private_user.namespace) { |p| p.add_master(private_user) } } diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb index 3e2a166cdd6..0cd712e2f40 100644 --- a/spec/models/project_services/hipchat_service_spec.rb +++ b/spec/models/project_services/hipchat_service_spec.rb @@ -253,6 +253,21 @@ describe HipchatService do "<b>#{title}</b>" \ "<pre>issue <strong>note</strong></pre>") end + + context 'with confidential issue' do + before do + issue.update!(confidential: true) + end + + it 'calls Hipchat API with issue comment' do + data = Gitlab::DataBuilder::Note.build(issue_note, user) + hipchat.execute(data) + + message = hipchat.send(:create_message, data) + + expect(message).to include("<pre>issue <strong>note</strong></pre>") + end + end end context 'when snippet comment event triggered' do diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 0e560be9eaa..7007f78e702 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -2022,6 +2022,22 @@ describe Project do expect(forked_project.lfs_storage_project).to eq forked_project end end + + describe '#all_lfs_objects' do + let(:lfs_object) { create(:lfs_object) } + + before do + project.lfs_objects << lfs_object + end + + it 'returns the lfs object for a project' do + expect(project.all_lfs_objects).to contain_exactly(lfs_object) + end + + it 'returns the lfs object for a fork' do + expect(forked_project.all_lfs_objects).to contain_exactly(lfs_object) + end + end end describe '#pushes_since_gc' do @@ -3227,6 +3243,7 @@ describe Project do expect(project).to receive(:update_project_counter_caches) expect(project).to receive(:remove_import_jid) expect(project).to receive(:after_create_default_branch) + expect(project).to receive(:refresh_markdown_cache!) project.after_import end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 83ed3b203e6..28c908ea425 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -10,6 +10,22 @@ describe Service do it { is_expected.to validate_presence_of(:type) } end + describe 'Scopes' do + describe '.confidential_note_hooks' do + it 'includes services where confidential_note_events is true' do + create(:service, active: true, confidential_note_events: true) + + expect(described_class.confidential_note_hooks.count).to eq 1 + end + + it 'excludes services where confidential_note_events is false' do + create(:service, active: true, confidential_note_events: false) + + expect(described_class.confidential_note_hooks.count).to eq 0 + end + end + end + describe "Test Button" do describe '#can_test?' do let(:service) { create(:service, project: project) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 4027c420e47..a600987d0bf 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -2071,6 +2071,8 @@ describe User do expect(ghost).to be_ghost expect(ghost).to be_persisted + expect(ghost.namespace).not_to be_nil + expect(ghost.namespace).to be_persisted end it "does not create a second ghost user if one is already present" do diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb index ea76e604153..905d82b3bb1 100644 --- a/spec/policies/project_policy_spec.rb +++ b/spec/policies/project_policy_spec.rb @@ -11,10 +11,10 @@ describe ProjectPolicy do let(:base_guest_permissions) do %i[ - read_project read_board read_list read_wiki read_issue read_label - read_milestone read_project_snippet read_project_member - read_note create_project create_issue create_note - upload_file + read_project read_board read_list read_wiki read_issue + read_project_for_iids read_issue_iid read_merge_request_iid read_label + read_milestone read_project_snippet read_project_member read_note + create_project create_issue create_note upload_file ] end @@ -120,7 +120,7 @@ describe ProjectPolicy do project.issues_enabled = false project.save! - expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue end end @@ -131,7 +131,7 @@ describe ProjectPolicy do project.issues_enabled = false project.save! - expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue + expect_disallowed :read_issue, :read_issue_iid, :create_issue, :update_issue, :admin_issue end end end diff --git a/spec/presenters/ci/build_presenter_spec.rb b/spec/presenters/ci/build_presenter_spec.rb index 1a8001be6ab..cc16d0f156b 100644 --- a/spec/presenters/ci/build_presenter_spec.rb +++ b/spec/presenters/ci/build_presenter_spec.rb @@ -72,13 +72,44 @@ describe Ci::BuildPresenter do end end - context 'when build is not auto-canceled' do - before do - expect(build).to receive(:auto_canceled?).and_return(false) + context 'when build failed' do + let(:build) { create(:ci_build, :failed, pipeline: pipeline) } + + it 'returns the reason of failure' do + status_title = presenter.status_title + + expect(status_title).to eq('Failed <br> (unknown failure)') end + end + + context 'when build has failed && retried' do + let(:build) { create(:ci_build, :failed, :retried, pipeline: pipeline) } - it 'does not have a status title' do - expect(presenter.status_title).to be_nil + it 'does not include retried title' do + status_title = presenter.status_title + + expect(status_title).not_to include('(retried)') + expect(status_title).to eq('Failed <br> (unknown failure)') + end + end + + context 'when build has failed and is allowed to' do + let(:build) { create(:ci_build, :failed, :allowed_to_fail, pipeline: pipeline) } + + it 'returns the reason of failure' do + status_title = presenter.status_title + + expect(status_title).to eq('Failed <br> (unknown failure)') + end + end + + context 'For any other build' do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + + it 'returns the status' do + tooltip_description = presenter.status_title + + expect(tooltip_description).to eq('Success') end end end @@ -134,4 +165,56 @@ describe Ci::BuildPresenter do end end end + + describe '#tooltip_message' do + context 'When build has failed' do + let(:build) { create(:ci_build, :script_failure, pipeline: pipeline) } + + it 'returns the reason of failure' do + tooltip = subject.tooltip_message + + expect(tooltip).to eq("#{build.name} - failed <br> (script failure)") + end + end + + context 'When build has failed and retried' do + let(:build) { create(:ci_build, :script_failure, :retried, pipeline: pipeline) } + + it 'should include the reason of failure and the retried title' do + tooltip = subject.tooltip_message + + expect(tooltip).to eq("#{build.name} - failed <br> (script failure) (retried)") + end + end + + context 'When build has failed and is allowed to' do + let(:build) { create(:ci_build, :script_failure, :allowed_to_fail, pipeline: pipeline) } + + it 'should include the reason of failure' do + tooltip = subject.tooltip_message + + expect(tooltip).to eq("#{build.name} - failed <br> (script failure) (allowed to fail)") + end + end + + context 'For any other build (no retried)' do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + + it 'should include build name and status' do + tooltip = subject.tooltip_message + + expect(tooltip).to eq("#{build.name} - passed") + end + end + + context 'For any other build (retried)' do + let(:build) { create(:ci_build, :success, :retried, pipeline: pipeline) } + + it 'should include build name and status' do + tooltip = subject.tooltip_message + + expect(tooltip).to eq("#{build.name} - passed (retried)") + end + end + end end diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb index c6c10025f7f..92b614b087e 100644 --- a/spec/requests/api/boards_spec.rb +++ b/spec/requests/api/boards_spec.rb @@ -48,5 +48,36 @@ describe API::Boards do expect(json_response['label']['name']).to eq(group_label.title) expect(json_response['position']).to eq(3) end + + it 'creates a new board list for ancestor group labels' do + group = create(:group) + sub_group = create(:group, parent: group) + group_label = create(:group_label, group: group) + board_parent.update(group: sub_group) + group.add_developer(user) + sub_group.add_developer(user) + + post api(url, user), label_id: group_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(group_label.title) + end + end + + describe "POST /groups/:id/boards/lists", :nested_groups do + set(:group) { create(:group) } + set(:board_parent) { create(:group, parent: group ) } + let(:url) { "/groups/#{board_parent.id}/boards/#{board.id}/lists" } + set(:board) { create(:board, group: board_parent) } + + it 'creates a new board list for ancestor group labels' do + group.add_developer(user) + group_label = create(:group_label, group: group) + + post api(url, user), label_id: group_label.id + + expect(response).to have_gitlab_http_status(201) + expect(json_response['label']['name']).to eq(group_label.title) + end end end diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb index 6614e8cea43..90f9c4ad214 100644 --- a/spec/requests/api/issues_spec.rb +++ b/spec/requests/api/issues_spec.rb @@ -384,6 +384,30 @@ describe API::Issues do end let(:base_url) { "/groups/#{group.id}/issues" } + context 'when group has subgroups', :nested_groups do + let(:subgroup_1) { create(:group, parent: group) } + let(:subgroup_2) { create(:group, parent: subgroup_1) } + + let(:subgroup_1_project) { create(:project, namespace: subgroup_1) } + let(:subgroup_2_project) { create(:project, namespace: subgroup_2) } + + let!(:issue_1) { create(:issue, project: subgroup_1_project) } + let!(:issue_2) { create(:issue, project: subgroup_2_project) } + + before do + group.add_developer(user) + end + + it 'also returns subgroups projects issues' do + get api(base_url, user) + + issue_ids = json_response.map { |issue| issue['id'] } + + expect_paginated_array_response(size: 5) + expect(issue_ids).to include(issue_1.id, issue_2.id) + end + end + it 'returns all group issues (including opened and closed)' do get api(base_url, admin) diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb index 392cad667be..12a183fed1e 100644 --- a/spec/requests/api/project_hooks_spec.rb +++ b/spec/requests/api/project_hooks_spec.rb @@ -33,6 +33,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response.first['merge_requests_events']).to eq(true) expect(json_response.first['tag_push_events']).to eq(true) expect(json_response.first['note_events']).to eq(true) + expect(json_response.first['confidential_note_events']).to eq(true) expect(json_response.first['job_events']).to eq(true) expect(json_response.first['pipeline_events']).to eq(true) expect(json_response.first['wiki_page_events']).to eq(true) @@ -62,6 +63,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) @@ -104,6 +106,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(false) expect(json_response['tag_push_events']).to eq(false) expect(json_response['note_events']).to eq(false) + expect(json_response['confidential_note_events']).to eq(nil) expect(json_response['job_events']).to eq(true) expect(json_response['pipeline_events']).to eq(false) expect(json_response['wiki_page_events']).to eq(true) @@ -152,6 +155,7 @@ describe API::ProjectHooks, 'ProjectHooks' do expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events) expect(json_response['tag_push_events']).to eq(hook.tag_push_events) expect(json_response['note_events']).to eq(hook.note_events) + expect(json_response['confidential_note_events']).to eq(hook.confidential_note_events) expect(json_response['job_events']).to eq(hook.job_events) expect(json_response['pipeline_events']).to eq(hook.pipeline_events) expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events) diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb index 987f6e26971..5d13e6de741 100644 --- a/spec/requests/api/project_import_spec.rb +++ b/spec/requests/api/project_import_spec.rb @@ -40,7 +40,7 @@ describe API::ProjectImport do expect(response).to have_gitlab_http_status(201) end - it 'schedules an import at the user namespace level' do + it 'does not shedule an import for a nampespace that does not exist' do expect_any_instance_of(Project).not_to receive(:import_schedule) expect(::Projects::CreateService).not_to receive(:new) @@ -71,6 +71,49 @@ describe API::ProjectImport do expect(json_response['error']).to eq('file is invalid') end + it 'stores params that can be overridden' do + stub_import(namespace) + override_params = { 'description' => 'Hello world' } + + post api('/projects/import', user), + path: 'test-import', + file: fixture_file_upload(file), + namespace: namespace.id, + override_params: override_params + import_project = Project.find(json_response['id']) + + expect(import_project.import_data.data['override_params']).to eq(override_params) + end + + it 'does not store params that are not allowed' do + stub_import(namespace) + override_params = { 'not_allowed' => 'Hello world' } + + post api('/projects/import', user), + path: 'test-import', + file: fixture_file_upload(file), + namespace: namespace.id, + override_params: override_params + import_project = Project.find(json_response['id']) + + expect(import_project.import_data.data['override_params']).to be_empty + end + + it 'correctly overrides params during the import' do + override_params = { 'description' => 'Hello world' } + + Sidekiq::Testing.inline! do + post api('/projects/import', user), + path: 'test-import', + file: fixture_file_upload(file), + namespace: namespace.id, + override_params: override_params + end + import_project = Project.find(json_response['id']) + + expect(import_project.description).to eq('Hello world') + end + def stub_import(namespace) expect_any_instance_of(Project).to receive(:import_schedule) expect(::Projects::CreateService).to receive(:new).with(user, hash_including(namespace_id: namespace.id)).and_call_original diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb index 4f3420cc0ad..28d51ac86c6 100644 --- a/spec/requests/api/runner_spec.rb +++ b/spec/requests/api/runner_spec.rb @@ -950,12 +950,53 @@ describe API::Runner do describe 'POST /api/v4/jobs/:id/artifacts/authorize' do context 'when using token as parameter' do - it 'authorizes posting artifacts to running job' do - authorize_artifacts_with_token_in_params + context 'posting artifacts to running job' do + subject do + authorize_artifacts_with_token_in_params + end - expect(response).to have_gitlab_http_status(200) - expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) - expect(json_response['TempPath']).not_to be_nil + shared_examples 'authorizes local file' do + it 'succeeds' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to be_nil + end + end + + context 'when using local storage' do + it_behaves_like 'authorizes local file' + end + + context 'when using remote storage' do + context 'when direct upload is enabled' do + before do + stub_artifacts_object_storage(enabled: true, direct_upload: true) + end + + it 'succeeds' do + subject + + expect(response).to have_gitlab_http_status(200) + expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE) + expect(json_response['TempPath']).to eq(JobArtifactUploader.workhorse_local_upload_path) + expect(json_response['RemoteObject']).to have_key('ID') + expect(json_response['RemoteObject']).to have_key('GetURL') + expect(json_response['RemoteObject']).to have_key('StoreURL') + expect(json_response['RemoteObject']).to have_key('DeleteURL') + end + end + + context 'when direct upload is disabled' do + before do + stub_artifacts_object_storage(enabled: true, direct_upload: false) + end + + it_behaves_like 'authorizes local file' + end + end end it 'fails to post too large artifact' do @@ -1051,20 +1092,45 @@ describe API::Runner do end end - context 'when uses regular file post' do - before do - upload_artifacts(file_upload, headers_with_token, false) + context 'when uses accelerated file post' do + context 'for file stored locally' do + before do + upload_artifacts(file_upload, headers_with_token) + end + + it_behaves_like 'successful artifacts upload' end - it_behaves_like 'successful artifacts upload' - end + context 'for file stored remotelly' do + let!(:fog_connection) do + stub_artifacts_object_storage(direct_upload: true) + end - context 'when uses accelerated file post' do - before do - upload_artifacts(file_upload, headers_with_token, true) - end + before do + fog_connection.directories.get('artifacts').files.create( + key: 'tmp/upload/12312300', + body: 'content' + ) - it_behaves_like 'successful artifacts upload' + upload_artifacts(file_upload, headers_with_token, + { 'file.remote_id' => remote_id }) + end + + context 'when valid remote_id is used' do + let(:remote_id) { '12312300' } + + it_behaves_like 'successful artifacts upload' + end + + context 'when invalid remote_id is used' do + let(:remote_id) { 'invalid id' } + + it 'responds with bad request' do + expect(response).to have_gitlab_http_status(500) + expect(json_response['message']).to eq("Missing file") + end + end + end end context 'when using runners token' do @@ -1208,15 +1274,19 @@ describe API::Runner do end context 'when artifacts are being stored outside of tmp path' do + let(:new_tmpdir) { Dir.mktmpdir } + before do + # init before overwriting tmp dir + file_upload + # by configuring this path we allow to pass file from @tmpdir only # but all temporary files are stored in system tmp directory - @tmpdir = Dir.mktmpdir - allow(JobArtifactUploader).to receive(:workhorse_upload_path).and_return(@tmpdir) + allow(Dir).to receive(:tmpdir).and_return(new_tmpdir) end after do - FileUtils.remove_entry @tmpdir + FileUtils.remove_entry(new_tmpdir) end it' "fails to post artifacts for outside of tmp path"' do @@ -1226,12 +1296,11 @@ describe API::Runner do end end - def upload_artifacts(file, headers = {}, accelerated = true) - params = if accelerated - { 'file.path' => file.path, 'file.name' => file.original_filename } - else - { 'file' => file } - end + def upload_artifacts(file, headers = {}, params = {}) + params = params.merge({ + 'file.path' => file.path, + 'file.name' => file.original_filename + }) post api("/jobs/#{job.id}/artifacts"), params, headers end diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb index 1e6bd993c08..f80abb06fca 100644 --- a/spec/requests/lfs_http_spec.rb +++ b/spec/requests/lfs_http_spec.rb @@ -1016,7 +1016,7 @@ describe 'Git LFS API and storage' do it_behaves_like 'a valid response' do it 'responds with status 200, location of lfs remote store and object details' do - expect(json_response['TempPath']).to be_nil + expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) expect(json_response['RemoteObject']).to have_key('ID') expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to have_key('StoreURL') @@ -1073,7 +1073,9 @@ describe 'Git LFS API and storage' do ['123123', '../../123123'].each do |remote_id| context "with invalid remote_id: #{remote_id}" do subject do - put_finalize_with_args('file.remote_id' => remote_id) + put_finalize(with_tempfile: true, args: { + 'file.remote_id' => remote_id + }) end it 'responds with status 403' do @@ -1093,9 +1095,10 @@ describe 'Git LFS API and storage' do end subject do - put_finalize_with_args( + put_finalize(with_tempfile: true, args: { 'file.remote_id' => '12312300', - 'file.name' => 'name') + 'file.name' => 'name' + }) end it 'responds with status 200' do @@ -1331,7 +1334,7 @@ describe 'Git LFS API and storage' do put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers end - def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false) + def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, args: {}) upload_path = LfsObjectUploader.workhorse_local_upload_path file_path = upload_path + '/' + lfs_tmp if lfs_tmp @@ -1340,12 +1343,12 @@ describe 'Git LFS API and storage' do FileUtils.touch(file_path) end - args = { + extra_args = { 'file.path' => file_path, 'file.name' => File.basename(file_path) - }.compact + } - put_finalize_with_args(args) + put_finalize_with_args(args.merge(extra_args).compact) end def put_finalize_with_args(args) diff --git a/spec/serializers/build_serializer_spec.rb b/spec/serializers/build_serializer_spec.rb index 9673b11c2a2..98cd15e248b 100644 --- a/spec/serializers/build_serializer_spec.rb +++ b/spec/serializers/build_serializer_spec.rb @@ -28,15 +28,31 @@ describe BuildSerializer do end describe '#represent_status' do - context 'when represents only status' do - let(:resource) { create(:ci_build) } + context 'for a failed build' do + let(:resource) { create(:ci_build, :failed) } + let(:status) { resource.detailed_status(double('user')) } + + subject { serializer.represent_status(resource) } + + it 'serializes only status' do + expect(subject[:text]).to eq(status.text) + expect(subject[:label]).to eq('failed') + expect(subject[:tooltip]).to eq('failed <br> (unknown failure)') + expect(subject[:icon]).to eq(status.icon) + expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") + end + end + + context 'for any other type of build' do + let(:resource) { create(:ci_build, :success) } let(:status) { resource.detailed_status(double('user')) } subject { serializer.represent_status(resource) } it 'serializes only status' do expect(subject[:text]).to eq(status.text) - expect(subject[:label]).to eq(status.label) + expect(subject[:label]).to eq('passed') + expect(subject[:tooltip]).to eq('passed') expect(subject[:icon]).to eq(status.icon) expect(subject[:favicon]).to match_asset_path("/assets/ci_favicons/#{status.favicon}.ico") end diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb index 026360e91a3..24a6f1a2a8a 100644 --- a/spec/serializers/job_entity_spec.rb +++ b/spec/serializers/job_entity_spec.rb @@ -38,7 +38,7 @@ describe JobEntity do it 'contains details' do expect(subject).to include :status - expect(subject[:status]).to include :icon, :favicon, :text, :label + expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip end context 'when job is retryable' do @@ -126,7 +126,29 @@ describe JobEntity do it 'contains details' do expect(subject).to include :status - expect(subject[:status]).to include :icon, :favicon, :text, :label + expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip + end + end + + context 'when job failed' do + let(:job) { create(:ci_build, :script_failure) } + + describe 'status' do + it 'should contain the failure reason inside label' do + expect(subject[:status]).to include :icon, :favicon, :text, :label, :tooltip + expect(subject[:status][:label]).to eq('failed') + expect(subject[:status][:tooltip]).to eq('failed <br> (script failure)') + end + end + end + + context 'when job passed' do + let(:job) { create(:ci_build, :success) } + + describe 'status' do + it 'should not contain the failure reason inside label' do + expect(subject[:status][:label]).to eq('passed') + end end end end diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb index 248552d1858..2473c561f4b 100644 --- a/spec/serializers/pipeline_entity_spec.rb +++ b/spec/serializers/pipeline_entity_spec.rb @@ -30,7 +30,7 @@ describe PipelineEntity do expect(subject).to include :details expect(subject[:details]) .to include :duration, :finished_at - expect(subject[:details][:status]).to include :icon, :favicon, :text, :label + expect(subject[:details][:status]).to include :icon, :favicon, :text, :label, :tooltip end it 'contains flags' do diff --git a/spec/serializers/stage_entity_spec.rb b/spec/serializers/stage_entity_spec.rb index 40e303f7b89..2034c7891ef 100644 --- a/spec/serializers/stage_entity_spec.rb +++ b/spec/serializers/stage_entity_spec.rb @@ -26,7 +26,7 @@ describe StageEntity do end it 'contains detailed status' do - expect(subject[:status]).to include :text, :label, :group, :icon + expect(subject[:status]).to include :text, :label, :group, :icon, :tooltip expect(subject[:status][:label]).to eq 'passed' end diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb index 70402bac2e2..559475e571c 100644 --- a/spec/serializers/status_entity_spec.rb +++ b/spec/serializers/status_entity_spec.rb @@ -16,7 +16,7 @@ describe StatusEntity do subject { entity.as_json } it 'contains status details' do - expect(subject).to include :text, :icon, :favicon, :label, :group + expect(subject).to include :text, :icon, :favicon, :label, :group, :tooltip expect(subject).to include :has_details, :details_path expect(subject[:favicon]).to match_asset_path('/assets/ci_favicons/favicon_status_success.ico') end diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 0a6b6d880d3..dd0ad5f11bd 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -48,7 +48,7 @@ describe Boards::Issues::MoveService do parent.add_developer(user) end - it_behaves_like 'issues move service' + it_behaves_like 'issues move service', true end end end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 97a563c1ce1..aa7cc268dd7 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -370,10 +370,89 @@ module Ci it_behaves_like 'validation is not active' end end + end - def execute(runner) - described_class.new(runner).execute.build + describe '#register_success' do + let!(:current_time) { Time.new(2018, 4, 5, 14, 0, 0) } + let!(:attempt_counter) { double('Gitlab::Metrics::NullMetric') } + let!(:job_queue_duration_seconds) { double('Gitlab::Metrics::NullMetric') } + + before do + allow(Time).to receive(:now).and_return(current_time) + + # Stub defaults for any metrics other than the ones we're testing + allow(Gitlab::Metrics).to receive(:counter) + .with(any_args) + .and_return(Gitlab::Metrics::NullMetric.instance) + allow(Gitlab::Metrics).to receive(:histogram) + .with(any_args) + .and_return(Gitlab::Metrics::NullMetric.instance) + + # Stub tested metrics + allow(Gitlab::Metrics).to receive(:counter) + .with(:job_register_attempts_total, anything) + .and_return(attempt_counter) + allow(Gitlab::Metrics).to receive(:histogram) + .with(:job_queue_duration_seconds, anything, anything, anything) + .and_return(job_queue_duration_seconds) + + project.update(shared_runners_enabled: true) + pending_job.update(created_at: current_time - 3600, queued_at: current_time - 1800) end + + shared_examples 'metrics collector' do + it 'increments attempt counter' do + allow(job_queue_duration_seconds).to receive(:observe) + expect(attempt_counter).to receive(:increment) + + execute(runner) + end + + it 'counts job queuing time histogram with expected labels' do + allow(attempt_counter).to receive(:increment) + expect(job_queue_duration_seconds).to receive(:observe) + .with({ shared_runner: expected_shared_runner, + jobs_running_for_project: expected_jobs_running_for_project_first_job }, 1800) + + execute(runner) + end + + context 'when project already has running jobs' do + let!(:build2) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } + let!(:build3) { create( :ci_build, :running, pipeline: pipeline, runner: shared_runner) } + + it 'counts job queuing time histogram with expected labels' do + allow(attempt_counter).to receive(:increment) + expect(job_queue_duration_seconds).to receive(:observe) + .with({ shared_runner: expected_shared_runner, + jobs_running_for_project: expected_jobs_running_for_project_third_job }, 1800) + + execute(runner) + end + end + end + + context 'when shared runner is used' do + let(:runner) { shared_runner } + let(:expected_shared_runner) { true } + let(:expected_jobs_running_for_project_first_job) { 0 } + let(:expected_jobs_running_for_project_third_job) { 2 } + + it_behaves_like 'metrics collector' + end + + context 'when specific runner is used' do + let(:runner) { specific_runner } + let(:expected_shared_runner) { false } + let(:expected_jobs_running_for_project_first_job) { '+Inf' } + let(:expected_jobs_running_for_project_third_job) { '+Inf' } + + it_behaves_like 'metrics collector' + end + end + + def execute(runner) + described_class.new(runner).execute.build end end end diff --git a/spec/services/issuable/destroy_service_spec.rb b/spec/services/issuable/destroy_service_spec.rb index 0a3647a814f..8ccbba7fa58 100644 --- a/spec/services/issuable/destroy_service_spec.rb +++ b/spec/services/issuable/destroy_service_spec.rb @@ -8,7 +8,7 @@ describe Issuable::DestroyService do describe '#execute' do context 'when issuable is an issue' do - let!(:issue) { create(:issue, project: project, author: user) } + let!(:issue) { create(:issue, project: project, author: user, assignees: [user]) } it 'destroys the issue' do expect { service.execute(issue) }.to change { project.issues.count }.by(-1) @@ -26,10 +26,15 @@ describe Issuable::DestroyService do expect { service.execute(issue) } .to change { user.todos_pending_count }.from(1).to(0) end + + it 'invalidates the issues count cache for the assignees' do + expect_any_instance_of(User).to receive(:invalidate_cache_counts).once + service.execute(issue) + end end context 'when issuable is a merge request' do - let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user) } + let!(:merge_request) { create(:merge_request, target_project: project, source_project: project, author: user, assignee: user) } it 'destroys the merge request' do expect { service.execute(merge_request) }.to change { project.merge_requests.count }.by(-1) @@ -41,6 +46,11 @@ describe Issuable::DestroyService do service.execute(merge_request) end + it 'invalidates the merge request caches for the MR assignee' do + expect_any_instance_of(User).to receive(:invalidate_cache_counts).once + service.execute(merge_request) + end + it 'updates the todo caches for users with todos on the merge request' do create(:todo, target: merge_request, user: user, author: user, project: project) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 41237dd7160..f95474208f3 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -97,6 +97,37 @@ describe Issues::UpdateService, :mailer do expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) end + context 'when moving issue between issues from different projects' do + let(:group) { create(:group) } + let(:project_1) { create(:project, namespace: group) } + let(:project_2) { create(:project, namespace: group) } + let(:project_3) { create(:project, namespace: group) } + + let(:issue_1) { create(:issue, project: project_1) } + let(:issue_2) { create(:issue, project: project_2) } + let(:issue_3) { create(:issue, project: project_3) } + + before do + group.add_developer(user) + end + + it 'sorts issues as specified by parameters' do + # Moving all issues to end here like the last example won't work since + # all projects only have the same issue count + # so their relative_position will be the same. + issue_1.move_to_end + issue_2.move_after(issue_1) + issue_3.move_after(issue_2) + [issue_1, issue_2, issue_3].map(&:save) + + opts[:move_between_ids] = [issue_1.id, issue_2.id] + opts[:board_group_id] = group.id + + described_class.new(issue_3.project, user, opts).execute(issue_3) + expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position) + end + end + context 'when current user cannot admin issues in the project' do let(:guest) { create(:user) } before do diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index 6ef5e93cb20..4e2ab919f0f 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -23,5 +23,23 @@ describe Notes::PostProcessService do described_class.new(@note).execute end + + context 'with a confidential issue' do + let(:issue) { create(:issue, :confidential, project: project) } + + it "doesn't call note hooks/services" do + expect(project).not_to receive(:execute_hooks).with(anything, :note_hooks) + expect(project).not_to receive(:execute_services).with(anything, :note_hooks) + + described_class.new(@note).execute + end + + it "calls confidential-note hooks/services" do + expect(project).to receive(:execute_hooks).with(anything, :confidential_note_hooks) + expect(project).to receive(:execute_services).with(anything, :confidential_note_hooks) + + described_class.new(@note).execute + end + end end end diff --git a/spec/services/projects/gitlab_projects_import_service_spec.rb b/spec/services/projects/gitlab_projects_import_service_spec.rb index 6b8f9619bc4..880b2aae66a 100644 --- a/spec/services/projects/gitlab_projects_import_service_spec.rb +++ b/spec/services/projects/gitlab_projects_import_service_spec.rb @@ -2,8 +2,10 @@ require 'spec_helper' describe Projects::GitlabProjectsImportService do set(:namespace) { create(:namespace) } + let(:path) { 'test-path' } let(:file) { fixture_file_upload(Rails.root + 'spec/fixtures/doc_sample.txt', 'text/plain') } - subject { described_class.new(namespace.owner, { namespace_id: namespace.id, path: path, file: file }) } + let(:import_params) { { namespace_id: namespace.id, path: path, file: file } } + subject { described_class.new(namespace.owner, import_params) } describe '#execute' do context 'with an invalid path' do @@ -18,8 +20,6 @@ describe Projects::GitlabProjectsImportService do end context 'with a valid path' do - let(:path) { 'test-path' } - it 'creates a project' do project = subject.execute @@ -27,5 +27,15 @@ describe Projects::GitlabProjectsImportService do expect(project).to be_valid end end + + context 'override params' do + it 'stores them as import data when passed' do + project = described_class + .new(namespace.owner, import_params, description: 'Hello') + .execute + + expect(project.import_data.data['override_params']['description']).to eq('Hello') + end + end end end diff --git a/spec/services/projects/import_export/export_service_spec.rb b/spec/services/projects/import_export/export_service_spec.rb index 51491c7d529..f9e5530bc9d 100644 --- a/spec/services/projects/import_export/export_service_spec.rb +++ b/spec/services/projects/import_export/export_service_spec.rb @@ -8,6 +8,49 @@ describe Projects::ImportExport::ExportService do let(:service) { described_class.new(project, user) } let!(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } + it 'saves the version' do + expect(Gitlab::ImportExport::VersionSaver).to receive(:new).and_call_original + + service.execute + end + + it 'saves the avatar' do + expect(Gitlab::ImportExport::AvatarSaver).to receive(:new).and_call_original + + service.execute + end + + it 'saves the models' do + expect(Gitlab::ImportExport::ProjectTreeSaver).to receive(:new).and_call_original + + service.execute + end + + it 'saves the uploads' do + expect(Gitlab::ImportExport::UploadsSaver).to receive(:new).and_call_original + + service.execute + end + + it 'saves the repo' do + # once for the normal repo, once for the wiki + expect(Gitlab::ImportExport::RepoSaver).to receive(:new).twice.and_call_original + + service.execute + end + + it 'saves the lfs objects' do + expect(Gitlab::ImportExport::LfsSaver).to receive(:new).and_call_original + + service.execute + end + + it 'saves the wiki repo' do + expect(Gitlab::ImportExport::WikiRepoSaver).to receive(:new).and_call_original + + service.execute + end + context 'when all saver services succeed' do before do allow(service).to receive(:save_services).and_return(true) diff --git a/spec/support/commit_trailers_spec_helper.rb b/spec/support/commit_trailers_spec_helper.rb new file mode 100644 index 00000000000..add359946db --- /dev/null +++ b/spec/support/commit_trailers_spec_helper.rb @@ -0,0 +1,41 @@ +module CommitTrailersSpecHelper + extend ActiveSupport::Concern + + def expect_to_have_user_link_with_avatar(doc, user:, trailer:, email: nil) + wrapper = find_user_wrapper(doc, trailer) + + expect_to_have_links_with_url_and_avatar(wrapper, urls.user_url(user), email || user.email) + expect(wrapper.attribute('data-user').value).to eq user.id.to_s + end + + def expect_to_have_mailto_link(doc, email:, trailer:) + wrapper = find_user_wrapper(doc, trailer) + + expect_to_have_links_with_url_and_avatar(wrapper, "mailto:#{CGI.escape_html(email)}", email) + end + + def expect_to_have_links_with_url_and_avatar(doc, url, email) + expect(doc).not_to be_nil + expect(doc.xpath("a[position()<3 and @href='#{url}']").size).to eq 2 + expect(doc.xpath("a[position()=3 and @href='mailto:#{CGI.escape_html(email)}']").size).to eq 1 + expect(doc.css('img').size).to eq 1 + end + + def find_user_wrapper(doc, trailer) + doc.xpath("descendant-or-self::node()[@data-trailer='#{trailer}']").first + end + + def build_commit_message(trailer:, name:, email:) + message = trailer_line(trailer, name, email) + + [message, commit_html(message)] + end + + def trailer_line(trailer, name, email) + "#{trailer} #{name} <#{email}>" + end + + def commit_html(message) + "<pre>#{CGI.escape_html(message)}</pre>" + end +end diff --git a/spec/support/filtered_search_helpers.rb b/spec/support/filtered_search_helpers.rb index f3f96bd1f0a..5f42ff77fb2 100644 --- a/spec/support/filtered_search_helpers.rb +++ b/spec/support/filtered_search_helpers.rb @@ -21,6 +21,29 @@ module FilteredSearchHelpers end end + # Select a label clicking in the search dropdown instead + # of entering label names on the input. + def select_label_on_dropdown(label_title) + input_filtered_search("label:", submit: false) + + within('#js-dropdown-label') do + wait_for_requests + + find('li', text: label_title).click + end + + filtered_search.send_keys(:enter) + end + + def expect_issues_list_count(open_count, closed_count = 0) + all_count = open_count + closed_count + + expect(page).to have_issuable_counts(open: open_count, closed: closed_count, all: all_count) + page.within '.issues-list' do + expect(page).to have_selector('.issue', count: open_count) + end + end + # Enables input to be added character by character def input_filtered_search_keys(search_term) # Add an extra space to engage visual tokens diff --git a/spec/support/reference_parser_helpers.rb b/spec/support/reference_parser_helpers.rb index 01689194eac..5d5e80851e6 100644 --- a/spec/support/reference_parser_helpers.rb +++ b/spec/support/reference_parser_helpers.rb @@ -2,4 +2,34 @@ module ReferenceParserHelpers def empty_html_link Nokogiri::HTML.fragment('<a></a>').children[0] end + + shared_examples 'no N+1 queries' do + it 'avoids N+1 queries in #nodes_visible_to_user', :request_store do + record_queries = lambda do |links| + ActiveRecord::QueryRecorder.new do + described_class.new(project, user).nodes_visible_to_user(user, links) + end + end + + control = record_queries.call(control_links) + actual = record_queries.call(actual_links) + + expect(actual.count).to be <= control.count + expect(actual.cached_count).to be <= control.cached_count + end + + it 'avoids N+1 queries in #records_for_nodes', :request_store do + record_queries = lambda do |links| + ActiveRecord::QueryRecorder.new do + described_class.new(project, user).records_for_nodes(links) + end + end + + control = record_queries.call(control_links) + actual = record_queries.call(actual_links) + + expect(actual.count).to be <= control.count + expect(actual.cached_count).to be <= control.cached_count + end + end end diff --git a/spec/support/shared_examples/services/boards/issues_move_service.rb b/spec/support/shared_examples/services/boards/issues_move_service.rb index 4a4fbaa3a0e..737863ea411 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service.rb @@ -1,4 +1,4 @@ -shared_examples 'issues move service' do +shared_examples 'issues move service' do |group| context 'when moving an issue between lists' do let(:issue) { create(:labeled_issue, project: project, labels: [bug, development]) } let(:params) { { board_id: board1.id, from_list_id: list1.id, to_list_id: list2.id } } @@ -83,5 +83,18 @@ shared_examples 'issues move service' do expect(issue.relative_position).to be_between(issue1.relative_position, issue2.relative_position) end + + if group + context 'when on a group board' do + it 'sends the board_group_id parameter' do + params.merge!(move_after_id: issue1.id, move_before_id: issue2.id) + + match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id } + expect(Issues::UpdateService).to receive(:new).with(issue.project, user, match_params).and_return(double(execute: build(:issue))) + + described_class.new(parent, user, params).execute(issue) + end + end + end end end diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb index 5e1ce19eafb..07bc3a51fd8 100644 --- a/spec/support/slack_mattermost_notifications_shared_examples.rb +++ b/spec/support/slack_mattermost_notifications_shared_examples.rb @@ -4,6 +4,11 @@ RSpec.shared_examples 'slack or mattermost notifications' do let(:chat_service) { described_class.new } let(:webhook_url) { 'https://example.gitlab.com/' } + def execute_with_options(options) + receive(:new).with(webhook_url, options) + .and_return(double(:slack_service).as_null_object) + end + describe "Associations" do it { is_expected.to belong_to :project } it { is_expected.to have_one :service_hook } @@ -33,6 +38,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do let(:project) { create(:project, :repository) } let(:username) { 'slack_username' } let(:channel) { 'slack_channel' } + let(:issue_service_options) { { title: 'Awesome issue', description: 'please fix' } } let(:push_sample_data) do Gitlab::DataBuilder::Push.build_sample(project, user) @@ -48,12 +54,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do WebMock.stub_request(:post, webhook_url) - opts = { - title: 'Awesome issue', - description: 'please fix' - } - - issue_service = Issues::CreateService.new(project, user, opts) + issue_service = Issues::CreateService.new(project, user, issue_service_options) @issue = issue_service.execute @issues_sample_data = issue_service.hook_data(@issue, 'open') @@ -164,6 +165,26 @@ RSpec.shared_examples 'slack or mattermost notifications' do chat_service.execute(@issues_sample_data) end + context 'for confidential issues' do + let(:issue_service_options) { { title: 'Secret', confidential: true } } + + it "uses confidential issue channel" do + chat_service.update_attributes(confidential_issue_channel: 'confidential') + + expect(Slack::Notifier).to execute_with_options(channel: 'confidential') + + chat_service.execute(@issues_sample_data) + end + + it 'falls back to issue channel' do + chat_service.update_attributes(issue_channel: 'fallback_channel') + + expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel') + + chat_service.execute(@issues_sample_data) + end + end + it "uses the right channel for wiki event" do chat_service.update_attributes(wiki_page_channel: "random") @@ -194,6 +215,32 @@ RSpec.shared_examples 'slack or mattermost notifications' do chat_service.execute(note_data) end + + context 'for confidential notes' do + before do + issue_note.noteable.update!(confidential: true) + end + + it "uses confidential channel" do + chat_service.update_attributes(confidential_note_channel: "confidential") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to execute_with_options(channel: 'confidential') + + chat_service.execute(note_data) + end + + it 'falls back to note channel' do + chat_service.update_attributes(note_channel: "fallback_channel") + + note_data = Gitlab::DataBuilder::Note.build(issue_note, user) + + expect(Slack::Notifier).to execute_with_options(channel: 'fallback_channel') + + chat_service.execute(note_data) + end + end end end end @@ -248,8 +295,9 @@ RSpec.shared_examples 'slack or mattermost notifications' do create(:note_on_issue, project: project, note: "issue note") end + let(:data) { Gitlab::DataBuilder::Note.build(issue_note, user) } + it "calls Slack API for issue comment events" do - data = Gitlab::DataBuilder::Note.build(issue_note, user) chat_service.execute(data) expect(WebMock).to have_requested(:post, webhook_url).once diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb index bad1d34df3a..a75a3eaefcb 100644 --- a/spec/support/stub_configuration.rb +++ b/spec/support/stub_configuration.rb @@ -45,6 +45,10 @@ module StubConfiguration allow(Gitlab.config.lfs).to receive_messages(to_settings(messages)) end + def stub_artifacts_setting(messages) + allow(Gitlab.config.artifacts).to receive_messages(to_settings(messages)) + end + def stub_storage_settings(messages) messages.deep_stringify_keys! diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb index 59e02fecbce..16455e2517b 100644 --- a/spec/uploaders/object_storage_spec.rb +++ b/spec/uploaders/object_storage_spec.rb @@ -62,10 +62,12 @@ describe ObjectStorage do end describe '#object_store' do + subject { uploader.object_store } + it "delegates to <mount>_store on model" do expect(object).to receive(:file_store) - uploader.object_store + subject end context 'when store is null' do @@ -73,8 +75,36 @@ describe ObjectStorage do expect(object).to receive(:file_store).and_return(nil) end - it "returns Store::LOCAL" do - expect(uploader.object_store).to eq(described_class::Store::LOCAL) + context 'when object storage is enabled' do + context 'when direct uploads are enabled' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: true) + end + + it "uses Store::REMOTE" do + is_expected.to eq(described_class::Store::REMOTE) + end + end + + context 'when direct uploads are disabled' do + before do + stub_uploads_object_storage(uploader_class, enabled: true, direct_upload: false) + end + + it "uses Store::LOCAL" do + is_expected.to eq(described_class::Store::LOCAL) + end + end + end + + context 'when object storage is disabled' do + before do + stub_uploads_object_storage(uploader_class, enabled: false) + end + + it "uses Store::LOCAL" do + is_expected.to eq(described_class::Store::LOCAL) + end end end @@ -84,7 +114,7 @@ describe ObjectStorage do end it "returns the given value" do - expect(uploader.object_store).to eq(described_class::Store::REMOTE) + is_expected.to eq(described_class::Store::REMOTE) end end end @@ -486,108 +516,46 @@ describe ObjectStorage do end end - describe '#store_workhorse_file!' do + describe '#cache!' do subject do - uploader.store_workhorse_file!(params, :file) + uploader.cache!(uploaded_file) end context 'when local file is used' do context 'when valid file is used' do - let(:target_path) do - File.join(uploader_class.root, uploader_class::TMP_UPLOAD_PATH) - end - - before do - FileUtils.mkdir_p(target_path) + let(:uploaded_file) do + fixture_file_upload(Rails.root + 'spec/fixtures/rails_sample.jpg', 'image/jpg') end - context 'when no filename is specified' do - let(:params) do - { "file.path" => "test/file" } - end - - it 'raises an error' do - expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/) - end - end - - context 'when invalid file is specified' do - let(:file_path) do - File.join(target_path, "..", "test.file") - end - - before do - FileUtils.touch(file_path) - end - - let(:params) do - { "file.path" => file_path, - "file.name" => "my_file.txt" } - end - - it 'raises an error' do - expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file path/) - end - end - - context 'when filename is specified' do - let(:params) do - { "file.path" => tmp_file, - "file.name" => "my_file.txt" } - end - - let(:tmp_file) { Tempfile.new('filename', target_path) } + it "properly caches the file" do + subject - before do - FileUtils.touch(tmp_file) - end - - after do - FileUtils.rm_f(tmp_file) - end - - it 'succeeds' do - expect { subject }.not_to raise_error - - expect(uploader).to be_exists - end - - it 'proper path is being used' do - subject - - expect(uploader.path).to start_with(uploader_class.root) - expect(uploader.path).to end_with("my_file.txt") - end - - it 'source file to not exist' do - subject - - expect(File.exist?(tmp_file.path)).to be_falsey - end + expect(uploader).to be_exists + expect(uploader.path).to start_with(uploader_class.root) + expect(uploader.filename).to eq('rails_sample.jpg') end end end context 'when remote file is used' do + let(:temp_file) { Tempfile.new("test") } + let!(:fog_connection) do stub_uploads_object_storage(uploader_class) end - context 'when valid file is used' do - context 'when no filename is specified' do - let(:params) do - { "file.remote_id" => "test/123123" } - end + before do + FileUtils.touch(temp_file) + end - it 'raises an error' do - expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Missing filename/) - end - end + after do + FileUtils.rm_f(temp_file) + end + context 'when valid file is used' do context 'when invalid file is specified' do - let(:params) do - { "file.remote_id" => "../test/123123", - "file.name" => "my_file.txt" } + let(:uploaded_file) do + UploadedFile.new(temp_file.path, remote_id: "../test/123123") end it 'raises an error' do @@ -596,9 +564,8 @@ describe ObjectStorage do end context 'when non existing file is specified' do - let(:params) do - { "file.remote_id" => "test/12312300", - "file.name" => "my_file.txt" } + let(:uploaded_file) do + UploadedFile.new(temp_file.path, remote_id: "test/123123") end it 'raises an error' do @@ -606,10 +573,9 @@ describe ObjectStorage do end end - context 'when filename is specified' do - let(:params) do - { "file.remote_id" => "test/123123", - "file.name" => "my_file.txt" } + context 'when valid file is specified' do + let(:uploaded_file) do + UploadedFile.new(temp_file.path, filename: "my_file.txt", remote_id: "test/123123") end let!(:fog_file) do @@ -619,36 +585,37 @@ describe ObjectStorage do ) end - it 'succeeds' do + it 'file to be cached and remote stored' do expect { subject }.not_to raise_error expect(uploader).to be_exists - end - - it 'path to not be temporary' do - subject - + expect(uploader).to be_cached expect(uploader.path).not_to be_nil - expect(uploader.path).not_to include('tmp/upload') - expect(uploader.url).to include('/my_file.txt') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.url).not_to be_nil + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.object_store).to eq(described_class::Store::REMOTE) end - it 'url is used' do - subject + context 'when file is stored' do + subject do + uploader.store!(uploaded_file) + end - expect(uploader.url).not_to be_nil - expect(uploader.url).to include('/my_file.txt') + it 'file to be remotely stored in permament location' do + subject + + expect(uploader).to be_exists + expect(uploader).not_to be_cached + expect(uploader.path).not_to be_nil + expect(uploader.path).not_to include('tmp/upload') + expect(uploader.path).not_to include('tmp/cache') + expect(uploader.url).to include('/my_file.txt') + expect(uploader.object_store).to eq(described_class::Store::REMOTE) + end end end end end - - context 'when no file is used' do - let(:params) { {} } - - it 'raises an error' do - expect { subject }.to raise_error(uploader_class::RemoteStoreError, /Bad file/) - end - end 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 64d6ae6d2a3..c93152b88e3 100644 --- a/spec/views/projects/jobs/show.html.haml_spec.rb +++ b/spec/views/projects/jobs/show.html.haml_spec.rb @@ -1,8 +1,10 @@ require 'spec_helper' describe 'projects/jobs/show' do + let(:user) { create(:user) } let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } + let(:builds) { project.builds.present(current_user: user) } let(:pipeline) do create(:ci_pipeline, project: project, sha: project.commit.id) @@ -11,6 +13,7 @@ describe 'projects/jobs/show' do before do assign(:build, build.present) assign(:project, project) + assign(:builds, builds) allow(view).to receive(:can?).and_return(true) end diff --git a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb b/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb index 7b300150874..be9a4d9c57c 100644 --- a/spec/views/projects/pipelines_settings/_show.html.haml_spec.rb +++ b/spec/views/projects/settings/ci_cd/_form.html.haml_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe 'projects/pipelines_settings/_show' do +describe 'projects/settings/ci_cd/_form' do let(:project) { create(:project, :repository) } before do |