diff options
Diffstat (limited to 'spec/services')
149 files changed, 5683 insertions, 1431 deletions
diff --git a/spec/services/access_token_validation_service_spec.rb b/spec/services/access_token_validation_service_spec.rb index 87f093ee8ce..38a3f522504 100644 --- a/spec/services/access_token_validation_service_spec.rb +++ b/spec/services/access_token_validation_service_spec.rb @@ -1,41 +1,72 @@ require 'spec_helper' -describe AccessTokenValidationService, services: true do +describe AccessTokenValidationService do describe ".include_any_scope?" do + let(:request) { double("request") } + it "returns true if the required scope is present in the token's scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [:api] - expect(described_class.new(token).include_any_scope?([:api])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if more than one of the required scopes is present in the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [:api, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes is an exact match for the token's scopes" do token = double("token", scopes: [:api, :read_user, :other_scope]) + scopes = [:api, :read_user, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns true if the list of required scopes contains all of the token's scopes, in addition to others" do token = double("token", scopes: [:api, :read_user]) + scopes = [:api, :read_user, :other_scope] - expect(described_class.new(token).include_any_scope?([:api, :read_user, :other_scope])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it 'returns true if the list of required scopes is blank' do token = double("token", scopes: []) + scopes = [] - expect(described_class.new(token).include_any_scope?([])).to be(true) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) end it "returns false if there are no scopes in common between the required scopes and the token scopes" do token = double("token", scopes: [:api, :read_user]) + scopes = [:other_scope] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) + end + + context "conditions" do + it "ignores any scopes whose `if` condition returns false" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { false })] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(false) + end + + it "does not ignore scopes whose `if` condition is not set" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { false }), :read_user] + + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) + end + + it "does not ignore scopes whose `if` condition returns true" do + token = double("token", scopes: [:api, :read_user]) + scopes = [API::Scope.new(:api, if: ->(_) { true }), API::Scope.new(:read_user, if: ->(_) { false })] - expect(described_class.new(token).include_any_scope?([:other_scope])).to be(false) + expect(described_class.new(token, request: request).include_any_scope?(scopes)).to be(true) + end end end end diff --git a/spec/services/after_branch_delete_service_spec.rb b/spec/services/after_branch_delete_service_spec.rb index 77ca17bc82c..bc9747d1413 100644 --- a/spec/services/after_branch_delete_service_spec.rb +++ b/spec/services/after_branch_delete_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe AfterBranchDeleteService, services: true do +describe AfterBranchDeleteService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb index e273dfe1552..1c2d0b3e0dc 100644 --- a/spec/services/auth/container_registry_authentication_service_spec.rb +++ b/spec/services/auth/container_registry_authentication_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Auth::ContainerRegistryAuthenticationService, services: true do +describe Auth::ContainerRegistryAuthenticationService do let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } @@ -8,7 +8,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:payload) { JWT.decode(subject[:token], rsa_key).first } let(:authentication_abilities) do - [:read_container_image, :create_container_image] + [:read_container_image, :create_container_image, :admin_container_image] end subject do @@ -34,7 +34,9 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for changed configuration' do - before { stub_application_setting(container_registry_token_expire_delay: expire_delay) } + before do + stub_application_setting(container_registry_token_expire_delay: expire_delay) + end it { expect(expires_at).to be_within(2.seconds).of(Time.now + expire_delay.minutes) } end @@ -44,7 +46,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do shared_examples 'an accessible' do let(:access) do [{ 'type' => 'repository', - 'name' => project.path_with_namespace, + 'name' => project.full_path, 'actions' => actions }] end @@ -57,6 +59,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do it { expect(payload).to include('access' => []) } end + shared_examples 'a deletable' do + it_behaves_like 'an accessible' do + let(:actions) { ['*'] } + end + end + shared_examples 'a pullable' do it_behaves_like 'an accessible' do let(:actions) { ['pull'] } @@ -94,8 +102,8 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end describe '#full_access_token' do - let(:project) { create(:empty_project) } - let(:token) { described_class.full_access_token(project.path_with_namespace) } + let(:project) { create(:project) } + let(:token) { described_class.full_access_token(project.full_path) } subject { { token: token } } @@ -110,29 +118,46 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do let(:current_user) { create(:user) } context 'for private project' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } context 'allow to use scope-less authentication' do it_behaves_like 'a valid token' end context 'allow developer to push images' do - before { project.team << [current_user, :developer] } + before do + project.add_developer(current_user) + end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + { scope: "repository:#{project.full_path}:push" } end it_behaves_like 'a pushable' it_behaves_like 'container repository factory' end + context 'disallow developer to delete images' do + before do + project.add_developer(current_user) + end + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'allow reporter to pull images' do - before { project.team << [current_user, :reporter] } + before do + project.add_reporter(current_user) + end context 'when pulling from root level repository' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end it_behaves_like 'a pullable' @@ -140,11 +165,26 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end + context 'disallow reporter to delete images' do + before do + project.add_reporter(current_user) + end + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + context 'return a least of privileges' do - before { project.team << [current_user, :reporter] } + before do + project.add_reporter(current_user) + end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push,pull" } + { scope: "repository:#{project.full_path}:push,pull" } end it_behaves_like 'a pullable' @@ -152,10 +192,25 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'disallow guest to pull or push images' do - before { project.team << [current_user, :guest] } + before do + project.add_guest(current_user) + end let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + { scope: "repository:#{project.full_path}:pull,push" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow guest to delete images' do + before do + project.add_guest(current_user) + end + + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } end it_behaves_like 'an inaccessible' @@ -164,11 +219,11 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for public project' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } context 'allow anyone to pull images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end it_behaves_like 'a pullable' @@ -177,7 +232,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow anyone to push images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + { scope: "repository:#{project.full_path}:push" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } end it_behaves_like 'an inaccessible' @@ -195,12 +259,12 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for internal project' do - let(:project) { create(:empty_project, :internal) } + let(:project) { create(:project, :internal) } context 'for internal user' do context 'allow anyone to pull images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end it_behaves_like 'a pullable' @@ -209,7 +273,16 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'disallow anyone to push images' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + { scope: "repository:#{project.full_path}:push" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + + context 'disallow anyone to delete images' do + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } end it_behaves_like 'an inaccessible' @@ -218,19 +291,56 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for external user' do - let(:current_user) { create(:user, external: true) } - let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + context 'disallow anyone to pull or push images' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:pull,push" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' end - it_behaves_like 'an inaccessible' - it_behaves_like 'not a container repository factory' + context 'disallow anyone to delete images' do + let(:current_user) { create(:user, external: true) } + let(:current_params) do + { scope: "repository:#{project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' + it_behaves_like 'not a container repository factory' + end + end + end + end + + context 'delete authorized as master' do + let(:current_project) { create(:project) } + let(:current_user) { create(:user) } + + let(:authentication_abilities) do + [:admin_container_image] + end + + before do + current_project.add_master(current_user) + end + + it_behaves_like 'a valid token' + + context 'allow to delete images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:*" } + end + + it_behaves_like 'a deletable' do + let(:project) { current_project } end end end context 'build authorized as user' do - let(:current_project) { create(:empty_project) } + let(:current_project) { create(:project) } let(:current_user) { create(:user) } let(:authentication_abilities) do @@ -238,14 +348,14 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end before do - current_project.team << [current_user, :developer] + current_project.add_developer(current_user) end it_behaves_like 'a valid token' context 'allow to pull and push images' do let(:current_params) do - { scope: "repository:#{current_project.path_with_namespace}:pull,push" } + { scope: "repository:#{current_project.full_path}:pull,push" } end it_behaves_like 'a pullable and pushable' do @@ -257,14 +367,24 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end end + context 'disallow to delete images' do + let(:current_params) do + { scope: "repository:#{current_project.path_with_namespace}:*" } + end + + it_behaves_like 'an inaccessible' do + let(:project) { current_project } + end + end + context 'for other projects' do context 'when pulling' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end context 'allow for public' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' @@ -278,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'when you are member' do before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'a pullable' @@ -286,7 +406,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'when you are owner' do - let(:project) { create(:empty_project, namespace: current_user.namespace) } + let(:project) { create(:project, namespace: current_user.namespace) } it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' @@ -294,7 +414,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for private' do - let(:project) { create(:empty_project, :private) } + let(:project) { create(:project, :private) } it_behaves_like 'pullable for being team member' @@ -308,7 +428,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'when you are member' do before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'a pullable' @@ -316,7 +436,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'when you are owner' do - let(:project) { create(:empty_project, namespace: current_user.namespace) } + let(:project) { create(:project, namespace: current_user.namespace) } it_behaves_like 'a pullable' it_behaves_like 'not a container repository factory' @@ -327,15 +447,15 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'when pushing' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + { scope: "repository:#{project.full_path}:push" } end context 'disallow for all' do context 'when you are member' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } before do - project.team << [current_user, :developer] + project.add_developer(current_user) end it_behaves_like 'an inaccessible' @@ -343,7 +463,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'when you are owner' do - let(:project) { create(:empty_project, :public, namespace: current_user.namespace) } + let(:project) { create(:project, :public, namespace: current_user.namespace) } it_behaves_like 'an inaccessible' it_behaves_like 'not a container repository factory' @@ -353,13 +473,15 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for project without container registry' do - let(:project) { create(:empty_project, :public, container_registry_enabled: false) } + let(:project) { create(:project, :public, container_registry_enabled: false) } - before { project.update(container_registry_enabled: false) } + before do + project.update(container_registry_enabled: false) + end context 'disallow when pulling' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end it_behaves_like 'an inaccessible' @@ -384,21 +506,21 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do end context 'for private project' do - let(:project) { create(:empty_project, :private) } + let(:project) { create(:project, :private) } let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull" } + { scope: "repository:#{project.full_path}:pull" } end it_behaves_like 'a forbidden' end context 'for public project' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } context 'when pulling and pushing' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:pull,push" } + { scope: "repository:#{project.full_path}:pull,push" } end it_behaves_like 'a pullable' @@ -407,7 +529,7 @@ describe Auth::ContainerRegistryAuthenticationService, services: true do context 'when pushing' do let(:current_params) do - { scope: "repository:#{project.path_with_namespace}:push" } + { scope: "repository:#{project.full_path}:push" } end it_behaves_like 'a forbidden' diff --git a/spec/services/boards/create_service_spec.rb b/spec/services/boards/create_service_spec.rb index a8555f5b4a0..db51a524e79 100644 --- a/spec/services/boards/create_service_spec.rb +++ b/spec/services/boards/create_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::CreateService, services: true do +describe Boards::CreateService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } subject(:service) { described_class.new(project, double) } @@ -14,8 +14,9 @@ describe Boards::CreateService, services: true do it 'creates the default lists' do board = service.execute - expect(board.lists.size).to eq 1 - expect(board.lists.first).to be_closed + expect(board.lists.size).to eq 2 + expect(board.lists.first).to be_backlog + expect(board.lists.last).to be_closed end end @@ -25,6 +26,8 @@ describe Boards::CreateService, services: true do end it 'does not create a new board' do + expect(service).to receive(:can_create_board?) { false } + expect { service.execute }.not_to change(project.boards, :count) end end diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb index 360ee398f77..f2ddaa903da 100644 --- a/spec/services/boards/issues/create_service_spec.rb +++ b/spec/services/boards/issues/create_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::Issues::CreateService, services: true do +describe Boards::Issues::CreateService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } let(:label) { create(:label, project: project, name: 'in-progress') } diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index c982031c791..01ee3856c99 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Boards::Issues::ListService, services: true do +describe Boards::Issues::ListService do describe '#execute' do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:bug) { create(:label, project: project, name: 'Bug') } @@ -13,13 +13,14 @@ describe Boards::Issues::ListService, services: true do let(:p2) { create(:label, title: 'P2', project: project, priority: 2) } let(:p3) { create(:label, title: 'P3', project: project, priority: 3) } + let!(:backlog) { create(:backlog_list, board: board) } let!(:list1) { create(:list, board: board, label: development, position: 0) } let!(:list2) { create(:list, board: board, label: testing, position: 1) } let!(:closed) { create(:closed_list, board: board) } let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) } let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) } - let!(:reopened_issue1) { create(:issue, :reopened, project: project) } + let!(:reopened_issue1) { create(:issue, :opened, project: project) } let!(:list1_issue1) { create(:labeled_issue, project: project, labels: [p2, development]) } let!(:list1_issue2) { create(:labeled_issue, project: project, labels: [development]) } @@ -53,6 +54,14 @@ describe Boards::Issues::ListService, services: true do expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] end + it 'returns opened issues when listing issues from Backlog' do + params = { board_id: board.id, id: backlog.id } + + issues = described_class.new(project, user, params).execute + + expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] + end + it 'returns closed issues when listing issues from Closed' do params = { board_id: board.id, id: closed.id } diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb index 4ff7ac6bb2f..63dfe80d672 100644 --- a/spec/services/boards/issues/move_service_spec.rb +++ b/spec/services/boards/issues/move_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Boards::Issues::MoveService, services: true do +describe Boards::Issues::MoveService do describe '#execute' do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board1) { create(:board, project: project) } let(:bug) { create(:label, project: project, name: 'Bug') } @@ -73,7 +73,7 @@ describe Boards::Issues::MoveService, services: true do issue.reload expect(issue.labels).to contain_exactly(bug, testing) - expect(issue).to be_reopened + expect(issue).to be_opened end end diff --git a/spec/services/boards/list_service_spec.rb b/spec/services/boards/list_service_spec.rb index dff33e4bcbb..1d0be99fb35 100644 --- a/spec/services/boards/list_service_spec.rb +++ b/spec/services/boards/list_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::ListService, services: true do +describe Boards::ListService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } subject(:service) { described_class.new(project, double) } diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb index ebac38e68f1..7d0b396cd06 100644 --- a/spec/services/boards/lists/create_service_spec.rb +++ b/spec/services/boards/lists/create_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::Lists::CreateService, services: true do +describe Boards::Lists::CreateService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } let(:label) { create(:label, project: project, name: 'in-progress') } diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb index af2d7c784bb..bd98625b44f 100644 --- a/spec/services/boards/lists/destroy_service_spec.rb +++ b/spec/services/boards/lists/destroy_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::Lists::DestroyService, services: true do +describe Boards::Lists::DestroyService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb index ed0337662af..592f25059ac 100644 --- a/spec/services/boards/lists/generate_service_spec.rb +++ b/spec/services/boards/lists/generate_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::Lists::GenerateService, services: true do +describe Boards::Lists::GenerateService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } diff --git a/spec/services/boards/lists/list_service_spec.rb b/spec/services/boards/lists/list_service_spec.rb index ab9fb1bc914..b189857e4f4 100644 --- a/spec/services/boards/lists/list_service_spec.rb +++ b/spec/services/boards/lists/list_service_spec.rb @@ -1,16 +1,33 @@ require 'spec_helper' -describe Boards::Lists::ListService, services: true do +describe Boards::Lists::ListService do + let(:project) { create(:project) } + let(:board) { create(:board, project: project) } + let(:label) { create(:label, project: project) } + let!(:list) { create(:list, board: board, label: label) } + let(:service) { described_class.new(project, double) } + describe '#execute' do - it "returns board's lists" do - project = create(:empty_project) - board = create(:board, project: project) - label = create(:label, project: project) - list = create(:list, board: board, label: label) + context 'when the board has a backlog list' do + let!(:backlog_list) { create(:backlog_list, board: board) } + + it 'does not create a backlog list' do + expect { service.execute(board) }.not_to change(board.lists, :count) + end + + it "returns board's lists" do + expect(service.execute(board)).to eq [backlog_list, list, board.closed_list] + end + end - service = described_class.new(project, double) + context 'when the board does not have a backlog list' do + it 'creates a backlog list' do + expect { service.execute(board) }.to change(board.lists, :count).by(1) + end - expect(service.execute(board)).to eq [list, board.closed_list] + it "returns board's lists" do + expect(service.execute(board)).to eq [board.backlog_list, list, board.closed_list] + end end end end diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb index 4b3bdd133f2..a9d218bad49 100644 --- a/spec/services/boards/lists/move_service_spec.rb +++ b/spec/services/boards/lists/move_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Boards::Lists::MoveService, services: true do +describe Boards::Lists::MoveService do describe '#execute' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:board) { create(:board, project: project) } let(:user) { create(:user) } diff --git a/spec/services/chat_names/authorize_user_service_spec.rb b/spec/services/chat_names/authorize_user_service_spec.rb index d50bfb0492c..d88b2504133 100644 --- a/spec/services/chat_names/authorize_user_service_spec.rb +++ b/spec/services/chat_names/authorize_user_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ChatNames::AuthorizeUserService, services: true do +describe ChatNames::AuthorizeUserService do describe '#execute' do let(:service) { create(:service) } diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb index 0dc96521fa8..79aaac3aeb6 100644 --- a/spec/services/chat_names/find_user_service_spec.rb +++ b/spec/services/chat_names/find_user_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ChatNames::FindUserService, services: true do +describe ChatNames::FindUserService do describe '#execute' do let(:service) { create(:service) } diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb index fa5014cee07..53d4fcfed18 100644 --- a/spec/services/ci/create_pipeline_service_spec.rb +++ b/spec/services/ci/create_pipeline_service_spec.rb @@ -1,21 +1,28 @@ require 'spec_helper' -describe Ci::CreatePipelineService, services: true do +describe Ci::CreatePipelineService do let(:project) { create(:project, :repository) } let(:user) { create(:admin) } + let(:ref_name) { 'refs/heads/master' } before do stub_ci_pipeline_to_return_yaml_file end describe '#execute' do - def execute_service(after: project.commit.id, message: 'Message', ref: 'refs/heads/master') + def execute_service( + source: :push, + after: project.commit.id, + message: 'Message', + ref: ref_name, + trigger_request: nil) params = { ref: ref, before: '00000000', after: after, commits: [{ message: message }] } - described_class.new(project, user, params).execute + described_class.new(project, user, params).execute( + source, trigger_request: trigger_request) end context 'valid params' do @@ -27,12 +34,72 @@ describe Ci::CreatePipelineService, services: true do ) end - it { expect(pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(pipeline).to be_valid } - it { expect(pipeline).to eq(project.pipelines.last) } - it { expect(pipeline).to have_attributes(user: user) } - it { expect(pipeline).to have_attributes(status: 'pending') } - it { expect(pipeline.builds.first).to be_kind_of(Ci::Build) } + it 'creates a pipeline' do + expect(pipeline).to be_kind_of(Ci::Pipeline) + expect(pipeline).to be_valid + expect(pipeline).to be_persisted + expect(pipeline).to be_push + expect(pipeline).to eq(project.pipelines.last) + expect(pipeline).to have_attributes(user: user) + expect(pipeline).to have_attributes(status: 'pending') + expect(pipeline.builds.first).to be_kind_of(Ci::Build) + end + + it 'increments the prometheus counter' do + expect(Gitlab::Metrics).to receive(:counter) + .with(:pipelines_created_total, "Counter of pipelines created") + .and_call_original + + pipeline + end + + context 'when merge requests already exist for this source branch' do + it 'updates head pipeline of each merge request' do + merge_request_1 = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) + merge_request_2 = create(:merge_request, source_branch: 'master', target_branch: "branch_2", source_project: project) + + head_pipeline = pipeline + + expect(merge_request_1.reload.head_pipeline).to eq(head_pipeline) + expect(merge_request_2.reload.head_pipeline).to eq(head_pipeline) + end + + context 'when there is no pipeline for source branch' do + it "does not update merge request head pipeline" do + merge_request = create(:merge_request, source_branch: 'feature', target_branch: "branch_1", source_project: project) + + head_pipeline = pipeline + + expect(merge_request.reload.head_pipeline).not_to eq(head_pipeline) + end + end + + context 'when merge request target project is different from source project' do + let!(:target_project) { create(:project, :repository) } + let!(:forked_project_link) { create(:forked_project_link, forked_to_project: project, forked_from_project: target_project) } + + it 'updates head pipeline for merge request' do + merge_request = + create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project, target_project: target_project) + + head_pipeline = pipeline + + expect(merge_request.reload.head_pipeline).to eq(head_pipeline) + end + end + + context 'when the pipeline is not the latest for the branch' do + it 'does not update merge request head pipeline' do + merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project) + + allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false) + + pipeline + + expect(merge_request.reload.head_pipeline).to be_nil + end + end + end context 'auto-cancel enabled' do before do @@ -245,5 +312,238 @@ describe Ci::CreatePipelineService, services: true do expect(Environment.find_by(name: "review/master")).not_to be_nil end end + + context 'when environment with invalid name' do + before do + config = YAML.dump(deploy: { environment: { name: 'name,with,commas' }, script: 'ls' }) + stub_ci_pipeline_yaml_file(config) + end + + it 'does not create an environment' do + expect do + result = execute_service + + expect(result).to be_persisted + end.not_to change { Environment.count } + end + end + + context 'when builds with auto-retries are configured' do + before do + config = YAML.dump(rspec: { script: 'rspec', retry: 2 }) + stub_ci_pipeline_yaml_file(config) + end + + it 'correctly creates builds with auto-retry value configured' do + pipeline = execute_service + + expect(pipeline).to be_persisted + expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2 + end + end + + shared_examples 'when ref is protected' do + let(:user) { create(:user) } + + context 'when user is developer' do + before do + project.add_developer(user) + end + + it 'does not create a pipeline' do + expect(execute_service).not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when user is master' do + before do + project.add_master(user) + end + + it 'creates a pipeline' do + expect(execute_service).to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + + context 'when trigger belongs to no one' do + let(:user) {} + let(:trigger_request) { create(:ci_trigger_request) } + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when trigger belongs to a developer' do + let(:user) {} + + let(:trigger_request) do + create(:ci_trigger_request).tap do |request| + user = create(:user) + project.add_developer(user) + request.trigger.update(owner: user) + end + end + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .not_to be_persisted + expect(Ci::Pipeline.count).to eq(0) + end + end + + context 'when trigger belongs to a master' do + let(:user) {} + + let(:trigger_request) do + create(:ci_trigger_request).tap do |request| + user = create(:user) + project.add_master(user) + request.trigger.update(owner: user) + end + end + + it 'does not create a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + end + + context 'when ref is a protected branch' do + before do + create(:protected_branch, project: project, name: 'master') + end + + it_behaves_like 'when ref is protected' + end + + context 'when ref is a protected tag' do + let(:ref_name) { 'refs/tags/v1.0.0' } + + before do + create(:protected_tag, project: project, name: '*') + end + + it_behaves_like 'when ref is protected' + end + + context 'when ref is not protected' do + context 'when trigger belongs to no one' do + let(:user) {} + let(:trigger_request) { create(:ci_trigger_request) } + + it 'creates a pipeline' do + expect(execute_service(trigger_request: trigger_request)) + .to be_persisted + expect(Ci::Pipeline.count).to eq(1) + end + end + end + end + + describe '#allowed_to_create?' do + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:ref) { 'master' } + + subject do + described_class.new(project, user, ref: ref) + .send(:allowed_to_create?, user) + end + + context 'when user is a developer' do + before do + project.add_developer(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to merge' do + let!(:protected_branch) do + create(:protected_branch, + :developers_can_merge, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_falsey } + + context 'when developers are allowed to create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :developers_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_truthy } + end + end + end + + context 'when user is a master' do + before do + project.add_master(user) + end + + it { is_expected.to be_truthy } + + context 'when the branch is protected' do + let!(:protected_branch) do + create(:protected_branch, project: project, name: ref) + end + + it { is_expected.to be_truthy } + end + + context 'when the tag is protected' do + let(:ref) { 'v1.0.0' } + + let!(:protected_tag) do + create(:protected_tag, project: project, name: ref) + end + + it { is_expected.to be_truthy } + + context 'when no one can create the tag' do + let!(:protected_tag) do + create(:protected_tag, + :no_one_can_create, + project: project, + name: ref) + end + + it { is_expected.to be_falsey } + end + end + end + + context 'when owner cannot create pipeline' do + it { is_expected.to be_falsey } + end end end diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb index 5a20102872a..8295813a1ca 100644 --- a/spec/services/ci/create_trigger_request_service_spec.rb +++ b/spec/services/ci/create_trigger_request_service_spec.rb @@ -1,12 +1,15 @@ require 'spec_helper' -describe Ci::CreateTriggerRequestService, services: true do - let(:service) { described_class.new } +describe Ci::CreateTriggerRequestService do + let(:service) { described_class } let(:project) { create(:project, :repository) } - let(:trigger) { create(:ci_trigger, project: project) } + let(:trigger) { create(:ci_trigger, project: project, owner: owner) } + let(:owner) { create(:user) } before do stub_ci_pipeline_to_return_yaml_file + + project.add_developer(owner) end describe '#execute' do @@ -14,27 +17,26 @@ describe Ci::CreateTriggerRequestService, services: true do subject { service.execute(project, trigger, 'master') } context 'without owner' do - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } - it { expect(subject.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject.pipeline).to be_trigger } end context 'with owner' do - let(:owner) { create(:user) } - let(:trigger) { create(:ci_trigger, project: project, owner: owner) } - - it { expect(subject).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) } + it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) } + it { expect(subject.trigger_request.builds.first.user).to eq(owner) } it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) } + it { expect(subject.pipeline).to be_trigger } it { expect(subject.pipeline.user).to eq(owner) } - it { expect(subject.builds.first).to be_kind_of(Ci::Build) } - it { expect(subject.builds.first.user).to eq(owner) } end end context 'no commit for ref' do subject { service.execute(project, trigger, 'other-branch') } - it { expect(subject).to be_nil } + it { expect(subject.pipeline).not_to be_persisted } end context 'no builds created' do @@ -44,7 +46,7 @@ describe Ci::CreateTriggerRequestService, services: true do stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }') end - it { expect(subject).to be_nil } + it { expect(subject.pipeline).not_to be_persisted } end end end diff --git a/spec/services/ci/pipeline_trigger_service_spec.rb b/spec/services/ci/pipeline_trigger_service_spec.rb new file mode 100644 index 00000000000..9a6875e448c --- /dev/null +++ b/spec/services/ci/pipeline_trigger_service_spec.rb @@ -0,0 +1,83 @@ +require 'spec_helper' + +describe Ci::PipelineTriggerService do + let(:project) { create(:project, :repository) } + + before do + stub_ci_pipeline_to_return_yaml_file + end + + describe '#execute' do + let(:user) { create(:user) } + let(:trigger) { create(:ci_trigger, project: project, owner: user) } + let(:result) { described_class.new(project, user, params).execute } + + before do + project.add_developer(user) + end + + context 'when trigger belongs to a different project' do + let(:params) { { token: trigger.token, ref: 'master', variables: nil } } + let(:trigger) { create(:ci_trigger, project: create(:project), owner: user) } + + it 'does nothing' do + expect { result }.not_to change { Ci::Pipeline.count } + end + end + + context 'when params have an existsed trigger token' do + context 'when params have an existsed ref' do + let(:params) { { token: trigger.token, ref: 'master', variables: nil } } + + it 'triggers a pipeline' do + expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:pipeline].ref).to eq('master') + expect(result[:pipeline].project).to eq(project) + expect(result[:pipeline].user).to eq(trigger.owner) + expect(result[:status]).to eq(:success) + end + + context 'when commit message has [ci skip]' do + before do + allow_any_instance_of(Ci::Pipeline).to receive(:git_commit_message) { '[ci skip]' } + end + + it 'ignores [ci skip] and create as general' do + expect { result }.to change { Ci::Pipeline.count }.by(1) + expect(result[:status]).to eq(:success) + end + end + + context 'when params have a variable' do + let(:params) { { token: trigger.token, ref: 'master', variables: variables } } + let(:variables) { { 'AAA' => 'AAA123' } } + + it 'has a variable' do + expect { result }.to change { Ci::PipelineVariable.count }.by(1) + .and change { Ci::TriggerRequest.count }.by(1) + expect(result[:pipeline].variables.map { |v| { v.key => v.value } }.first).to eq(variables) + expect(result[:pipeline].trigger_requests.last.variables).to be_nil + end + end + end + + context 'when params have a non-existsed ref' do + let(:params) { { token: trigger.token, ref: 'invalid-ref', variables: nil } } + + it 'does not trigger a pipeline' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result[:http_status]).to eq(400) + end + end + end + + context 'when params have a non-existsed trigger token' do + let(:params) { { token: 'invalid-token', ref: nil, variables: nil } } + + it 'does not trigger a pipeline' do + expect { result }.not_to change { Ci::Pipeline.count } + expect(result).to be_nil + end + end + end +end diff --git a/spec/services/ci/play_build_service_spec.rb b/spec/services/ci/play_build_service_spec.rb new file mode 100644 index 00000000000..330ec81e87d --- /dev/null +++ b/spec/services/ci/play_build_service_spec.rb @@ -0,0 +1,114 @@ +require 'spec_helper' + +describe Ci::PlayBuildService, '#execute' do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:pipeline) { create(:ci_pipeline, project: project) } + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + let(:service) do + described_class.new(project, user) + end + + context 'when project does not have repository yet' do + let(:project) { create(:project) } + + it 'allows user to play build if protected branch rules are met' do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + + service.execute(build) + + expect(build.reload).to be_pending + end + + it 'does not allow user with developer role to play build' do + project.add_developer(user) + + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'when project has repository' do + let(:project) { create(:project, :repository) } + + it 'allows user with developer role to play a build' do + project.add_developer(user) + + service.execute(build) + + expect(build.reload).to be_pending + end + end + + context 'when build is a playable manual action' do + let(:build) { create(:ci_build, :manual, pipeline: pipeline) } + + before do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end + + it 'enqueues the build' do + expect(service.execute(build)).to eq build + expect(build.reload).to be_pending + end + + it 'reassignes build user correctly' do + service.execute(build) + + expect(build.reload.user).to eq user + end + end + + context 'when build is not a playable manual action' do + let(:build) { create(:ci_build, when: :manual, pipeline: pipeline) } + + before do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: build.ref, project: project) + end + + it 'duplicates the build' do + duplicate = service.execute(build) + + expect(duplicate).not_to eq build + expect(duplicate).to be_pending + end + + it 'assigns users correctly' do + duplicate = service.execute(build) + + expect(build.user).not_to eq user + expect(duplicate.user).to eq user + end + end + + context 'when build is not action' do + let(:build) { create(:ci_build, :success, pipeline: pipeline) } + + it 'raises an error' do + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'when user does not have ability to trigger action' do + before do + create(:protected_branch, :no_one_can_push, + name: build.ref, project: project) + end + + it 'raises an error' do + expect { service.execute(build) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end +end diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb index 245e19822f3..214adc9960f 100644 --- a/spec/services/ci/process_pipeline_service_spec.rb +++ b/spec/services/ci/process_pipeline_service_spec.rb @@ -1,14 +1,16 @@ require 'spec_helper' -describe Ci::ProcessPipelineService, '#execute', :services do +describe Ci::ProcessPipelineService, '#execute' do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:pipeline) do create(:ci_empty_pipeline, ref: 'master', project: project) end before do + stub_not_protect_default_branch + project.add_developer(user) end @@ -62,6 +64,10 @@ describe Ci::ProcessPipelineService, '#execute', :services do fail_running_or_pending expect(builds_statuses).to eq %w(failed pending) + + fail_running_or_pending + + expect(pipeline.reload).to be_success end end @@ -268,6 +274,24 @@ describe Ci::ProcessPipelineService, '#execute', :services do end end + context 'when there are only manual actions in stages' do + before do + create_build('image', stage_idx: 0, when: 'manual', allow_failure: true) + create_build('build', stage_idx: 1, when: 'manual', allow_failure: true) + create_build('deploy', stage_idx: 2, when: 'manual') + create_build('check', stage_idx: 3) + + process_pipeline + end + + it 'processes all jobs until blocking actions encountered' do + expect(all_builds_statuses).to eq(%w[manual manual manual created]) + expect(all_builds_names).to eq(%w[image build deploy check]) + + expect(pipeline.reload).to be_blocked + end + end + context 'when blocking manual actions are defined' do before do create_build('code:test', stage_idx: 0) @@ -314,6 +338,14 @@ describe Ci::ProcessPipelineService, '#execute', :services do end context 'when pipeline is promoted sequentially up to the end' do + before do + # Users need ability to merge into a branch in order to trigger + # protected manual actions. + # + create(:protected_branch, :developers_can_merge, + name: 'master', project: project) + end + it 'properly processes entire pipeline' do process_pipeline @@ -418,6 +450,50 @@ describe Ci::ProcessPipelineService, '#execute', :services do end end + context 'updates a list of retried builds' do + subject { described_class.retried.order(:id) } + + let!(:build_retried) { create_build('build') } + let!(:build) { create_build('build') } + let!(:test) { create_build('test') } + + it 'returns unique statuses' do + process_pipeline + + expect(all_builds.latest).to contain_exactly(build, test) + expect(all_builds.retried).to contain_exactly(build_retried) + end + end + + context 'when builds with auto-retries are configured' do + before do + create_build('build:1', stage_idx: 0, user: user, options: { retry: 2 }) + create_build('test:1', stage_idx: 1, user: user, when: :on_failure) + create_build('test:2', stage_idx: 1, user: user, options: { retry: 1 }) + end + + it 'automatically retries builds in a valid order' do + expect(process_pipeline).to be_truthy + + fail_running_or_pending + + expect(builds_names).to eq %w[build:1 build:1] + expect(builds_statuses).to eq %w[failed pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[build:1 build:1 test:2] + expect(builds_statuses).to eq %w[failed success pending] + + succeed_running_or_pending + + expect(builds_names).to eq %w[build:1 build:1 test:2] + expect(builds_statuses).to eq %w[failed success success] + + expect(pipeline.reload).to be_success + end + end + def process_pipeline described_class.new(pipeline.project, user).execute(pipeline) end @@ -434,6 +510,10 @@ describe Ci::ProcessPipelineService, '#execute', :services do builds.pluck(:name) end + def all_builds_names + all_builds.pluck(:name) + end + def builds_statuses builds.pluck(:status) end diff --git a/spec/services/ci/register_job_service_spec.rb b/spec/services/ci/register_job_service_spec.rb index 62ba0b01339..8eb0d2d10a4 100644 --- a/spec/services/ci/register_job_service_spec.rb +++ b/spec/services/ci/register_job_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' module Ci - describe RegisterJobService, services: true do - let!(:project) { FactoryGirl.create :empty_project, shared_runners_enabled: false } + describe RegisterJobService do + let!(:project) { FactoryGirl.create :project, shared_runners_enabled: false } let!(:pipeline) { FactoryGirl.create :ci_pipeline, project: project } let!(:pending_build) { FactoryGirl.create :ci_build, pipeline: pipeline } let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) } @@ -72,9 +72,9 @@ module Ci end context 'for multiple builds' do - let!(:project2) { create :empty_project, shared_runners_enabled: true } + let!(:project2) { create :project, shared_runners_enabled: true } let!(:pipeline2) { create :ci_pipeline, project: project2 } - let!(:project3) { create :empty_project, shared_runners_enabled: true } + let!(:project3) { create :project, shared_runners_enabled: true } let!(:pipeline3) { create :ci_pipeline, project: project3 } let!(:build1_project1) { pending_build } let!(:build2_project1) { FactoryGirl.create :ci_build, pipeline: pipeline } diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb index b2d37657770..cec667071cc 100644 --- a/spec/services/ci/retry_build_service_spec.rb +++ b/spec/services/ci/retry_build_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Ci::RetryBuildService, :services do +describe Ci::RetryBuildService do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } let(:build) { create(:ci_build, pipeline: pipeline) } @@ -22,15 +22,27 @@ describe Ci::RetryBuildService, :services do %i[type lock_version target_url base_tags commit_id deployments erased_by_id last_deployment project_id runner_id tag_taggings taggings tags trigger_request_id - user_id auto_canceled_by_id].freeze + user_id auto_canceled_by_id retried].freeze shared_examples 'build duplication' do + let(:stage) do + # TODO, we still do not have factory for new stages, we will need to + # switch existing factory to persist stages, instead of using LegacyStage + # + Ci::Stage.create!(project: project, pipeline: pipeline, name: 'test') + end + let(:build) do create(:ci_build, :failed, :artifacts_expired, :erased, :queued, :coverage, :tags, :allowed_to_fail, :on_tag, - :teardown_environment, :triggered, :trace, - description: 'some build', pipeline: pipeline, - auto_canceled_by: create(:ci_empty_pipeline)) + :triggered, :trace, :teardown_environment, + description: 'my-job', stage: 'test', pipeline: pipeline, + auto_canceled_by: create(:ci_empty_pipeline)) do |build| + ## + # TODO, workaround for FactoryGirl limitation when having both + # stage (text) and stage_id (integer) columns in the table. + build.stage_id = stage.id + end end describe 'clone accessors' do @@ -73,6 +85,8 @@ describe Ci::RetryBuildService, :services do context 'when user has ability to execute build' do before do + stub_not_protect_default_branch + project.add_developer(user) end @@ -115,10 +129,12 @@ describe Ci::RetryBuildService, :services do end describe '#reprocess' do - let(:new_build) { service.reprocess(build) } + let(:new_build) { service.reprocess!(build) } context 'when user has ability to execute build' do before do + stub_not_protect_default_branch + project.add_developer(user) end @@ -131,11 +147,16 @@ describe Ci::RetryBuildService, :services do it 'does not enqueue the new build' do expect(new_build).to be_created end + + it 'does mark old build as retried' do + expect(new_build).to be_latest + expect(build.reload).to be_retried + end end context 'when user does not have ability to execute build' do it 'raises an error' do - expect { service.reprocess(build) } + expect { service.reprocess!(build) } .to raise_error Gitlab::Access::AccessDeniedError end end diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index f1b2d3a4798..6ce75c65c8c 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -1,17 +1,22 @@ require 'spec_helper' -describe Ci::RetryPipelineService, '#execute', :services do +describe Ci::RetryPipelineService, '#execute' do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:pipeline) { create(:ci_pipeline, project: project) } let(:service) { described_class.new(project, user) } - context 'when user has ability to modify pipeline' do - let(:user) { create(:admin) } + context 'when user has full ability to modify pipeline' do + before do + project.add_developer(user) + + create(:protected_branch, :developers_can_merge, + name: pipeline.ref, project: project) + end context 'when there are already retried jobs present' do before do - create_build('rspec', :canceled, 0) + create_build('rspec', :canceled, 0, retried: true) create_build('rspec', :failed, 0) end @@ -227,6 +232,38 @@ describe Ci::RetryPipelineService, '#execute', :services do end end + context 'when user is not allowed to trigger manual action' do + before do + project.add_developer(user) + end + + context 'when there is a failed manual action present' do + before do + create_build('test', :failed, 0) + create_build('deploy', :failed, 0, when: :manual) + create_build('verify', :canceled, 1) + end + + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + + context 'when there is a failed manual action in later stage' do + before do + create_build('test', :failed, 0) + create_build('deploy', :failed, 1, when: :manual) + create_build('verify', :canceled, 2) + end + + it 'raises an error' do + expect { service.execute(pipeline) } + .to raise_error Gitlab::Access::AccessDeniedError + end + end + end + def statuses pipeline.reload.statuses end diff --git a/spec/services/ci/stop_environments_service_spec.rb b/spec/services/ci/stop_environments_service_spec.rb index 32c72a9cf5e..e2a9ed27e87 100644 --- a/spec/services/ci/stop_environments_service_spec.rb +++ b/spec/services/ci/stop_environments_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::StopEnvironmentsService, services: true do +describe Ci::StopEnvironmentsService do let(:project) { create(:project, :private, :repository) } let(:user) { create(:user) } @@ -55,8 +55,22 @@ describe Ci::StopEnvironmentsService, services: true do end context 'when user does not have permission to stop environment' do + context 'when user has no access to manage deployments' do + before do + project.team << [user, :guest] + end + + it 'does not stop environment' do + expect_environment_not_stopped_on('master') + end + end + end + + context 'when branch for stop action is protected' do before do - project.team << [user, :guest] + project.add_developer(user) + create(:protected_branch, :no_one_can_push, + name: 'master', project: project) end it 'does not stop environment' do diff --git a/spec/services/ci/update_build_queue_service_spec.rb b/spec/services/ci/update_build_queue_service_spec.rb index c44e6b2a48b..0da0e57dbcd 100644 --- a/spec/services/ci/update_build_queue_service_spec.rb +++ b/spec/services/ci/update_build_queue_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::UpdateBuildQueueService, :services do +describe Ci::UpdateBuildQueueService do let(:project) { create(:project, :repository) } let(:build) { create(:ci_build, pipeline: pipeline) } let(:pipeline) { create(:ci_pipeline, project: project) } @@ -9,7 +9,9 @@ describe Ci::UpdateBuildQueueService, :services do let(:runner) { create(:ci_runner) } context 'when there are runner that can pick build' do - before { build.project.runners << runner } + before do + build.project.runners << runner + end it 'ticks runner queue value' do expect { subject.execute(build) } @@ -36,7 +38,9 @@ describe Ci::UpdateBuildQueueService, :services do end context 'when there are no runners that can pick build' do - before { build.tag_list = [:docker] } + before do + build.tag_list = [:docker] + end it 'does not tick runner queue value' do expect { subject.execute(build) } diff --git a/spec/services/ci/update_runner_service_spec.rb b/spec/services/ci/update_runner_service_spec.rb index e429fcfc72f..7cc04c92d27 100644 --- a/spec/services/ci/update_runner_service_spec.rb +++ b/spec/services/ci/update_runner_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Ci::UpdateRunnerService, :services do +describe Ci::UpdateRunnerService do let(:runner) { create(:ci_runner) } describe '#update' do diff --git a/spec/services/cohorts_service_spec.rb b/spec/services/cohorts_service_spec.rb index 1e99442fdcb..77595d7ba2d 100644 --- a/spec/services/cohorts_service_spec.rb +++ b/spec/services/cohorts_service_spec.rb @@ -89,7 +89,7 @@ describe CohortsService do activity_months: [{ total: 2, percentage: 100 }], total: 2, inactive: 1 - }, + } ] expect(described_class.new.execute).to eq(months_included: 12, diff --git a/spec/services/compare_service_spec.rb b/spec/services/compare_service_spec.rb index bea7c965233..9e15eae8c13 100644 --- a/spec/services/compare_service_spec.rb +++ b/spec/services/compare_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CompareService, services: true do +describe CompareService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:service) { described_class.new(project, 'feature') } diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb index 3f548688c20..38096a080a7 100644 --- a/spec/services/create_branch_service_spec.rb +++ b/spec/services/create_branch_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateBranchService, services: true do +describe CreateBranchService do let(:user) { create(:user) } let(:service) { described_class.new(project, user) } diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb index a883705bd45..08267d6e6a0 100644 --- a/spec/services/create_deployment_service_spec.rb +++ b/spec/services/create_deployment_service_spec.rb @@ -1,195 +1,207 @@ require 'spec_helper' -describe CreateDeploymentService, services: true do - let(:project) { create(:empty_project) } +describe CreateDeploymentService do let(:user) { create(:user) } + let(:options) { nil } + + let(:job) do + create(:ci_build, + ref: 'master', + tag: false, + environment: 'production', + options: { environment: options }) + end - let(:service) { described_class.new(project, user, params) } + let(:project) { job.project } - describe '#execute' do - let(:options) { nil } - let(:params) do - { - environment: 'production', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142', - options: options - } - end + let!(:environment) do + create(:environment, project: project, name: 'production') + end - subject { service.execute } + let(:service) { described_class.new(job) } - context 'when no environments exist' do - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) - end + before do + allow_any_instance_of(Deployment).to receive(:create_ref) + end - it 'does create a deployment' do + describe '#execute' do + subject { service.execute } + + context 'when environment exists' do + it 'creates a deployment' do expect(subject).to be_persisted end end - context 'when environment exist' do - let!(:environment) { create(:environment, project: project, name: 'production') } - - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end + context 'when environment does not exist' do + let(:environment) {} - it 'does create a deployment' do - expect(subject).to be_persisted + it 'does not create a deployment' do + expect do + expect(subject).to be_nil + end.not_to change { Deployment.count } end + end - context 'and start action is defined' do - let(:options) { { action: 'start' } } + context 'when start action is defined' do + let(:options) { { action: 'start' } } - context 'and environment is stopped' do - before do - environment.stop - end + context 'and environment is stopped' do + before do + environment.stop + end - it 'makes environment available' do - subject + it 'makes environment available' do + subject - expect(environment.reload).to be_available - end + expect(environment.reload).to be_available + end - it 'does create a deployment' do - expect(subject).to be_persisted - end + it 'creates a deployment' do + expect(subject).to be_persisted end end + end - context 'and stop action is defined' do - let(:options) { { action: 'stop' } } + context 'when stop action is defined' do + let(:options) { { action: 'stop' } } - context 'and environment is available' do - before do - environment.start - end + context 'and environment is available' do + before do + environment.start + end - it 'makes environment stopped' do - subject + it 'makes environment stopped' do + subject - expect(environment.reload).to be_stopped - end + expect(environment.reload).to be_stopped + end - it 'does not create a deployment' do - expect(subject).to be_nil - end + it 'does not create a deployment' do + expect(subject).to be_nil end end end - context 'for environment with invalid name' do - let(:params) do - { - environment: 'name,with,commas', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142' - } + context 'when variables are used' do + let(:options) do + { name: 'review-apps/$CI_COMMIT_REF_NAME', + url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' } + end + + before do + environment.update(name: 'review-apps/master') + job.update(environment: 'review-apps/$CI_COMMIT_REF_NAME') + end + + it 'creates a new deployment' do + expect(subject).to be_persisted end it 'does not create a new environment' do expect { subject }.not_to change { Environment.count } end - it 'does not create a deployment' do - expect(subject).to be_nil + it 'updates external url' do + subject + + expect(subject.environment.name).to eq('review-apps/master') + expect(subject.environment.external_url).to eq('http://master.review-apps.gitlab.com') end end - context 'when variables are used' do - let(:params) do - { - environment: 'review-apps/$CI_COMMIT_REF_NAME', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142', - options: { - name: 'review-apps/$CI_COMMIT_REF_NAME', - url: 'http://$CI_COMMIT_REF_NAME.review-apps.gitlab.com' - }, - variables: [ - { key: 'CI_COMMIT_REF_NAME', value: 'feature-review-apps' } - ] - } + context 'when project was removed' do + let(:environment) {} + + before do + job.update(project: nil) end - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) + it 'does not create deployment or environment' do + expect { subject }.not_to raise_error - expect(subject.environment.name).to eq('review-apps/feature-review-apps') - expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') + expect(Environment.count).to be_zero + expect(Deployment.count).to be_zero end + end + end - it 'does create a new deployment' do - expect(subject).to be_persisted + describe '#expanded_environment_url' do + subject { service.send(:expanded_environment_url) } + + context 'when yaml environment uses $CI_COMMIT_REF_NAME' do + let(:job) do + create(:ci_build, + ref: 'master', + options: { environment: { url: 'http://review/$CI_COMMIT_REF_NAME' } }) end - context 'and environment exist' do - let!(:environment) { create(:environment, project: project, name: 'review-apps/feature-review-apps') } + it { is_expected.to eq('http://review/master') } + end - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end + context 'when yaml environment uses $CI_ENVIRONMENT_SLUG' do + let(:job) do + create(:ci_build, + ref: 'master', + environment: 'production', + options: { environment: { url: 'http://review/$CI_ENVIRONMENT_SLUG' } }) + end - it 'updates external url' do - subject + let!(:environment) do + create(:environment, + project: job.project, + name: 'production', + slug: 'prod-slug', + external_url: 'http://review/old') + end - expect(subject.environment.name).to eq('review-apps/feature-review-apps') - expect(subject.environment.external_url).to eq('http://feature-review-apps.review-apps.gitlab.com') - end + it { is_expected.to eq('http://review/prod-slug') } + end - it 'does create a new deployment' do - expect(subject).to be_persisted - end + context 'when yaml environment uses yaml_variables containing symbol keys' do + let(:job) do + create(:ci_build, + yaml_variables: [{ key: :APP_HOST, value: 'host' }], + options: { environment: { url: 'http://review/$APP_HOST' } }) end + + it { is_expected.to eq('http://review/host') } end - context 'when project was removed' do - let(:project) { nil } + context 'when yaml environment does not have url' do + let(:job) { create(:ci_build, environment: 'staging') } - it 'does not create deployment or environment' do - expect { subject }.not_to raise_error + let!(:environment) do + create(:environment, project: job.project, name: job.environment) + end - expect(Environment.count).to be_zero - expect(Deployment.count).to be_zero + it 'returns the external_url from persisted environment' do + is_expected.to be_nil end end end describe 'processing of builds' do - let(:environment) { nil } - - shared_examples 'does not create environment and deployment' do - it 'does not create a new environment' do - expect { subject }.not_to change { Environment.count } - end - + shared_examples 'does not create deployment' do it 'does not create a new deployment' do expect { subject }.not_to change { Deployment.count } end it 'does not call a service' do expect_any_instance_of(described_class).not_to receive(:execute) + subject end end - shared_examples 'does create environment and deployment' do - it 'does create a new environment' do - expect { subject }.to change { Environment.count }.by(1) - end - - it 'does create a new deployment' do + shared_examples 'creates deployment' do + it 'creates a new deployment' do expect { subject }.to change { Deployment.count }.by(1) end - it 'does call a service' do + it 'calls a service' do expect_any_instance_of(described_class).to receive(:execute) + subject end @@ -199,7 +211,7 @@ describe CreateDeploymentService, services: true do expect(Deployment.last.deployable).to eq(deployable) end - it 'create environment has URL set' do + it 'updates environment URL' do subject expect(Deployment.last.environment.external_url).not_to be_nil @@ -207,41 +219,41 @@ describe CreateDeploymentService, services: true do end context 'without environment specified' do - let(:build) { create(:ci_build, project: project) } + let(:job) { create(:ci_build) } - it_behaves_like 'does not create environment and deployment' do - subject { build.success } + it_behaves_like 'does not create deployment' do + subject { job.success } end end context 'when environment is specified' do - let(:pipeline) { create(:ci_pipeline, project: project) } - let(:build) { create(:ci_build, pipeline: pipeline, environment: 'production', options: options) } + let(:deployable) { job } + let(:options) do { environment: { name: 'production', url: 'http://gitlab.com' } } end - context 'when build succeeds' do - it_behaves_like 'does create environment and deployment' do - let(:deployable) { build } - - subject { build.success } + context 'when job succeeds' do + it_behaves_like 'creates deployment' do + subject { job.success } end end - context 'when build fails' do - it_behaves_like 'does not create environment and deployment' do - subject { build.drop } + context 'when job fails' do + it_behaves_like 'does not create deployment' do + subject { job.drop } end end - context 'when build is retried' do - it_behaves_like 'does create environment and deployment' do + context 'when job is retried' do + it_behaves_like 'creates deployment' do before do + stub_not_protect_default_branch + project.add_developer(user) end - let(:deployable) { Ci::Build.retry(build, user) } + let(:deployable) { Ci::Build.retry(job, user) } subject { deployable.success } end @@ -250,19 +262,12 @@ describe CreateDeploymentService, services: true do end describe "merge request metrics" do - let(:params) do - { - environment: 'production', - ref: 'master', - tag: false, - sha: '97de212e80737a608d939f648d959671fb0a0142b', - } - end - let(:merge_request) { create(:merge_request, target_branch: 'master', source_branch: 'feature', source_project: project) } context "while updating the 'first_deployed_to_production_at' time" do - before { merge_request.mark_as_merged } + before do + merge_request.mark_as_merged + end context "for merge requests merged before the current deploy" do it "sets the time if the deploy's environment is 'production'" do @@ -273,8 +278,8 @@ describe CreateDeploymentService, services: true do end it "doesn't set the time if the deploy's environment is not 'production'" do - staging_params = params.merge(environment: 'staging') - service = described_class.new(project, user, staging_params) + job.update(environment: 'staging') + service = described_class.new(job) service.execute expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil @@ -298,7 +303,7 @@ describe CreateDeploymentService, services: true do expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) # Current deploy - service = described_class.new(project, user, params) + service = described_class.new(job) Timecop.freeze(time + 12.hours) { service.execute } expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_like_time(time) @@ -318,7 +323,7 @@ describe CreateDeploymentService, services: true do expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil # Current deploy - service = described_class.new(project, user, params) + service = described_class.new(job) Timecop.freeze(time + 12.hours) { service.execute } expect(merge_request.reload.metrics.first_deployed_to_production_at).to be_nil diff --git a/spec/services/create_release_service_spec.rb b/spec/services/create_release_service_spec.rb index 271ccfe7968..ac0a0458f56 100644 --- a/spec/services/create_release_service_spec.rb +++ b/spec/services/create_release_service_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe CreateReleaseService, services: true do +describe CreateReleaseService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } - let(:service) { CreateReleaseService.new(project, user) } + let(:service) { described_class.new(project, user) } it 'creates a new release' do result = service.execute(tag_name, description) diff --git a/spec/services/create_snippet_service_spec.rb b/spec/services/create_snippet_service_spec.rb index d81d0fd76c9..b6ab6b8271c 100644 --- a/spec/services/create_snippet_service_spec.rb +++ b/spec/services/create_snippet_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe CreateSnippetService, services: true do +describe CreateSnippetService do before do @user = create :user @admin = create :user, admin: true diff --git a/spec/services/delete_branch_service_spec.rb b/spec/services/delete_branch_service_spec.rb index c4685c9aa31..19855c9bee2 100644 --- a/spec/services/delete_branch_service_spec.rb +++ b/spec/services/delete_branch_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe DeleteBranchService, services: true do +describe DeleteBranchService do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/delete_merged_branches_service_spec.rb b/spec/services/delete_merged_branches_service_spec.rb index 7b921f606f8..03c682ae0d7 100644 --- a/spec/services/delete_merged_branches_service_spec.rb +++ b/spec/services/delete_merged_branches_service_spec.rb @@ -1,38 +1,43 @@ require 'spec_helper' -describe DeleteMergedBranchesService, services: true do +describe DeleteMergedBranchesService do subject(:service) { described_class.new(project, project.owner) } let(:project) { create(:project, :repository) } context '#execute' do - context 'unprotected branches' do - before do - service.execute - end + it 'deletes a branch that was merged' do + service.execute - it 'deletes a branch that was merged' do - expect(project.repository.branch_names).not_to include('improve/awesome') - end + expect(project.repository.branch_names).not_to include('improve/awesome') + end - it 'keeps branch that is unmerged' do - expect(project.repository.branch_names).to include('feature') - end + it 'keeps branch that is unmerged' do + service.execute - it 'keeps "master"' do - expect(project.repository.branch_names).to include('master') - end + expect(project.repository.branch_names).to include('feature') end - context 'protected branches' do - before do - create(:protected_branch, name: 'improve/awesome', project: project) - service.execute - end + it 'keeps "master"' do + service.execute - it 'keeps protected branch' do - expect(project.repository.branch_names).to include('improve/awesome') - end + expect(project.repository.branch_names).to include('master') + end + + it 'keeps protected branches' do + create(:protected_branch, project: project, name: 'improve/awesome') + + service.execute + + expect(project.repository.branch_names).to include('improve/awesome') + end + + it 'keeps wildcard protected branches' do + create(:protected_branch, project: project, name: 'improve/*') + + service.execute + + expect(project.repository.branch_names).to include('improve/awesome') end context 'user without rights' do @@ -46,7 +51,7 @@ describe DeleteMergedBranchesService, services: true do context 'open merge requests' do it 'does not delete branches from open merge requests' do fork_link = create(:forked_project_link, forked_from_project: project) - create(:merge_request, :reopened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') + create(:merge_request, :opened, source_project: project, target_project: project, source_branch: 'branch-merged', target_branch: 'master') create(:merge_request, :opened, source_project: fork_link.forked_to_project, target_project: project, target_branch: 'improve/awesome', source_branch: 'master') service.execute diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb index d73ae51fbc3..c239494298b 100644 --- a/spec/services/notes/diff_position_update_service_spec.rb +++ b/spec/services/discussions/update_diff_position_service_spec.rb @@ -1,7 +1,8 @@ require 'spec_helper' -describe Notes::DiffPositionUpdateService, services: true do +describe Discussions::UpdateDiffPositionService do let(:project) { create(:project, :repository) } + let(:current_user) { project.owner } let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") } let(:modify_commit) { project.commit("874797c3a73b60d2187ed6e2fcabd289ff75171e") } let(:edit_commit) { project.commit("570e7b2abdd848b95f2f578043fc23bd6f6fd24d") } @@ -25,7 +26,7 @@ describe Notes::DiffPositionUpdateService, services: true do subject do described_class.new( project, - nil, + current_user, old_diff_refs: old_diff_refs, new_diff_refs: new_diff_refs, paths: [path] @@ -137,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do # .. .. describe "#execute" do - let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) } + let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion } let(:old_position) do Gitlab::Diff::Position.new( @@ -153,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do let(:line) { 16 } it "updates the position" do - subject.execute(note) + subject.execute(discussion) - expect(note.original_position).to eq(old_position) - expect(note.position).not_to eq(old_position) - expect(note.position.new_line).to eq(22) + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).not_to eq(old_position) + expect(discussion.position.new_line).to eq(22) end end @@ -165,10 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do let(:line) { 9 } it "doesn't update the position" do - subject.execute(note) + subject.execute(discussion) - expect(note.original_position).to eq(old_position) - expect(note.position).to eq(old_position) + expect(discussion.original_position).to eq(old_position) + expect(discussion.position).to eq(old_position) + end + + it 'sets the change position' do + subject.execute(discussion) + + change_position = discussion.change_position + expect(change_position.start_sha).to eq(old_diff_refs.head_sha) + expect(change_position.head_sha).to eq(new_diff_refs.head_sha) + expect(change_position.old_line).to eq(9) + expect(change_position.new_line).to be_nil + end + + it 'creates a system discussion' do + expect(SystemNoteService).to receive(:diff_discussion_outdated).with( + discussion, project, current_user, instance_of(Gitlab::Diff::Position)) + + subject.execute(discussion) end end end diff --git a/spec/services/emails/create_service_spec.rb b/spec/services/emails/create_service_spec.rb new file mode 100644 index 00000000000..641d5538de8 --- /dev/null +++ b/spec/services/emails/create_service_spec.rb @@ -0,0 +1,21 @@ +require 'spec_helper' + +describe Emails::CreateService do + let(:user) { create(:user) } + let(:opts) { { email: 'new@email.com' } } + + subject(:service) { described_class.new(user, opts) } + + describe '#execute' do + it 'creates an email with valid attributes' do + expect { service.execute }.to change { Email.count }.by(1) + expect(Email.where(opts)).not_to be_empty + end + + it 'has the right user association' do + service.execute + + expect(user.emails).to eq(Email.where(opts)) + end + end +end diff --git a/spec/services/emails/destroy_service_spec.rb b/spec/services/emails/destroy_service_spec.rb new file mode 100644 index 00000000000..1f4294dd905 --- /dev/null +++ b/spec/services/emails/destroy_service_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe Emails::DestroyService do + let!(:user) { create(:user) } + let!(:email) { create(:email, user: user) } + + subject(:service) { described_class.new(user, email: email.email) } + + describe '#execute' do + it 'removes an email' do + expect { service.execute }.to change { user.emails.count }.by(-1) + end + end +end diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb index b06cefe071d..02d7ddeb86b 100644 --- a/spec/services/event_create_service_spec.rb +++ b/spec/services/event_create_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe EventCreateService, services: true do +describe EventCreateService do include UserActivitiesHelpers - let(:service) { EventCreateService.new } + let(:service) { described_class.new } describe 'Issues' do describe '#open_issue' do @@ -113,22 +113,62 @@ describe EventCreateService, services: true do end end - describe '#push', :redis do - let(:project) { create(:empty_project) } + describe '#push', :clean_gitlab_redis_shared_state do + let(:project) { create(:project) } let(:user) { create(:user) } + let(:push_data) do + { + commits: [ + { + id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + message: 'This is a commit' + } + ], + before: '0000000000000000000000000000000000000000', + after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + total_commits_count: 1, + ref: 'refs/heads/my-branch' + } + end + it 'creates a new event' do - expect { service.push(project, user, {}) }.to change { Event.count } + expect { service.push(project, user, push_data) }.to change { Event.count } + end + + it 'creates the push event payload' do + expect(PushEventPayloadService).to receive(:new) + .with(an_instance_of(PushEvent), push_data) + .and_call_original + + service.push(project, user, push_data) end it 'updates user last activity' do - expect { service.push(project, user, {}) }.to change { user_activity(user) } + expect { service.push(project, user, push_data) } + .to change { user_activity(user) } + end + + it 'does not create any event data when an error is raised' do + payload_service = double(:service) + + allow(payload_service).to receive(:execute) + .and_raise(RuntimeError) + + allow(PushEventPayloadService).to receive(:new) + .and_return(payload_service) + + expect { service.push(project, user, push_data) } + .to raise_error(RuntimeError) + + expect(Event.count).to eq(0) + expect(PushEventPayload.count).to eq(0) end end describe 'Project' do let(:user) { create :user } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } describe '#join_project' do subject { service.join_project(project, user) } diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb index 16bca66766a..cc950ae6bb3 100644 --- a/spec/services/files/update_service_spec.rb +++ b/spec/services/files/update_service_spec.rb @@ -32,8 +32,8 @@ describe Files::UpdateService do let(:last_commit_sha) { "foo" } it "returns a hash with the correct error message and a :error status " do - expect { subject.execute }. - to raise_error(Files::UpdateService::FileChangedError, + expect { subject.execute } + .to raise_error(Files::UpdateService::FileChangedError, "You are attempting to update a file that has changed since you started editing it.") end end diff --git a/spec/services/git_hooks_service_spec.rb b/spec/services/git_hooks_service_spec.rb index ac7ccfbaab0..3ce01a995b4 100644 --- a/spec/services/git_hooks_service_spec.rb +++ b/spec/services/git_hooks_service_spec.rb @@ -1,18 +1,17 @@ require 'spec_helper' -describe GitHooksService, services: true do +describe GitHooksService do include RepoHelpers let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:service) { GitHooksService.new } + let(:service) { described_class.new } before do @blankrev = Gitlab::Git::BLANK_SHA @oldrev = sample_commit.parent_id @newrev = sample_commit.id @ref = 'refs/heads/feature' - @repo_path = project.repository.path_to_repo end describe '#execute' do @@ -21,7 +20,7 @@ describe GitHooksService, services: true do hook = double(trigger: [true, nil]) expect(Gitlab::Git::Hook).to receive(:new).exactly(3).times.and_return(hook) - service.execute(user, @repo_path, @blankrev, @newrev, @ref) { } + service.execute(user, project, @blankrev, @newrev, @ref) { } end end @@ -31,7 +30,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end @@ -43,7 +42,7 @@ describe GitHooksService, services: true do expect(service).not_to receive(:run_hook).with('post-receive') expect do - service.execute(user, @repo_path, @blankrev, @newrev, @ref) + service.execute(user, project, @blankrev, @newrev, @ref) end.to raise_error(GitHooksService::PreReceiveError) end end diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb index 0477cac6677..e3c1bdce300 100644 --- a/spec/services/git_push_service_spec.rb +++ b/spec/services/git_push_service_spec.rb @@ -3,27 +3,24 @@ require 'spec_helper' describe GitPushService, services: true do include RepoHelpers - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:project) { create(:project, :repository) } + let(:blankrev) { Gitlab::Git::BLANK_SHA } + let(:oldrev) { sample_commit.parent_id } + let(:newrev) { sample_commit.id } + let(:ref) { 'refs/heads/master' } before do project.team << [user, :master] - @blankrev = Gitlab::Git::BLANK_SHA - @oldrev = sample_commit.parent_id - @newrev = sample_commit.id - @ref = 'refs/heads/master' end describe 'Push branches' do - let(:oldrev) { @oldrev } - let(:newrev) { @newrev } - subject do - execute_service(project, user, oldrev, newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) end context 'new branch' do - let(:oldrev) { @blankrev } + let(:oldrev) { blankrev } it { is_expected.to be_truthy } @@ -51,7 +48,7 @@ describe GitPushService, services: true do end context 'rm branch' do - let(:newrev) { @blankrev } + let(:newrev) { blankrev } it { is_expected.to be_truthy } @@ -70,24 +67,20 @@ describe GitPushService, services: true do end describe "Git Push Data" do - before do - service = execute_service(project, user, @oldrev, @newrev, @ref ) - @push_data = service.push_data - @commit = project.commit(@newrev) - end + let(:commit) { project.commit(newrev) } - subject { @push_data } + subject { push_data_from_service(project, user, oldrev, newrev, ref) } it { is_expected.to include(object_kind: 'push') } - it { is_expected.to include(before: @oldrev) } - it { is_expected.to include(after: @newrev) } - it { is_expected.to include(ref: @ref) } + it { is_expected.to include(before: oldrev) } + it { is_expected.to include(after: newrev) } + it { is_expected.to include(ref: ref) } it { is_expected.to include(user_id: user.id) } it { is_expected.to include(user_name: user.name) } it { is_expected.to include(project_id: project.id) } context "with repository data" do - subject { @push_data[:repository] } + subject { push_data_from_service(project, user, oldrev, newrev, ref)[:repository] } it { is_expected.to include(name: project.name) } it { is_expected.to include(url: project.url_to_repo) } @@ -96,7 +89,7 @@ describe GitPushService, services: true do end context "with commits" do - subject { @push_data[:commits] } + subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits] } it { is_expected.to be_an(Array) } it 'has 1 element' do @@ -104,11 +97,11 @@ describe GitPushService, services: true do end context "the commit" do - subject { @push_data[:commits].first } + subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first } - it { is_expected.to include(id: @commit.id) } - it { is_expected.to include(message: @commit.safe_message) } - it { is_expected.to include(timestamp: @commit.date.xmlschema) } + it { is_expected.to include(id: commit.id) } + it { is_expected.to include(message: commit.safe_message) } + it { expect(subject[:timestamp].in_time_zone).to eq(commit.date.in_time_zone) } it do is_expected.to include( url: [ @@ -116,45 +109,58 @@ describe GitPushService, services: true do project.namespace.to_param, project.to_param, 'commit', - @commit.id + commit.id ].join('/') ) end context "with a author" do - subject { @push_data[:commits].first[:author] } + subject { push_data_from_service(project, user, oldrev, newrev, ref)[:commits].first[:author] } - it { is_expected.to include(name: @commit.author_name) } - it { is_expected.to include(email: @commit.author_email) } + it { is_expected.to include(name: commit.author_name) } + it { is_expected.to include(email: commit.author_email) } end end end end - describe "Push Event" do + describe "Pipelines" do + subject { execute_service(project, user, oldrev, newrev, ref) } + before do - service = execute_service(project, user, @oldrev, @newrev, @ref ) - @event = Event.find_by_action(Event::PUSHED) - @push_data = service.push_data + stub_ci_pipeline_to_return_yaml_file + end + + it "creates a new pipeline" do + expect { subject }.to change { Ci::Pipeline.count } + expect(Ci::Pipeline.last).to be_push end + end + + describe "Push Event" do + let!(:push_data) { push_data_from_service(project, user, oldrev, newrev, ref) } + let(:event) { Event.find_by_action(Event::PUSHED) } - it { expect(@event).not_to be_nil } - it { expect(@event.project).to eq(project) } - it { expect(@event.action).to eq(Event::PUSHED) } - it { expect(@event.data).to eq(@push_data) } + it { expect(event).to be_an_instance_of(PushEvent) } + it { expect(event.project).to eq(project) } + it { expect(event.action).to eq(Event::PUSHED) } + it { expect(event.push_event_payload).to be_an_instance_of(PushEventPayload) } + it { expect(event.push_event_payload.commit_from).to eq(oldrev) } + it { expect(event.push_event_payload.commit_to).to eq(newrev) } + it { expect(event.push_event_payload.ref).to eq('master') } context "Updates merge requests" do it "when pushing a new branch for the first time" do - expect(UpdateMergeRequestsWorker).to receive(:perform_async). - with(project.id, user.id, @blankrev, 'newrev', 'refs/heads/master') - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + expect(UpdateMergeRequestsWorker).to receive(:perform_async) + .with(project.id, user.id, blankrev, 'newrev', ref) + execute_service(project, user, blankrev, 'newrev', ref ) end end - + context "Sends System Push data" do it "when pushing on a branch" do - expect(SystemHookPushWorker).to receive(:perform_async).with(@push_data, :push_hooks) - execute_service(project, user, @oldrev, @newrev, @ref ) + expect(SystemHookPushWorker).to receive(:perform_async).with(push_data, :push_hooks) + execute_service(project, user, oldrev, newrev, ref) end end end @@ -164,13 +170,13 @@ describe GitPushService, services: true do it "calls the copy attributes method for the first push to the default branch" do expect(project.repository).to receive(:copy_gitattributes).with('master') - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master') + execute_service(project, user, blankrev, 'newrev', ref) end it "calls the copy attributes method for changes to the default branch" do - expect(project.repository).to receive(:copy_gitattributes).with('refs/heads/master') + expect(project.repository).to receive(:copy_gitattributes).with(ref) - execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master') + execute_service(project, user, 'oldrev', 'newrev', ref) end end @@ -183,7 +189,7 @@ describe GitPushService, services: true do it "does not call copy attributes method" do expect(project.repository).not_to receive(:copy_gitattributes) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) end end end @@ -193,7 +199,7 @@ describe GitPushService, services: true do it "when pushing a branch for the first time" do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + execute_service(project, user, blankrev, 'newrev', ref) expect(project.protected_branches).not_to be_empty expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) @@ -204,7 +210,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + execute_service(project, user, blankrev, 'newrev', ref) expect(project.protected_branches).to be_empty end @@ -214,7 +220,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + execute_service(project, user, blankrev, 'newrev', ref) expect(project.protected_branches).not_to be_empty expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) @@ -229,7 +235,7 @@ describe GitPushService, services: true do expect(project.default_branch).to eq("master") expect_any_instance_of(ProtectedBranches::CreateService).not_to receive(:execute) - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + execute_service(project, user, blankrev, 'newrev', ref) expect(project.protected_branches).not_to be_empty expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::NO_ACCESS]) @@ -241,7 +247,7 @@ describe GitPushService, services: true do expect(project).to receive(:execute_hooks) expect(project.default_branch).to eq("master") - execute_service(project, user, @blankrev, 'newrev', 'refs/heads/master' ) + execute_service(project, user, blankrev, 'newrev', ref) expect(project.protected_branches).not_to be_empty expect(project.protected_branches.first.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MASTER]) expect(project.protected_branches.first.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::DEVELOPER]) @@ -249,7 +255,7 @@ describe GitPushService, services: true do it "when pushing new commits to existing branch" do expect(project).to receive(:execute_hooks) - execute_service(project, user, 'oldrev', 'newrev', 'refs/heads/master' ) + execute_service(project, user, 'oldrev', 'newrev', ref) end end end @@ -270,8 +276,8 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) end @@ -279,7 +285,7 @@ describe GitPushService, services: true do it "creates a note if a pushed commit mentions an issue" do expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) end it "only creates a cross-reference note if one doesn't already exist" do @@ -287,7 +293,7 @@ describe GitPushService, services: true do expect(SystemNoteService).not_to receive(:cross_reference).with(issue, commit, commit_author) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) end it "defaults to the pushing user if the commit's author is not known" do @@ -297,16 +303,16 @@ describe GitPushService, services: true do ) expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, user) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) end it "finds references in the first push to a non-default branch" do - allow(project.repository).to receive(:commits_between).with(@blankrev, @newrev).and_return([]) - allow(project.repository).to receive(:commits_between).with("master", @newrev).and_return([commit]) + allow(project.repository).to receive(:commits_between).with(blankrev, newrev).and_return([]) + allow(project.repository).to receive(:commits_between).with("master", newrev).and_return([commit]) expect(SystemNoteService).to receive(:cross_reference).with(issue, commit, commit_author) - execute_service(project, user, @blankrev, @newrev, 'refs/heads/other' ) + execute_service(project, user, blankrev, newrev, 'refs/heads/other') end end @@ -328,22 +334,22 @@ describe GitPushService, services: true do committed_date: commit_time ) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(commit) allow(project.repository).to receive(:commits_between).and_return([commit]) end context "while saving the 'first_mentioned_in_commit_at' metric for an issue" do it 'sets the metric for referenced issues' do - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) expect(issue.reload.metrics.first_mentioned_in_commit_at).to be_like_time(commit_time) end it 'does not set the metric for non-referenced issues' do non_referenced_issue = create(:issue, project: project) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) expect(non_referenced_issue.reload.metrics.first_mentioned_in_commit_at).to be_nil end @@ -364,41 +370,29 @@ describe GitPushService, services: true do author_email: commit_author.email ) - allow(project.repository).to receive(:commits_between). - and_return([closing_commit]) + allow(project.repository).to receive(:commits_between) + .and_return([closing_commit]) - allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit). - and_return(closing_commit) + allow_any_instance_of(ProcessCommitWorker).to receive(:build_commit) + .and_return(closing_commit) project.team << [commit_author, :master] end context "to default branches" do it "closes issues" do - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, oldrev, newrev, ref) expect(Issue.find(issue.id)).to be_closed end it "adds a note indicating that the issue is now closed" do expect(SystemNoteService).to receive(:change_status).with(issue, project, commit_author, "closed", closing_commit) - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, oldrev, newrev, ref) end it "doesn't create additional cross-reference notes" do expect(SystemNoteService).not_to receive(:cross_reference) - execute_service(project, commit_author, @oldrev, @newrev, @ref ) - end - - it "doesn't close issues when external issue tracker is in use" do - allow_any_instance_of(Project).to receive(:default_issues_tracker?). - and_return(false) - external_issue_tracker = double(title: 'My Tracker', issue_path: issue.iid, reference_pattern: project.issue_reference_pattern) - allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(external_issue_tracker) - - # The push still shouldn't create cross-reference notes. - expect do - execute_service(project, commit_author, @oldrev, @newrev, 'refs/heads/hurf' ) - end.not_to change { Note.where(project_id: project.id, system: true).count } + execute_service(project, commit_author, oldrev, newrev, ref) end end @@ -410,11 +404,11 @@ describe GitPushService, services: true do it "creates cross-reference notes" do expect(SystemNoteService).to receive(:cross_reference).with(issue, closing_commit, commit_author) - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) end it "doesn't close issues" do - execute_service(project, user, @oldrev, @newrev, @ref ) + execute_service(project, user, oldrev, newrev, ref) expect(Issue.find(issue.id)).to be_opened end end @@ -431,11 +425,13 @@ describe GitPushService, services: true do stub_jira_urls("JIRA-1") allow(closing_commit).to receive_messages({ - issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), - safe_message: message, - author_name: commit_author.name, - author_email: commit_author.email - }) + issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern), + safe_message: message, + author_name: commit_author.name, + author_email: commit_author.email + }) + + allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) allow(project.repository).to receive_messages(commits_between: [closing_commit]) end @@ -448,7 +444,7 @@ describe GitPushService, services: true do let(:message) { "this is some work.\n\nrelated to JIRA-1" } it "initiates one api call to jira server to mention the issue" do - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: /mentioned this issue in/ @@ -458,7 +454,11 @@ describe GitPushService, services: true do context "closing an issue" do let(:message) { "this is some work.\n\ncloses JIRA-1" } - let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json } + let(:comment_body) do + { + body: "Issue solved with [#{closing_commit.id}|http://#{Gitlab.config.gitlab.host}/#{project.full_path}/commit/#{closing_commit.id}]." + }.to_json + end before do open_issue = JIRA::Resource::Issue.new(jira_tracker.client, attrs: { "id" => "JIRA-1" }) @@ -472,13 +472,13 @@ describe GitPushService, services: true do context "using right markdown" do it "initiates one api call to jira server to close the issue" do - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, oldrev, newrev, ref) expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once end it "initiates one api call to jira server to comment on the issue" do - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + execute_service(project, commit_author, oldrev, newrev, ref) expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( body: comment_body @@ -486,21 +486,57 @@ describe GitPushService, services: true do end end - context "using wrong markdown" do - let(:message) { "this is some work.\n\ncloses #1" } + context "using internal issue reference" do + context 'when internal issues are disabled' do + before do + project.issues_enabled = false + project.save! + end + let(:message) { "this is some work.\n\ncloses #1" } + + it "does not initiates one api call to jira server to close the issue" do + execute_service(project, commit_author, oldrev, newrev, ref) + + expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1')) + end - it "does not initiates one api call to jira server to close the issue" do - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + it "does not initiates one api call to jira server to comment on the issue" do + execute_service(project, commit_author, oldrev, newrev, ref) - expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1')) + expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with( + body: comment_body + ).once + end end - it "does not initiates one api call to jira server to comment on the issue" do - execute_service(project, commit_author, @oldrev, @newrev, @ref ) + context 'when internal issues are enabled' do + let(:issue) { create(:issue, project: project) } + let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" } - expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with( - body: comment_body - ).once + it "initiates one api call to jira server to close the jira issue" do + execute_service(project, commit_author, oldrev, newrev, ref) + + expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once + end + + it "initiates one api call to jira server to comment on the jira issue" do + execute_service(project, commit_author, oldrev, newrev, ref) + + expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with( + body: comment_body + ).once + end + + it "closes the internal issue" do + execute_service(project, commit_author, oldrev, newrev, ref) + expect(issue.reload).to be_closed + end + + it "adds a note indicating that the issue is now closed" do + expect(SystemNoteService).to receive(:change_status) + .with(issue, project, commit_author, "closed", closing_commit) + execute_service(project, commit_author, oldrev, newrev, ref) + end end end end @@ -509,7 +545,7 @@ describe GitPushService, services: true do describe "empty project" do let(:project) { create(:project_empty_repo) } - let(:new_ref) { 'refs/heads/feature'} + let(:new_ref) { 'refs/heads/feature' } before do allow(project).to receive(:default_branch).and_return('feature') @@ -517,7 +553,7 @@ describe GitPushService, services: true do end it 'push to first branch updates HEAD' do - execute_service(project, user, @blankrev, @newrev, new_ref ) + execute_service(project, user, blankrev, newrev, new_ref) end end @@ -525,20 +561,24 @@ describe GitPushService, services: true do let(:housekeeping) { Projects::HousekeepingService.new(project) } before do - # Flush any raw Redis data stored by the housekeeping code. - Gitlab::Redis.with { |conn| conn.flushall } + # Flush any raw key-value data stored by the housekeeping code. + Gitlab::Redis::Cache.with { |conn| conn.flushall } + Gitlab::Redis::Queues.with { |conn| conn.flushall } + Gitlab::Redis::SharedState.with { |conn| conn.flushall } allow(Projects::HousekeepingService).to receive(:new).and_return(housekeeping) end after do - Gitlab::Redis.with { |conn| conn.flushall } + Gitlab::Redis::Cache.with { |conn| conn.flushall } + Gitlab::Redis::Queues.with { |conn| conn.flushall } + Gitlab::Redis::SharedState.with { |conn| conn.flushall } end it 'does not perform housekeeping when not needed' do expect(housekeeping).not_to receive(:execute) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) end context 'when housekeeping is needed' do @@ -549,20 +589,20 @@ describe GitPushService, services: true do it 'performs housekeeping' do expect(housekeeping).to receive(:execute) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) end it 'does not raise an exception' do allow(housekeeping).to receive(:try_obtain_lease).and_return(false) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) end end it 'increments the push counter' do expect(housekeeping).to receive(:increment!) - execute_service(project, user, @oldrev, @newrev, @ref) + execute_service(project, user, oldrev, newrev, ref) end end @@ -570,9 +610,9 @@ describe GitPushService, services: true do let(:service) do described_class.new(project, user, - oldrev: sample_commit.parent_id, - newrev: sample_commit.id, - ref: 'refs/heads/master') + oldrev: oldrev, + newrev: newrev, + ref: ref) end context 'on the default branch' do @@ -584,13 +624,13 @@ describe GitPushService, services: true do commit = double(:commit) diff = double(:diff, new_path: 'README.md') - expect(commit).to receive(:raw_diffs).with(deltas_only: true). - and_return([diff]) + expect(commit).to receive(:raw_deltas) + .and_return([diff]) service.push_commits = [commit] - expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, %i(readme), %i(commit_count repository_size)) + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, %i(readme), %i(commit_count repository_size)) service.update_caches end @@ -602,9 +642,9 @@ describe GitPushService, services: true do end it 'does not flush any conditional caches' do - expect(ProjectCacheWorker).to receive(:perform_async). - with(project.id, [], %i(commit_count repository_size)). - and_call_original + expect(ProjectCacheWorker).to receive(:perform_async) + .with(project.id, [], %i(commit_count repository_size)) + .and_call_original service.update_caches end @@ -615,24 +655,81 @@ describe GitPushService, services: true do let(:service) do described_class.new(project, user, - oldrev: sample_commit.parent_id, - newrev: sample_commit.id, - ref: 'refs/heads/master') + oldrev: oldrev, + newrev: newrev, + ref: ref) end it 'only schedules a limited number of commits' do - allow(service).to receive(:push_commits). - and_return(Array.new(1000, double(:commit, to_hash: {}))) + service.push_commits = Array.new(1000, double(:commit, to_hash: {}, matches_cross_reference_regex?: true)) expect(ProcessCommitWorker).to receive(:perform_async).exactly(100).times service.process_commit_messages end + + it "skips commits which don't include cross-references" do + service.push_commits = [double(:commit, to_hash: {}, matches_cross_reference_regex?: false)] + + expect(ProcessCommitWorker).not_to receive(:perform_async) + + service.process_commit_messages + end + end + + describe '#update_signatures' do + let(:service) do + described_class.new( + project, + user, + oldrev: oldrev, + newrev: newrev, + ref: 'refs/heads/master' + ) + end + + context 'when the commit has a signature' do + context 'when the signature is already cached' do + before do + create(:gpg_signature, commit_sha: sample_commit.id) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + + context 'when the signature is not yet cached' do + it 'queues a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end + end + + context 'when the commit does not have a signature' do + before do + allow(Gitlab::Git::Commit).to receive(:shas_with_signatures).with(project.repository, [sample_commit.id]).and_return([]) + end + + it 'does not queue a CreateGpgSignatureWorker' do + expect(CreateGpgSignatureWorker).not_to receive(:perform_async).with(sample_commit.id, project.id) + + execute_service(project, user, oldrev, newrev, ref) + end + end end def execute_service(project, user, oldrev, newrev, ref) - service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref ) + service = described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) service.execute service end + + def push_data_from_service(project, user, oldrev, newrev, ref) + execute_service(project, user, oldrev, newrev, ref).push_data + end end diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb index b73beb3f6fc..05695aa8188 100644 --- a/spec/services/git_tag_push_service_spec.rb +++ b/spec/services/git_tag_push_service_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe GitTagPushService, services: true do +describe GitTagPushService do include RepoHelpers let(:user) { create(:user) } let(:project) { create(:project, :repository) } - let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } + let(:service) { described_class.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) } let(:oldrev) { Gitlab::Git::BLANK_SHA } let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0 @@ -30,6 +30,20 @@ describe GitTagPushService, services: true do end end + describe "Pipelines" do + subject { service.execute } + + before do + stub_ci_pipeline_to_return_yaml_file + project.team << [user, :developer] + end + + it "creates a new pipeline" do + expect { subject }.to change { Ci::Pipeline.count } + expect(Ci::Pipeline.last).to be_push + end + end + describe "Git Tag Push Data" do subject { @push_data } let(:tag) { project.repository.find_tag(tag_name) } @@ -170,7 +184,7 @@ describe GitTagPushService, services: true do describe "Webhooks" do context "execute webhooks" do - let(:service) { GitTagPushService.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') } + let(:service) { described_class.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') } it "when pushing tags" do expect(project).to receive(:execute_hooks) diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb new file mode 100644 index 00000000000..d2cc53fe0ee --- /dev/null +++ b/spec/services/gravatar_service_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +describe GravatarService do + describe '#execute' do + let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' } + + before do + allow(Gitlab.config.gravatar).to receive(:plain_url).and_return(url) + end + + it 'replaces the placeholders' do + avatar_url = described_class.new.execute('user@example.com', 100, 2, username: 'user') + + expect(avatar_url).to include("hash=#{Digest::MD5.hexdigest('user@example.com')}") + expect(avatar_url).to include("size=200") + expect(avatar_url).to include("email=user%40example.com") + expect(avatar_url).to include("username=user") + end + end +end diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb index bcb62429275..b2175717a70 100644 --- a/spec/services/groups/create_service_spec.rb +++ b/spec/services/groups/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Groups::CreateService, '#execute', services: true do +describe Groups::CreateService, '#execute' do let!(:user) { create(:user) } let!(:group_params) { { path: "group_path", visibility_level: Gitlab::VisibilityLevel::PUBLIC } } @@ -14,7 +14,9 @@ describe Groups::CreateService, '#execute', services: true do end context "cannot create group with restricted visibility level" do - before { allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) } + before do + allow_any_instance_of(ApplicationSetting).to receive(:restricted_visibility_levels).and_return([Gitlab::VisibilityLevel::PUBLIC]) + end it { is_expected.not_to be_persisted } end @@ -25,7 +27,9 @@ describe Groups::CreateService, '#execute', services: true do let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) } context 'as group owner' do - before { group.add_owner(user) } + before do + group.add_owner(user) + end it { is_expected.to be_persisted } end diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb index a37257d1bf4..1b2ce3cd03e 100644 --- a/spec/services/groups/destroy_service_spec.rb +++ b/spec/services/groups/destroy_service_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe Groups::DestroyService, services: true do +describe Groups::DestroyService do include DatabaseConnectionHelpers let!(:user) { create(:user) } let!(:group) { create(:group) } let!(:nested_group) { create(:group, parent: group) } - let!(:project) { create(:empty_project, namespace: group) } + let!(:project) { create(:project, namespace: group) } let!(:notification_setting) { create(:notification_setting, source: group)} let!(:gitlab_shell) { Gitlab::Shell.new } let!(:remove_path) { group.path + "+#{group.id}+deleted" } @@ -15,6 +15,14 @@ describe Groups::DestroyService, services: true do group.add_user(user, Gitlab::Access::OWNER) end + def destroy_group(group, user, async) + if async + Groups::DestroyService.new(group, user).async_execute + else + Groups::DestroyService.new(group, user).execute + end + end + shared_examples 'group destruction' do |async| context 'database records' do before do @@ -27,33 +35,27 @@ describe Groups::DestroyService, services: true do it { expect(NotificationSetting.unscoped.all).not_to include(notification_setting) } end + context 'mattermost team' do + let!(:chat_team) { create(:chat_team, namespace: group) } + + it 'destroys the team too' do + expect_any_instance_of(Mattermost::Team).to receive(:destroy) + + destroy_group(group, user, async) + end + end + context 'file system' do context 'Sidekiq inline' do before do - # Run sidekiq immediatly to check that renamed dir will be removed + # Run sidekiq immediately to check that renamed dir will be removed Sidekiq::Testing.inline! { destroy_group(group, user, async) } end - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey } - end - - context 'Sidekiq fake' do - before do - # Don't run sidekiq to check if renamed repository exists - Sidekiq::Testing.fake! { destroy_group(group, user, async) } + it 'verifies that paths have been deleted' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey end - - it { expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_falsey } - it { expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_truthy } - end - end - - def destroy_group(group, user, async) - if async - Groups::DestroyService.new(group, user).async_execute - else - Groups::DestroyService.new(group, user).execute end end end @@ -61,6 +63,26 @@ describe Groups::DestroyService, services: true do describe 'asynchronous delete' do it_behaves_like 'group destruction', true + context 'Sidekiq fake' do + before do + # Don't run Sidekiq to verify that group and projects are not actually destroyed + Sidekiq::Testing.fake! { destroy_group(group, user, true) } + end + + after do + # Clean up stale directories + gitlab_shell.rm_namespace(project.repository_storage_path, group.path) + gitlab_shell.rm_namespace(project.repository_storage_path, remove_path) + end + + it 'verifies original paths and projects still exist' do + expect(gitlab_shell.exists?(project.repository_storage_path, group.path)).to be_truthy + expect(gitlab_shell.exists?(project.repository_storage_path, remove_path)).to be_falsey + expect(Project.unscoped.count).to eq(1) + expect(Group.unscoped.count).to eq(2) + end + end + context 'potential race conditions' do context "when the `GroupDestroyWorker` task runs immediately" do it "deletes the group" do @@ -88,7 +110,7 @@ describe Groups::DestroyService, services: true do # Kick off the initial group destroy in a new thread, so that # it doesn't share this spec's database transaction. - Thread.new { Groups::DestroyService.new(group, user).async_execute }.join(5) + Thread.new { described_class.new(group, user).async_execute }.join(5) group_record = run_with_new_database_connection do |conn| conn.execute("SELECT * FROM namespaces WHERE id = #{group.id}").first diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb index f6ad5cebd2c..44f22a3b37b 100644 --- a/spec/services/groups/update_service_spec.rb +++ b/spec/services/groups/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Groups::UpdateService, services: true do +describe Groups::UpdateService do let!(:user) { create(:user) } let!(:private_group) { create(:group, :private) } let!(:internal_group) { create(:group, :internal) } @@ -13,7 +13,7 @@ describe Groups::UpdateService, services: true do before do public_group.add_user(user, Gitlab::Access::MASTER) - create(:empty_project, :public, group: public_group) + create(:project, :public, group: public_group) end it "does not change permission level" do @@ -27,7 +27,7 @@ describe Groups::UpdateService, services: true do before do internal_group.add_user(user, Gitlab::Access::MASTER) - create(:empty_project, :internal, group: internal_group) + create(:project, :internal, group: internal_group) end it "does not change permission level" do @@ -69,7 +69,7 @@ describe Groups::UpdateService, services: true do before do internal_group.add_user(user, Gitlab::Access::MASTER) - create(:empty_project, :internal, group: internal_group) + create(:project, :internal, group: internal_group) end it 'returns true' do diff --git a/spec/services/import_export_clean_up_service_spec.rb b/spec/services/import_export_clean_up_service_spec.rb index 81b1d327696..1875d0448cd 100644 --- a/spec/services/import_export_clean_up_service_spec.rb +++ b/spec/services/import_export_clean_up_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ImportExportCleanUpService, services: true do +describe ImportExportCleanUpService do describe '#execute' do let(:service) { described_class.new } diff --git a/spec/services/issuable/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb index 7a1ac027310..bdaab88d673 100644 --- a/spec/services/issuable/bulk_update_service_spec.rb +++ b/spec/services/issuable/bulk_update_service_spec.rb @@ -1,14 +1,15 @@ require 'spec_helper' -describe Issuable::BulkUpdateService, services: true do +describe Issuable::BulkUpdateService do let(:user) { create(:user) } - let(:project) { create(:empty_project, namespace: user.namespace) } + let(:project) { create(:project, namespace: user.namespace) } - def bulk_update(issues, extra_params = {}) + def bulk_update(issuables, extra_params = {}) bulk_update_params = extra_params - .reverse_merge(issuable_ids: Array(issues).map(&:id).join(',')) + .reverse_merge(issuable_ids: Array(issuables).map(&:id).join(',')) - Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue') + type = Array(issuables).first.model_name.param_key + Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute(type) end describe 'close issues' do @@ -47,40 +48,77 @@ describe Issuable::BulkUpdateService, services: true do end end - describe 'updating assignee' do - let(:issue) { create(:issue, project: project, assignee: user) } + describe 'updating merge request assignee' do + let(:merge_request) { create(:merge_request, target_project: project, source_project: project, assignee: user) } context 'when the new assignee ID is a valid user' do it 'succeeds' do new_assignee = create(:user) project.team << [new_assignee, :developer] - result = bulk_update(issue, assignee_id: new_assignee.id) + result = bulk_update(merge_request, assignee_id: new_assignee.id) expect(result[:success]).to be_truthy expect(result[:count]).to eq(1) end - it 'updates the assignee to the use ID passed' do + it 'updates the assignee to the user ID passed' do assignee = create(:user) project.team << [assignee, :developer] - expect { bulk_update(issue, assignee_id: assignee.id) } - .to change { issue.reload.assignee }.from(user).to(assignee) + expect { bulk_update(merge_request, assignee_id: assignee.id) } + .to change { merge_request.reload.assignee }.from(user).to(assignee) + end + end + + context "when the new assignee ID is #{IssuableFinder::NONE}" do + it 'unassigns the issues' do + expect { bulk_update(merge_request, assignee_id: IssuableFinder::NONE) } + .to change { merge_request.reload.assignee }.to(nil) + end + end + + context 'when the new assignee ID is not present' do + it 'does not unassign' do + expect { bulk_update(merge_request, assignee_id: nil) } + .not_to change { merge_request.reload.assignee } + end + end + end + + describe 'updating issue assignee' do + let(:issue) { create(:issue, project: project, assignees: [user]) } + + context 'when the new assignee ID is a valid user' do + it 'succeeds' do + new_assignee = create(:user) + project.team << [new_assignee, :developer] + + result = bulk_update(issue, assignee_ids: [new_assignee.id]) + + expect(result[:success]).to be_truthy + expect(result[:count]).to eq(1) + end + + it 'updates the assignee to the user ID passed' do + assignee = create(:user) + project.team << [assignee, :developer] + expect { bulk_update(issue, assignee_ids: [assignee.id]) } + .to change { issue.reload.assignees.first }.from(user).to(assignee) end end context "when the new assignee ID is #{IssuableFinder::NONE}" do it "unassigns the issues" do - expect { bulk_update(issue, assignee_id: IssuableFinder::NONE) } - .to change { issue.reload.assignee }.to(nil) + expect { bulk_update(issue, assignee_ids: [IssuableFinder::NONE.to_s]) } + .to change { issue.reload.assignees.count }.from(1).to(0) end end context 'when the new assignee ID is not present' do it 'does not unassign' do - expect { bulk_update(issue, assignee_id: nil) } - .not_to change { issue.reload.assignee } + expect { bulk_update(issue, assignee_ids: []) } + .not_to change { issue.reload.assignees } end end end @@ -125,7 +163,7 @@ describe Issuable::BulkUpdateService, services: true do { label_ids: labels.map(&:id), add_label_ids: add_labels.map(&:id), - remove_label_ids: remove_labels.map(&:id), + remove_label_ids: remove_labels.map(&:id) } end diff --git a/spec/services/issues/build_service_spec.rb b/spec/services/issues/build_service_spec.rb index 55d635235b0..03f76bd428d 100644 --- a/spec/services/issues/build_service_spec.rb +++ b/spec/services/issues/build_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper.rb' -describe Issues::BuildService, services: true do +describe Issues::BuildService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -136,7 +136,7 @@ describe Issues::BuildService, services: true do user, title: 'Issue #1', description: 'Issue description', - milestone_id: milestone.id, + milestone_id: milestone.id ).execute expect(issue.title).to eq('Issue #1') diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb index 7a54373963e..a03f68434de 100644 --- a/spec/services/issues/close_service_spec.rb +++ b/spec/services/issues/close_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Issues::CloseService, services: true do +describe Issues::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } - let(:issue) { create(:issue, assignee: user2) } + let(:issue) { create(:issue, assignees: [user2]) } let(:project) { issue.project } let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) } @@ -18,26 +18,32 @@ describe Issues::CloseService, services: true do let(:service) { described_class.new(project, user) } it 'checks if the user is authorized to update the issue' do - expect(service).to receive(:can?).with(user, :update_issue, issue). - and_call_original + expect(service).to receive(:can?).with(user, :update_issue, issue) + .and_call_original service.execute(issue) end it 'does not close the issue when the user is not authorized to do so' do - allow(service).to receive(:can?).with(user, :update_issue, issue). - and_return(false) + allow(service).to receive(:can?).with(user, :update_issue, issue) + .and_return(false) expect(service).not_to receive(:close_issue) expect(service.execute(issue)).to eq(issue) end it 'closes the issue when the user is authorized to do so' do - allow(service).to receive(:can?).with(user, :update_issue, issue). - and_return(true) + allow(service).to receive(:can?).with(user, :update_issue, issue) + .and_return(true) - expect(service).to receive(:close_issue). - with(issue, commit: nil, notifications: true, system_note: true) + expect(service).to receive(:close_issue) + .with(issue, commit: nil, notifications: true, system_note: true) + + service.execute(issue) + end + + it 'invalidates counter cache for assignees' do + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) service.execute(issue) end @@ -51,8 +57,10 @@ describe Issues::CloseService, services: true do end end - it { expect(issue).to be_valid } - it { expect(issue).to be_closed } + it 'closes the issue' do + expect(issue).to be_valid + expect(issue).to be_closed + end it 'sends email to user2 about assign of new issue' do email = ActionMailer::Base.deliveries.last @@ -90,15 +98,17 @@ describe Issues::CloseService, services: true do end end - context 'external issue tracker' do + context 'internal issues disabled' do before do - allow(project).to receive(:default_issues_tracker?).and_return(false) - described_class.new(project, user).close_issue(issue) + project.issues_enabled = false + project.save! end - it { expect(issue).to be_valid } - it { expect(issue).to be_opened } - it { expect(todo.reload).to be_pending } + it 'does not close the issue' do + expect(issue).to be_valid + expect(issue).to be_opened + expect(todo.reload).to be_pending + end end end end diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb index 80bfb731550..9b2d9e79f4f 100644 --- a/spec/services/issues/create_service_spec.rb +++ b/spec/services/issues/create_service_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper' -describe Issues::CreateService, services: true do - let(:project) { create(:empty_project) } +describe Issues::CreateService do + let(:project) { create(:project) } let(:user) { create(:user) } describe '#execute' do let(:issue) { described_class.new(project, user, opts).execute } + let(:assignee) { create(:user) } + let(:milestone) { create(:milestone, project: project) } context 'when params are valid' do - let(:assignee) { create(:user) } - let(:milestone) { create(:milestone, project: project) } let(:labels) { create_pair(:label, project: project) } before do @@ -20,7 +20,7 @@ describe Issues::CreateService, services: true do let(:opts) do { title: 'Awesome issue', description: 'please fix', - assignee_id: assignee.id, + assignee_ids: [assignee.id], label_ids: labels.map(&:id), milestone_id: milestone.id, due_date: Date.tomorrow } @@ -29,7 +29,7 @@ describe Issues::CreateService, services: true do it 'creates the issue with the given params' do expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') - expect(issue.assignee).to eq assignee + expect(issue.assignees).to eq [assignee] expect(issue.labels).to match_array labels expect(issue.milestone).to eq milestone expect(issue.due_date).to eq Date.tomorrow @@ -37,6 +37,7 @@ describe Issues::CreateService, services: true do context 'when current user cannot admin issues in the project' do let(:guest) { create(:user) } + before do project.team << [guest, :guest] end @@ -47,7 +48,7 @@ describe Issues::CreateService, services: true do expect(issue).to be_persisted expect(issue.title).to eq('Awesome issue') expect(issue.description).to eq('please fix') - expect(issue.assignee).to be_nil + expect(issue.assignees).to be_empty expect(issue.labels).to be_empty expect(issue.milestone).to be_nil expect(issue.due_date).to be_nil @@ -117,6 +118,22 @@ describe Issues::CreateService, services: true do end end + context 'when assignee is set' do + let(:opts) do + { title: 'Title', + description: 'Description', + assignees: [assignee] } + end + + it 'invalidates open issues counter for assignees when issue is assigned' do + project.team << [assignee, :master] + + described_class.new(project, user, opts).execute + + expect(assignee.assigned_open_issues_count).to eq 1 + end + end + it 'executes issue hooks when issue is not confidential' do opts = { title: 'Title', description: 'Description', confidential: false } @@ -136,9 +153,84 @@ describe Issues::CreateService, services: true do end end - it_behaves_like 'issuable create service' + context 'issue create service' do + context 'assignees' do + before do + project.team << [user, :master] + end + + it 'removes assignee when user id is invalid' do + opts = { title: 'Title', description: 'Description', assignee_ids: [-1] } + + issue = described_class.new(project, user, opts).execute + + expect(issue.assignees).to be_empty + end + + it 'removes assignee when user id is 0' do + opts = { title: 'Title', description: 'Description', assignee_ids: [0] } + + issue = described_class.new(project, user, opts).execute + + expect(issue.assignees).to be_empty + end + + it 'saves assignee when user id is valid' do + project.team << [assignee, :master] + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } + + issue = described_class.new(project, user, opts).execute + + expect(issue.assignees).to eq([assignee]) + end + + context "when issuable feature is private" do + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + project.update(visibility_level: level) + opts = { title: 'Title', description: 'Description', assignee_ids: [assignee.id] } + + issue = described_class.new(project, user, opts).execute + + expect(issue.assignees).to be_empty + end + end + end + end + end + + it_behaves_like 'new issuable record that supports quick actions' - it_behaves_like 'new issuable record that supports slash commands' + context 'Quick actions' do + context 'with assignee and milestone in params and command' do + let(:opts) do + { + assignee_ids: [create(:user).id], + milestone_id: 1, + title: 'Title', + description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}") + } + end + + before do + project.team << [user, :master] + project.team << [assignee, :master] + end + + it 'assigns and sets milestone to issuable from command' do + expect(issue).to be_persisted + expect(issue.assignees).to eq([assignee]) + expect(issue.milestone).to eq(milestone) + end + end + end context 'resolving discussions' do let(:discussion) { create(:diff_note_on_merge_request).to_discussion } @@ -259,14 +351,14 @@ describe Issues::CreateService, services: true do end it 'marks related spam_log as recaptcha_verified' do - expect { issue }.to change{SpamLog.last.recaptcha_verified}.from(false).to(true) + expect { issue }.to change {SpamLog.last.recaptcha_verified}.from(false).to(true) end context 'when spam log does not belong to a user' do let(:log_user) { create(:user) } it 'does not mark spam_log as recaptcha_verified' do - expect { issue }.not_to change{SpamLog.last.recaptcha_verified} + expect { issue }.not_to change {SpamLog.last.recaptcha_verified} end end end @@ -286,7 +378,7 @@ describe Issues::CreateService, services: true do end it 'creates a new spam_log' do - expect{issue}.to change{SpamLog.count}.from(0).to(1) + expect {issue}.to change {SpamLog.count}.from(0).to(1) end it 'assigns a spam_log to an issue' do diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb new file mode 100644 index 00000000000..089e77cc88b --- /dev/null +++ b/spec/services/issues/duplicate_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe Issues::DuplicateService do + let(:user) { create(:user) } + let(:canonical_project) { create(:project) } + let(:duplicate_project) { create(:project) } + + let(:canonical_issue) { create(:issue, project: canonical_project) } + let(:duplicate_issue) { create(:issue, project: duplicate_project) } + + subject { described_class.new(duplicate_project, user, {}) } + + describe '#execute' do + context 'when the issues passed are the same' do + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, duplicate_issue) + end + end + + context 'when the user cannot update the duplicate issue' do + before do + canonical_project.add_reporter(user) + end + + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, canonical_issue) + end + end + + context 'when the user cannot comment on the canonical issue' do + before do + duplicate_project.add_reporter(user) + end + + it 'does nothing' do + expect(subject).not_to receive(:close_service) + expect(SystemNoteService).not_to receive(:mark_duplicate_issue) + expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate) + + subject.execute(duplicate_issue, canonical_issue) + end + end + + context 'when the user can mark the issue as a duplicate' do + before do + canonical_project.add_reporter(user) + duplicate_project.add_reporter(user) + end + + it 'closes the duplicate issue' do + subject.execute(duplicate_issue, canonical_issue) + + expect(duplicate_issue.reload).to be_closed + expect(canonical_issue.reload).to be_open + end + + it 'adds a system note to the duplicate issue' do + expect(SystemNoteService) + .to receive(:mark_duplicate_issue).with(duplicate_issue, duplicate_project, user, canonical_issue) + + subject.execute(duplicate_issue, canonical_issue) + end + + it 'adds a system note to the canonical issue' do + expect(SystemNoteService) + .to receive(:mark_canonical_issue_of_duplicate).with(canonical_issue, canonical_project, user, duplicate_issue) + + subject.execute(duplicate_issue, canonical_issue) + end + end + end +end diff --git a/spec/services/issues/move_service_spec.rb b/spec/services/issues/move_service_spec.rb index 9f8346d52bb..f2b35a8fadf 100644 --- a/spec/services/issues/move_service_spec.rb +++ b/spec/services/issues/move_service_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe Issues::MoveService, services: true do +describe Issues::MoveService do let(:user) { create(:user) } let(:author) { create(:user) } let(:title) { 'Some issue' } let(:description) { 'Some issue description' } - let(:old_project) { create(:empty_project) } - let(:new_project) { create(:empty_project) } + let(:old_project) { create(:project) } + let(:new_project) { create(:project) } let(:milestone1) { create(:milestone, project_id: old_project.id, title: 'v9.0') } let(:old_issue) do @@ -37,9 +37,6 @@ describe Issues::MoveService, services: true do describe '#execute' do shared_context 'issue move executed' do - let!(:milestone2) do - create(:milestone, project_id: new_project.id, title: 'v9.0') - end let!(:award_emoji) { create(:award_emoji, awardable: old_issue) } let!(:new_issue) { move_service.execute(old_issue, new_project) } @@ -48,6 +45,63 @@ describe Issues::MoveService, services: true do context 'issue movable' do include_context 'user can move issue' + context 'move to new milestone' do + let(:new_issue) { move_service.execute(old_issue, new_project) } + + context 'project milestone' do + let!(:milestone2) do + create(:milestone, project_id: new_project.id, title: 'v9.0') + end + + it 'assigns milestone to new issue' do + expect(new_issue.reload.milestone.title).to eq 'v9.0' + expect(new_issue.reload.milestone).to eq(milestone2) + end + end + + context 'group milestones' do + let!(:group) { create(:group, :private) } + let!(:group_milestone_1) do + create(:milestone, group_id: group.id, title: 'v9.0_group') + end + + before do + old_issue.update(milestone: group_milestone_1) + old_project.update(namespace: group) + new_project.update(namespace: group) + + group.add_users([user], GroupMember::DEVELOPER) + end + + context 'when moving to a project of the same group' do + it 'keeps the same group milestone' do + expect(new_issue.reload.project).to eq(new_project) + expect(new_issue.reload.milestone).to eq(group_milestone_1) + end + end + + context 'when moving to a project of a different group' do + let!(:group_2) { create(:group, :private) } + + let!(:group_milestone_2) do + create(:milestone, group_id: group_2.id, title: 'v9.0_group') + end + + before do + old_issue.update(milestone: group_milestone_1) + new_project.update(namespace: group_2) + + group_2.add_users([user], GroupMember::DEVELOPER) + end + + it 'assigns to new group milestone of same title' do + expect(new_issue.reload.project).to eq(new_project) + expect(new_issue.reload.milestone).to eq(group_milestone_2) + end + end + end + end + context 'generic issue' do include_context 'issue move executed' @@ -55,11 +109,6 @@ describe Issues::MoveService, services: true do expect(new_issue.project).to eq new_project end - it 'assigns milestone to new issue' do - expect(new_issue.reload.milestone.title).to eq 'v9.0' - expect(new_issue.reload.milestone).to eq(milestone2) - end - it 'assign labels to new issue' do expected_label_titles = new_issue.reload.labels.map(&:title) expect(expected_label_titles).to include 'label1' @@ -251,12 +300,18 @@ describe Issues::MoveService, services: true do end context 'user is reporter only in new project' do - before { new_project.team << [user, :reporter] } + before do + new_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end context 'user is reporter only in old project' do - before { old_project.team << [user, :reporter] } + before do + old_project.team << [user, :reporter] + end + it { expect { move }.to raise_error(StandardError, /permissions/) } end diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb index 93a8270fd16..205e9ebd237 100644 --- a/spec/services/issues/reopen_service_spec.rb +++ b/spec/services/issues/reopen_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Issues::ReopenService, services: true do - let(:project) { create(:empty_project) } +describe Issues::ReopenService do + let(:project) { create(:project) } let(:issue) { create(:issue, :closed, project: project) } describe '#execute' do @@ -27,6 +27,13 @@ describe Issues::ReopenService, services: true do project.team << [user, :master] end + it 'invalidates counter cache for assignees' do + issue.assignees << user + expect_any_instance_of(User).to receive(:invalidate_issue_cache_counts) + + described_class.new(project, user).execute(issue) + end + context 'when issue is not confidential' do it 'executes issue hooks' do expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks) diff --git a/spec/services/issues/resolve_discussions_spec.rb b/spec/services/issues/resolve_discussions_spec.rb index 4a4929daefc..67a86c50fc1 100644 --- a/spec/services/issues/resolve_discussions_spec.rb +++ b/spec/services/issues/resolve_discussions_spec.rb @@ -1,15 +1,15 @@ require 'spec_helper.rb' -class DummyService < Issues::BaseService - include ::Issues::ResolveDiscussions +describe Issues::ResolveDiscussions do + class DummyService < Issues::BaseService + include ::Issues::ResolveDiscussions - def initialize(*args) - super - filter_resolve_discussion_params + def initialize(*args) + super + filter_resolve_discussion_params + end end -end -describe DummyService, services: true do let(:project) { create(:project, :repository) } let(:user) { create(:user) } @@ -20,10 +20,10 @@ describe DummyService, services: true do describe "for resolving discussions" do let(:discussion) { create(:diff_note_on_merge_request, project: project, note: "Almost done").to_discussion } let(:merge_request) { discussion.noteable } - let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "other") } + let(:other_merge_request) { create(:merge_request, source_project: project, source_branch: "fix") } describe "#merge_request_for_resolving_discussion" do - let(:service) { described_class.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } + let(:service) { DummyService.new(project, user, merge_request_to_resolve_discussions_of: merge_request.iid) } it "finds the merge request" do expect(service.merge_request_to_resolve_discussions_of).to eq(merge_request) @@ -43,7 +43,7 @@ describe DummyService, services: true do describe "#discussions_to_resolve" do it "contains a single discussion when matching merge request and discussion are passed" do - service = described_class.new( + service = DummyService.new( project, user, discussion_to_resolve: discussion.id, @@ -61,7 +61,7 @@ describe DummyService, services: true do noteable: merge_request, project: merge_request.target_project, line_number: 15)]) - service = described_class.new( + service = DummyService.new( project, user, merge_request_to_resolve_discussions_of: merge_request.iid @@ -77,9 +77,9 @@ describe DummyService, services: true do _second_discussion = Discussion.new([create(:diff_note_on_merge_request, :resolved, noteable: merge_request, project: merge_request.target_project, - line_number: 15, + line_number: 15 )]) - service = described_class.new( + service = DummyService.new( project, user, merge_request_to_resolve_discussions_of: merge_request.iid @@ -92,7 +92,7 @@ describe DummyService, services: true do end it "is empty when a discussion and another merge request are passed" do - service = described_class.new( + service = DummyService.new( project, user, discussion_to_resolve: discussion.id, diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index 5b324f3c706..34fb16edc84 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -1,20 +1,18 @@ # coding: utf-8 require 'spec_helper' -describe Issues::UpdateService, services: true do - include EmailHelpers - +describe Issues::UpdateService, :mailer do let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:label) { create(:label, project: project) } let(:label2) { create(:label) } let(:issue) do create(:issue, title: 'Old title', description: "for #{user2.to_reference}", - assignee_id: user3.id, + assignee_ids: [user3.id], project: project) end @@ -31,6 +29,13 @@ describe Issues::UpdateService, services: true do end end + def find_notes(action) + issue + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_issue(opts) described_class.new(project, user, opts).execute(issue) end @@ -40,7 +45,7 @@ describe Issues::UpdateService, services: true do { title: 'New title', description: 'Also please fix', - assignee_id: user2.id, + assignee_ids: [user2.id], state_event: 'close', label_ids: [label.id], due_date: Date.tomorrow @@ -53,15 +58,22 @@ describe Issues::UpdateService, services: true do expect(issue).to be_valid expect(issue.title).to eq 'New title' expect(issue.description).to eq 'Also please fix' - expect(issue.assignee).to eq user2 + expect(issue.assignees).to match_array([user2]) expect(issue).to be_closed expect(issue.labels).to match_array [label] expect(issue.due_date).to eq Date.tomorrow end + it 'updates open issue counter for assignees when issue is reassigned' do + update_issue(assignee_ids: [user2.id]) + + expect(user3.assigned_open_issues_count).to eq 0 + expect(user2.assigned_open_issues_count).to eq 1 + end + it 'sorts issues as specified by parameters' do - issue1 = create(:issue, project: project, assignee_id: user3.id) - issue2 = create(:issue, project: project, assignee_id: user3.id) + issue1 = create(:issue, project: project, assignees: [user3]) + issue2 = create(:issue, project: project, assignees: [user3]) [issue, issue1, issue2].each do |issue| issue.move_to_end @@ -87,7 +99,7 @@ describe Issues::UpdateService, services: true do expect(issue).to be_valid expect(issue.title).to eq 'New title' expect(issue.description).to eq 'Also please fix' - expect(issue.assignee).to eq user3 + expect(issue.assignees).to match_array [user3] expect(issue.labels).to be_empty expect(issue.milestone).to be_nil expect(issue.due_date).to be_nil @@ -132,12 +144,23 @@ describe Issues::UpdateService, services: true do end end + context 'when description changed' do + it 'creates system note about description change' do + update_issue(description: 'Changed description') + + note = find_note('changed the description') + + expect(note).not_to be_nil + expect(note.note).to eq('changed the description') + end + end + context 'when issue turns confidential' do let(:opts) do { title: 'New title', description: 'Also please fix', - assignee_id: user2.id, + assignee_ids: [user2], state_event: 'close', label_ids: [label.id], confidential: true @@ -163,12 +186,12 @@ describe Issues::UpdateService, services: true do it 'does not update assignee_id with unauthorized users' do project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC) update_issue(confidential: true) - non_member = create(:user) - original_assignee = issue.assignee + non_member = create(:user) + original_assignees = issue.assignees - update_issue(assignee_id: non_member.id) + update_issue(assignee_ids: [non_member.id]) - expect(issue.reload.assignee_id).to eq(original_assignee.id) + expect(issue.reload.assignees).to eq(original_assignees) end end @@ -205,7 +228,7 @@ describe Issues::UpdateService, services: true do context 'when is reassigned' do before do - update_issue(assignee: user2) + update_issue(assignees: [user2]) end it 'marks previous assignee todos as done' do @@ -228,13 +251,13 @@ describe Issues::UpdateService, services: true do end context 'when the milestone change' do - before do + it 'marks todos as done' do update_issue(milestone: create(:milestone)) - end - it 'marks todos as done' do expect(todo.reload.done?).to eq true end + + it_behaves_like 'system notes for milestones' end context 'when the labels change' do @@ -270,7 +293,9 @@ describe Issues::UpdateService, services: true do end context 'when issue has the `label` label' do - before { issue.labels << label } + before do + issue.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } @@ -304,7 +329,9 @@ describe Issues::UpdateService, services: true do it { expect(issue.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_issue(description: "- [x] Task 1\n- [X] Task 2") } + before do + update_issue(description: "- [x] Task 1\n- [X] Task 2") + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') @@ -312,6 +339,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -327,6 +357,9 @@ describe Issues::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -336,10 +369,12 @@ describe Issues::UpdateService, services: true do update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2") end - it 'does not create a system note' do - note = find_note('marked the task **Task 2** as incomplete') + it 'does not create a system note for the task' do + task_note = find_note('marked the task **Task 2** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end end @@ -350,9 +385,11 @@ describe Issues::UpdateService, services: true do end it 'does not create a system note referencing the position the old item' do - note = find_note('marked the task **Two** as incomplete') + task_note = find_note('marked the task **Two** as incomplete') + description_notes = find_notes('description') - expect(note).to be_nil + expect(task_note).to be_nil + expect(description_notes.length).to eq(2) end it 'does not generate a new note at all' do @@ -382,7 +419,9 @@ describe Issues::UpdateService, services: true do context 'when remove_label_ids and label_ids are passed' do let(:params) { { label_ids: [], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label, label3]) } + before do + issue.update_attributes(labels: [label, label3]) + end it 'ignores the label_ids parameter' do expect(result.label_ids).not_to be_empty @@ -396,7 +435,9 @@ describe Issues::UpdateService, services: true do context 'when add_label_ids and remove_label_ids are passed' do let(:params) { { add_label_ids: [label3.id], remove_label_ids: [label.id] } } - before { issue.update_attributes(labels: [label]) } + before do + issue.update_attributes(labels: [label]) + end it 'adds the passed labels' do expect(result.label_ids).to include(label3.id) @@ -408,9 +449,65 @@ describe Issues::UpdateService, services: true do end end + context 'updating asssignee_id' do + it 'does not update assignee when assignee_id is invalid' do + update_issue(assignee_ids: [-1]) + + expect(issue.reload.assignees).to eq([user3]) + end + + it 'unassigns assignee when user id is 0' do + update_issue(assignee_ids: [0]) + + expect(issue.reload.assignees).to be_empty + end + + it 'does not update assignee_id when user cannot read issue' do + update_issue(assignee_ids: [create(:user).id]) + + expect(issue.reload.assignees).to eq([user3]) + end + + context "when issuable feature is private" do + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + assignee = create(:user) + project.update(visibility_level: level) + feature_visibility_attr = :"#{issue.model_name.plural}_access_level" + project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) + + expect { update_issue(assignee_ids: [assignee.id]) }.not_to change { issue.assignees } + end + end + end + end + context 'updating mentions' do let(:mentionable) { issue } - include_examples 'updating mentions', Issues::UpdateService + include_examples 'updating mentions', described_class + end + + context 'duplicate issue' do + let(:canonical_issue) { create(:issue, project: project) } + + context 'invalid canonical_issue_id' do + it 'does not call the duplicate service' do + expect(Issues::DuplicateService).not_to receive(:new) + + update_issue(canonical_issue_id: 123456789) + end + end + + context 'valid canonical_issue_id' do + it 'calls the duplicate service with both issues' do + expect_any_instance_of(Issues::DuplicateService) + .to receive(:execute).with(issue, canonical_issue) + + update_issue(canonical_issue_id: canonical_issue.id) + end + end end include_examples 'issuable update service' do diff --git a/spec/services/labels/create_service_spec.rb b/spec/services/labels/create_service_spec.rb index 0ecab0c3475..438e6dbc628 100644 --- a/spec/services/labels/create_service_spec.rb +++ b/spec/services/labels/create_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Labels::CreateService, services: true do +describe Labels::CreateService do describe '#execute' do let(:project) { create(:project) } let(:group) { create(:group) } - + let(:hex_color) { '#FF0000' } let(:named_color) { 'red' } let(:upcase_color) { 'RED' } @@ -17,7 +17,7 @@ describe Labels::CreateService, services: true do context 'in a project' do context 'with color in hex-code' do it 'creates a label' do - label = Labels::CreateService.new(params_with(hex_color)).execute(project: project) + label = described_class.new(params_with(hex_color)).execute(project: project) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -26,7 +26,7 @@ describe Labels::CreateService, services: true do context 'with color in allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(named_color)).execute(project: project) + label = described_class.new(params_with(named_color)).execute(project: project) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -35,7 +35,7 @@ describe Labels::CreateService, services: true do context 'with color in up-case allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(upcase_color)).execute(project: project) + label = described_class.new(params_with(upcase_color)).execute(project: project) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -44,7 +44,7 @@ describe Labels::CreateService, services: true do context 'with color surrounded by spaces' do it 'creates a label' do - label = Labels::CreateService.new(params_with(spaced_color)).execute(project: project) + label = described_class.new(params_with(spaced_color)).execute(project: project) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -53,7 +53,7 @@ describe Labels::CreateService, services: true do context 'with unknown color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(unknown_color)).execute(project: project) + label = described_class.new(params_with(unknown_color)).execute(project: project) expect(label).not_to be_persisted end @@ -61,7 +61,7 @@ describe Labels::CreateService, services: true do context 'with no color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(no_color)).execute(project: project) + label = described_class.new(params_with(no_color)).execute(project: project) expect(label).not_to be_persisted end @@ -71,7 +71,7 @@ describe Labels::CreateService, services: true do context 'in a group' do context 'with color in hex-code' do it 'creates a label' do - label = Labels::CreateService.new(params_with(hex_color)).execute(group: group) + label = described_class.new(params_with(hex_color)).execute(group: group) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -80,7 +80,7 @@ describe Labels::CreateService, services: true do context 'with color in allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(named_color)).execute(group: group) + label = described_class.new(params_with(named_color)).execute(group: group) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -89,7 +89,7 @@ describe Labels::CreateService, services: true do context 'with color in up-case allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(upcase_color)).execute(group: group) + label = described_class.new(params_with(upcase_color)).execute(group: group) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -98,7 +98,7 @@ describe Labels::CreateService, services: true do context 'with color surrounded by spaces' do it 'creates a label' do - label = Labels::CreateService.new(params_with(spaced_color)).execute(group: group) + label = described_class.new(params_with(spaced_color)).execute(group: group) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -107,7 +107,7 @@ describe Labels::CreateService, services: true do context 'with unknown color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(unknown_color)).execute(group: group) + label = described_class.new(params_with(unknown_color)).execute(group: group) expect(label).not_to be_persisted end @@ -115,7 +115,7 @@ describe Labels::CreateService, services: true do context 'with no color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(no_color)).execute(group: group) + label = described_class.new(params_with(no_color)).execute(group: group) expect(label).not_to be_persisted end @@ -125,7 +125,7 @@ describe Labels::CreateService, services: true do context 'in admin area' do context 'with color in hex-code' do it 'creates a label' do - label = Labels::CreateService.new(params_with(hex_color)).execute(template: true) + label = described_class.new(params_with(hex_color)).execute(template: true) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -134,7 +134,7 @@ describe Labels::CreateService, services: true do context 'with color in allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(named_color)).execute(template: true) + label = described_class.new(params_with(named_color)).execute(template: true) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -143,7 +143,7 @@ describe Labels::CreateService, services: true do context 'with color in up-case allowed name' do it 'creates a label' do - label = Labels::CreateService.new(params_with(upcase_color)).execute(template: true) + label = described_class.new(params_with(upcase_color)).execute(template: true) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -152,7 +152,7 @@ describe Labels::CreateService, services: true do context 'with color surrounded by spaces' do it 'creates a label' do - label = Labels::CreateService.new(params_with(spaced_color)).execute(template: true) + label = described_class.new(params_with(spaced_color)).execute(template: true) expect(label).to be_persisted expect(label.color).to eq expected_saved_color @@ -161,7 +161,7 @@ describe Labels::CreateService, services: true do context 'with unknown color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(unknown_color)).execute(template: true) + label = described_class.new(params_with(unknown_color)).execute(template: true) expect(label).not_to be_persisted end @@ -169,7 +169,7 @@ describe Labels::CreateService, services: true do context 'with no color' do it 'doesn\'t create a label' do - label = Labels::CreateService.new(params_with(no_color)).execute(template: true) + label = described_class.new(params_with(no_color)).execute(template: true) expect(label).not_to be_persisted end diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb index de8f1827cce..a781fbc7f7d 100644 --- a/spec/services/labels/find_or_create_service_spec.rb +++ b/spec/services/labels/find_or_create_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Labels::FindOrCreateService, services: true do +describe Labels::FindOrCreateService do describe '#execute' do let(:group) { create(:group) } - let(:project) { create(:empty_project, namespace: group) } + let(:project) { create(:project, namespace: group) } let(:params) do { diff --git a/spec/services/labels/promote_service_spec.rb b/spec/services/labels/promote_service_spec.rb index 4b90ad19640..8809b282127 100644 --- a/spec/services/labels/promote_service_spec.rb +++ b/spec/services/labels/promote_service_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe Labels::PromoteService, services: true do +describe Labels::PromoteService do describe '#execute' do let!(:user) { create(:user) } context 'project without group' do - let!(:project_1) { create(:empty_project) } + let!(:project_1) { create(:project) } let!(:project_label_1_1) { create(:label, project: project_1) } @@ -27,10 +27,10 @@ describe Labels::PromoteService, services: true do let!(:group_1) { create(:group) } let!(:group_2) { create(:group) } - let!(:project_1) { create(:empty_project, namespace: group_1) } - let!(:project_2) { create(:empty_project, namespace: group_1) } - let!(:project_3) { create(:empty_project, namespace: group_1) } - let!(:project_4) { create(:empty_project, namespace: group_2) } + let!(:project_1) { create(:project, namespace: group_1) } + let!(:project_2) { create(:project, namespace: group_1) } + let!(:project_3) { create(:project, namespace: group_1) } + let!(:project_4) { create(:project, namespace: group_2) } # Labels/issues can't be lazily created so we might as well eager initialize # all other objects too since we use them inside @@ -66,9 +66,9 @@ describe Labels::PromoteService, services: true do end it 'recreates the label as a group label' do - expect { service.execute(project_label_1_1) }. - to change(project_1.labels, :count).by(-1). - and change(group_1.labels, :count).by(1) + expect { service.execute(project_label_1_1) } + .to change(project_1.labels, :count).by(-1) + .and change(group_1.labels, :count).by(1) expect(new_label).not_to be_nil end diff --git a/spec/services/labels/transfer_service_spec.rb b/spec/services/labels/transfer_service_spec.rb index 11d5f1cad5e..ae819c011de 100644 --- a/spec/services/labels/transfer_service_spec.rb +++ b/spec/services/labels/transfer_service_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe Labels::TransferService, services: true do +describe Labels::TransferService do describe '#execute' do let(:user) { create(:admin) } let(:group_1) { create(:group) } let(:group_2) { create(:group) } let(:group_3) { create(:group) } - let(:project_1) { create(:empty_project, namespace: group_2) } - let(:project_2) { create(:empty_project, namespace: group_3) } + let(:project_1) { create(:project, namespace: group_2) } + let(:project_2) { create(:project, namespace: group_3) } let(:group_label_1) { create(:group_label, group: group_1, name: 'Group Label 1') } let(:group_label_2) { create(:group_label, group: group_1, name: 'Group Label 2') } diff --git a/spec/services/labels/update_service_spec.rb b/spec/services/labels/update_service_spec.rb index f2498ea6990..c3fe33045fa 100644 --- a/spec/services/labels/update_service_spec.rb +++ b/spec/services/labels/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Labels::UpdateService, services: true do +describe Labels::UpdateService do describe '#execute' do let(:project) { create(:project) } @@ -13,14 +13,14 @@ describe Labels::UpdateService, services: true do let(:expected_saved_color) { hex_color } - before(:each) do + before do @label = Labels::CreateService.new(title: 'Initial', color: '#000000').execute(project: project) expect(@label).to be_persisted end context 'with color in hex-code' do it 'updates the label' do - label = Labels::UpdateService.new(params_with(hex_color)).execute(@label) + label = described_class.new(params_with(hex_color)).execute(@label) expect(label).to be_valid expect(label.reload.color).to eq expected_saved_color @@ -29,7 +29,7 @@ describe Labels::UpdateService, services: true do context 'with color in allowed name' do it 'updates the label' do - label = Labels::UpdateService.new(params_with(named_color)).execute(@label) + label = described_class.new(params_with(named_color)).execute(@label) expect(label).to be_valid expect(label.reload.color).to eq expected_saved_color @@ -38,7 +38,7 @@ describe Labels::UpdateService, services: true do context 'with color in up-case allowed name' do it 'updates the label' do - label = Labels::UpdateService.new(params_with(upcase_color)).execute(@label) + label = described_class.new(params_with(upcase_color)).execute(@label) expect(label).to be_valid expect(label.reload.color).to eq expected_saved_color @@ -47,7 +47,7 @@ describe Labels::UpdateService, services: true do context 'with color surrounded by spaces' do it 'updates the label' do - label = Labels::UpdateService.new(params_with(spaced_color)).execute(@label) + label = described_class.new(params_with(spaced_color)).execute(@label) expect(label).to be_valid expect(label.reload.color).to eq expected_saved_color @@ -56,7 +56,7 @@ describe Labels::UpdateService, services: true do context 'with unknown color' do it 'doesn\'t update the label' do - label = Labels::UpdateService.new(params_with(unknown_color)).execute(@label) + label = described_class.new(params_with(unknown_color)).execute(@label) expect(label).not_to be_valid end @@ -64,7 +64,7 @@ describe Labels::UpdateService, services: true do context 'with no color' do it 'doesn\'t update the label' do - label = Labels::UpdateService.new(params_with(no_color)).execute(@label) + label = described_class.new(params_with(no_color)).execute(@label) expect(label).not_to be_valid end diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb index 7d5a66801db..302c488d6c6 100644 --- a/spec/services/members/approve_access_request_service_spec.rb +++ b/spec/services/members/approve_access_request_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Members::ApproveAccessRequestService, services: true do +describe Members::ApproveAccessRequestService do let(:user) { create(:user) } let(:access_requester) { create(:user) } - let(:project) { create(:empty_project, :public, :access_requestable) } + let(:project) { create(:project, :public, :access_requestable) } let(:group) { create(:group, :public, :access_requestable) } let(:opts) { {} } diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb index 3b35a3b8e3a..2d04d824180 100644 --- a/spec/services/members/authorized_destroy_service_spec.rb +++ b/spec/services/members/authorized_destroy_service_spec.rb @@ -1,21 +1,42 @@ require 'spec_helper' -describe Members::AuthorizedDestroyService, services: true do +describe Members::AuthorizedDestroyService do let(:member_user) { create(:user) } - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } - let(:group_project) { create(:empty_project, :public, group: group) } + let(:group_project) { create(:project, :public, group: group) } def number_of_assigned_issuables(user) Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count end + context 'Invited users' do + # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504 + it 'destroys invited project member' do + project.team << [member_user, :developer] + + member = create :project_member, :invited, project: project + + expect { described_class.new(member, member_user).execute } + .to change { Member.count }.from(3).to(2) + end + + it 'destroys invited group member' do + group.add_developer(member_user) + + member = create :group_member, :invited, group: group + + expect { described_class.new(member, member_user).execute } + .to change { Member.count }.from(2).to(1) + end + end + context 'Group member' do it "unassigns issues and merge requests" do group.add_developer(member_user) - issue = create :issue, project: group_project, assignee: member_user - create :issue, assignee: member_user + issue = create :issue, project: group_project, assignees: [member_user] + create :issue, assignees: [member_user] merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user create :merge_request, target_project: project, source_project: project, assignee: member_user @@ -33,7 +54,7 @@ describe Members::AuthorizedDestroyService, services: true do it "unassigns issues and merge requests" do project.team << [member_user, :developer] - create :issue, project: project, assignee: member_user + create :issue, project: project, assignees: [member_user] create :merge_request, target_project: project, source_project: project, assignee: member_user member = project.members.find_by(user_id: member_user.id) diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb index 0670ac2faa2..2a793e0eb4d 100644 --- a/spec/services/members/create_service_spec.rb +++ b/spec/services/members/create_service_spec.rb @@ -1,17 +1,19 @@ require 'spec_helper' -describe Members::CreateService, services: true do - let(:project) { create(:empty_project) } +describe Members::CreateService do + let(:project) { create(:project) } let(:user) { create(:user) } let(:project_user) { create(:user) } - before { project.team << [user, :master] } + before do + project.team << [user, :master] + end it 'adds user to members' do params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST } result = described_class.new(project, user, params).execute - expect(result).to be_truthy + expect(result[:status]).to eq(:success) expect(project.users).to include project_user end @@ -19,7 +21,19 @@ describe Members::CreateService, services: true do params = { user_ids: '', access_level: Gitlab::Access::GUEST } result = described_class.new(project, user, params).execute - expect(result).to be_falsey + expect(result[:status]).to eq(:error) + expect(result[:message]).to be_present + expect(project.users).not_to include project_user + end + + it 'limits the number of users to 100' do + user_ids = 1.upto(101).to_a.join(',') + params = { user_ids: user_ids, access_level: Gitlab::Access::GUEST } + + result = described_class.new(project, user, params).execute + + expect(result[:status]).to eq(:error) + expect(result[:message]).to be_present expect(project.users).not_to include project_user end end diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb index 41450c67d7e..72f5e27180d 100644 --- a/spec/services/members/destroy_service_spec.rb +++ b/spec/services/members/destroy_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Members::DestroyService, services: true do +describe Members::DestroyService do let(:user) { create(:user) } let(:member_user) { create(:user) } - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:group) { create(:group, :public) } shared_examples 'a service raising ActiveRecord::RecordNotFound' do @@ -104,8 +104,8 @@ describe Members::DestroyService, services: true do let(:params) { { id: project.members.find_by!(user_id: user.id).id } } it 'destroys the member' do - expect { described_class.new(project, user, params).execute }. - to change { project.members.count }.by(-1) + expect { described_class.new(project, user, params).execute } + .to change { project.members.count }.by(-1) end end end diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb index 814c17b9ad0..0a704bba521 100644 --- a/spec/services/members/request_access_service_spec.rb +++ b/spec/services/members/request_access_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Members::RequestAccessService, services: true do +describe Members::RequestAccessService do let(:user) { create(:user) } shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do @@ -29,7 +29,7 @@ describe Members::RequestAccessService, services: true do end context 'when current user cannot request access to the project' do - %i[empty_project group].each do |source_type| + %i[project group].each do |source_type| it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { create(source_type, :private) } end @@ -37,7 +37,7 @@ describe Members::RequestAccessService, services: true do end context 'when access requests are disabled' do - %i[empty_project group].each do |source_type| + %i[project group].each do |source_type| it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do let(:source) { create(source_type, :public) } end @@ -45,7 +45,7 @@ describe Members::RequestAccessService, services: true do end context 'when current user can request access to the project' do - %i[empty_project group].each do |source_type| + %i[project group].each do |source_type| it_behaves_like 'a service creating a access request' do let(:source) { create(source_type, :public, :access_requestable) } end diff --git a/spec/services/merge_requests/assign_issues_service_spec.rb b/spec/services/merge_requests/assign_issues_service_spec.rb index fe75757dd29..fcbe0e5985f 100644 --- a/spec/services/merge_requests/assign_issues_service_spec.rb +++ b/spec/services/merge_requests/assign_issues_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::AssignIssuesService, services: true do +describe MergeRequests::AssignIssuesService do let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } let(:issue) { create(:issue, project: project) } @@ -15,14 +15,14 @@ describe MergeRequests::AssignIssuesService, services: true do expect(service.assignable_issues.map(&:id)).to include(issue.id) end - it 'ignores issues already assigned to any user' do - issue.update!(assignee: create(:user)) + it 'ignores issues the user cannot update assignee on' do + project.team.truncate expect(service.assignable_issues).to be_empty end - it 'ignores issues the user cannot update assignee on' do - project.team.truncate + it 'ignores issues already assigned to any user' do + issue.assignees = [create(:user)] expect(service.assignable_issues).to be_empty end @@ -44,7 +44,7 @@ describe MergeRequests::AssignIssuesService, services: true do end it 'assigns these to the merge request owner' do - expect { service.execute }.to change { issue.reload.assignee }.to(user) + expect { service.execute }.to change { issue.assignees.first }.to(user) end it 'ignores external issues' do diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb index be9f9ea2dec..b46c419de14 100644 --- a/spec/services/merge_requests/build_service_spec.rb +++ b/spec/services/merge_requests/build_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::BuildService, services: true do +describe MergeRequests::BuildService do include RepoHelpers let(:project) { create(:project, :repository) } @@ -19,7 +19,7 @@ describe MergeRequests::BuildService, services: true do let(:commits) { nil } let(:service) do - MergeRequests::BuildService.new(project, user, + described_class.new(project, user, description: description, source_branch: source_branch, target_branch: target_branch, @@ -206,7 +206,9 @@ describe MergeRequests::BuildService, services: true do context 'branch starts with external issue IID followed by a hyphen' do let(:source_branch) { '12345-fix-issue' } - before { allow(project).to receive(:default_issues_tracker?).and_return(false) } + before do + allow(project).to receive(:external_issue_tracker).and_return(true) + end it 'sets the title to: Resolves External Issue $issue-iid' do expect(merge_request.title).to eq('Resolve External Issue 12345') @@ -261,6 +263,16 @@ describe MergeRequests::BuildService, services: true do end end + context 'upstream project has disabled merge requests' do + let(:upstream_project) { create(:project, :merge_requests_disabled) } + let(:project) { create(:project, forked_from_project: upstream_project) } + let(:commits) { Commit.decorate([commit_1], project) } + + it 'sets target project correctly' do + expect(merge_request.target_project).to eq(project) + end + end + context 'target_project is set and accessible by current_user' do let(:target_project) { create(:project, :public, :repository)} let(:commits) { Commit.decorate([commit_1], project) } diff --git a/spec/services/merge_requests/close_service_spec.rb b/spec/services/merge_requests/close_service_spec.rb index d55a7657c0e..04bf267d167 100644 --- a/spec/services/merge_requests/close_service_spec.rb +++ b/spec/services/merge_requests/close_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::CloseService, services: true do +describe MergeRequests::CloseService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } @@ -15,6 +15,8 @@ describe MergeRequests::CloseService, services: true do end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } @@ -30,8 +32,8 @@ describe MergeRequests::CloseService, services: true do it { expect(@merge_request).to be_closed } it 'executes hooks with close action' do - expect(service).to have_received(:execute_hooks). - with(@merge_request, 'close') + expect(service).to have_received(:execute_hooks) + .with(@merge_request, 'close') end it 'sends email to user2 about assign of new merge_request' do diff --git a/spec/services/merge_requests/conflicts/list_service_spec.rb b/spec/services/merge_requests/conflicts/list_service_spec.rb new file mode 100644 index 00000000000..23982b9e6e1 --- /dev/null +++ b/spec/services/merge_requests/conflicts/list_service_spec.rb @@ -0,0 +1,80 @@ +require 'spec_helper' + +describe MergeRequests::Conflicts::ListService do + describe '#can_be_resolved_in_ui?' do + def create_merge_request(source_branch) + create(:merge_request, source_branch: source_branch, target_branch: 'conflict-start') do |mr| + mr.mark_as_unmergeable + end + end + + def conflicts_service(merge_request) + described_class.new(merge_request) + end + + it 'returns a falsey value when the MR can be merged without conflicts' do + merge_request = create_merge_request('master') + merge_request.mark_as_mergeable + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the MR is marked as having conflicts, but has none' do + merge_request = create_merge_request('master') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when one of the MR branches is missing' do + merge_request = create_merge_request('conflict-resolvable') + merge_request.project.repository.rm_branch(merge_request.author, 'conflict-resolvable') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the MR has a missing ref after a force push' do + merge_request = create_merge_request('conflict-resolvable') + service = conflicts_service(merge_request) + allow(service.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError) + + expect(service.can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the MR does not support new diff notes' do + merge_request = create_merge_request('conflict-resolvable') + merge_request.merge_request_diff.update_attributes(start_commit_sha: nil) + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the conflicts contain a large file' do + merge_request = create_merge_request('conflict-too-large') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the conflicts contain a binary file' do + merge_request = create_merge_request('conflict-binary-file') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a falsey value when the conflicts contain a file edited in one branch and deleted in another' do + merge_request = create_merge_request('conflict-missing-side') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_falsey + end + + it 'returns a truthy value when the conflicts are resolvable in the UI' do + merge_request = create_merge_request('conflict-resolvable') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy + end + + it 'returns a truthy value when the conflicts have to be resolved in an editor' do + merge_request = create_merge_request('conflict-contains-conflict-markers') + + expect(conflicts_service(merge_request).can_be_resolved_in_ui?).to be_truthy + end + end +end diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb index eaf7785e549..6f49a65d795 100644 --- a/spec/services/merge_requests/resolve_service_spec.rb +++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::ResolveService do +describe MergeRequests::Conflicts::ResolveService do let(:user) { create(:user) } let(:project) { create(:project, :repository) } @@ -24,6 +24,12 @@ describe MergeRequests::ResolveService do end describe '#execute' do + let(:service) { described_class.new(merge_request) } + + def blob_content(project, ref, path) + project.repository.blob_at(ref, path).data + end + context 'with section params' do let(:params) do { @@ -50,7 +56,7 @@ describe MergeRequests::ResolveService do context 'when the source and target project are the same' do before do - MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + service.execute(user, params) end it 'creates a commit with the message' do @@ -58,9 +64,38 @@ describe MergeRequests::ResolveService do end it 'creates a commit with the correct parents' do - expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 - 824be604a34828eb682305f0d963056cfac87b2d)) + expect(merge_request.source_branch_head.parents.map(&:id)) + .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) + end + end + + context 'when some files have trailing newlines' do + let!(:source_head) do + branch = 'conflict-resolvable' + path = 'files/ruby/popen.rb' + popen_content = blob_content(project, branch, path) + + project.repository.update_file( + user, + path, + popen_content.chomp("\n"), + message: 'Remove trailing newline from popen.rb', + branch_name: branch + ) + end + + before do + service.execute(user, params) + end + + it 'preserves trailing newlines from our side of the conflicts' do + head_sha = merge_request.source_branch_head.sha + popen_content = blob_content(project, head_sha, 'files/ruby/popen.rb') + regex_content = blob_content(project, head_sha, 'files/ruby/regex.rb') + + expect(popen_content).not_to end_with("\n") + expect(regex_content).to end_with("\n") end end @@ -74,18 +109,28 @@ describe MergeRequests::ResolveService do branch_name: 'conflict-start') end - before do - MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork) + def resolve_conflicts + described_class.new(merge_request_from_fork).execute(user, params) + end + + it 'gets conflicts from the source project' do + expect(fork_project.repository.rugged).to receive(:merge_commits).and_call_original + expect(project.repository.rugged).not_to receive(:merge_commits) + + resolve_conflicts end it 'creates a commit with the message' do + resolve_conflicts + expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message]) end it 'creates a commit with the correct parents' do - expect(merge_request_from_fork.source_branch_head.parents.map(&:id)). - to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', - target_head]) + resolve_conflicts + + expect(merge_request_from_fork.source_branch_head.parents.map(&:id)) + .to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813', target_head]) end end end @@ -115,7 +160,7 @@ describe MergeRequests::ResolveService do end before do - MergeRequests::ResolveService.new(project, user, params).execute(merge_request) + service.execute(user, params) end it 'creates a commit with the message' do @@ -123,16 +168,19 @@ describe MergeRequests::ResolveService do end it 'creates a commit with the correct parents' do - expect(merge_request.source_branch_head.parents.map(&:id)). - to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 - 824be604a34828eb682305f0d963056cfac87b2d)) + expect(merge_request.source_branch_head.parents.map(&:id)) + .to eq(%w(1450cd639e0bc6721eb02800169e464f212cde06 + 824be604a34828eb682305f0d963056cfac87b2d)) end it 'sets the content to the content given' do - blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha, - 'files/ruby/popen.rb') + blob = blob_content( + merge_request.source_project, + merge_request.source_branch_head.sha, + 'files/ruby/popen.rb' + ) - expect(blob.data).to eq(popen_content) + expect(blob).to eq(popen_content) end end @@ -154,15 +202,15 @@ describe MergeRequests::ResolveService do } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } - it 'raises a MissingResolution error' do - expect { service.execute(merge_request) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { service.execute(user, invalid_params) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) end end context 'when the content of a file is unchanged' do + let(:list_service) { MergeRequests::Conflicts::ListService.new(merge_request) } + let(:invalid_params) do { files: [ @@ -173,18 +221,16 @@ describe MergeRequests::ResolveService do }, { old_path: 'files/ruby/regex.rb', new_path: 'files/ruby/regex.rb', - content: merge_request.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content + content: list_service.conflicts.file_for_path('files/ruby/regex.rb', 'files/ruby/regex.rb').content } ], commit_message: 'This is a commit message!' } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } - it 'raises a MissingResolution error' do - expect { service.execute(merge_request) }. - to raise_error(Gitlab::Conflict::File::MissingResolution) + expect { service.execute(user, invalid_params) } + .to raise_error(Gitlab::Conflict::File::MissingResolution) end end @@ -202,11 +248,9 @@ describe MergeRequests::ResolveService do } end - let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) } - it 'raises a MissingFiles error' do - expect { service.execute(merge_request) }. - to raise_error(MergeRequests::ResolveService::MissingFiles) + expect { service.execute(user, invalid_params) } + .to raise_error(described_class::MissingFiles) end end end diff --git a/spec/services/merge_requests/create_from_issue_service_spec.rb b/spec/services/merge_requests/create_from_issue_service_spec.rb new file mode 100644 index 00000000000..492b55cdece --- /dev/null +++ b/spec/services/merge_requests/create_from_issue_service_spec.rb @@ -0,0 +1,74 @@ +require 'spec_helper' + +describe MergeRequests::CreateFromIssueService do + let(:project) { create(:project, :repository) } + let(:user) { create(:user) } + let(:issue) { create(:issue, project: project) } + + subject(:service) { described_class.new(project, user, issue_iid: issue.iid) } + + before do + project.add_developer(user) + end + + describe '#execute' do + it 'returns an error with invalid issue iid' do + result = described_class.new(project, user, issue_iid: -1).execute + + expect(result[:status]).to eq :error + expect(result[:message]).to eq 'Invalid issue iid' + end + + it 'delegates issue search to IssuesFinder' do + expect_any_instance_of(IssuesFinder).to receive(:execute).once.and_call_original + + described_class.new(project, user, issue_iid: -1).execute + end + + it 'delegates the branch creation to CreateBranchService' do + expect_any_instance_of(CreateBranchService).to receive(:execute).once.and_call_original + + service.execute + end + + it 'creates a branch based on issue title' do + service.execute + + expect(project.repository.branch_exists?(issue.to_branch_name)).to be_truthy + end + + it 'creates a system note' do + expect(SystemNoteService).to receive(:new_issue_branch).with(issue, project, user, issue.to_branch_name) + + service.execute + end + + it 'creates a merge request' do + expect { service.execute }.to change(project.merge_requests, :count).by(1) + end + + it 'sets the merge request title to: "WIP: Resolves "$issue-title"' do + result = service.execute + + expect(result[:merge_request].title).to eq("WIP: Resolve \"#{issue.title}\"") + end + + it 'sets the merge request author to current user' do + result = service.execute + + expect(result[:merge_request].author).to eq user + end + + it 'sets the merge request source branch to the new issue branch' do + result = service.execute + + expect(result[:merge_request].source_branch).to eq issue.to_branch_name + end + + it 'sets the merge request target branch to the project default branch' do + result = service.execute + + expect(result[:merge_request].target_branch).to eq project.default_branch + end + end +end diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb index 0e16c7cc94b..a1f3bec42cc 100644 --- a/spec/services/merge_requests/create_service_spec.rb +++ b/spec/services/merge_requests/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::CreateService, services: true do +describe MergeRequests::CreateService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:assignee) { create(:user) } @@ -27,10 +27,12 @@ describe MergeRequests::CreateService, services: true do @merge_request = service.execute end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.title).to eq('Awesome merge_request') } - it { expect(@merge_request.assignee).to be_nil } - it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } + it 'creates an MR' do + expect(@merge_request).to be_valid + expect(@merge_request.title).to eq('Awesome merge_request') + expect(@merge_request.assignee).to be_nil + expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') + end it 'executes hooks with default action' do expect(service).to have_received(:execute_hooks).with(@merge_request) @@ -46,6 +48,16 @@ describe MergeRequests::CreateService, services: true do expect(Todo.where(attributes).count).to be_zero end + it 'creates exactly 1 create MR event' do + attributes = { + action: Event::CREATED, + target_id: @merge_request.id, + target_type: @merge_request.class.name + } + + expect(Event.where(attributes).count).to eq(1) + end + context 'when merge request is assigned to someone' do let(:opts) do { @@ -73,9 +85,40 @@ describe MergeRequests::CreateService, services: true do expect(Todo.where(attributes).count).to eq 1 end end + + context 'when head pipelines already exist for merge request source branch' do + let(:sha) { project.commit(opts[:source_branch]).id } + let!(:pipeline_1) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: sha) } + let!(:pipeline_2) { create(:ci_pipeline, project: project, ref: opts[:source_branch], project_id: project.id, sha: sha) } + let!(:pipeline_3) { create(:ci_pipeline, project: project, ref: "other_branch", project_id: project.id) } + + before do + project.merge_requests + .where(source_branch: opts[:source_branch], target_branch: opts[:target_branch]) + .destroy_all + end + + it 'sets head pipeline' do + merge_request = service.execute + + expect(merge_request.head_pipeline).to eq(pipeline_2) + expect(merge_request).to be_persisted + end + + context 'when merge request head commit sha does not match pipeline sha' do + it 'sets the head pipeline correctly' do + pipeline_2.update(sha: 1234) + + merge_request = service.execute + + expect(merge_request.head_pipeline).to eq(pipeline_1) + expect(merge_request).to be_persisted + end + end + end end - it_behaves_like 'new issuable record that supports slash commands' do + it_behaves_like 'new issuable record that supports quick actions' do let(:default_params) do { source_branch: 'feature', @@ -84,7 +127,109 @@ describe MergeRequests::CreateService, services: true do end end - it_behaves_like 'issuable create service' + context 'Quick actions' do + context 'with assignee and milestone in params and command' do + let(:merge_request) { described_class.new(project, user, opts).execute } + let(:milestone) { create(:milestone, project: project) } + + let(:opts) do + { + assignee_id: create(:user).id, + milestone_id: 1, + title: 'Title', + description: %(/assign @#{assignee.username}\n/milestone %"#{milestone.name}"), + source_branch: 'feature', + target_branch: 'master' + } + end + + before do + project.team << [user, :master] + project.team << [assignee, :master] + end + + it 'assigns and sets milestone to issuable from command' do + expect(merge_request).to be_persisted + expect(merge_request.assignee).to eq(assignee) + expect(merge_request.milestone).to eq(milestone) + end + end + end + + context 'merge request create service' do + context 'asssignee_id' do + let(:assignee) { create(:user) } + + before do + project.team << [user, :master] + end + + it 'removes assignee_id when user id is invalid' do + opts = { title: 'Title', description: 'Description', assignee_id: -1 } + + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.assignee_id).to be_nil + end + + it 'removes assignee_id when user id is 0' do + opts = { title: 'Title', description: 'Description', assignee_id: 0 } + + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.assignee_id).to be_nil + end + + it 'saves assignee when user id is valid' do + project.team << [assignee, :master] + opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.assignee).to eq(assignee) + end + + context 'when assignee is set' do + let(:opts) do + { + title: 'Title', + description: 'Description', + assignee_id: assignee.id, + source_branch: 'feature', + target_branch: 'master' + } + end + + it 'invalidates open merge request counter for assignees when merge request is assigned' do + project.team << [assignee, :master] + + described_class.new(project, user, opts).execute + + expect(assignee.assigned_open_merge_requests_count).to eq 1 + end + end + + context "when issuable feature is private" do + before do + project.project_feature.update(issues_access_level: ProjectFeature::PRIVATE, + merge_requests_access_level: ProjectFeature::PRIVATE) + end + + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "removes not authorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + project.update(visibility_level: level) + opts = { title: 'Title', description: 'Description', assignee_id: assignee.id } + + merge_request = described_class.new(project, user, opts).execute + + expect(merge_request.assignee_id).to be_nil + end + end + end + end + end context 'while saving references to issues that the created merge request closes' do let(:first_issue) { create(:issue, project: project) } diff --git a/spec/services/merge_requests/get_urls_service_spec.rb b/spec/services/merge_requests/get_urls_service_spec.rb index 290e00ea1ba..25599dea19f 100644 --- a/spec/services/merge_requests/get_urls_service_spec.rb +++ b/spec/services/merge_requests/get_urls_service_spec.rb @@ -2,8 +2,8 @@ require "spec_helper" describe MergeRequests::GetUrlsService do let(:project) { create(:project, :public, :repository) } - let(:service) { MergeRequests::GetUrlsService.new(project) } - let(:source_branch) { "my_branch" } + let(:service) { described_class.new(project) } + let(:source_branch) { "merge-test" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=#{source_branch}" } let(:show_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/#{merge_request.iid}" } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/#{source_branch}" } @@ -78,7 +78,7 @@ describe MergeRequests::GetUrlsService do end context 'pushing to existing branch and merge request is reopened' do - let!(:merge_request) { create(:merge_request, :reopened, source_project: project, source_branch: source_branch) } + let!(:merge_request) { create(:merge_request, :opened, source_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } it_behaves_like 'show_merge_request_url' end @@ -89,7 +89,7 @@ describe MergeRequests::GetUrlsService do let!(:merge_request) { create(:merge_request, source_project: forked_project, target_project: project, source_branch: source_branch) } let(:changes) { existing_branch_changes } # Source project is now the forked one - let(:service) { MergeRequests::GetUrlsService.new(forked_project) } + let(:service) { described_class.new(forked_project) } before do allow(forked_project).to receive(:empty_repo?).and_return(false) @@ -111,9 +111,9 @@ describe MergeRequests::GetUrlsService do end context 'pushing new branch and existing branch (with merge request created) at once' do - let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "existing_branch") } + let!(:merge_request) { create(:merge_request, source_project: project, source_branch: "markdown") } let(:new_branch_changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" } - let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/existing_branch" } + let(:existing_branch_changes) { "d14d6c0abdd253381df51a723d58691b2ee1ab08 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/markdown" } let(:changes) { "#{new_branch_changes}\n#{existing_branch_changes}" } let(:new_merge_request_url) { "http://#{Gitlab.config.gitlab.host}/#{project.namespace.name}/#{project.path}/merge_requests/new?merge_request%5Bsource_branch%5D=new_branch" } @@ -124,7 +124,7 @@ describe MergeRequests::GetUrlsService do url: new_merge_request_url, new_merge_request: true }, { - branch_name: "existing_branch", + branch_name: "markdown", url: show_merge_request_url, new_merge_request: false }]) diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb index 35804d41b46..bb46e1dd9ab 100644 --- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb +++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe MergeRequests::MergeRequestDiffCacheService do - let(:subject) { MergeRequests::MergeRequestDiffCacheService.new } + let(:subject) { described_class.new } describe '#execute' do it 'retrieves the diff files to cache the highlighted result' do @@ -10,8 +10,8 @@ describe MergeRequests::MergeRequestDiffCacheService do expect(Rails.cache).to receive(:read).with(cache_key).and_return({}) expect(Rails.cache).to receive(:write).with(cache_key, anything) - allow_any_instance_of(Gitlab::Diff::File).to receive(:blob).and_return(double("text?" => true)) - allow_any_instance_of(Repository).to receive(:diffable?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:text?).and_return(true) + allow_any_instance_of(Gitlab::Diff::File).to receive(:diffable?).and_return(true) subject.execute(merge_request) end diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb index d96f819e66a..e593bfeeaf7 100644 --- a/spec/services/merge_requests/merge_service_spec.rb +++ b/spec/services/merge_requests/merge_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe MergeRequests::MergeService, services: true do +describe MergeRequests::MergeService do let(:user) { create(:user) } let(:user2) { create(:user) } - let(:merge_request) { create(:merge_request, assignee: user2) } + let(:merge_request) { create(:merge_request, :simple, author: user2, assignee: user2) } let(:project) { merge_request.project } before do @@ -13,7 +13,7 @@ describe MergeRequests::MergeService, services: true do describe '#execute' do context 'valid params' do - let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } before do allow(service).to receive(:execute_hooks) @@ -81,7 +81,9 @@ describe MergeRequests::MergeService, services: true do end context "when jira_issue_transition_id is not present" do - before { allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) } + before do + allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(nil) + end it "does not close issue" do allow(jira_tracker).to receive_messages(jira_issue_transition_id: nil) @@ -110,7 +112,7 @@ describe MergeRequests::MergeService, services: true do context 'closes related todos' do let(:merge_request) { create(:merge_request, assignee: user, author: user) } let(:project) { merge_request.project } - let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } let!(:todo) do create(:todo, :assigned, project: project, @@ -131,23 +133,70 @@ describe MergeRequests::MergeService, services: true do it { expect(todo).to be_done } end - context 'remove source branch by author' do - let(:service) do - merge_request.merge_params['force_remove_source_branch'] = '1' - merge_request.save! - MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') + context 'source branch removal' do + context 'when the source branch is protected' do + let(:service) do + described_class.new(project, user, should_remove_source_branch: '1') + end + + before do + create(:protected_branch, project: project, name: merge_request.source_branch) + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + service.execute(merge_request) + end end - it 'removes the source branch' do - expect(DeleteBranchService).to receive(:new). - with(merge_request.source_project, merge_request.author). - and_call_original - service.execute(merge_request) + context 'when the source branch is the default branch' do + let(:service) do + described_class.new(project, user, should_remove_source_branch: '1') + end + + before do + allow(project).to receive(:root_ref?).with(merge_request.source_branch).and_return(true) + end + + it 'does not delete the source branch' do + expect(DeleteBranchService).not_to receive(:new) + service.execute(merge_request) + end + end + + context 'when the source branch can be removed' do + context 'when MR author set the source branch to be removed' do + let(:service) do + merge_request.merge_params['force_remove_source_branch'] = '1' + merge_request.save! + described_class.new(project, user, commit_message: 'Awesome message') + end + + it 'removes the source branch using the author user' do + expect(DeleteBranchService).to receive(:new) + .with(merge_request.source_project, merge_request.author) + .and_call_original + service.execute(merge_request) + end + end + + context 'when MR merger set the source branch to be removed' do + let(:service) do + described_class.new(project, user, commit_message: 'Awesome message', should_remove_source_branch: '1') + end + + it 'removes the source branch using the current user' do + expect(DeleteBranchService).to receive(:new) + .with(merge_request.source_project, user) + .and_call_original + service.execute(merge_request) + end + end end end context "error handling" do - let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } + let(:service) { described_class.new(project, user, commit_message: 'Awesome message') } before do allow(Rails.logger).to receive(:error) diff --git a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb index 769b3193275..f17db70faf6 100644 --- a/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb +++ b/spec/services/merge_requests/merge_when_pipeline_succeeds_service_spec.rb @@ -79,7 +79,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do context 'when triggered by pipeline with valid ref and sha' do let(:triggering_pipeline) do create(:ci_pipeline, project: project, ref: merge_request_ref, - sha: merge_request_head, status: 'success') + sha: merge_request_head, status: 'success', + head_pipeline_of: mr_merge_if_green_enabled) end it "merges all merge requests with merge when the pipeline succeeds enabled" do @@ -121,7 +122,8 @@ describe MergeRequests::MergeWhenPipelineSucceedsService do let(:conflict_pipeline) do create(:ci_pipeline, project: project, ref: mr_conflict.source_branch, - sha: mr_conflict.diff_head_sha, status: 'success') + sha: mr_conflict.diff_head_sha, status: 'success', + head_pipeline_of: mr_conflict) end it 'does not merge the merge request' do diff --git a/spec/services/merge_requests/post_merge_service_spec.rb b/spec/services/merge_requests/post_merge_service_spec.rb new file mode 100644 index 00000000000..a37cdab8928 --- /dev/null +++ b/spec/services/merge_requests/post_merge_service_spec.rb @@ -0,0 +1,15 @@ +require 'spec_helper' + +describe MergeRequests::PostMergeService do + let(:user) { create(:user) } + let(:merge_request) { create(:merge_request, assignee: user) } + let(:project) { merge_request.project } + + before do + project.team << [user, :master] + end + + describe '#execute' do + it_behaves_like 'cache counters invalidator' + end +end diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb index 03215a4624a..2af2485eeed 100644 --- a/spec/services/merge_requests/refresh_service_spec.rb +++ b/spec/services/merge_requests/refresh_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe MergeRequests::RefreshService, services: true do +describe MergeRequests::RefreshService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } - let(:service) { MergeRequests::RefreshService } + let(:service) { described_class } describe '#execute' do before do @@ -57,8 +57,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@merge_request, 'update', @oldrev) expect(@merge_request.notes).not_to be_empty expect(@merge_request).to be_open @@ -83,8 +83,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@merge_request, 'update', @oldrev) expect(@merge_request.notes).not_to be_empty expect(@merge_request).to be_open @@ -98,18 +98,52 @@ describe MergeRequests::RefreshService, services: true do end context 'push to origin repo target branch' do - before do - service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') - reload_mrs + context 'when all MRs to the target branch had diffs' do + before do + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + end + + it 'updates the merge state' do + expect(@merge_request.notes.last.note).to include('merged') + expect(@merge_request).to be_merged + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + expect(@build_failed_todo).to be_done + expect(@fork_build_failed_todo).to be_done + end end - it 'updates the merge state' do - expect(@merge_request.notes.last.note).to include('merged') - expect(@merge_request).to be_merged - expect(@fork_merge_request).to be_merged - expect(@fork_merge_request.notes.last.note).to include('merged') - expect(@build_failed_todo).to be_done - expect(@fork_build_failed_todo).to be_done + context 'when an MR to be closed was empty already' do + let!(:empty_fork_merge_request) do + create(:merge_request, + source_project: @fork_project, + source_branch: 'master', + target_branch: 'master', + target_project: @project) + end + + before do + # This spec already has a fake push, so pretend that we were targeting + # feature all along. + empty_fork_merge_request.update_columns(target_branch: 'feature') + + service.new(@project, @user).execute(@oldrev, @newrev, 'refs/heads/feature') + reload_mrs + empty_fork_merge_request.reload + end + + it 'only updates the non-empty MRs' do + expect(@merge_request).to be_merged + expect(@merge_request.notes.last.note).to include('merged') + + expect(@fork_merge_request).to be_merged + expect(@fork_merge_request.notes.last.note).to include('merged') + + expect(empty_fork_merge_request).to be_open + expect(empty_fork_merge_request.merge_request_diff.state).to eq('empty') + expect(empty_fork_merge_request.notes).to be_empty + end end end @@ -146,8 +180,8 @@ describe MergeRequests::RefreshService, services: true do end it 'executes hooks with update action' do - expect(refresh_service).to have_received(:execute_hooks). - with(@fork_merge_request, 'update', @oldrev) + expect(refresh_service).to have_received(:execute_hooks) + .with(@fork_merge_request, 'update', @oldrev) expect(@merge_request.notes).to be_empty expect(@merge_request).to be_open @@ -228,8 +262,8 @@ describe MergeRequests::RefreshService, services: true do let(:refresh_service) { service.new(@fork_project, @user) } it 'refreshes the merge request' do - expect(refresh_service).to receive(:execute_hooks). - with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) + expect(refresh_service).to receive(:execute_hooks) + .with(@fork_merge_request, 'update', Gitlab::Git::BLANK_SHA) allow_any_instance_of(Repository).to receive(:merge_base).and_return(@oldrev) refresh_service.execute(Gitlab::Git::BLANK_SHA, @newrev, 'refs/heads/master') @@ -348,7 +382,7 @@ describe MergeRequests::RefreshService, services: true do title: 'fixup! Fix issue', work_in_progress?: true, to_reference: 'ccccccc' - ), + ) ]) refresh_service.execute(@oldrev, @newrev, 'refs/heads/wip') reload_mrs diff --git a/spec/services/merge_requests/reopen_service_spec.rb b/spec/services/merge_requests/reopen_service_spec.rb index a99d4eac9bd..f02af0c582e 100644 --- a/spec/services/merge_requests/reopen_service_spec.rb +++ b/spec/services/merge_requests/reopen_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::ReopenService, services: true do +describe MergeRequests::ReopenService do let(:user) { create(:user) } let(:user2) { create(:user) } let(:guest) { create(:user) } @@ -14,6 +14,8 @@ describe MergeRequests::ReopenService, services: true do end describe '#execute' do + it_behaves_like 'cache counters invalidator' + context 'valid params' do let(:service) { described_class.new(project, user, {}) } @@ -26,11 +28,11 @@ describe MergeRequests::ReopenService, services: true do end it { expect(merge_request).to be_valid } - it { expect(merge_request).to be_reopened } + it { expect(merge_request).to be_opened } it 'executes hooks with reopen action' do - expect(service).to have_received(:execute_hooks). - with(merge_request, 'reopen') + expect(service).to have_received(:execute_hooks) + .with(merge_request, 'reopen') end it 'sends email to user2 about reopen of merge_request' do diff --git a/spec/services/merge_requests/resolved_discussion_notification_service.rb b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb index 7ddd812e513..e3fd906fe7b 100644 --- a/spec/services/merge_requests/resolved_discussion_notification_service.rb +++ b/spec/services/merge_requests/resolved_discussion_notification_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe MergeRequests::ResolvedDiscussionNotificationService, services: true do +describe MergeRequests::ResolvedDiscussionNotificationService do let(:merge_request) { create(:merge_request) } let(:user) { create(:user) } let(:project) { merge_request.project } diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index f2ca1e6fcbd..681feee61d1 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -1,8 +1,6 @@ require 'spec_helper' -describe MergeRequests::UpdateService, services: true do - include EmailHelpers - +describe MergeRequests::UpdateService, :mailer do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:user2) { create(:user) } @@ -30,6 +28,13 @@ describe MergeRequests::UpdateService, services: true do end end + def find_notes(action) + @merge_request + .notes + .joins(:system_note_metadata) + .where(system_note_metadata: { action: action }) + end + def update_merge_request(opts) @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) @merge_request.reload @@ -48,7 +53,7 @@ describe MergeRequests::UpdateService, services: true do } end - let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + let(:service) { described_class.new(project, user, opts) } before do allow(service).to receive(:execute_hooks) @@ -59,18 +64,20 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.title).to eq('New title') } - it { expect(@merge_request.assignee).to eq(user2) } - it { expect(@merge_request).to be_closed } - it { expect(@merge_request.labels.count).to eq(1) } - it { expect(@merge_request.labels.first.title).to eq(label.name) } - it { expect(@merge_request.target_branch).to eq('target') } - it { expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') } + it 'mathces base expectations' do + expect(@merge_request).to be_valid + expect(@merge_request.title).to eq('New title') + expect(@merge_request.assignee).to eq(user2) + expect(@merge_request).to be_closed + expect(@merge_request.labels.count).to eq(1) + expect(@merge_request.labels.first.title).to eq(label.name) + expect(@merge_request.target_branch).to eq('target') + expect(@merge_request.merge_params['force_remove_source_branch']).to eq('1') + end it 'executes hooks with update action' do - expect(service).to have_received(:execute_hooks). - with(@merge_request, 'update') + expect(service).to have_received(:execute_hooks) + .with(@merge_request, 'update') end it 'sends email to user2 about assign of new merge request and email to user3 about merge request unassignment' do @@ -102,6 +109,13 @@ describe MergeRequests::UpdateService, services: true do expect(note.note).to eq 'changed title from **{-Old-} title** to **{+New+} title**' end + it 'creates system note about description change' do + note = find_note('changed the description') + + expect(note).not_to be_nil + expect(note.note).to eq('changed the description') + end + it 'creates system note about branch change' do note = find_note('changed target') @@ -129,7 +143,7 @@ describe MergeRequests::UpdateService, services: true do } end - let(:service) { MergeRequests::UpdateService.new(project, user, opts) } + let(:service) { described_class.new(project, user, opts) } context 'without pipeline' do before do @@ -141,9 +155,11 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.state).to eq('merged') } - it { expect(@merge_request.merge_error).to be_nil } + it 'merges the MR' do + expect(@merge_request).to be_valid + expect(@merge_request.state).to eq('merged') + expect(@merge_request.merge_error).to be_nil + end end context 'with finished pipeline' do @@ -160,20 +176,25 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request).to be_valid } - it { expect(@merge_request.state).to eq('merged') } + it 'merges the MR' do + expect(@merge_request).to be_valid + expect(@merge_request.state).to eq('merged') + end end context 'with active pipeline' do before do service_mock = double - create(:ci_pipeline_with_one_job, + create( + :ci_pipeline_with_one_job, project: project, - ref: merge_request.source_branch, - sha: merge_request.diff_head_sha) + ref: merge_request.source_branch, + sha: merge_request.diff_head_sha, + head_pipeline_of: merge_request + ) - expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user). - and_return(service_mock) + expect(MergeRequests::MergeWhenPipelineSucceedsService).to receive(:new).with(project, user) + .and_return(service_mock) expect(service_mock).to receive(:execute).with(merge_request) end @@ -182,7 +203,7 @@ describe MergeRequests::UpdateService, services: true do context 'with a non-authorised user' do let(:visitor) { create(:user) } - let(:service) { MergeRequests::UpdateService.new(project, visitor, opts) } + let(:service) { described_class.new(project, visitor, opts) } before do merge_request.update_attribute(:merge_error, 'Error') @@ -193,8 +214,10 @@ describe MergeRequests::UpdateService, services: true do end end - it { expect(@merge_request.state).to eq('opened') } - it { expect(@merge_request.merge_error).not_to be_nil } + it 'does not merge the MR' do + expect(@merge_request.state).to eq('opened') + expect(@merge_request.merge_error).not_to be_nil + end end context 'MR can not be merged when note sha != MR sha' do @@ -271,13 +294,13 @@ describe MergeRequests::UpdateService, services: true do end context 'when the milestone change' do - before do + it 'marks pending todos as done' do update_merge_request({ milestone: create(:milestone) }) - end - it 'marks pending todos as done' do expect(pending_todo.reload).to be_done end + + it_behaves_like 'system notes for milestones' end context 'when the labels change' do @@ -290,6 +313,15 @@ describe MergeRequests::UpdateService, services: true do end end + context 'when the assignee changes' do + it 'updates open merge request counter for assignees when merge request is reassigned' do + update_merge_request(assignee_id: user2.id) + + expect(user3.assigned_open_merge_requests_count).to eq 0 + expect(user2.assigned_open_merge_requests_count).to eq 1 + end + end + context 'when the target branch change' do before do update_merge_request({ target_branch: 'target' }) @@ -314,7 +346,7 @@ describe MergeRequests::UpdateService, services: true do opts = { label_ids: [label.id] } perform_enqueued_jobs do - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project, user, opts).execute(merge_request) end should_email(subscriber) @@ -322,13 +354,15 @@ describe MergeRequests::UpdateService, services: true do end context 'when issue has the `label` label' do - before { merge_request.labels << label } + before do + merge_request.labels << label + end it 'does not send notifications for existing labels' do opts = { label_ids: [label.id, label2.id] } perform_enqueued_jobs do - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project, user, opts).execute(merge_request) end should_not_email(subscriber) @@ -339,7 +373,7 @@ describe MergeRequests::UpdateService, services: true do opts = { label_ids: [label2.id] } perform_enqueued_jobs do - @merge_request = MergeRequests::UpdateService.new(project, user, opts).execute(merge_request) + @merge_request = described_class.new(project, user, opts).execute(merge_request) end should_not_email(subscriber) @@ -350,16 +384,20 @@ describe MergeRequests::UpdateService, services: true do context 'updating mentions' do let(:mentionable) { merge_request } - include_examples 'updating mentions', MergeRequests::UpdateService + include_examples 'updating mentions', described_class end context 'when MergeRequest has tasks' do - before { update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) } + before do + update_merge_request({ description: "- [ ] Task 1\n- [ ] Task 2" }) + end it { expect(@merge_request.tasks?).to eq(true) } context 'when tasks are marked as completed' do - before { update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) } + before do + update_merge_request({ description: "- [x] Task 1\n- [X] Task 2" }) + end it 'creates system note about task status change' do note1 = find_note('marked the task **Task 1** as completed') @@ -367,6 +405,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end @@ -382,6 +423,9 @@ describe MergeRequests::UpdateService, services: true do expect(note1).not_to be_nil expect(note2).not_to be_nil + + description_notes = find_notes('description') + expect(description_notes.length).to eq(1) end end end @@ -423,6 +467,54 @@ describe MergeRequests::UpdateService, services: true do end end + context 'updating asssignee_id' do + it 'does not update assignee when assignee_id is invalid' do + merge_request.update(assignee_id: user.id) + + update_merge_request(assignee_id: -1) + + expect(merge_request.reload.assignee).to eq(user) + end + + it 'unassigns assignee when user id is 0' do + merge_request.update(assignee_id: user.id) + + update_merge_request(assignee_id: 0) + + expect(merge_request.assignee_id).to be_nil + end + + it 'saves assignee when user id is valid' do + update_merge_request(assignee_id: user.id) + + expect(merge_request.assignee_id).to eq(user.id) + end + + it 'does not update assignee_id when user cannot read issue' do + non_member = create(:user) + original_assignee = merge_request.assignee + + update_merge_request(assignee_id: non_member.id) + + expect(merge_request.assignee_id).to eq(original_assignee.id) + end + + context "when issuable feature is private" do + levels = [Gitlab::VisibilityLevel::INTERNAL, Gitlab::VisibilityLevel::PUBLIC] + + levels.each do |level| + it "does not update with unauthorized assignee when project is #{Gitlab::VisibilityLevel.level_name(level)}" do + assignee = create(:user) + project.update(visibility_level: level) + feature_visibility_attr = :"#{merge_request.model_name.plural}_access_level" + project.project_feature.update_attribute(feature_visibility_attr, ProjectFeature::PRIVATE) + + expect { update_merge_request(assignee_id: assignee) }.not_to change { merge_request.assignee } + end + end + end + end + include_examples 'issuable update service' do let(:open_issuable) { merge_request } let(:closed_issuable) { create(:closed_merge_request, source_project: project) } diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb index d581b94ff43..2bdf224804d 100644 --- a/spec/services/milestones/close_service_spec.rb +++ b/spec/services/milestones/close_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Milestones::CloseService, services: true do +describe Milestones::CloseService do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:milestone) { create(:milestone, title: "Milestone v1.2", project: project) } before do @@ -11,7 +11,7 @@ describe Milestones::CloseService, services: true do describe '#execute' do before do - Milestones::CloseService.new(project, user, {}).execute(milestone) + described_class.new(project, user, {}).execute(milestone) end it { expect(milestone).to be_valid } diff --git a/spec/services/milestones/create_service_spec.rb b/spec/services/milestones/create_service_spec.rb index 6d29edb449a..8837b91051d 100644 --- a/spec/services/milestones/create_service_spec.rb +++ b/spec/services/milestones/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Milestones::CreateService, services: true do - let(:project) { create(:empty_project) } +describe Milestones::CreateService do + let(:project) { create(:project) } let(:user) { create(:user) } describe '#execute' do @@ -14,7 +14,7 @@ describe Milestones::CreateService, services: true do description: 'Patch release to fix security issue' } - @milestone = Milestones::CreateService.new(project, user, opts).execute + @milestone = described_class.new(project, user, opts).execute end it { expect(@milestone).to be_valid } diff --git a/spec/services/milestones/destroy_service_spec.rb b/spec/services/milestones/destroy_service_spec.rb new file mode 100644 index 00000000000..5739386dd0d --- /dev/null +++ b/spec/services/milestones/destroy_service_spec.rb @@ -0,0 +1,51 @@ +require 'spec_helper' + +describe Milestones::DestroyService do + let(:user) { create(:user) } + let(:project) { create(:project) } + let(:milestone) { create(:milestone, title: 'Milestone v1.0', project: project) } + let(:issue) { create(:issue, project: project, milestone: milestone) } + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + before do + project.team << [user, :master] + end + + def service + described_class.new(project, user, {}) + end + + describe '#execute' do + it 'deletes milestone' do + service.execute(milestone) + + expect { milestone.reload }.to raise_error ActiveRecord::RecordNotFound + end + + it 'deletes milestone id from issuables' do + service.execute(milestone) + + expect(issue.reload.milestone).to be_nil + expect(merge_request.reload.milestone).to be_nil + end + + context 'group milestones' do + let(:group) { create(:group) } + let(:group_milestone) { create(:milestone, group: group) } + + before do + project.update(namespace: group) + group.add_developer(user) + end + + it { expect(service.execute(group_milestone)).to be_nil } + + it 'does not update milestone issuables' do + expect(MergeRequests::UpdateService).not_to receive(:new) + expect(Issues::UpdateService).not_to receive(:new) + + service.execute(group_milestone) + end + end + end +end diff --git a/spec/services/note_summary_spec.rb b/spec/services/note_summary_spec.rb index 39f06f8f8e7..a6cc2251e48 100644 --- a/spec/services/note_summary_spec.rb +++ b/spec/services/note_summary_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe NoteSummary, services: true do - let(:project) { build(:empty_project) } +describe NoteSummary do + let(:project) { build(:project) } let(:noteable) { build(:issue) } let(:user) { build(:user) } diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb index f9dd5541b10..6e1c1fe6c02 100644 --- a/spec/services/notes/build_service_spec.rb +++ b/spec/services/notes/build_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Notes::BuildService, services: true do +describe Notes::BuildService do let(:note) { create(:discussion_note_on_issue) } let(:project) { note.project } let(:author) { note.author } @@ -29,10 +29,82 @@ describe Notes::BuildService, services: true do expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found') end end + + context 'personal snippet note' do + def reply(note, user = nil) + user ||= create(:user) + + described_class.new(nil, + user, + note: 'Test', + in_reply_to_discussion_id: note.discussion_id).execute + end + + let(:snippet_author) { create(:user) } + + context 'when a snippet is public' do + it 'creates a reply note' do + snippet = create(:personal_snippet, :public) + note = create(:discussion_note_on_personal_snippet, noteable: snippet) + + new_note = reply(note) + + expect(new_note).to be_valid + expect(new_note.in_reply_to?(note)).to be_truthy + end + end + + context 'when a snippet is private' do + let(:snippet) { create(:personal_snippet, :private, author: snippet_author) } + let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) } + + it 'creates a reply note when the author replies' do + new_note = reply(note, snippet_author) + + expect(new_note).to be_valid + expect(new_note.in_reply_to?(note)).to be_truthy + end + + it 'sets an error when another user replies' do + new_note = reply(note) + + expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found') + end + end + + context 'when a snippet is internal' do + let(:snippet) { create(:personal_snippet, :internal, author: snippet_author) } + let(:note) { create(:discussion_note_on_personal_snippet, noteable: snippet) } + + it 'creates a reply note when the author replies' do + new_note = reply(note, snippet_author) + + expect(new_note).to be_valid + expect(new_note.in_reply_to?(note)).to be_truthy + end + + it 'creates a reply note when a regular user replies' do + new_note = reply(note) + + expect(new_note).to be_valid + expect(new_note.in_reply_to?(note)).to be_truthy + end + + it 'sets an error when an external user replies' do + new_note = reply(note, create(:user, :external)) + + expect(new_note.errors[:base]).to include('Discussion to reply to cannot be found') + end + end + end end it 'builds a note without saving it' do - new_note = described_class.new(project, author, noteable_type: note.noteable_type, noteable_id: note.noteable_id, note: 'Test').execute + new_note = described_class.new(project, + author, + noteable_type: note.noteable_type, + noteable_id: note.noteable_id, + note: 'Test').execute expect(new_note).to be_valid expect(new_note).not_to be_persisted end diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb index 152c6d20daa..661d26946e7 100644 --- a/spec/services/notes/create_service_spec.rb +++ b/spec/services/notes/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Notes::CreateService, services: true do - let(:project) { create(:empty_project) } +describe Notes::CreateService do + let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } let(:opts) do diff --git a/spec/services/notes/destroy_service_spec.rb b/spec/services/notes/destroy_service_spec.rb index f53f96e0c2b..c9a99a43edb 100644 --- a/spec/services/notes/destroy_service_spec.rb +++ b/spec/services/notes/destroy_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe Notes::DestroyService, services: true do +describe Notes::DestroyService do describe '#execute' do it 'deletes a note' do - project = create(:empty_project) + project = create(:project) issue = create(:issue, project: project) note = create(:note, project: project, noteable: issue) diff --git a/spec/services/notes/post_process_service_spec.rb b/spec/services/notes/post_process_service_spec.rb index e33a611929b..a2b3638059f 100644 --- a/spec/services/notes/post_process_service_spec.rb +++ b/spec/services/notes/post_process_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Notes::PostProcessService, services: true do - let(:project) { create(:empty_project) } +describe Notes::PostProcessService do + let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } let(:user) { create(:user) } @@ -21,7 +21,7 @@ describe Notes::PostProcessService, services: true do expect(project).to receive(:execute_hooks) expect(project).to receive(:execute_services) - Notes::PostProcessService.new(@note).execute + described_class.new(@note).execute end end end diff --git a/spec/services/notes/slash_commands_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb index 1a64c8bbf00..0280a19098b 100644 --- a/spec/services/notes/slash_commands_service_spec.rb +++ b/spec/services/notes/quick_actions_service_spec.rb @@ -1,15 +1,17 @@ require 'spec_helper' -describe Notes::SlashCommandsService, services: true do +describe Notes::QuickActionsService do shared_context 'note on noteable' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:master) { create(:user).tap { |u| project.team << [u, :master] } } let(:assignee) { create(:user) } - before { project.team << [assignee, :master] } + before do + project.team << [assignee, :master] + end end - shared_examples 'note on noteable that does not support slash commands' do + shared_examples 'note on noteable that does not support quick actions' do include_context 'note on noteable' before do @@ -43,7 +45,7 @@ describe Notes::SlashCommandsService, services: true do end end - shared_examples 'note on noteable that supports slash commands' do + shared_examples 'note on noteable that supports quick actions' do include_context 'note on noteable' before do @@ -66,7 +68,7 @@ describe Notes::SlashCommandsService, services: true do expect(content).to eq '' expect(note.noteable).to be_closed expect(note.noteable.labels).to match_array(labels) - expect(note.noteable.assignee).to eq(assignee) + expect(note.noteable.assignees).to eq([assignee]) expect(note.noteable.milestone).to eq(milestone) end end @@ -113,7 +115,7 @@ describe Notes::SlashCommandsService, services: true do expect(content).to eq "HELLO\nWORLD" expect(note.noteable).to be_closed expect(note.noteable.labels).to match_array(labels) - expect(note.noteable.assignee).to eq(assignee) + expect(note.noteable.assignees).to eq([assignee]) expect(note.noteable.milestone).to eq(milestone) end end @@ -208,16 +210,43 @@ describe Notes::SlashCommandsService, services: true do describe '#execute' do let(:service) { described_class.new(project, master) } - it_behaves_like 'note on noteable that supports slash commands' do + it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_issue, project: project) } end - it_behaves_like 'note on noteable that supports slash commands' do + it_behaves_like 'note on noteable that supports quick actions' do let(:note) { build(:note_on_merge_request, project: project) } end - it_behaves_like 'note on noteable that does not support slash commands' do + it_behaves_like 'note on noteable that does not support quick actions' do let(:note) { build(:note_on_commit, project: project) } end end + + context 'CE restriction for issue assignees' do + describe '/assign' do + let(:project) { create(:project) } + let(:master) { create(:user).tap { |u| project.team << [u, :master] } } + let(:assignee) { create(:user) } + let(:master) { create(:user) } + let(:service) { described_class.new(project, master) } + let(:note) { create(:note_on_issue, note: note_text, project: project) } + + let(:note_text) do + %(/assign @#{assignee.username} @#{master.username}\n") + end + + before do + project.team << [master, :master] + project.team << [assignee, :master] + end + + it 'adds only one assignee from the list' do + _, command_params = service.extract_commands(note) + service.execute(command_params, note) + + expect(note.noteable.assignees.count).to eq(1) + end + end + end end diff --git a/spec/services/notes/update_service_spec.rb b/spec/services/notes/update_service_spec.rb index 905e2f46bde..3210539f3ee 100644 --- a/spec/services/notes/update_service_spec.rb +++ b/spec/services/notes/update_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Notes::UpdateService, services: true do - let(:project) { create(:empty_project) } +describe Notes::UpdateService do + let(:project) { create(:project) } let(:user) { create(:user) } let(:user2) { create(:user) } let(:user3) { create(:user) } diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb index 989fd90cda9..44b2d28d1d4 100644 --- a/spec/services/notification_service_spec.rb +++ b/spec/services/notification_service_spec.rb @@ -1,11 +1,10 @@ require 'spec_helper' -describe NotificationService, services: true do - include EmailHelpers +describe NotificationService, :mailer do + let(:notification) { described_class.new } + let(:assignee) { create(:user) } - let(:notification) { NotificationService.new } - - around(:each) do |example| + around do |example| perform_enqueued_jobs do example.run end @@ -13,7 +12,6 @@ describe NotificationService, services: true do shared_examples 'notifications for new mentions' do def send_notifications(*new_mentions) - reset_delivered_emails! notification.send(notification_method, mentionable, new_mentions, @u_disabled) end @@ -52,7 +50,11 @@ describe NotificationService, services: true do shared_examples 'participating by assignee notification' do it 'emails the participant' do - issuable.update_attribute(:assignee, participant) + if issuable.is_a?(Issue) + issuable.assignees << participant + else + issuable.update_attribute(:assignee, participant) + end notification_trigger @@ -78,12 +80,28 @@ describe NotificationService, services: true do describe 'Keys' do describe '#new_key' do - let!(:key) { create(:personal_key) } + let(:key_options) { {} } + let!(:key) { create(:personal_key, key_options) } it { expect(notification.new_key(key)).to be_truthy } + it { should_email(key.user) } + + describe 'never emails the ghost user' do + let(:key_options) { { user: User.ghost } } + + it { should_not_email_anyone } + end + end + end + + describe 'GpgKeys' do + describe '#new_gpg_key' do + let!(:key) { create(:gpg_key) } + + it { expect(notification.new_gpg_key(key)).to be_truthy } it 'sends email to key owner' do - expect{ notification.new_key(key) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect { notification.new_gpg_key(key) }.to change { ActionMailer::Base.deliveries.size }.by(1) end end end @@ -95,22 +113,22 @@ describe NotificationService, services: true do it { expect(notification.new_email(email)).to be_truthy } it 'sends email to email owner' do - expect{ notification.new_email(email) }.to change{ ActionMailer::Base.deliveries.size }.by(1) + expect { notification.new_email(email) }.to change { ActionMailer::Base.deliveries.size }.by(1) end end end describe 'Notes' do context 'issue note' do - let(:project) { create(:empty_project, :private) } - let(:issue) { create(:issue, project: project, assignee: create(:user)) } - let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } + let(:project) { create(:project, :private) } + let(:issue) { create(:issue, project: project, assignees: [assignee]) } + let(:mentioned_issue) { create(:issue, assignees: issue.assignees) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@mention referenced, @outsider also') } before do build_team(note.project) project.add_master(issue.author) - project.add_master(issue.assignee) + project.add_master(assignee) project.add_master(note.author) create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy') update_custom_notification(:new_note, @u_guest_custom, resource: project) @@ -120,17 +138,16 @@ describe NotificationService, services: true do describe '#new_note' do it do add_users_with_subscription(note.project, issue) + reset_delivered_emails! # Ensure create SentNotification by noteable = issue 6 times, not noteable = note expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times - reset_delivered_emails! - notification.new_note(note) should_email(@u_watcher) should_email(note.noteable.author) - should_email(note.noteable.assignee) + should_email(note.noteable.assignees.first) should_email(@u_custom_global) should_email(@u_mentioned) should_email(@subscriber) @@ -148,9 +165,10 @@ describe NotificationService, services: true do it "emails the note author if they've opted into notifications about their activity" do add_users_with_subscription(note.project, issue) - note.author.notified_of_own_activity = true reset_delivered_emails! + note.author.notified_of_own_activity = true + notification.new_note(note) should_email(note.author) @@ -196,7 +214,7 @@ describe NotificationService, services: true do notification.new_note(note) should_email(note.noteable.author) - should_email(note.noteable.assignee) + should_email(note.noteable.assignees.first) should_email(@u_mentioned) should_email(@u_custom_global) should_not_email(@u_guest_custom) @@ -211,14 +229,14 @@ describe NotificationService, services: true do end context 'confidential issue note' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:author) { create(:user) } let(:assignee) { create(:user) } let(:non_member) { create(:user) } let(:member) { create(:user) } let(:guest) { create(:user) } let(:admin) { create(:admin) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) } let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") } let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") } @@ -243,9 +261,9 @@ describe NotificationService, services: true do end context 'issue note mention' do - let(:project) { create(:empty_project, :public) } - let(:issue) { create(:issue, project: project, assignee: create(:user)) } - let(:mentioned_issue) { create(:issue, assignee: issue.assignee) } + let(:project) { create(:project, :public) } + let(:issue) { create(:issue, project: project, assignees: [assignee]) } + let(:mentioned_issue) { create(:issue, assignees: issue.assignees) } let(:note) { create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@all mentioned') } before do @@ -269,7 +287,7 @@ describe NotificationService, services: true do should_email(@u_guest_watcher) should_email(note.noteable.author) - should_email(note.noteable.assignee) + should_email(note.noteable.assignees.first) should_not_email(note.author) should_email(@u_mentioned) should_not_email(@u_disabled) @@ -286,12 +304,17 @@ describe NotificationService, services: true do end context 'project snippet note' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:snippet) { create(:project_snippet, project: project, author: create(:user)) } let(:note) { create(:note_on_project_snippet, noteable: snippet, project_id: snippet.project.id, note: '@all mentioned') } before do build_team(note.project) + + # make sure these users can read the project snippet! + project.add_guest(@u_guest_watcher) + project.add_guest(@u_guest_custom) + note.project.add_master(note.author) reset_delivered_emails! end @@ -345,7 +368,7 @@ describe NotificationService, services: true do create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_participant), create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_mentioned), create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_disabled), - create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author), + create(:note_on_personal_snippet, noteable: snippet, note: 'note', author: @u_note_author) ] end @@ -378,7 +401,7 @@ describe NotificationService, services: true do before do build_team(note.project) reset_delivered_emails! - allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer) + allow(note.noteable).to receive(:author).and_return(@u_committer) update_custom_notification(:new_note, @u_guest_custom, resource: project) update_custom_notification(:new_note, @u_custom_global) end @@ -447,9 +470,9 @@ describe NotificationService, services: true do describe 'Issues' do let(:group) { create(:group) } - let(:project) { create(:empty_project, :public, namespace: group) } - let(:another_project) { create(:empty_project, :public, namespace: group) } - let(:issue) { create :issue, project: project, assignee: create(:user), description: 'cc @participant' } + let(:project) { create(:project, :public, namespace: group) } + let(:another_project) { create(:project, :public, namespace: group) } + let(:issue) { create :issue, project: project, assignees: [assignee], description: 'cc @participant' } before do build_team(issue.project) @@ -465,7 +488,7 @@ describe NotificationService, services: true do it do notification.new_issue(issue, @u_disabled) - should_email(issue.assignee) + should_email(assignee) should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -480,10 +503,10 @@ describe NotificationService, services: true do end it do - create_global_setting_for(issue.assignee, :mention) + create_global_setting_for(issue.assignees.first, :mention) notification.new_issue(issue, @u_disabled) - should_not_email(issue.assignee) + should_not_email(issue.assignees.first) end it "emails the author if they've opted into notifications about their activity" do @@ -528,7 +551,7 @@ describe NotificationService, services: true do let(:member) { create(:user) } let(:guest) { create(:user) } let(:admin) { create(:admin) } - let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } it "emails subscribers of the issue's labels that can read the issue" do project.add_developer(member) @@ -572,9 +595,9 @@ describe NotificationService, services: true do end it 'emails new assignee' do - notification.reassigned_issue(issue, @u_disabled) + notification.reassigned_issue(issue, @u_disabled, [assignee]) - should_email(issue.assignee) + should_email(issue.assignees.first) should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -588,9 +611,8 @@ describe NotificationService, services: true do end it 'emails previous assignee even if he has the "on mention" notif level' do - issue.update_attribute(:assignee, @u_mentioned) - issue.update_attributes(assignee: @u_watcher) - notification.reassigned_issue(issue, @u_disabled) + issue.assignees = [@u_mentioned] + notification.reassigned_issue(issue, @u_disabled, [@u_watcher]) should_email(@u_mentioned) should_email(@u_watcher) @@ -606,11 +628,11 @@ describe NotificationService, services: true do end it 'emails new assignee even if he has the "on mention" notif level' do - issue.update_attributes(assignee: @u_mentioned) - notification.reassigned_issue(issue, @u_disabled) + issue.assignees = [@u_mentioned] + notification.reassigned_issue(issue, @u_disabled, [@u_mentioned]) - expect(issue.assignee).to be @u_mentioned - should_email(issue.assignee) + expect(issue.assignees.first).to be @u_mentioned + should_email(issue.assignees.first) should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -624,11 +646,11 @@ describe NotificationService, services: true do end it 'emails new assignee' do - issue.update_attribute(:assignee, @u_mentioned) - notification.reassigned_issue(issue, @u_disabled) + issue.assignees = [@u_mentioned] + notification.reassigned_issue(issue, @u_disabled, [@u_mentioned]) - expect(issue.assignee).to be @u_mentioned - should_email(issue.assignee) + expect(issue.assignees.first).to be @u_mentioned + should_email(issue.assignees.first) should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) @@ -642,17 +664,17 @@ describe NotificationService, services: true do end it 'does not email new assignee if they are the current user' do - issue.update_attribute(:assignee, @u_mentioned) - notification.reassigned_issue(issue, @u_mentioned) + issue.assignees = [@u_mentioned] + notification.reassigned_issue(issue, @u_mentioned, [@u_mentioned]) - expect(issue.assignee).to be @u_mentioned + expect(issue.assignees.first).to be @u_mentioned should_email(@u_watcher) should_email(@u_guest_watcher) should_email(@u_guest_custom) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@u_custom_global) - should_not_email(issue.assignee) + should_not_email(issue.assignees.first) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -662,7 +684,7 @@ describe NotificationService, services: true do it_behaves_like 'participating notifications' do let(:participant) { create(:user, username: 'user-participant') } let(:issuable) { issue } - let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled) } + let(:notification_trigger) { notification.reassigned_issue(issue, @u_disabled, [assignee]) } end end @@ -678,17 +700,6 @@ describe NotificationService, services: true do let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } } let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } } - it "emails subscribers of the issue's added labels only" do - notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) - - should_not_email(subscriber_to_label_1) - should_not_email(subscriber_to_group_label_1) - should_not_email(subscriber_to_group_label_2_on_another_project) - should_email(subscriber_1_to_group_label_2) - should_email(subscriber_2_to_group_label_2) - should_email(subscriber_to_label_2) - end - it "emails the current user if they've opted into notifications about their activity" do subscriber_to_label_2.notified_of_own_activity = true notification.relabeled_issue(issue, [group_label_2, label_2], subscriber_to_label_2) @@ -705,7 +716,13 @@ describe NotificationService, services: true do it "doesn't send email to anyone but subscribers of the given labels" do notification.relabeled_issue(issue, [group_label_2, label_2], @u_disabled) - should_not_email(issue.assignee) + should_not_email(subscriber_to_label_1) + should_not_email(subscriber_to_group_label_1) + should_not_email(subscriber_to_group_label_2_on_another_project) + should_email(subscriber_1_to_group_label_2) + should_email(subscriber_2_to_group_label_2) + should_email(subscriber_to_label_2) + should_not_email(issue.assignees.first) should_not_email(issue.author) should_not_email(@u_watcher) should_not_email(@u_guest_watcher) @@ -714,12 +731,6 @@ describe NotificationService, services: true do should_not_email(@watcher_and_subscriber) should_not_email(@unsubscriber) should_not_email(@u_participating) - should_not_email(subscriber_to_label_1) - should_not_email(subscriber_to_group_label_1) - should_not_email(subscriber_to_group_label_2_on_another_project) - should_email(subscriber_1_to_group_label_2) - should_email(subscriber_2_to_group_label_2) - should_email(subscriber_to_label_2) end context 'confidential issues' do @@ -729,7 +740,7 @@ describe NotificationService, services: true do let(:member) { create(:user) } let(:guest) { create(:user) } let(:admin) { create(:admin) } - let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignee: assignee) } + let(:confidential_issue) { create(:issue, :confidential, project: project, title: 'Confidential issue', author: author, assignees: [assignee]) } let!(:label_1) { create(:label, project: project, issues: [confidential_issue]) } let!(:label_2) { create(:label, project: project) } @@ -767,7 +778,7 @@ describe NotificationService, services: true do it 'sends email to issue assignee and issue author' do notification.close_issue(issue, @u_disabled) - should_email(issue.assignee) + should_email(issue.assignees.first) should_email(issue.author) should_email(@u_watcher) should_email(@u_guest_watcher) @@ -798,7 +809,7 @@ describe NotificationService, services: true do it 'sends email to issue notification recipients' do notification.reopen_issue(issue, @u_disabled) - should_email(issue.assignee) + should_email(issue.assignees.first) should_email(issue.author) should_email(@u_watcher) should_email(@u_guest_watcher) @@ -826,7 +837,7 @@ describe NotificationService, services: true do it 'sends email to issue notification recipients' do notification.issue_moved(issue, new_issue, @u_disabled) - should_email(issue.assignee) + should_email(issue.assignees.first) should_email(issue.author) should_email(@u_watcher) should_email(@u_guest_watcher) @@ -850,7 +861,7 @@ describe NotificationService, services: true do describe 'Merge Requests' do let(:group) { create(:group) } let(:project) { create(:project, :public, :repository, namespace: group) } - let(:another_project) { create(:empty_project, :public, namespace: group) } + let(:another_project) { create(:project, :public, namespace: group) } let(:merge_request) { create :merge_request, source_project: project, assignee: create(:user), description: 'cc @participant' } before do @@ -862,11 +873,6 @@ describe NotificationService, services: true do end describe '#new_merge_request' do - before do - update_custom_notification(:new_merge_request, @u_guest_custom, resource: project) - update_custom_notification(:new_merge_request, @u_custom_global) - end - it do notification.new_merge_request(merge_request, @u_disabled) @@ -992,7 +998,7 @@ describe NotificationService, services: true do let!(:subscriber_to_label_1) { create(:user) { |u| label_1.toggle_subscription(u, project) } } let!(:subscriber_to_label_2) { create(:user) { |u| label_2.toggle_subscription(u, project) } } - it "emails subscribers of the merge request's added labels only" do + it "doesn't send email to anyone but subscribers of the given labels" do notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) should_not_email(subscriber_to_label_1) @@ -1001,11 +1007,6 @@ describe NotificationService, services: true do should_email(subscriber_1_to_group_label_2) should_email(subscriber_2_to_group_label_2) should_email(subscriber_to_label_2) - end - - it "doesn't send email to anyone but subscribers of the given labels" do - notification.relabeled_merge_request(merge_request, [group_label_2, label_2], @u_disabled) - should_not_email(merge_request.assignee) should_not_email(merge_request.author) should_not_email(@u_watcher) @@ -1015,12 +1016,6 @@ describe NotificationService, services: true do should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_lazy_participant) - should_not_email(subscriber_to_label_1) - should_not_email(subscriber_to_group_label_1) - should_not_email(subscriber_to_group_label_2_on_another_project) - should_email(subscriber_1_to_group_label_2) - should_email(subscriber_2_to_group_label_2) - should_email(subscriber_to_label_2) end end @@ -1065,12 +1060,12 @@ describe NotificationService, services: true do should_email(merge_request.assignee) should_email(@u_watcher) + should_email(@u_guest_watcher) + should_email(@u_guest_custom) + should_email(@u_custom_global) should_email(@u_participant_mentioned) should_email(@subscriber) should_email(@watcher_and_subscriber) - should_email(@u_guest_watcher) - should_email(@u_custom_global) - should_email(@u_guest_custom) should_not_email(@unsubscriber) should_not_email(@u_participating) should_not_email(@u_disabled) @@ -1161,7 +1156,7 @@ describe NotificationService, services: true do end describe 'Projects' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } before do build_team(project) @@ -1182,19 +1177,39 @@ describe NotificationService, services: true do end end - describe '#project_exported' do - it do - notification.project_exported(project, @u_disabled) + context 'user with notifications disabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_disabled) - should_only_email(@u_disabled) + should_not_email_anyone + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_disabled, ['error']) + + should_not_email_anyone + end end end - describe '#project_not_exported' do - it do - notification.project_not_exported(project, @u_disabled, ['error']) + context 'user with notifications enabled' do + describe '#project_exported' do + it do + notification.project_exported(project, @u_participating) - should_only_email(@u_disabled) + should_only_email(@u_participating) + end + end + + describe '#project_not_exported' do + it do + notification.project_not_exported(project, @u_participating, ['error']) + + should_only_email(@u_participating) + end end end end @@ -1205,7 +1220,7 @@ describe NotificationService, services: true do let(:group) { create(:group) } let(:member) { create(:user) } - before(:each) do + before do group.add_owner(creator) group.add_developer(member, creator) end @@ -1218,14 +1233,43 @@ describe NotificationService, services: true do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_group_member' do + let(:group) { create(:group) } + let(:added_user) { create(:user) } + + def create_member! + GroupMember.create( + group: group, + user: added_user, + access_level: Gitlab::Access::GUEST + ) + end + + it 'sends a notification' do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it 'does not send a notification' do + create_member! + should_not_email_anyone + end + end + end end describe 'ProjectMember' do describe '#decline_group_invite' do - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:member) { create(:user) } - before(:each) do + before do project.add_developer(member, current_user: project.owner) end @@ -1237,10 +1281,35 @@ describe NotificationService, services: true do end.to change { ActionMailer::Base.deliveries.size }.by(1) end end + + describe '#new_project_member' do + let(:project) { create(:project) } + let(:added_user) { create(:user) } + + def create_member! + create(:project_member, user: added_user, project: project) + end + + it do + create_member! + should_only_email(added_user) + end + + describe 'when notifications are disabled' do + before do + create_global_setting_for(added_user, :disabled) + end + + it do + create_member! + should_not_email_anyone + end + end + end end context 'guest user in private project' do - let(:private_project) { create(:empty_project, :private) } + let(:private_project) { create(:project, :private) } let(:guest) { create(:user) } let(:developer) { create(:user) } let(:assignee) { create(:user) } @@ -1535,8 +1604,7 @@ describe NotificationService, services: true do # When resource is nil it means global notification def update_custom_notification(event, user, resource: nil, value: true) setting = user.notification_settings_for(resource) - setting.events[event] = value - setting.save + setting.update!(event => value) end def add_users_with_subscription(project, issuable) diff --git a/spec/services/pages_service_spec.rb b/spec/services/pages_service_spec.rb index aa63fe3a5c1..f8db6900a0a 100644 --- a/spec/services/pages_service_spec.rb +++ b/spec/services/pages_service_spec.rb @@ -1,19 +1,23 @@ require 'spec_helper' -describe PagesService, services: true do +describe PagesService do let(:build) { create(:ci_build) } let(:data) { Gitlab::DataBuilder::Build.build(build) } - let(:service) { PagesService.new(data) } + let(:service) { described_class.new(data) } before do allow(Gitlab.config.pages).to receive(:enabled).and_return(true) end context 'execute asynchronously for pages job' do - before { build.name = 'pages' } + before do + build.name = 'pages' + end context 'on success' do - before { build.success } + before do + build.success + end it 'executes worker' do expect(PagesWorker).to receive(:perform_async) @@ -23,7 +27,9 @@ describe PagesService, services: true do %w(pending running failed canceled).each do |status| context "on #{status}" do - before { build.status = status } + before do + build.status = status + end it 'does not execute worker' do expect(PagesWorker).not_to receive(:perform_async) diff --git a/spec/services/preview_markdown_service_spec.rb b/spec/services/preview_markdown_service_spec.rb new file mode 100644 index 00000000000..64a9559791f --- /dev/null +++ b/spec/services/preview_markdown_service_spec.rb @@ -0,0 +1,67 @@ +require 'spec_helper' + +describe PreviewMarkdownService do + let(:user) { create(:user) } + let(:project) { create(:project) } + + before do + project.add_developer(user) + end + + describe 'user references' do + let(:params) { { text: "Take a look #{user.to_reference}" } } + let(:service) { described_class.new(project, user, params) } + + it 'returns users referenced in text' do + result = service.execute + + expect(result[:users]).to eq [user.username] + end + end + + context 'new note with quick actions' do + let(:issue) { create(:issue, project: project) } + let(:params) do + { + text: "Please do it\n/assign #{user.to_reference}", + quick_actions_target_type: 'Issue', + quick_actions_target_id: issue.id + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes quick actions from text' do + result = service.execute + + expect(result[:text]).to eq 'Please do it' + end + + it 'explains quick actions effect' do + result = service.execute + + expect(result[:commands]).to eq "Assigns #{user.to_reference}." + end + end + + context 'merge request description' do + let(:params) do + { + text: "My work\n/estimate 2y", + quick_actions_target_type: 'MergeRequest' + } + end + let(:service) { described_class.new(project, user, params) } + + it 'removes quick actions from text' do + result = service.execute + + expect(result[:text]).to eq 'My work' + end + + it 'explains quick actions effect' do + result = service.execute + + expect(result[:commands]).to eq 'Sets time estimate to 2y.' + end + end +end diff --git a/spec/services/projects/autocomplete_service_spec.rb b/spec/services/projects/autocomplete_service_spec.rb index 7916c2d957c..426593be428 100644 --- a/spec/services/projects/autocomplete_service_spec.rb +++ b/spec/services/projects/autocomplete_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::AutocompleteService, services: true do +describe Projects::AutocompleteService do describe '#issues' do describe 'confidential issues' do let(:author) { create(:user) } @@ -8,10 +8,10 @@ describe Projects::AutocompleteService, services: true do let(:non_member) { create(:user) } let(:member) { create(:user) } let(:admin) { create(:admin) } - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let!(:issue) { create(:issue, project: project, title: 'Issue 1') } let!(:security_issue_1) { create(:issue, :confidential, project: project, title: 'Security issue 1', author: author) } - let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignee: assignee) } + let!(:security_issue_2) { create(:issue, :confidential, title: 'Security issue 2', project: project, assignees: [assignee]) } it 'does not list project confidential issues for guests' do autocomplete = described_class.new(project, nil) @@ -88,4 +88,31 @@ describe Projects::AutocompleteService, services: true do end end end + + describe '#milestones' do + let(:user) { create(:user) } + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } + let!(:group_milestone) { create(:milestone, group: group) } + let!(:project_milestone) { create(:milestone, project: project) } + + let(:milestone_titles) { described_class.new(project, user).milestones.map(&:title) } + + it 'includes project and group milestones' do + expect(milestone_titles).to eq([group_milestone.title, project_milestone.title]) + end + + it 'does not include closed milestones' do + group_milestone.close + + expect(milestone_titles).to eq([project_milestone.title]) + end + + it 'does not include milestones from other projects in the group' do + other_project = create(:project, group: group) + project_milestone.update!(project: other_project) + + expect(milestone_titles).to eq([group_milestone.title]) + end + end end diff --git a/spec/services/projects/create_from_template_service_spec.rb b/spec/services/projects/create_from_template_service_spec.rb new file mode 100644 index 00000000000..9919ec254c6 --- /dev/null +++ b/spec/services/projects/create_from_template_service_spec.rb @@ -0,0 +1,26 @@ +require 'spec_helper' + +describe Projects::CreateFromTemplateService do + let(:user) { create(:user) } + let(:project_params) do + { + path: user.to_param, + template_name: 'rails' + } + end + + subject { described_class.new(user, project_params) } + + it 'calls the importer service' do + expect_any_instance_of(Projects::GitlabProjectsImportService).to receive(:execute) + + subject.execute + end + + it 'returns the project thats created' do + project = subject.execute + + expect(project).to be_saved + expect(project.scheduled?).to be(true) + end +end diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb index 033e6ecd18c..b0dc7488b5f 100644 --- a/spec/services/projects/create_service_spec.rb +++ b/spec/services/projects/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::CreateService, '#execute', services: true do +describe Projects::CreateService, '#execute' do let(:user) { create :user } let(:opts) do { @@ -115,7 +115,7 @@ describe Projects::CreateService, '#execute', services: true do stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC]) opts.merge!( - visibility_level: Gitlab::VisibilityLevel.options['Public'] + visibility_level: Gitlab::VisibilityLevel::PUBLIC ) end @@ -161,15 +161,13 @@ describe Projects::CreateService, '#execute', services: true do end context 'when a bad service template is created' do - before do - create(:service, type: 'DroneCiService', project: nil, template: true, active: true) - end - it 'reports an error in the imported project' do opts[:import_url] = 'http://www.gitlab.com/gitlab-org/gitlab-ce' + create(:service, type: 'DroneCiService', project: nil, template: true, active: true) + project = create_project(user, opts) - expect(project.errors.full_messages_for(:base).first).to match /Unable to save project. Error: Unable to save DroneCiService/ + expect(project.errors.full_messages_for(:base).first).to match(/Unable to save project. Error: Unable to save DroneCiService/) expect(project.services.count).to eq 0 end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 4b8589b2736..c867139d1de 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Projects::DestroyService, services: true do +describe Projects::DestroyService do let!(:user) { create(:user) } let!(:project) { create(:project, :repository, namespace: user.namespace) } let!(:path) { project.repository.path_to_repo } @@ -15,8 +15,9 @@ describe Projects::DestroyService, services: true do shared_examples 'deleting the project' do it 'deletes the project' do expect(Project.unscoped.all).not_to include(project) - expect(Dir.exist?(path)).to be_falsey - expect(Dir.exist?(remove_path)).to be_falsey + + expect(project.gitlab_shell.exists?(project.repository_storage_path, path + '.git')).to be_falsey + expect(project.gitlab_shell.exists?(project.repository_storage_path, remove_path + '.git')).to be_falsey end end @@ -35,6 +36,27 @@ describe Projects::DestroyService, services: true do end end + shared_examples 'handles errors thrown during async destroy' do |error_message| + it 'does not allow the error to bubble up' do + expect do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end.not_to raise_error + end + + it 'unmarks the project as "pending deletion"' do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + + expect(project.reload.pending_delete).to be(false) + end + + it 'stores an error message in `projects.delete_error`' do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + + expect(project.reload.delete_error).to be_present + expect(project.delete_error).to include(error_message) + end + end + context 'Sidekiq inline' do before do # Run sidekiq immediatly to check that renamed repository will be removed @@ -59,18 +81,18 @@ describe Projects::DestroyService, services: true do before do new_user = create(:user) project.team.add_user(new_user, Gitlab::Access::DEVELOPER) - allow_any_instance_of(Projects::DestroyService).to receive(:flush_caches).and_raise(Redis::CannotConnectError) + allow_any_instance_of(described_class).to receive(:flush_caches).and_raise(::Redis::CannotConnectError) end it 'keeps project team intact upon an error' do Sidekiq::Testing.inline! do begin destroy_project(project, user, {}) - rescue Redis::CannotConnectError + rescue ::Redis::CannotConnectError end end - expect(project.team.members.count).to eq 1 + expect(project.team.members.count).to eq 2 end end @@ -88,10 +110,51 @@ describe Projects::DestroyService, services: true do end it_behaves_like 'deleting the project with pipeline and build' - end - context 'with execute' do - it_behaves_like 'deleting the project with pipeline and build' + context 'errors' do + context 'when `remove_legacy_registry_tags` fails' do + before do + expect_any_instance_of(described_class) + .to receive(:remove_legacy_registry_tags).and_return(false) + end + + it_behaves_like 'handles errors thrown during async destroy', "Failed to remove some tags" + end + + context 'when `remove_repository` fails' do + before do + expect_any_instance_of(described_class) + .to receive(:remove_repository).and_return(false) + end + + it_behaves_like 'handles errors thrown during async destroy', "Failed to remove project repository" + end + + context 'when `execute` raises expected error' do + before do + expect_any_instance_of(Project) + .to receive(:destroy!).and_raise(StandardError.new("Other error message")) + end + + it_behaves_like 'handles errors thrown during async destroy', "Other error message" + end + + context 'when `execute` raises unexpected error' do + before do + expect_any_instance_of(Project) + .to receive(:destroy!).and_raise(Exception.new('Other error message')) + end + + it 'allows error to bubble up and rolls back project deletion' do + expect do + Sidekiq::Testing.inline! { destroy_project(project, user, {}) } + end.to raise_error(Exception, 'Other error message') + + expect(project.reload.pending_delete).to be(false) + expect(project.delete_error).to include("Other error message") + end + end + end end describe 'container registry' do @@ -118,8 +181,7 @@ describe Projects::DestroyService, services: true do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(false) - expect{ destroy_project(project, user) } - .to raise_error(ActiveRecord::RecordNotDestroyed) + expect(destroy_project(project, user)).to be false end end end @@ -144,8 +206,7 @@ describe Projects::DestroyService, services: true do expect_any_instance_of(ContainerRepository) .to receive(:delete_tags!).and_return(false) - expect { destroy_project(project, user) } - .to raise_error(Projects::DestroyService::DestroyError) + expect(destroy_project(project, user)).to be false end end end diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb index 33b267c069c..da236052ebf 100644 --- a/spec/services/projects/download_service_spec.rb +++ b/spec/services/projects/download_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Projects::DownloadService, services: true do +describe Projects::DownloadService do describe 'File service' do before do @user = create(:user) - @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace) + @project = create(:project, creator_id: @user.id, namespace: @user.namespace) end context 'for a URL that is not on whitelist' do diff --git a/spec/services/projects/enable_deploy_key_service_spec.rb b/spec/services/projects/enable_deploy_key_service_spec.rb index a37510cf159..835dae68fcd 100644 --- a/spec/services/projects/enable_deploy_key_service_spec.rb +++ b/spec/services/projects/enable_deploy_key_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Projects::EnableDeployKeyService, services: true do +describe Projects::EnableDeployKeyService do let(:deploy_key) { create(:deploy_key, public: true) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:user) { project.creator} let!(:params) { { key_id: deploy_key.id } } @@ -21,6 +21,16 @@ describe Projects::EnableDeployKeyService, services: true do end end + context 'add the same key twice' do + before do + project.deploy_keys << deploy_key + end + + it 'returns existing key' do + expect(service.execute).to eq(deploy_key) + end + end + def service Projects::EnableDeployKeyService.new(project, user, params) end diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb index f8eb34f2ef4..21c4b30734c 100644 --- a/spec/services/projects/fork_service_spec.rb +++ b/spec/services/projects/fork_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Projects::ForkService, services: true do +describe Projects::ForkService do describe 'fork by user' do before do - @from_namespace = create(:namespace) - @from_user = create(:user, namespace: @from_namespace ) + @from_user = create(:user) + @from_namespace = @from_user.namespace avatar = fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png") @from_project = create(:project, :repository, @@ -13,8 +13,8 @@ describe Projects::ForkService, services: true do star_count: 107, avatar: avatar, description: 'wow such project') - @to_namespace = create(:namespace) - @to_user = create(:user, namespace: @to_namespace) + @to_user = create(:user) + @to_namespace = @to_user.namespace @from_project.add_user(@to_user, :developer) end @@ -50,6 +50,14 @@ describe Projects::ForkService, services: true do expect(@from_project.avatar.file).to be_exists end + + it 'flushes the forks count cache of the source project' do + expect(@from_project.forks_count).to be_zero + + fork_project(@from_project, @to_user) + + expect(@from_project.forks_count).to eq(1) + end end end diff --git a/spec/services/projects/forks_count_service_spec.rb b/spec/services/projects/forks_count_service_spec.rb new file mode 100644 index 00000000000..cf299c5d09b --- /dev/null +++ b/spec/services/projects/forks_count_service_spec.rb @@ -0,0 +1,40 @@ +require 'spec_helper' + +describe Projects::ForksCountService do + let(:project) { build(:project, id: 42) } + let(:service) { described_class.new(project) } + + describe '#count' do + it 'returns the number of forks' do + allow(service).to receive(:uncached_count).and_return(1) + + expect(service.count).to eq(1) + end + + it 'caches the forks count', :use_clean_rails_memory_store_caching do + expect(service).to receive(:uncached_count).once.and_return(1) + + 2.times { service.count } + end + end + + describe '#refresh_cache', :use_clean_rails_memory_store_caching do + it 'refreshes the cache' do + expect(service).to receive(:uncached_count).once.and_return(1) + + service.refresh_cache + + expect(service.count).to eq(1) + end + end + + describe '#delete_cache', :use_clean_rails_memory_store_caching do + it 'removes the cache' do + expect(service).to receive(:uncached_count).twice.and_return(1) + + service.count + service.delete_cache + service.count + end + end +end diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb index eaf63457b32..385f56e447f 100644 --- a/spec/services/projects/housekeeping_service_spec.rb +++ b/spec/services/projects/housekeeping_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Projects::HousekeepingService do - subject { Projects::HousekeepingService.new(project) } + subject { described_class.new(project) } let(:project) { create(:project, :repository) } before do @@ -24,7 +24,7 @@ describe Projects::HousekeepingService do end context 'when no lease can be obtained' do - before(:each) do + before do expect(subject).to receive(:try_obtain_lease).and_return(false) end @@ -66,14 +66,14 @@ describe Projects::HousekeepingService do allow(subject).to receive(:lease_key).and_return(:the_lease_key) # At push 200 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid). - exactly(1).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :gc, :the_lease_key, :the_uuid) + .exactly(1).times # At push 50, 100, 150 - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid). - exactly(3).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :full_repack, :the_lease_key, :the_uuid) + .exactly(3).times # At push 10, 20, ... (except those above) - expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid). - exactly(16).times + expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :incremental_repack, :the_lease_key, :the_uuid) + .exactly(16).times 201.times do subject.increment! diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb index 852a4ac852f..034065aab00 100644 --- a/spec/services/projects/import_service_spec.rb +++ b/spec/services/projects/import_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Projects::ImportService, services: true do - let!(:project) { create(:empty_project) } +describe Projects::ImportService do + let!(:project) { create(:project) } let(:user) { project.creator } subject { described_class.new(project, user) } @@ -26,7 +26,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The repository could not be created." + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The repository could not be created." end end @@ -38,8 +38,7 @@ describe Projects::ImportService, services: true do context 'with a Github repository' do it 'succeeds if repository import is successfully' do - expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true) - expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + expect_any_instance_of(Github::Import).to receive(:execute).and_return(true) result = subject.execute @@ -52,16 +51,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" - end - - it 'does not remove the GitHub remote' do - expect_any_instance_of(Repository).to receive(:fetch_remote).and_return(true) - expect_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) - - subject.execute - - expect(project.repository.raw_repository.remote_names).to include('github') + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported." end end @@ -86,7 +76,7 @@ describe Projects::ImportService, services: true do result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Failed to import the repository" + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Failed to import the repository" end end end @@ -102,8 +92,7 @@ describe Projects::ImportService, services: true do end it 'succeeds if importer succeeds' do - allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true) - allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(true) + allow_any_instance_of(Github::Import).to receive(:execute).and_return(true) result = subject.execute @@ -111,11 +100,8 @@ describe Projects::ImportService, services: true do end it 'flushes various caches' do - allow_any_instance_of(Repository).to receive(:fetch_remote). - and_return(true) - - allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute). - and_return(true) + allow_any_instance_of(Github::Import).to receive(:execute) + .and_return(true) expect_any_instance_of(Repository).to receive(:expire_content_cache) @@ -123,29 +109,27 @@ describe Projects::ImportService, services: true do end it 'fails if importer fails' do - allow_any_instance_of(Repository).to receive(:fetch_remote).and_return(true) - allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_return(false) + allow_any_instance_of(Github::Import).to receive(:execute).and_return(false) result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - The remote data could not be imported." + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - The remote data could not be imported." end it 'fails if importer raise an error' do - allow_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_return(true) - allow_any_instance_of(Gitlab::GithubImport::Importer).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API')) + allow_any_instance_of(Github::Import).to receive(:execute).and_raise(Projects::ImportService::Error.new('Github: failed to connect API')) result = subject.execute expect(result[:status]).to eq :error - expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.path_with_namespace} - Github: failed to connect API" + expect(result[:message]).to eq "Error importing repository #{project.import_url} into #{project.full_path} - Github: failed to connect API" end it 'expires content cache after error' do - allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true) + allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false) - expect_any_instance_of(Gitlab::Shell).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new('Failed to import the repository')) + expect_any_instance_of(Repository).to receive(:fetch_remote).and_raise(Gitlab::Shell::Error.new) expect_any_instance_of(Repository).to receive(:expire_content_cache) subject.execute @@ -186,7 +170,7 @@ describe Projects::ImportService, services: true do } ) - allow(Gitlab.config.omniauth).to receive(:providers).and_return([provider]) + stub_omniauth_setting(providers: [provider]) end end end diff --git a/spec/services/projects/participants_service_spec.rb b/spec/services/projects/participants_service_spec.rb index 063b3bd76eb..0d18ceb8ff8 100644 --- a/spec/services/projects/participants_service_spec.rb +++ b/spec/services/projects/participants_service_spec.rb @@ -1,12 +1,11 @@ require 'spec_helper' -describe Projects::ParticipantsService, services: true do +describe Projects::ParticipantsService do describe '#groups' do describe 'avatar_url' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:group) { create(:group, avatar: fixture_file_upload(Rails.root + 'spec/fixtures/dk.png')) } let(:user) { create(:user) } - let(:base_url) { Settings.send(:build_base_gitlab_url) } let!(:group_member) { create(:group_member, group: group, user: user) } it 'should return an url for the avatar' do @@ -14,7 +13,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq "#{base_url}/uploads/group/avatar/#{group.id}/dk.png" + expect(groups.first[:avatar_url]).to eq("/uploads/-/system/group/avatar/#{group.id}/dk.png") end it 'should return an url for the avatar with relative url' do @@ -25,7 +24,7 @@ describe Projects::ParticipantsService, services: true do groups = participants.groups expect(groups.size).to eq 1 - expect(groups.first[:avatar_url]).to eq "#{base_url}/gitlab/uploads/group/avatar/#{group.id}/dk.png" + expect(groups.first[:avatar_url]).to eq("/gitlab/uploads/-/system/group/avatar/#{group.id}/dk.png") end end end diff --git a/spec/services/projects/propagate_service_template_spec.rb b/spec/services/projects/propagate_service_template_spec.rb new file mode 100644 index 00000000000..f4c59735c43 --- /dev/null +++ b/spec/services/projects/propagate_service_template_spec.rb @@ -0,0 +1,107 @@ +require 'spec_helper' + +describe Projects::PropagateServiceTemplate do + describe '.propagate' do + let!(:service_template) do + PushoverService.create( + template: true, + active: true, + properties: { + device: 'MyDevice', + sound: 'mic', + priority: 4, + user_key: 'asdf', + api_key: '123456789' + }) + end + + let!(:project) { create(:project) } + + it 'creates services for projects' do + expect(project.pushover_service).to be_nil + + described_class.propagate(service_template) + + expect(project.reload.pushover_service).to be_present + end + + it 'creates services for a project that has another service' do + BambooService.create( + template: true, + active: true, + project: project, + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password", + build_key: 'build' + } + ) + + expect(project.pushover_service).to be_nil + + described_class.propagate(service_template) + + expect(project.reload.pushover_service).to be_present + end + + it 'does not create the service if it exists already' do + other_service = BambooService.create( + template: true, + active: true, + properties: { + bamboo_url: 'http://gitlab.com', + username: 'mic', + password: "password", + build_key: 'build' + } + ) + + Service.build_from_template(project.id, service_template).save! + Service.build_from_template(project.id, other_service).save! + + expect { described_class.propagate(service_template) } + .not_to change { Service.count } + end + + it 'creates the service containing the template attributes' do + described_class.propagate(service_template) + + expect(project.pushover_service.properties).to eq(service_template.properties) + end + + describe 'bulk update' do + let(:project_total) { 5 } + + before do + stub_const 'Projects::PropagateServiceTemplate::BATCH_SIZE', 3 + + project_total.times { create(:project) } + + described_class.propagate(service_template) + end + + it 'creates services for all projects' do + expect(Service.all.reload.count).to eq(project_total + 2) + end + end + + describe 'external tracker' do + it 'updates the project external tracker' do + service_template.update!(category: 'issue_tracker', default: false) + + expect { described_class.propagate(service_template) } + .to change { project.reload.has_external_issue_tracker }.to(true) + end + end + + describe 'external wiki' do + it 'updates the project external tracker' do + service_template.update!(type: 'ExternalWikiService') + + expect { described_class.propagate(service_template) } + .to change { project.reload.has_external_wiki }.to(true) + end + end + end +end diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb index 29ccce59c53..2cb60cbcfc4 100644 --- a/spec/services/projects/transfer_service_spec.rb +++ b/spec/services/projects/transfer_service_spec.rb @@ -1,16 +1,16 @@ require 'spec_helper' -describe Projects::TransferService, services: true do +describe Projects::TransferService do let(:user) { create(:user) } let(:group) { create(:group) } let(:project) { create(:project, :repository, namespace: user.namespace) } context 'namespace -> namespace' do before do - allow_any_instance_of(Gitlab::UploadsTransfer). - to receive(:move_project).and_return(true) - allow_any_instance_of(Gitlab::PagesTransfer). - to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::UploadsTransfer) + .to receive(:move_project).and_return(true) + allow_any_instance_of(Gitlab::PagesTransfer) + .to receive(:move_project).and_return(true) group.add_owner(user) @result = transfer_project(project, user, group) end @@ -19,6 +19,73 @@ describe Projects::TransferService, services: true do it { expect(project.namespace).to eq(group) } end + context 'when transfer succeeds' do + before do + group.add_owner(user) + end + + it 'sends notifications' do + expect_any_instance_of(NotificationService).to receive(:project_was_moved) + + transfer_project(project, user, group) + end + + it 'expires full_path cache' do + expect(project).to receive(:expires_full_path_cache) + + transfer_project(project, user, group) + end + + it 'executes system hooks' do + transfer_project(project, user, group) do |service| + expect(service).to receive(:execute_system_hooks) + end + end + end + + context 'when transfer fails' do + let!(:original_path) { project_path(project) } + + def attempt_project_transfer(&block) + expect do + transfer_project(project, user, group, &block) + end.to raise_error(ActiveRecord::ActiveRecordError) + end + + before do + group.add_owner(user) + + expect_any_instance_of(Labels::TransferService).to receive(:execute).and_raise(ActiveRecord::StatementInvalid, "PG ERROR") + end + + def project_path(project) + File.join(project.repository_storage_path, "#{project.disk_path}.git") + end + + def current_path + project_path(project) + end + + it 'rolls back repo location' do + attempt_project_transfer + + expect(Dir.exist?(original_path)).to be_truthy + expect(original_path).to eq current_path + end + + it "doesn't send move notifications" do + expect_any_instance_of(NotificationService).not_to receive(:project_was_moved) + + attempt_project_transfer + end + + it "doesn't run system hooks" do + attempt_project_transfer do |service| + expect(service).not_to receive(:execute_system_hooks) + end + end + end + context 'namespace -> no namespace' do before do @result = transfer_project(project, user, nil) @@ -26,6 +93,7 @@ describe Projects::TransferService, services: true do it { expect(@result).to eq false } it { expect(project.namespace).to eq(user.namespace) } + it { expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.' } end context 'disallow transfering of project with tags' do @@ -52,18 +120,26 @@ describe Projects::TransferService, services: true do end def transfer_project(project, user, new_namespace) - Projects::TransferService.new(project, user).execute(new_namespace) + service = Projects::TransferService.new(project, user) + + yield(service) if block_given? + + service.execute(new_namespace) end context 'visibility level' do let(:internal_group) { create(:group, :internal) } - before { internal_group.add_owner(user) } + before do + internal_group.add_owner(user) + end context 'when namespace visibility level < project visibility level' do let(:public_project) { create(:project, :public, :repository, namespace: user.namespace) } - before { transfer_project(public_project, user, internal_group) } + before do + transfer_project(public_project, user, internal_group) + end it { expect(public_project.visibility_level).to eq(internal_group.visibility_level) } end @@ -71,7 +147,9 @@ describe Projects::TransferService, services: true do context 'when namespace visibility level > project visibility level' do let(:private_project) { create(:project, :private, :repository, namespace: user.namespace) } - before { transfer_project(private_project, user, internal_group) } + before do + transfer_project(private_project, user, internal_group) + end it { expect(private_project.visibility_level).to eq(Gitlab::VisibilityLevel::PRIVATE) } end @@ -105,9 +183,9 @@ describe Projects::TransferService, services: true do end it 'only schedules a single job for every user' do - expect(UserProjectAccessChangedService).to receive(:new). - with([owner.id, group_member.id]). - and_call_original + expect(UserProjectAccessChangedService).to receive(:new) + .with([owner.id, group_member.id]) + .and_call_original transfer_project(project, owner, group) end diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb index 23f5555d3e0..4f1ab697460 100644 --- a/spec/services/projects/unlink_fork_service_spec.rb +++ b/spec/services/projects/unlink_fork_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Projects::UnlinkForkService, services: true do - subject { Projects::UnlinkForkService.new(fork_project, user) } +describe Projects::UnlinkForkService do + subject { described_class.new(fork_project, user) } let(:fork_link) { create(:forked_project_link) } let(:fork_project) { fork_link.forked_to_project } @@ -12,9 +12,9 @@ describe Projects::UnlinkForkService, services: true do let(:mr_close_service) { MergeRequests::CloseService.new(fork_project, user) } before do - allow(MergeRequests::CloseService).to receive(:new). - with(fork_project, user). - and_return(mr_close_service) + allow(MergeRequests::CloseService).to receive(:new) + .with(fork_project, user) + .and_return(mr_close_service) end it 'close all pending merge requests' do @@ -29,4 +29,14 @@ describe Projects::UnlinkForkService, services: true do subject.execute end + + it 'refreshes the forks count cache of the source project' do + source = fork_project.forked_from_project + + expect(source.forks_count).to eq(1) + + subject.execute + + expect(source.forks_count).to be_zero + end end diff --git a/spec/services/projects/update_pages_configuration_service_spec.rb b/spec/services/projects/update_pages_configuration_service_spec.rb index 8b329bc21c3..e4d4e6ff3dd 100644 --- a/spec/services/projects/update_pages_configuration_service_spec.rb +++ b/spec/services/projects/update_pages_configuration_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe Projects::UpdatePagesConfigurationService, services: true do - let(:project) { create(:empty_project) } +describe Projects::UpdatePagesConfigurationService do + let(:project) { create(:project) } subject { described_class.new(project) } describe "#update" do diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb index fc0a17296f3..aa6ad6340f5 100644 --- a/spec/services/projects/update_pages_service_spec.rb +++ b/spec/services/projects/update_pages_service_spec.rb @@ -96,6 +96,78 @@ describe Projects::UpdatePagesService do expect(execute).not_to eq(:success) end + describe 'maximum pages artifacts size' do + let(:metadata) { spy('metadata') } + + before do + file = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip') + metafile = fixture_file_upload(Rails.root + 'spec/fixtures/pages.zip.meta') + + build.update_attributes(artifacts_file: file) + build.update_attributes(artifacts_metadata: metafile) + + allow(build).to receive(:artifacts_metadata_entry) + .and_return(metadata) + end + + shared_examples 'pages size limit exceeded' do + it 'limits the maximum size of gitlab pages' do + subject.execute + + expect(deploy_status.description) + .to match(/artifacts for pages are too large/) + end + end + + context 'when maximum pages size is set to zero' do + before do + stub_application_setting(max_pages_size: 0) + end + + context 'when page size does not exceed internal maximum' do + before do + allow(metadata).to receive(:total_size).and_return(200.megabytes) + end + + it 'updates pages correctly' do + subject.execute + + expect(deploy_status.description).not_to be_present + end + end + + context 'when pages size does exceed internal maximum' do + before do + allow(metadata).to receive(:total_size).and_return(2.terabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + end + + context 'when pages size is greater than max size setting' do + before do + stub_application_setting(max_pages_size: 200) + allow(metadata).to receive(:total_size).and_return(201.megabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + + context 'when max size setting is greater than internal max size' do + before do + stub_application_setting(max_pages_size: 3.terabytes / 1.megabyte) + allow(metadata).to receive(:total_size).and_return(2.terabytes) + end + + it_behaves_like 'pages size limit exceeded' + end + end + + def deploy_status + GenericCommitStatus.find_by(name: 'pages:deploy') + end + def execute subject.execute[:status] end diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb index 05b18fef061..1b282e82187 100644 --- a/spec/services/projects/update_service_spec.rb +++ b/spec/services/projects/update_service_spec.rb @@ -1,11 +1,14 @@ require 'spec_helper' -describe Projects::UpdateService, services: true do +describe Projects::UpdateService, '#execute' do let(:user) { create(:user) } let(:admin) { create(:admin) } - let(:project) { create(:empty_project, creator_id: user.id, namespace: user.namespace) } - describe 'update_by_user' do + let(:project) do + create(:project, creator: user, namespace: user.namespace) + end + + context 'when changing visibility level' do context 'when visibility_level is INTERNAL' do it 'updates the project to internal' do result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::INTERNAL) @@ -40,7 +43,7 @@ describe Projects::UpdateService, services: true do it 'does not update the project to public' do result = update_project(project, user, visibility_level: Gitlab::VisibilityLevel::PUBLIC) - expect(result).to eq({ status: :error, message: 'Visibility level unallowed' }) + expect(result).to eq({ status: :error, message: 'New visibility level not allowed!' }) expect(project).to be_private end @@ -55,12 +58,13 @@ describe Projects::UpdateService, services: true do end end - describe 'visibility_level' do - let(:project) { create(:empty_project, :internal) } + describe 'when updating project that has forks' do + let(:project) { create(:project, :internal) } let(:forked_project) { create(:forked_project_with_submodules, :internal) } before do - forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, forked_from_project_id: project.id) + forked_project.build_forked_project_link(forked_to_project_id: forked_project.id, + forked_from_project_id: project.id) forked_project.save end @@ -89,10 +93,52 @@ describe Projects::UpdateService, services: true do end end - it 'returns an error result when record cannot be updated' do - result = update_project(project, admin, { name: 'foo&bar' }) + context 'when updating a default branch' do + let(:project) { create(:project, :repository) } + + it 'changes a default branch' do + update_project(project, admin, default_branch: 'feature') + + expect(Project.find(project.id).default_branch).to eq 'feature' + end + + it 'does not change a default branch' do + # The branch 'unexisted-branch' does not exist. + update_project(project, admin, default_branch: 'unexisted-branch') - expect(result).to eq({ status: :error, message: 'Project could not be updated' }) + expect(Project.find(project.id).default_branch).to eq 'master' + end + end + + context 'when updating a project that contains container images' do + before do + stub_container_registry_config(enabled: true) + stub_container_registry_tags(repository: /image/, tags: %w[rc1]) + create(:container_repository, project: project, name: :image) + end + + it 'does not allow to rename the project' do + result = update_project(project, admin, path: 'renamed') + + expect(result).to include(status: :error) + expect(result[:message]).to match(/contains container registry tags/) + end + + it 'allows to update other settings' do + result = update_project(project, admin, public_builds: true) + + expect(result[:status]).to eq :success + expect(project.reload.public_builds).to be true + end + end + + context 'when passing invalid parameters' do + it 'returns an error result when record cannot be updated' do + result = update_project(project, admin, { name: 'foo&bar' }) + + expect(result).to eq({ status: :error, + message: 'Project could not be updated!' }) + end end def update_project(project, user, opts) diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index 6ea8f309981..835e83d6dba 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe ProtectedBranches::CreateService, services: true do - let(:project) { create(:empty_project) } +describe ProtectedBranches::CreateService do + let(:project) { create(:project) } let(:user) { project.owner } let(:params) do { diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index 62bdd49a4d7..9fa5983db66 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ProtectedBranches::UpdateService, services: true do +describe ProtectedBranches::UpdateService do let(:protected_branch) { create(:protected_branch) } let(:project) { protected_branch.project } let(:user) { project.owner } @@ -19,7 +19,7 @@ describe ProtectedBranches::UpdateService, services: true do let(:user) { create(:user) } it "raises error" do - expect{ service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) end end end diff --git a/spec/services/protected_tags/create_service_spec.rb b/spec/services/protected_tags/create_service_spec.rb index d91a58e8de5..c3ed95aaebf 100644 --- a/spec/services/protected_tags/create_service_spec.rb +++ b/spec/services/protected_tags/create_service_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' -describe ProtectedTags::CreateService, services: true do - let(:project) { create(:empty_project) } +describe ProtectedTags::CreateService do + let(:project) { create(:project) } let(:user) { project.owner } let(:params) do { diff --git a/spec/services/protected_tags/update_service_spec.rb b/spec/services/protected_tags/update_service_spec.rb index e78fde4c48d..2ece4e3b07b 100644 --- a/spec/services/protected_tags/update_service_spec.rb +++ b/spec/services/protected_tags/update_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe ProtectedTags::UpdateService, services: true do +describe ProtectedTags::UpdateService do let(:protected_tag) { create(:protected_tag) } let(:project) { protected_tag.project } let(:user) { project.owner } @@ -19,7 +19,7 @@ describe ProtectedTags::UpdateService, services: true do let(:user) { create(:user) } it "raises error" do - expect{ service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { service.execute(protected_tag) }.to raise_error(Gitlab::Access::AccessDeniedError) end end end diff --git a/spec/services/push_event_payload_service_spec.rb b/spec/services/push_event_payload_service_spec.rb new file mode 100644 index 00000000000..81956200bff --- /dev/null +++ b/spec/services/push_event_payload_service_spec.rb @@ -0,0 +1,218 @@ +require 'spec_helper' + +describe PushEventPayloadService do + let(:event) { create(:push_event) } + + describe '#execute' do + let(:push_data) do + { + commits: [ + { + id: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + message: 'This is a commit' + } + ], + before: '0000000000000000000000000000000000000000', + after: '1cf19a015df3523caf0a1f9d40c98a267d6a2fc2', + total_commits_count: 1, + ref: 'refs/heads/my-branch' + } + end + + it 'creates a new PushEventPayload row' do + payload = described_class.new(event, push_data).execute + + expect(payload.commit_count).to eq(1) + expect(payload.action).to eq('created') + expect(payload.ref_type).to eq('branch') + expect(payload.commit_from).to be_nil + expect(payload.commit_to).to eq(push_data[:after]) + expect(payload.ref).to eq('my-branch') + expect(payload.commit_title).to eq('This is a commit') + expect(payload.event_id).to eq(event.id) + end + + it 'sets the push_event_payload association of the used event' do + payload = described_class.new(event, push_data).execute + + expect(event.push_event_payload).to eq(payload) + end + end + + describe '#commit_title' do + it 'returns nil if no commits were pushed' do + service = described_class.new(event, commits: []) + + expect(service.commit_title).to be_nil + end + + it 'returns a String limited to 70 characters' do + service = described_class.new(event, commits: [{ message: 'a' * 100 }]) + + expect(service.commit_title).to eq(('a' * 67) + '...') + end + + it 'does not truncate the commit message if it is shorter than 70 characters' do + service = described_class.new(event, commits: [{ message: 'Hello' }]) + + expect(service.commit_title).to eq('Hello') + end + + it 'includes the first line of a commit message if the message spans multiple lines' do + service = described_class + .new(event, commits: [{ message: "Hello\n\nworld" }]) + + expect(service.commit_title).to eq('Hello') + end + end + + describe '#commit_from_id' do + it 'returns nil when creating a new ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.commit_from_id).to be_nil + end + + it 'returns the ID of the first commit when pushing to an existing ref' do + service = described_class.new(event, before: '123') + + expect(service.commit_from_id).to eq('123') + end + end + + describe '#commit_to_id' do + it 'returns nil when removing an existing ref' do + service = described_class.new(event, after: Gitlab::Git::BLANK_SHA) + + expect(service.commit_to_id).to be_nil + end + end + + describe '#commit_count' do + it 'returns the number of commits' do + service = described_class.new(event, total_commits_count: 1) + + expect(service.commit_count).to eq(1) + end + + it 'raises when the push data does not contain the commits count' do + service = described_class.new(event, {}) + + expect { service.commit_count }.to raise_error(KeyError) + end + end + + describe '#ref' do + it 'returns the name of the ref' do + service = described_class.new(event, ref: 'refs/heads/foo') + + expect(service.ref).to eq('refs/heads/foo') + end + + it 'raises when the push data does not contain the ref name' do + service = described_class.new(event, {}) + + expect { service.ref }.to raise_error(KeyError) + end + end + + describe '#revision_before' do + it 'returns the revision from before the push' do + service = described_class.new(event, before: 'foo') + + expect(service.revision_before).to eq('foo') + end + + it 'raises when the push data does not contain the before revision' do + service = described_class.new(event, {}) + + expect { service.revision_before }.to raise_error(KeyError) + end + end + + describe '#revision_after' do + it 'returns the revision from after the push' do + service = described_class.new(event, after: 'foo') + + expect(service.revision_after).to eq('foo') + end + + it 'raises when the push data does not contain the after revision' do + service = described_class.new(event, {}) + + expect { service.revision_after }.to raise_error(KeyError) + end + end + + describe '#trimmed_ref' do + it 'returns the ref name without its prefix' do + service = described_class.new(event, ref: 'refs/heads/foo') + + expect(service.trimmed_ref).to eq('foo') + end + end + + describe '#create?' do + it 'returns true when creating a new ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.create?).to eq(true) + end + + it 'returns false when pushing to an existing ref' do + service = described_class.new(event, before: 'foo') + + expect(service.create?).to eq(false) + end + end + + describe '#remove?' do + it 'returns true when removing an existing ref' do + service = described_class.new(event, after: Gitlab::Git::BLANK_SHA) + + expect(service.remove?).to eq(true) + end + + it 'returns false pushing to an existing ref' do + service = described_class.new(event, after: 'foo') + + expect(service.remove?).to eq(false) + end + end + + describe '#action' do + it 'returns :created when creating a ref' do + service = described_class.new(event, before: Gitlab::Git::BLANK_SHA) + + expect(service.action).to eq(:created) + end + + it 'returns :removed when removing an existing ref' do + service = described_class.new(event, + before: '123', + after: Gitlab::Git::BLANK_SHA) + + expect(service.action).to eq(:removed) + end + + it 'returns :pushed when pushing to an existing ref' do + service = described_class.new(event, before: '123', after: '456') + + expect(service.action).to eq(:pushed) + end + end + + describe '#ref_type' do + it 'returns :tag for a tag' do + service = described_class.new(event, ref: 'refs/tags/1.2') + + expect(service.ref_type).to eq(:tag) + end + + it 'returns :branch for a branch' do + service = described_class.new(event, ref: 'refs/heads/master') + + expect(service.ref_type).to eq(:branch) + end + end +end diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index a63281f0eab..30fa0ee6873 100644 --- a/spec/services/slash_commands/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -1,20 +1,21 @@ require 'spec_helper' -describe SlashCommands::InterpretService, services: true do - let(:project) { create(:empty_project, :public) } +describe QuickActions::InterpretService do + let(:project) { create(:project, :public) } let(:developer) { create(:user) } + let(:developer2) { create(:user) } let(:issue) { create(:issue, project: project) } let(:milestone) { create(:milestone, project: project, title: '9.10') } let(:inprogress) { create(:label, project: project, title: 'In Progress') } let(:bug) { create(:label, project: project, title: 'Bug') } let(:note) { build(:note, commit_id: merge_request.diff_head_sha) } + let(:service) { described_class.new(project, developer) } before do project.team << [developer, :developer] end describe '#execute' do - let(:service) { described_class.new(project, developer) } let(:merge_request) { create(:merge_request, source_project: project) } shared_examples 'reopen command' do @@ -42,23 +43,6 @@ describe SlashCommands::InterpretService, services: true do end end - shared_examples 'assign command' do - it 'fetches assignee and populates assignee_id if content contains /assign' do - _, updates = service.execute(content, issuable) - - expect(updates).to eq(assignee_id: developer.id) - end - end - - shared_examples 'unassign command' do - it 'populates assignee_id: nil if content contains /unassign' do - issuable.update(assignee_id: developer.id) - _, updates = service.execute(content, issuable) - - expect(updates).to eq(assignee_id: nil) - end - end - shared_examples 'milestone command' do it 'fetches milestone and populates milestone_id if content contains /milestone' do milestone # populate the milestone @@ -70,7 +54,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'remove_milestone command' do it 'populates milestone_id: nil if content contains /remove_milestone' do - issuable.update(milestone_id: milestone.id) + issuable.update!(milestone_id: milestone.id) _, updates = service.execute(content, issuable) expect(updates).to eq(milestone_id: nil) @@ -108,7 +92,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'unlabel command' do it 'fetches label ids and populates remove_label_ids if content contains /unlabel' do - issuable.update(label_ids: [inprogress.id]) # populate the label + issuable.update!(label_ids: [inprogress.id]) # populate the label _, updates = service.execute(content, issuable) expect(updates).to eq(remove_label_ids: [inprogress.id]) @@ -117,7 +101,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'multiple unlabel command' do it 'fetches label ids and populates remove_label_ids if content contains mutiple /unlabel' do - issuable.update(label_ids: [inprogress.id, bug.id]) # populate the label + issuable.update!(label_ids: [inprogress.id, bug.id]) # populate the label _, updates = service.execute(content, issuable) expect(updates).to eq(remove_label_ids: [inprogress.id, bug.id]) @@ -126,7 +110,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'unlabel command with no argument' do it 'populates label_ids: [] if content contains /unlabel with no arguments' do - issuable.update(label_ids: [inprogress.id]) # populate the label + issuable.update!(label_ids: [inprogress.id]) # populate the label _, updates = service.execute(content, issuable) expect(updates).to eq(label_ids: []) @@ -135,7 +119,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'relabel command' do it 'populates label_ids: [] if content contains /relabel' do - issuable.update(label_ids: [bug.id]) # populate the label + issuable.update!(label_ids: [bug.id]) # populate the label inprogress # populate the label _, updates = service.execute(content, issuable) @@ -187,7 +171,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'remove_due_date command' do it 'populates due_date: nil if content contains /remove_due_date' do - issuable.update(due_date: Date.today) + issuable.update!(due_date: Date.today) _, updates = service.execute(content, issuable) expect(updates).to eq(due_date: nil) @@ -204,7 +188,7 @@ describe SlashCommands::InterpretService, services: true do shared_examples 'unwip command' do it 'returns wip_event: "unwip" if content contains /wip' do - issuable.update(title: issuable.wip_title) + issuable.update!(title: issuable.wip_title) _, updates = service.execute(content, issuable) expect(updates).to eq(wip_event: 'unwip') @@ -277,6 +261,31 @@ describe SlashCommands::InterpretService, services: true do end end + shared_examples 'duplicate command' do + it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do + issue_duplicate # populate the issue + _, updates = service.execute(content, issuable) + + expect(updates).to eq(canonical_issue_id: issue_duplicate.id) + end + end + + shared_examples 'shrug command' do + it 'appends ¯\_(ツ)_/¯ to the comment' do + new_content, _ = service.execute(content, issuable) + + expect(new_content).to end_with(described_class::SHRUG) + end + end + + shared_examples 'tableflip command' do + it 'appends (╯°□°)╯︵ ┻━┻ to the comment' do + new_content, _ = service.execute(content, issuable) + + expect(new_content).to end_with(described_class::TABLEFLIP) + end + end + it_behaves_like 'reopen command' do let(:content) { '/reopen' } let(:issuable) { issue } @@ -371,14 +380,68 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end - it_behaves_like 'assign command' do + context 'assign command' do let(:content) { "/assign @#{developer.username}" } - let(:issuable) { issue } + + context 'Issue' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, issue) + + expect(updates[:assignee_ids]).to match_array([developer.id]) + end + end + + context 'Merge Request' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, merge_request) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end end - it_behaves_like 'assign command' do - let(:content) { "/assign @#{developer.username}" } - let(:issuable) { merge_request } + context 'assign command with multiple assignees' do + let(:content) { "/assign @#{developer.username} @#{developer2.username}" } + + before do + project.team << [developer2, :developer] + end + + context 'Issue' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, issue) + + expect(updates[:assignee_ids]).to match_array([developer.id]) + end + end + + context 'Merge Request' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, merge_request) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end + end + + context 'assign command with me alias' do + let(:content) { "/assign me" } + + context 'Issue' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, issue) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end + + context 'Merge Request' do + it 'fetches assignee and populates assignee_ids if content contains /assign' do + _, updates = service.execute(content, merge_request) + + expect(updates).to eq(assignee_ids: [developer.id]) + end + end end it_behaves_like 'empty command' do @@ -391,14 +454,26 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end - it_behaves_like 'unassign command' do + context 'unassign command' do let(:content) { '/unassign' } - let(:issuable) { issue } - end - it_behaves_like 'unassign command' do - let(:content) { '/unassign' } - let(:issuable) { merge_request } + context 'Issue' do + it 'populates assignee_ids: [] if content contains /unassign' do + issue.update(assignee_ids: [developer.id]) + _, updates = service.execute(content, issue) + + expect(updates).to eq(assignee_ids: []) + end + end + + context 'Merge Request' do + it 'populates assignee_ids: [] if content contains /unassign' do + merge_request.update(assignee_ids: [developer.id]) + _, updates = service.execute(content, merge_request) + + expect(updates).to eq(assignee_ids: []) + end + end end it_behaves_like 'milestone command' do @@ -614,6 +689,41 @@ describe SlashCommands::InterpretService, services: true do let(:issuable) { issue } end + context '/duplicate command' do + it_behaves_like 'duplicate command' do + let(:issue_duplicate) { create(:issue, project: project) } + let(:content) { "/duplicate #{issue_duplicate.to_reference}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate' } + let(:issuable) { issue } + end + + context 'cross project references' do + it_behaves_like 'duplicate command' do + let(:other_project) { create(:project, :public) } + let(:issue_duplicate) { create(:issue, project: other_project) } + let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { "/duplicate imaginary#1234" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :private) } + let(:issue_duplicate) { create(:issue, project: other_project) } + + let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" } + let(:issuable) { issue } + end + end + end + context 'when current_user cannot :admin_issue' do let(:visitor) { create(:user) } let(:issue) { create(:issue, project: project, author: visitor) } @@ -663,6 +773,11 @@ describe SlashCommands::InterpretService, services: true do let(:content) { '/remove_due_date' } let(:issuable) { issue } end + + it_behaves_like 'empty command' do + let(:content) { '/duplicate #{issue.to_reference}' } + let(:issuable) { issue } + end end context '/award command' do @@ -696,6 +811,30 @@ describe SlashCommands::InterpretService, services: true do end end + context '/shrug command' do + it_behaves_like 'shrug command' do + let(:content) { '/shrug people are people' } + let(:issuable) { issue } + end + + it_behaves_like 'shrug command' do + let(:content) { '/shrug' } + let(:issuable) { issue } + end + end + + context '/tableflip command' do + it_behaves_like 'tableflip command' do + let(:content) { '/tableflip curse your sudden but enviable betrayal' } + let(:issuable) { issue } + end + + it_behaves_like 'tableflip command' do + let(:content) { '/tableflip' } + let(:issuable) { issue } + end + end + context '/target_branch command' do let(:non_empty_project) { create(:project, :repository) } let(:another_merge_request) { create(:merge_request, author: developer, source_project: non_empty_project) } @@ -727,5 +866,286 @@ describe SlashCommands::InterpretService, services: true do end end end + + context '/board_move command' do + let(:todo) { create(:label, project: project, title: 'To Do') } + let(:inreview) { create(:label, project: project, title: 'In Review') } + let(:content) { %{/board_move ~"#{inreview.title}"} } + + let!(:board) { create(:board, project: project) } + let!(:todo_list) { create(:list, board: board, label: todo) } + let!(:inreview_list) { create(:list, board: board, label: inreview) } + let!(:inprogress_list) { create(:list, board: board, label: inprogress) } + + it 'populates remove_label_ids for all current board columns' do + issue.update!(label_ids: [todo.id, inprogress.id]) + + _, updates = service.execute(content, issue) + + expect(updates[:remove_label_ids]).to match_array([todo.id, inprogress.id]) + end + + it 'populates add_label_ids with the id of the given label' do + _, updates = service.execute(content, issue) + + expect(updates[:add_label_ids]).to eq([inreview.id]) + end + + it 'does not include the given label id in remove_label_ids' do + issue.update!(label_ids: [todo.id, inreview.id]) + + _, updates = service.execute(content, issue) + + expect(updates[:remove_label_ids]).to match_array([todo.id]) + end + + it 'does not remove label ids that are not lists on the board' do + issue.update!(label_ids: [todo.id, bug.id]) + + _, updates = service.execute(content, issue) + + expect(updates[:remove_label_ids]).to match_array([todo.id]) + end + + context 'if the project has multiple boards' do + let(:issuable) { issue } + + before do + create(:board, project: project) + end + + it_behaves_like 'empty command' + end + + context 'if the given label does not exist' do + let(:issuable) { issue } + let(:content) { '/board_move ~"Fake Label"' } + it_behaves_like 'empty command' + end + + context 'if multiple labels are given' do + let(:issuable) { issue } + let(:content) { %{/board_move ~"#{inreview.title}" ~"#{todo.title}"} } + it_behaves_like 'empty command' + end + + context 'if the given label is not a list on the board' do + let(:issuable) { issue } + let(:content) { %{/board_move ~"#{bug.title}"} } + it_behaves_like 'empty command' + end + + context 'if issuable is not an Issue' do + let(:issuable) { merge_request } + it_behaves_like 'empty command' + end + end + end + + describe '#explain' do + let(:service) { described_class.new(project, developer) } + let(:merge_request) { create(:merge_request, source_project: project) } + + describe 'close command' do + let(:content) { '/close' } + + it 'includes issuable name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Closes this issue.']) + end + end + + describe 'reopen command' do + let(:content) { '/reopen' } + let(:merge_request) { create(:merge_request, :closed, source_project: project) } + + it 'includes issuable name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Reopens this merge request.']) + end + end + + describe 'title command' do + let(:content) { '/title This is new title' } + + it 'includes new title' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Changes the title to "This is new title".']) + end + end + + describe 'assign command' do + let(:content) { "/assign @#{developer.username} do it!" } + + it 'includes only the user reference' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(["Assigns @#{developer.username}."]) + end + end + + describe 'unassign command' do + let(:content) { '/unassign' } + let(:issue) { create(:issue, project: project, assignees: [developer]) } + + it 'includes current assignee reference' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Removes assignee @#{developer.username}."]) + end + end + + describe 'milestone command' do + let(:content) { '/milestone %wrong-milestone' } + let!(:milestone) { create(:milestone, project: project, title: '9.10') } + + it 'is empty when milestone reference is wrong' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq([]) + end + end + + describe 'remove milestone command' do + let(:content) { '/remove_milestone' } + let(:merge_request) { create(:merge_request, source_project: project, milestone: milestone) } + + it 'includes current milestone name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Removes %"9.10" milestone.']) + end + end + + describe 'label command' do + let(:content) { '/label ~missing' } + let!(:label) { create(:label, project: project) } + + it 'is empty when there are no correct labels' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq([]) + end + end + + describe 'unlabel command' do + let(:content) { '/unlabel' } + + it 'says all labels if no parameter provided' do + merge_request.update!(label_ids: [bug.id]) + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Removes all labels.']) + end + end + + describe 'relabel command' do + let(:content) { '/relabel Bug' } + let!(:bug) { create(:label, project: project, title: 'Bug') } + let(:feature) { create(:label, project: project, title: 'Feature') } + + it 'includes label name' do + issue.update!(label_ids: [feature.id]) + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Replaces all labels with ~#{bug.id} label."]) + end + end + + describe 'subscribe command' do + let(:content) { '/subscribe' } + + it 'includes issuable name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Subscribes to this issue.']) + end + end + + describe 'unsubscribe command' do + let(:content) { '/unsubscribe' } + + it 'includes issuable name' do + merge_request.subscribe(developer, project) + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Unsubscribes from this merge request.']) + end + end + + describe 'due command' do + let(:content) { '/due April 1st 2016' } + + it 'includes the date' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Sets the due date to Apr 1, 2016.']) + end + end + + describe 'wip command' do + let(:content) { '/wip' } + + it 'includes the new status' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Marks this merge request as Work In Progress.']) + end + end + + describe 'award command' do + let(:content) { '/award :confetti_ball: ' } + + it 'includes the emoji' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Toggles :confetti_ball: emoji award.']) + end + end + + describe 'estimate command' do + let(:content) { '/estimate 79d' } + + it 'includes the formatted duration' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Sets time estimate to 3mo 3w 4d.']) + end + end + + describe 'spend command' do + let(:content) { '/spend -120m' } + + it 'includes the formatted duration and proper verb' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(['Substracts 2h spent time.']) + end + end + + describe 'target branch command' do + let(:content) { '/target_branch my-feature ' } + + it 'includes the branch name' do + _, explanations = service.explain(content, merge_request) + + expect(explanations).to eq(['Sets target branch to my-feature.']) + end + end + + describe 'board move command' do + let(:content) { '/board_move ~bug' } + let!(:bug) { create(:label, project: project, title: 'bug') } + let!(:board) { create(:board, project: project) } + + it 'includes the label name' do + _, explanations = service.explain(content, issue) + + expect(explanations).to eq(["Moves issue to ~#{bug.id} column in the board."]) + end + end end end diff --git a/spec/services/repair_ldap_blocked_user_service_spec.rb b/spec/services/repair_ldap_blocked_user_service_spec.rb index 87192457298..bf79cfe74b7 100644 --- a/spec/services/repair_ldap_blocked_user_service_spec.rb +++ b/spec/services/repair_ldap_blocked_user_service_spec.rb @@ -1,9 +1,9 @@ require 'spec_helper' -describe RepairLdapBlockedUserService, services: true do +describe RepairLdapBlockedUserService do let(:user) { create(:omniauth_user, provider: 'ldapmain', state: 'ldap_blocked') } let(:identity) { user.ldap_identity } - subject(:service) { RepairLdapBlockedUserService.new(user) } + subject(:service) { described_class.new(user) } describe '#execute' do it 'changes to normal block after destroying last ldap identity' do diff --git a/spec/services/repository_archive_clean_up_service_spec.rb b/spec/services/repository_archive_clean_up_service_spec.rb index 842585f9e54..2d7fa3f80f7 100644 --- a/spec/services/repository_archive_clean_up_service_spec.rb +++ b/spec/services/repository_archive_clean_up_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe RepositoryArchiveCleanUpService, services: true do +describe RepositoryArchiveCleanUpService do describe '#execute' do subject(:service) { described_class.new } diff --git a/spec/services/search/global_service_spec.rb b/spec/services/search/global_service_spec.rb index cbf4f56213d..1309240b430 100644 --- a/spec/services/search/global_service_spec.rb +++ b/spec/services/search/global_service_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe Search::GlobalService, services: true do +describe Search::GlobalService do let(:user) { create(:user) } let(:internal_user) { create(:user) } - let!(:found_project) { create(:empty_project, :private, name: 'searchable_project') } - let!(:unfound_project) { create(:empty_project, :private, name: 'unfound_project') } - let!(:internal_project) { create(:empty_project, :internal, name: 'searchable_internal_project') } - let!(:public_project) { create(:empty_project, :public, name: 'searchable_public_project') } + let!(:found_project) { create(:project, :private, name: 'searchable_project') } + let!(:unfound_project) { create(:project, :private, name: 'unfound_project') } + let!(:internal_project) { create(:project, :internal, name: 'searchable_internal_project') } + let!(:public_project) { create(:project, :public, name: 'searchable_public_project') } before do found_project.add_master(user) @@ -16,7 +16,7 @@ describe Search::GlobalService, services: true do describe '#execute' do context 'unauthenticated' do it 'returns public projects only' do - results = Search::GlobalService.new(nil, search: "searchable").execute + results = described_class.new(nil, search: "searchable").execute expect(results.objects('projects')).to match_array [public_project] end @@ -24,19 +24,19 @@ describe Search::GlobalService, services: true do context 'authenticated' do it 'returns public, internal and private projects' do - results = Search::GlobalService.new(user, search: "searchable").execute + results = described_class.new(user, search: "searchable").execute expect(results.objects('projects')).to match_array [public_project, found_project, internal_project] end it 'returns only public & internal projects' do - results = Search::GlobalService.new(internal_user, search: "searchable").execute + results = described_class.new(internal_user, search: "searchable").execute expect(results.objects('projects')).to match_array [internal_project, public_project] end it 'namespace name is searchable' do - results = Search::GlobalService.new(user, search: found_project.namespace.path).execute + results = described_class.new(user, search: found_project.namespace.path).execute expect(results.objects('projects')).to match_array [found_project] end diff --git a/spec/services/search/group_service_spec.rb b/spec/services/search/group_service_spec.rb index 38f264f6e7b..cbc553a60cf 100644 --- a/spec/services/search/group_service_spec.rb +++ b/spec/services/search/group_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Search::GroupService, services: true do +describe Search::GroupService do shared_examples_for 'group search' do context 'finding projects by name' do let(:user) { create(:user) } @@ -8,16 +8,16 @@ describe Search::GroupService, services: true do let(:nested_group) { create(:group, :nested) } # These projects shouldn't be found - let!(:outside_project) { create(:empty_project, :public, name: "Outside #{term}") } - let!(:private_project) { create(:empty_project, :private, namespace: nested_group, name: "Private #{term}" )} - let!(:other_project) { create(:empty_project, :public, namespace: nested_group, name: term.reverse) } + let!(:outside_project) { create(:project, :public, name: "Outside #{term}") } + let!(:private_project) { create(:project, :private, namespace: nested_group, name: "Private #{term}" )} + let!(:other_project) { create(:project, :public, namespace: nested_group, name: term.reverse) } # These projects should be found - let!(:project1) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 1") } - let!(:project2) { create(:empty_project, :internal, namespace: nested_group, name: "Inner #{term} 2") } - let!(:project3) { create(:empty_project, :internal, namespace: nested_group.parent, name: "Outer #{term}") } + let!(:project1) { create(:project, :internal, namespace: nested_group, name: "Inner #{term} 1") } + let!(:project2) { create(:project, :internal, namespace: nested_group, name: "Inner #{term} 2") } + let!(:project3) { create(:project, :internal, namespace: nested_group.parent, name: "Outer #{term}") } - let(:results) { Search::GroupService.new(user, search_group, search: term).execute } + let(:results) { described_class.new(user, search_group, search: term).execute } subject { results.objects('projects') } context 'in parent group' do diff --git a/spec/services/search/snippet_service_spec.rb b/spec/services/search/snippet_service_spec.rb index 14f3301d9f4..eae9bd4f5cf 100644 --- a/spec/services/search/snippet_service_spec.rb +++ b/spec/services/search/snippet_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe Search::SnippetService, services: true do +describe Search::SnippetService do let(:author) { create(:author) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let!(:public_snippet) { create(:snippet, :public, content: 'password: XXX') } let!(:internal_snippet) { create(:snippet, :internal, content: 'password: XXX') } diff --git a/spec/services/search_service_spec.rb b/spec/services/search_service_spec.rb index 2112f1cf9ea..02de83a2df8 100644 --- a/spec/services/search_service_spec.rb +++ b/spec/services/search_service_spec.rb @@ -1,19 +1,19 @@ require 'spec_helper' -describe SearchService, services: true do +describe SearchService do let(:user) { create(:user) } let(:accessible_group) { create(:group, :private) } let(:inaccessible_group) { create(:group, :private) } let!(:group_member) { create(:group_member, group: accessible_group, user: user) } - let!(:accessible_project) { create(:empty_project, :private, name: 'accessible_project') } - let!(:inaccessible_project) { create(:empty_project, :private, name: 'inaccessible_project') } + let!(:accessible_project) { create(:project, :private, name: 'accessible_project') } + let!(:inaccessible_project) { create(:project, :private, name: 'inaccessible_project') } let(:note) { create(:note_on_issue, project: accessible_project) } let(:snippet) { create(:snippet, author: user) } - let(:group_project) { create(:empty_project, group: accessible_group, name: 'group_project') } - let(:public_project) { create(:empty_project, :public, name: 'public_project') } + let(:group_project) { create(:project, group: accessible_group, name: 'group_project') } + let(:public_project) { create(:project, :public, name: 'public_project') } before do accessible_project.add_master(user) @@ -22,15 +22,24 @@ describe SearchService, services: true do describe '#project' do context 'when the project is accessible' do it 'returns the project' do - project = SearchService.new(user, project_id: accessible_project.id).project + project = described_class.new(user, project_id: accessible_project.id).project expect(project).to eq accessible_project end + + it 'returns the project for guests' do + search_project = create :project + search_project.add_guest(user) + + project = described_class.new(user, project_id: search_project.id).project + + expect(project).to eq search_project + end end context 'when the project is not accessible' do it 'returns nil' do - project = SearchService.new(user, project_id: inaccessible_project.id).project + project = described_class.new(user, project_id: inaccessible_project.id).project expect(project).to be_nil end @@ -38,7 +47,7 @@ describe SearchService, services: true do context 'when there is no project_id' do it 'returns nil' do - project = SearchService.new(user).project + project = described_class.new(user).project expect(project).to be_nil end @@ -48,7 +57,7 @@ describe SearchService, services: true do describe '#group' do context 'when the group is accessible' do it 'returns the group' do - group = SearchService.new(user, group_id: accessible_group.id).group + group = described_class.new(user, group_id: accessible_group.id).group expect(group).to eq accessible_group end @@ -56,7 +65,7 @@ describe SearchService, services: true do context 'when the group is not accessible' do it 'returns nil' do - group = SearchService.new(user, group_id: inaccessible_group.id).group + group = described_class.new(user, group_id: inaccessible_group.id).group expect(group).to be_nil end @@ -64,7 +73,7 @@ describe SearchService, services: true do context 'when there is no group_id' do it 'returns nil' do - group = SearchService.new(user).group + group = described_class.new(user).group expect(group).to be_nil end @@ -74,7 +83,7 @@ describe SearchService, services: true do describe '#show_snippets?' do context 'when :snippets is \'true\'' do it 'returns true' do - show_snippets = SearchService.new(user, snippets: 'true').show_snippets? + show_snippets = described_class.new(user, snippets: 'true').show_snippets? expect(show_snippets).to be_truthy end @@ -82,7 +91,7 @@ describe SearchService, services: true do context 'when :snippets is not \'true\'' do it 'returns false' do - show_snippets = SearchService.new(user, snippets: 'tru').show_snippets? + show_snippets = described_class.new(user, snippets: 'tru').show_snippets? expect(show_snippets).to be_falsey end @@ -90,7 +99,7 @@ describe SearchService, services: true do context 'when :snippets is missing' do it 'returns false' do - show_snippets = SearchService.new(user).show_snippets? + show_snippets = described_class.new(user).show_snippets? expect(show_snippets).to be_falsey end @@ -101,7 +110,7 @@ describe SearchService, services: true do context 'with accessible project_id' do context 'and allowed scope' do it 'returns the specified scope' do - scope = SearchService.new(user, project_id: accessible_project.id, scope: 'notes').scope + scope = described_class.new(user, project_id: accessible_project.id, scope: 'notes').scope expect(scope).to eq 'notes' end @@ -109,7 +118,7 @@ describe SearchService, services: true do context 'and disallowed scope' do it 'returns the default scope' do - scope = SearchService.new(user, project_id: accessible_project.id, scope: 'projects').scope + scope = described_class.new(user, project_id: accessible_project.id, scope: 'projects').scope expect(scope).to eq 'blobs' end @@ -117,7 +126,7 @@ describe SearchService, services: true do context 'and no scope' do it 'returns the default scope' do - scope = SearchService.new(user, project_id: accessible_project.id).scope + scope = described_class.new(user, project_id: accessible_project.id).scope expect(scope).to eq 'blobs' end @@ -127,7 +136,7 @@ describe SearchService, services: true do context 'with \'true\' snippets' do context 'and allowed scope' do it 'returns the specified scope' do - scope = SearchService.new(user, snippets: 'true', scope: 'snippet_titles').scope + scope = described_class.new(user, snippets: 'true', scope: 'snippet_titles').scope expect(scope).to eq 'snippet_titles' end @@ -135,7 +144,7 @@ describe SearchService, services: true do context 'and disallowed scope' do it 'returns the default scope' do - scope = SearchService.new(user, snippets: 'true', scope: 'projects').scope + scope = described_class.new(user, snippets: 'true', scope: 'projects').scope expect(scope).to eq 'snippet_blobs' end @@ -143,7 +152,7 @@ describe SearchService, services: true do context 'and no scope' do it 'returns the default scope' do - scope = SearchService.new(user, snippets: 'true').scope + scope = described_class.new(user, snippets: 'true').scope expect(scope).to eq 'snippet_blobs' end @@ -153,7 +162,7 @@ describe SearchService, services: true do context 'with no project_id, no snippets' do context 'and allowed scope' do it 'returns the specified scope' do - scope = SearchService.new(user, scope: 'issues').scope + scope = described_class.new(user, scope: 'issues').scope expect(scope).to eq 'issues' end @@ -161,7 +170,7 @@ describe SearchService, services: true do context 'and disallowed scope' do it 'returns the default scope' do - scope = SearchService.new(user, scope: 'blobs').scope + scope = described_class.new(user, scope: 'blobs').scope expect(scope).to eq 'projects' end @@ -169,7 +178,7 @@ describe SearchService, services: true do context 'and no scope' do it 'returns the default scope' do - scope = SearchService.new(user).scope + scope = described_class.new(user).scope expect(scope).to eq 'projects' end @@ -180,7 +189,7 @@ describe SearchService, services: true do describe '#search_results' do context 'with accessible project_id' do it 'returns an instance of Gitlab::ProjectSearchResults' do - search_results = SearchService.new( + search_results = described_class.new( user, project_id: accessible_project.id, scope: 'notes', @@ -192,7 +201,7 @@ describe SearchService, services: true do context 'with accessible project_id and \'true\' snippets' do it 'returns an instance of Gitlab::ProjectSearchResults' do - search_results = SearchService.new( + search_results = described_class.new( user, project_id: accessible_project.id, snippets: 'true', @@ -205,7 +214,7 @@ describe SearchService, services: true do context 'with \'true\' snippets' do it 'returns an instance of Gitlab::SnippetSearchResults' do - search_results = SearchService.new( + search_results = described_class.new( user, snippets: 'true', search: snippet.content).search_results @@ -216,7 +225,7 @@ describe SearchService, services: true do context 'with no project_id and no snippets' do it 'returns an instance of Gitlab::SearchResults' do - search_results = SearchService.new( + search_results = described_class.new( user, search: public_project.name).search_results @@ -228,7 +237,7 @@ describe SearchService, services: true do describe '#search_objects' do context 'with accessible project_id' do it 'returns objects in the project' do - search_objects = SearchService.new( + search_objects = described_class.new( user, project_id: accessible_project.id, scope: 'notes', @@ -240,7 +249,7 @@ describe SearchService, services: true do context 'with accessible project_id and \'true\' snippets' do it 'returns objects in the project' do - search_objects = SearchService.new( + search_objects = described_class.new( user, project_id: accessible_project.id, snippets: 'true', @@ -253,7 +262,7 @@ describe SearchService, services: true do context 'with \'true\' snippets' do it 'returns objects in snippets' do - search_objects = SearchService.new( + search_objects = described_class.new( user, snippets: 'true', search: snippet.content).search_objects @@ -264,7 +273,7 @@ describe SearchService, services: true do context 'with accessible group_id' do it 'returns objects in the group' do - search_objects = SearchService.new( + search_objects = described_class.new( user, group_id: accessible_group.id, search: group_project.name).search_objects @@ -275,7 +284,7 @@ describe SearchService, services: true do context 'with no project_id, group_id or snippets' do it 'returns objects in global' do - search_objects = SearchService.new( + search_objects = described_class.new( user, search: public_project.name).search_objects diff --git a/spec/services/spam_service_spec.rb b/spec/services/spam_service_spec.rb index 74cba8c014b..a14dfa3f01f 100644 --- a/spec/services/spam_service_spec.rb +++ b/spec/services/spam_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe SpamService, services: true do +describe SpamService do describe '#when_recaptcha_verified' do def check_spam(issue, request, recaptcha_verified) described_class.new(issue, request).when_recaptcha_verified(recaptcha_verified) do @@ -15,7 +15,7 @@ describe SpamService, services: true do end context 'when recaptcha was not verified' do - let(:project) { create(:empty_project, :public) } + let(:project) { create(:project, :public) } let(:issue) { create(:issue, project: project) } let(:request) { double(:request, env: {}) } @@ -70,7 +70,9 @@ describe SpamService, services: true do end context 'when not indicated as spam by akismet' do - before { allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) } + before do + allow(AkismetService).to receive(:new).and_return(double(is_spam?: false)) + end it 'returns false' do expect(check_spam(issue, request, false)).to be_falsey diff --git a/spec/services/submit_usage_ping_service_spec.rb b/spec/services/submit_usage_ping_service_spec.rb new file mode 100644 index 00000000000..c8a6fc1a99b --- /dev/null +++ b/spec/services/submit_usage_ping_service_spec.rb @@ -0,0 +1,106 @@ +require 'spec_helper' + +describe SubmitUsagePingService do + context 'when usage ping is disabled' do + before do + stub_application_setting(usage_ping_enabled: false) + end + + it 'does not run' do + expect(HTTParty).not_to receive(:post) + + result = subject.execute + + expect(result).to eq false + end + end + + context 'when usage ping is enabled' do + before do + stub_application_setting(usage_ping_enabled: true) + end + + it 'sends a POST request' do + response = stub_response(without_conv_index_params) + + subject.execute + + expect(response).to have_been_requested + end + + it 'refreshes usage data statistics before submitting' do + stub_response(without_conv_index_params) + + expect(Gitlab::UsageData).to receive(:to_json) + .with(force_refresh: true) + .and_call_original + + subject.execute + end + + it 'saves conversational development index data from the response' do + stub_response(with_conv_index_params) + + expect { subject.execute } + .to change { ConversationalDevelopmentIndex::Metric.count } + .by(1) + + expect(ConversationalDevelopmentIndex::Metric.last.leader_issues).to eq 10.2 + expect(ConversationalDevelopmentIndex::Metric.last.instance_issues).to eq 3.2 + expect(ConversationalDevelopmentIndex::Metric.last.percentage_issues).to eq 31.37 + end + end + + def without_conv_index_params + { + conv_index: {} + } + end + + def with_conv_index_params + { + conv_index: { + leader_issues: 10.2, + instance_issues: 3.2, + percentage_issues: 31.37, + + leader_notes: 25.3, + instance_notes: 23.2, + + leader_milestones: 16.2, + instance_milestones: 5.5, + + leader_boards: 5.2, + instance_boards: 3.2, + + leader_merge_requests: 5.2, + instance_merge_requests: 3.2, + + leader_ci_pipelines: 25.1, + instance_ci_pipelines: 21.3, + + leader_environments: 3.3, + instance_environments: 2.2, + + leader_deployments: 41.3, + instance_deployments: 15.2, + + leader_projects_prometheus_active: 0.31, + instance_projects_prometheus_active: 0.30, + + leader_service_desk_issues: 15.8, + instance_service_desk_issues: 15.1, + + non_existing_column: 'value' + } + } + end + + def stub_response(body) + stub_request(:post, 'https://version.gitlab.com/usage_data') + .to_return( + headers: { 'Content-Type' => 'application/json' }, + body: body.to_json + ) + end +end diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb index 667059f230c..8b5d9187785 100644 --- a/spec/services/system_hooks_service_spec.rb +++ b/spec/services/system_hooks_service_spec.rb @@ -1,8 +1,8 @@ require 'spec_helper' -describe SystemHooksService, services: true do +describe SystemHooksService do let(:user) { create(:user) } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:project_member) { create(:project_member) } let(:key) { create(:key, user: user) } let(:deploy_key) { create(:key) } diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb index 75d7caf2508..6d36affa9dc 100644 --- a/spec/services/system_note_service_spec.rb +++ b/spec/services/system_note_service_spec.rb @@ -1,11 +1,13 @@ require 'spec_helper' -describe SystemNoteService, services: true do - include Gitlab::Routing.url_helpers +describe SystemNoteService do + include Gitlab::Routing - let(:project) { create(:empty_project) } + let(:group) { create(:group) } + let(:project) { create(:project, group: group) } let(:author) { create(:user) } let(:noteable) { create(:issue, project: project) } + let(:issue) { noteable } shared_examples_for 'a system note' do let(:expected_noteable) { noteable } @@ -33,7 +35,7 @@ describe SystemNoteService, services: true do context 'metadata' do it 'creates a new system note metadata record' do - expect { subject }.to change{ SystemNoteMetadata.count }.from(0).to(1) + expect { subject }.to change { SystemNoteMetadata.count }.from(0).to(1) end it 'creates a record correctly' do @@ -155,6 +157,52 @@ describe SystemNoteService, services: true do end end + describe '.change_issue_assignees' do + subject { described_class.change_issue_assignees(noteable, project, author, [assignee]) } + + let(:assignee) { create(:user) } + let(:assignee1) { create(:user) } + let(:assignee2) { create(:user) } + let(:assignee3) { create(:user) } + + it_behaves_like 'a system note' do + let(:action) { 'assignee' } + end + + def build_note(old_assignees, new_assignees) + issue.assignees = new_assignees + described_class.change_issue_assignees(issue, project, author, old_assignees).note + end + + it 'builds a correct phrase when an assignee is added to a non-assigned issue' do + expect(build_note([], [assignee1])).to eq "assigned to @#{assignee1.username}" + end + + it 'builds a correct phrase when assignee removed' do + expect(build_note([assignee1], [])).to eq 'removed assignee' + end + + it 'builds a correct phrase when assignees changed' do + expect(build_note([assignee1], [assignee2])).to eq \ + "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}" + end + + it 'builds a correct phrase when three assignees removed and one added' do + expect(build_note([assignee, assignee1, assignee2], [assignee3])).to eq \ + "assigned to @#{assignee3.username} and unassigned @#{assignee.username}, @#{assignee1.username}, and @#{assignee2.username}" + end + + it 'builds a correct phrase when one assignee changed from a set' do + expect(build_note([assignee, assignee1], [assignee, assignee2])).to eq \ + "assigned to @#{assignee2.username} and unassigned @#{assignee1.username}" + end + + it 'builds a correct phrase when one assignee removed from a set' do + expect(build_note([assignee, assignee1, assignee2], [assignee, assignee1])).to eq \ + "unassigned @#{assignee2.username}" + end + end + describe '.change_label' do subject { described_class.change_label(noteable, project, author, added, removed) } @@ -195,25 +243,51 @@ describe SystemNoteService, services: true do end describe '.change_milestone' do - subject { described_class.change_milestone(noteable, project, author, milestone) } + context 'for a project milestone' do + subject { described_class.change_milestone(noteable, project, author, milestone) } - let(:milestone) { create(:milestone, project: project) } + let(:milestone) { create(:milestone, project: project) } - it_behaves_like 'a system note' do - let(:action) { 'milestone' } - end + it_behaves_like 'a system note' do + let(:action) { 'milestone' } + end - context 'when milestone added' do - it 'sets the note text' do - expect(subject.note).to eq "changed milestone to #{milestone.to_reference}" + context 'when milestone added' do + it 'sets the note text' do + expect(subject.note).to eq "changed milestone to #{milestone.to_reference}" + end + end + + context 'when milestone removed' do + let(:milestone) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'removed milestone' + end end end - context 'when milestone removed' do - let(:milestone) { nil } + context 'for a group milestone' do + subject { described_class.change_milestone(noteable, project, author, milestone) } - it 'sets the note text' do - expect(subject.note).to eq 'removed milestone' + let(:milestone) { create(:milestone, group: group) } + + it_behaves_like 'a system note' do + let(:action) { 'milestone' } + end + + context 'when milestone added' do + it 'sets the note text to use the milestone name' do + expect(subject.note).to eq "changed milestone to #{milestone.to_reference(format: :name)}" + end + end + + context 'when milestone removed' do + let(:milestone) { nil } + + it 'sets the note text' do + expect(subject.note).to eq 'removed milestone' + end end end end @@ -286,8 +360,22 @@ describe SystemNoteService, services: true do end it 'sets the note text' do - expect(subject.note). - to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" + expect(subject.note) + .to eq "changed title from **{-Old title-}** to **{+Lorem ipsum+}**" + end + end + end + + describe '.change_description' do + subject { described_class.change_description(noteable, project, author) } + + context 'when noteable responds to `description`' do + it_behaves_like 'a system note' do + let(:action) { 'description' } + end + + it 'sets the note text' do + expect(subject.note).to eq('changed the description') end end end @@ -389,7 +477,7 @@ describe SystemNoteService, services: true do end it 'does not create a system note metadata record' do - expect { subject }.not_to change{ SystemNoteMetadata.count } + expect { subject }.not_to change { SystemNoteMetadata.count } end end @@ -460,8 +548,8 @@ describe SystemNoteService, services: true do context 'when mentioner is not a MergeRequest' do it 'is falsey' do mentioner = noteable.dup - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_falsey + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_falsey end end @@ -472,14 +560,14 @@ describe SystemNoteService, services: true do it 'is truthy when noteable is in commits' do expect(mentioner).to receive(:commits).and_return([noteable]) - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_truthy + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_truthy end it 'is falsey when noteable is not in commits' do expect(mentioner).to receive(:commits).and_return([]) - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_falsey + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_falsey end end @@ -487,8 +575,8 @@ describe SystemNoteService, services: true do let(:noteable) { ExternalIssue.new('EXT-1234', project) } it 'is truthy' do mentioner = noteable.dup - expect(described_class.cross_reference_disallowed?(noteable, mentioner)). - to be_truthy + expect(described_class.cross_reference_disallowed?(noteable, mentioner)) + .to be_truthy end end end @@ -505,13 +593,13 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit0)). - to be_truthy + expect(described_class.cross_reference_exists?(noteable, commit0)) + .to be_truthy end it 'is falsey when not already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit1)). - to be_falsey + expect(described_class.cross_reference_exists?(noteable, commit1)) + .to be_falsey end context 'legacy capitalized cross reference' do @@ -522,8 +610,8 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(noteable, commit0)). - to be_truthy + expect(described_class.cross_reference_exists?(noteable, commit0)) + .to be_truthy end end end @@ -535,13 +623,13 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(commit0, commit1)). - to be_truthy + expect(described_class.cross_reference_exists?(commit0, commit1)) + .to be_truthy end it 'is falsey when not already mentioned' do - expect(described_class.cross_reference_exists?(commit1, commit0)). - to be_falsey + expect(described_class.cross_reference_exists?(commit1, commit0)) + .to be_falsey end context 'legacy capitalized cross reference' do @@ -552,8 +640,8 @@ describe SystemNoteService, services: true do end it 'is truthy when already mentioned' do - expect(described_class.cross_reference_exists?(commit0, commit1)). - to be_truthy + expect(described_class.cross_reference_exists?(commit0, commit1)) + .to be_truthy end end end @@ -568,8 +656,8 @@ describe SystemNoteService, services: true do end it 'is true when a fork mentions an external issue' do - expect(described_class.cross_reference_exists?(noteable, commit2)). - to be true + expect(described_class.cross_reference_exists?(noteable, commit2)) + .to be true end context 'legacy capitalized cross reference' do @@ -579,15 +667,15 @@ describe SystemNoteService, services: true do end it 'is true when a fork mentions an external issue' do - expect(described_class.cross_reference_exists?(noteable, commit2)). - to be true + expect(described_class.cross_reference_exists?(noteable, commit2)) + .to be true end end end end describe '.noteable_moved' do - let(:new_project) { create(:empty_project) } + let(:new_project) { create(:project) } let(:new_noteable) { create(:issue, project: new_project) } subject do @@ -606,7 +694,7 @@ describe SystemNoteService, services: true do end it 'mentions referenced project' do - expect(subject.note).to include new_project.path_with_namespace + expect(subject.note).to include new_project.full_path end end @@ -657,7 +745,7 @@ describe SystemNoteService, services: true do describe 'JIRA integration' do include JiraServiceHelper - let(:project) { create(:jira_project) } + let(:project) { create(:jira_project, :repository) } let(:author) { create(:user) } let(:issue) { create(:issue, project: project) } let(:merge_request) { create(:merge_request, :simple, target_project: project, source_project: project) } @@ -672,6 +760,26 @@ describe SystemNoteService, services: true do jira_service_settings end + def cross_reference(type, link_exists = false) + noteable = type == 'commit' ? commit : merge_request + + links = [] + if link_exists + url = if type == 'commit' + "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/commit/#{commit.id}" + else + "#{Settings.gitlab.base_url}/#{project.namespace.path}/#{project.path}/merge_requests/#{merge_request.iid}" + end + link = double(object: { 'url' => url }) + links << link + expect(link).to receive(:save!) + end + + allow(JIRA::Resource::Remotelink).to receive(:all).and_return(links) + + described_class.cross_reference(jira_issue, noteable, author) + end + noteable_types = %w(merge_requests commit) noteable_types.each do |type| @@ -679,24 +787,39 @@ describe SystemNoteService, services: true do it "blocks cross reference when #{type.underscore}_events is false" do jira_tracker.update("#{type}_events" => false) - noteable = type == "commit" ? commit : merge_request - result = described_class.cross_reference(jira_issue, noteable, author) - - expect(result).to eq("Events for #{noteable.class.to_s.underscore.humanize.pluralize.downcase} are disabled.") + expect(cross_reference(type)).to eq("Events for #{type.pluralize.humanize.downcase} are disabled.") end it "blocks cross reference when #{type.underscore}_events is true" do jira_tracker.update("#{type}_events" => true) - noteable = type == "commit" ? commit : merge_request - result = described_class.cross_reference(jira_issue, noteable, author) + expect(cross_reference(type)).to eq(success_message) + end + end + + context 'when a new cross reference is created' do + it 'creates a new comment and remote link' do + cross_reference(type) - expect(result).to eq(success_message) + expect(WebMock).to have_requested(:post, jira_api_comment_url(jira_issue)) + expect(WebMock).to have_requested(:post, jira_api_remote_link_url(jira_issue)) + end + end + + context 'when a link exists' do + it 'updates a link but does not create a new comment' do + expect(WebMock).not_to have_requested(:post, jira_api_comment_url(jira_issue)) + + cross_reference(type, true) end end end describe "new reference" do + before do + allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) + end + context 'for commits' do it "creates comment" do result = described_class.cross_reference(jira_issue, commit, author) @@ -711,7 +834,7 @@ describe SystemNoteService, services: true do body: hash_including( GlobalID: "GitLab", object: { - url: namespace_project_commit_url(project.namespace, project, commit), + url: project_commit_url(project, commit), title: "GitLab: Mentioned on commit - #{commit.title}", icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, status: { resolved: false } @@ -737,7 +860,7 @@ describe SystemNoteService, services: true do body: hash_including( GlobalID: "GitLab", object: { - url: namespace_project_issue_url(project.namespace, project, issue), + url: project_issue_url(project, issue), title: "GitLab: Mentioned on issue - #{issue.title}", icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, status: { resolved: false } @@ -763,7 +886,7 @@ describe SystemNoteService, services: true do body: hash_including( GlobalID: "GitLab", object: { - url: namespace_project_snippet_url(project.namespace, project, snippet), + url: project_snippet_url(project, snippet), title: "GitLab: Mentioned on snippet - #{snippet.title}", icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" }, status: { resolved: false } @@ -776,7 +899,8 @@ describe SystemNoteService, services: true do describe "existing reference" do before do - message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title.chomp}'" + allow(JIRA::Resource::Remotelink).to receive(:all).and_return([]) + message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.full_path}|http://localhost/#{project.full_path}/commit/#{commit.id}]:\n'#{commit.title.chomp}'" allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)]) end @@ -955,7 +1079,7 @@ describe SystemNoteService, services: true do let(:action) { 'task' } end - it "posts the 'marked as a Work In Progress from commit' system note" do + it "posts the 'marked the task as complete' system note" do expect(subject.note).to eq("marked the task **task** as completed") end end @@ -973,4 +1097,85 @@ describe SystemNoteService, services: true do expect(subject.note).to eq 'resolved all discussions' end end + + describe '.diff_discussion_outdated' do + let(:discussion) { create(:diff_note_on_merge_request).to_discussion } + let(:merge_request) { discussion.noteable } + let(:project) { merge_request.source_project } + let(:change_position) { discussion.position } + + def reloaded_merge_request + MergeRequest.find(merge_request.id) + end + + subject { described_class.diff_discussion_outdated(discussion, project, author, change_position) } + + it_behaves_like 'a system note' do + let(:expected_noteable) { discussion.first_note.noteable } + let(:action) { 'outdated' } + end + + it 'creates a new note in the discussion' do + # we need to completely rebuild the merge request object, or the `@discussions` on the merge request are not reloaded. + expect { subject }.to change { reloaded_merge_request.discussions.first.notes.size }.by(1) + end + + it 'links to the diff in the system note' do + expect(subject.note).to include('version 1') + + diff_id = merge_request.merge_request_diff.id + line_code = change_position.line_code(project.repository) + expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code)) + end + end + + describe '.mark_duplicate_issue' do + subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) } + + context 'within the same project' do + let(:canonical_issue) { create(:issue, project: project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" } + end + + context 'across different projects' do + let(:other_project) { create(:project) } + let(:canonical_issue) { create(:issue, project: other_project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" } + end + end + + describe '.mark_canonical_issue_of_duplicate' do + subject { described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) } + + context 'within the same project' do + let(:duplicate_issue) { create(:issue, project: project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" } + end + + context 'across different projects' do + let(:other_project) { create(:project) } + let(:duplicate_issue) { create(:issue, project: other_project) } + + it_behaves_like 'a system note' do + let(:action) { 'duplicate' } + end + + it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" } + end + end end diff --git a/spec/services/tags/create_service_spec.rb b/spec/services/tags/create_service_spec.rb index b9121b1de49..1b31ce29f7a 100644 --- a/spec/services/tags/create_service_spec.rb +++ b/spec/services/tags/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Tags::CreateService, services: true do +describe Tags::CreateService do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } @@ -26,9 +26,9 @@ describe Tags::CreateService, services: true do context 'when tag already exists' do it 'returns an error' do - expect(repository).to receive(:add_tag). - with(user, 'v1.1.0', 'master', 'Foo'). - and_raise(Rugged::TagError) + expect(repository).to receive(:add_tag) + .with(user, 'v1.1.0', 'master', 'Foo') + .and_raise(Rugged::TagError) response = service.execute('v1.1.0', 'master', 'Foo') @@ -39,9 +39,9 @@ describe Tags::CreateService, services: true do context 'when pre-receive hook fails' do it 'returns an error' do - expect(repository).to receive(:add_tag). - with(user, 'v1.1.0', 'master', 'Foo'). - and_raise(GitHooksService::PreReceiveError, 'something went wrong') + expect(repository).to receive(:add_tag) + .with(user, 'v1.1.0', 'master', 'Foo') + .and_raise(GitHooksService::PreReceiveError, 'something went wrong') response = service.execute('v1.1.0', 'master', 'Foo') diff --git a/spec/services/tags/destroy_service_spec.rb b/spec/services/tags/destroy_service_spec.rb index 28396fc3658..7c8c1dd0d3a 100644 --- a/spec/services/tags/destroy_service_spec.rb +++ b/spec/services/tags/destroy_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Tags::DestroyService, services: true do +describe Tags::DestroyService do let(:project) { create(:project, :repository) } let(:repository) { project.repository } let(:user) { create(:user) } diff --git a/spec/services/test_hook_service_spec.rb b/spec/services/test_hook_service_spec.rb deleted file mode 100644 index f99fd8434c2..00000000000 --- a/spec/services/test_hook_service_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -describe TestHookService, services: true do - let(:user) { create(:user) } - let(:project) { create(:project, :repository) } - let(:hook) { create(:project_hook, project: project) } - - describe '#execute' do - it "executes successfully" do - stub_request(:post, hook.url).to_return(status: 200) - expect(TestHookService.new.execute(hook, user)).to be_truthy - end - end -end diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb new file mode 100644 index 00000000000..4218c15a3ce --- /dev/null +++ b/spec/services/test_hooks/project_service_spec.rb @@ -0,0 +1,188 @@ +require 'spec_helper' + +describe TestHooks::ProjectService do + let(:current_user) { create(:user) } + + describe '#execute' do + let(:project) { create(:project, :repository) } + let(:hook) { create(:project_hook, project: project) } + let(:service) { described_class.new(hook, current_user, trigger) } + let(:sample_data) { { data: 'sample' } } + let(:success_result) { { status: :success, http_status: 200, message: 'ok' } } + + context 'hook with not implemented test' do + let(:trigger) { 'not_implemented_events' } + + it 'returns error message' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Testing not available for this hook' }) + end + end + + context 'push_events' do + let(:trigger) { 'push_events' } + + it 'returns error message if not enough data' do + allow(project).to receive(:empty_repo?).and_return(true) + + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has at least one commit.' }) + end + + it 'executes hook' do + allow(project).to receive(:empty_repo?).and_return(false) + allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'tag_push_events' do + let(:trigger) { 'tag_push_events' } + + it 'returns error message if not enough data' do + allow(project).to receive(:empty_repo?).and_return(true) + + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has at least one commit.' }) + end + + it 'executes hook' do + allow(project).to receive(:empty_repo?).and_return(false) + allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'note_events' do + let(:trigger) { 'note_events' } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has notes.' }) + end + + it 'executes hook' do + allow(project).to receive(:notes).and_return([Note.new]) + allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'issues_events' do + let(:trigger) { 'issues_events' } + let(:issue) { build(:issue) } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has issues.' }) + end + + it 'executes hook' do + allow(project).to receive(:issues).and_return([issue]) + allow(issue).to receive(:to_hook_data).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'confidential_issues_events' do + let(:trigger) { 'confidential_issues_events' } + let(:issue) { build(:issue) } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has issues.' }) + end + + it 'executes hook' do + allow(project).to receive(:issues).and_return([issue]) + allow(issue).to receive(:to_hook_data).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'merge_requests_events' do + let(:trigger) { 'merge_requests_events' } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has merge requests.' }) + end + + it 'executes hook' do + create(:merge_request, source_project: project) + allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'job_events' do + let(:trigger) { 'job_events' } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has CI jobs.' }) + end + + it 'executes hook' do + create(:ci_build, project: project) + allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'pipeline_events' do + let(:trigger) { 'pipeline_events' } + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the project has CI pipelines.' }) + end + + it 'executes hook' do + create(:ci_empty_pipeline, project: project) + allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'wiki_page_events' do + let(:trigger) { 'wiki_page_events' } + + it 'returns error message if wiki disabled' do + allow(project).to receive(:wiki_enabled?).and_return(false) + + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the wiki is enabled and has pages.' }) + end + + it 'returns error message if not enough data' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Ensure the wiki is enabled and has pages.' }) + end + + it 'executes hook' do + create(:wiki_page, wiki: project.wiki) + allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + end +end diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb new file mode 100644 index 00000000000..00d89924766 --- /dev/null +++ b/spec/services/test_hooks/system_service_spec.rb @@ -0,0 +1,82 @@ +require 'spec_helper' + +describe TestHooks::SystemService do + let(:current_user) { create(:user) } + + describe '#execute' do + let(:project) { create(:project, :repository) } + let(:hook) { create(:system_hook) } + let(:service) { described_class.new(hook, current_user, trigger) } + let(:sample_data) { { data: 'sample' }} + let(:success_result) { { status: :success, http_status: 200, message: 'ok' } } + + before do + allow(Project).to receive(:first).and_return(project) + end + + context 'hook with not implemented test' do + let(:trigger) { 'not_implemented_events' } + + it 'returns error message' do + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: 'Testing not available for this hook' }) + end + end + + context 'push_events' do + let(:trigger) { 'push_events' } + + it 'returns error message if not enough data' do + allow(project).to receive(:empty_repo?).and_return(true) + + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has commits." }) + end + + it 'executes hook' do + allow(project).to receive(:empty_repo?).and_return(false) + allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'tag_push_events' do + let(:trigger) { 'tag_push_events' } + + it 'returns error message if not enough data' do + allow(project.repository).to receive(:tags).and_return([]) + + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has tags." }) + end + + it 'executes hook' do + allow(project.repository).to receive(:tags).and_return(['tag']) + allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + + context 'repository_update_events' do + let(:trigger) { 'repository_update_events' } + + it 'returns error message if not enough data' do + allow(project).to receive(:commit).and_return(nil) + expect(hook).not_to receive(:execute) + expect(service.execute).to include({ status: :error, message: "Ensure project \"#{project.human_name}\" has commits." }) + end + + it 'executes hook' do + allow(project).to receive(:empty_repo?).and_return(false) + allow(Gitlab::DataBuilder::Repository).to receive(:update).and_return(sample_data) + + expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result) + expect(service.execute).to include(success_result) + end + end + end +end diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb index 89b3b6aad10..a9b34a5258a 100644 --- a/spec/services/todo_service_spec.rb +++ b/spec/services/todo_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe TodoService, services: true do +describe TodoService do let(:author) { create(:user) } let(:assignee) { create(:user) } let(:non_member) { create(:user) } @@ -10,7 +10,7 @@ describe TodoService, services: true do let(:john_doe) { create(:user) } let(:skipped) { create(:user) } let(:skip_users) { [skipped] } - let(:project) { create(:empty_project) } + let(:project) { create(:project) } let(:mentions) { 'FYI: ' + [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed) { [author, assignee, john_doe, member, guest, non_member, admin, skipped].map(&:to_reference).join(' ') } let(:directly_addressed_and_mentioned) { member.to_reference + ", what do you think? cc: " + [guest, admin, skipped].map(&:to_reference).join(' ') } @@ -25,11 +25,11 @@ describe TodoService, services: true do end describe 'Issues' do - let(:issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } - let(:addressed_issue) { create(:issue, project: project, assignee: john_doe, author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } - let(:unassigned_issue) { create(:issue, project: project, assignee: nil) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: mentions) } - let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee, description: directly_addressed) } + let(:issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "- [ ] Task 1\n- [ ] Task 2 #{mentions}") } + let(:addressed_issue) { create(:issue, project: project, assignees: [john_doe], author: author, description: "#{directly_addressed}\n- [ ] Task 1\n- [ ] Task 2") } + let(:unassigned_issue) { create(:issue, project: project, assignees: []) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: mentions) } + let(:addressed_confident_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee], description: directly_addressed) } describe '#new_issue' do it 'creates a todo if assigned' do @@ -43,7 +43,7 @@ describe TodoService, services: true do end it 'creates a todo if assignee is the current user' do - unassigned_issue.update_attribute(:assignee, john_doe) + unassigned_issue.assignees = [john_doe] service.new_issue(unassigned_issue, john_doe) should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) @@ -103,7 +103,7 @@ describe TodoService, services: true do context 'when a private group is mentioned' do let(:group) { create(:group, :private) } - let(:project) { create(:empty_project, :private, group: group) } + let(:project) { create(:project, :private, group: group) } let(:issue) { create(:issue, author: author, project: project, description: group.to_reference) } before do @@ -258,20 +258,20 @@ describe TodoService, services: true do describe '#reassigned_issue' do it 'creates a pending todo for new assignee' do - unassigned_issue.update_attribute(:assignee, john_doe) + unassigned_issue.assignees << john_doe service.reassigned_issue(unassigned_issue, author) should_create_todo(user: john_doe, target: unassigned_issue, action: Todo::ASSIGNED) end it 'does not create a todo if unassigned' do - issue.update_attribute(:assignee, nil) + issue.assignees.destroy_all should_not_create_any_todo { service.reassigned_issue(issue, author) } end it 'creates a todo if new assignee is the current user' do - unassigned_issue.update_attribute(:assignee, john_doe) + unassigned_issue.assignees << john_doe service.reassigned_issue(unassigned_issue, john_doe) should_create_todo(user: john_doe, target: unassigned_issue, author: john_doe, action: Todo::ASSIGNED) @@ -336,7 +336,7 @@ describe TodoService, services: true do describe '#mark_todos_as_done' do it_behaves_like 'updating todos state', :mark_todos_as_done, :pending, :done do - let(:collection) { [first_todo, second_todo] } + let(:collection) { Todo.all } end end @@ -348,7 +348,7 @@ describe TodoService, services: true do describe '#mark_todos_as_pending' do it_behaves_like 'updating todos state', :mark_todos_as_pending, :done, :pending do - let(:collection) { [first_todo, second_todo] } + let(:collection) { Todo.all } end end @@ -361,7 +361,7 @@ describe TodoService, services: true do describe '#new_note' do let!(:first_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } let!(:second_todo) { create(:todo, :assigned, user: john_doe, project: project, target: issue, author: author) } - let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) } + let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignees: [assignee]) } let(:note) { create(:note, project: project, noteable: issue, author: john_doe, note: mentions) } let(:addressed_note) { create(:note, project: project, noteable: issue, author: john_doe, note: directly_addressed) } let(:note_on_commit) { create(:note_on_commit, project: project, author: john_doe, note: mentions) } @@ -586,13 +586,13 @@ describe TodoService, services: true do it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is pending' do create(:todo, :directly_addressed, user: member, project: project, target: addressed_mr_assigned, author: author) - expect{ service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) + expect { service.update_merge_request(addressed_mr_assigned, author) }.not_to change(member.todos, :count) end it 'does not create a directly addressed todo if user was already mentioned or addressed and todo is done' do create(:todo, :directly_addressed, user: skipped, project: project, target: addressed_mr_assigned, author: author) - expect{ service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count) + expect { service.update_merge_request(addressed_mr_assigned, author, skip_users) }.not_to change(skipped.todos, :count) end context 'with a task list' do @@ -854,7 +854,7 @@ describe TodoService, services: true do end it 'updates cached counts when a todo is created' do - issue = create(:issue, project: project, assignee: john_doe, author: author, description: mentions) + issue = create(:issue, project: project, assignees: [john_doe], author: author, description: mentions) expect(john_doe.todos_pending_count).to eq(0) expect(john_doe).to receive(:update_todos_count_cache).and_call_original @@ -866,28 +866,30 @@ describe TodoService, services: true do end describe '#mark_todos_as_done' do - let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) } - let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) } + let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } it 'marks a relation of todos as done' do create(:todo, :mentioned, user: john_doe, target: issue, project: project) todos = TodosFinder.new(john_doe, {}).execute - expect { TodoService.new.mark_todos_as_done(todos, john_doe) } + expect { described_class.new.mark_todos_as_done(todos, john_doe) } .to change { john_doe.todos.done.count }.from(0).to(1) end it 'marks an array of todos as done' do todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - expect { TodoService.new.mark_todos_as_done([todo], john_doe) } + todos = TodosFinder.new(john_doe, {}).execute + expect { described_class.new.mark_todos_as_done(todos, john_doe) } .to change { todo.reload.state }.from('pending').to('done') end it 'returns the ids of updated todos' do # Needed on API todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq([todo.id]) + todos = TodosFinder.new(john_doe, {}).execute + expect(described_class.new.mark_todos_as_done(todos, john_doe)).to eq([todo.id]) end context 'when some of the todos are done already' do @@ -895,23 +897,44 @@ describe TodoService, services: true do let!(:second_todo) { create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) } it 'returns the ids of those still pending' do - TodoService.new.mark_pending_todos_as_done(issue, john_doe) + described_class.new.mark_pending_todos_as_done(issue, john_doe) - expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id]) + expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([second_todo.id]) end it 'returns an empty array if all are done' do - TodoService.new.mark_pending_todos_as_done(issue, john_doe) - TodoService.new.mark_pending_todos_as_done(another_issue, john_doe) + described_class.new.mark_pending_todos_as_done(issue, john_doe) + described_class.new.mark_pending_todos_as_done(another_issue, john_doe) - expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq([]) + expect(described_class.new.mark_todos_as_done(Todo.all, john_doe)).to eq([]) end end + end + + describe '#mark_todos_as_done_by_ids' do + let(:issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + let(:another_issue) { create(:issue, project: project, author: author, assignees: [john_doe]) } + + it 'marks an array of todo ids as done' do + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) + another_todo = create(:todo, :mentioned, user: john_doe, target: another_issue, project: project) + + expect { described_class.new.mark_todos_as_done_by_ids([todo.id, another_todo.id], john_doe) } + .to change { john_doe.todos.done.count }.from(0).to(2) + end + + it 'marks a single todo id as done' do + todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - it 'caches the number of todos of a user', :caching do + expect { described_class.new.mark_todos_as_done_by_ids(todo.id, john_doe) } + .to change { todo.reload.state }.from('pending').to('done') + end + + it 'caches the number of todos of a user', :use_clean_rails_memory_store_caching do create(:todo, :mentioned, user: john_doe, target: issue, project: project) todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project) - TodoService.new.mark_todos_as_done([todo], john_doe) + + described_class.new.mark_todos_as_done_by_ids(todo, john_doe) expect_any_instance_of(TodosFinder).not_to receive(:execute) diff --git a/spec/services/update_release_service_spec.rb b/spec/services/update_release_service_spec.rb index 69ed8de9c31..dc2d0e2d47a 100644 --- a/spec/services/update_release_service_spec.rb +++ b/spec/services/update_release_service_spec.rb @@ -1,12 +1,12 @@ require 'spec_helper' -describe UpdateReleaseService, services: true do +describe UpdateReleaseService do let(:project) { create(:project, :repository) } let(:user) { create(:user) } let(:tag_name) { project.repository.tag_names.first } let(:description) { 'Awesome release!' } let(:new_description) { 'The best release!' } - let(:service) { UpdateReleaseService.new(project, user) } + let(:service) { described_class.new(project, user) } context 'with an existing release' do let(:create_service) { CreateReleaseService.new(project, user) } diff --git a/spec/services/update_snippet_service_spec.rb b/spec/services/update_snippet_service_spec.rb index 37c2e861362..ef535c5cf1f 100644 --- a/spec/services/update_snippet_service_spec.rb +++ b/spec/services/update_snippet_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe UpdateSnippetService, services: true do +describe UpdateSnippetService do before do @user = create :user @admin = create :user, admin: true diff --git a/spec/services/projects/upload_service_spec.rb b/spec/services/upload_service_spec.rb index d2cefa46bfa..24f3a5c5ff0 100644 --- a/spec/services/projects/upload_service_spec.rb +++ b/spec/services/upload_service_spec.rb @@ -1,10 +1,10 @@ require 'spec_helper' -describe Projects::UploadService, services: true do +describe UploadService do describe 'File service' do before do @user = create(:user) - @project = create(:empty_project, creator_id: @user.id, namespace: @user.namespace) + @project = create(:project, creator_id: @user.id, namespace: @user.namespace) end context 'for valid gif file' do @@ -68,6 +68,6 @@ describe Projects::UploadService, services: true do end def upload_file(project, file) - Projects::UploadService.new(project, file).execute + described_class.new(project, file, FileUploader).execute end end diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb index b4efe7de431..14a5e40350a 100644 --- a/spec/services/user_project_access_changed_service_spec.rb +++ b/spec/services/user_project_access_changed_service_spec.rb @@ -3,8 +3,8 @@ require 'spec_helper' describe UserProjectAccessChangedService do describe '#execute' do it 'schedules the user IDs' do - expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait). - with([[1], [2]]) + expect(AuthorizedProjectsWorker).to receive(:bulk_perform_and_wait) + .with([[1], [2]]) described_class.new([1, 2]).execute end diff --git a/spec/services/users/activity_service_spec.rb b/spec/services/users/activity_service_spec.rb index 8d67ebe3231..fef4da0c76e 100644 --- a/spec/services/users/activity_service_spec.rb +++ b/spec/services/users/activity_service_spec.rb @@ -1,13 +1,13 @@ require 'spec_helper' -describe Users::ActivityService, services: true do +describe Users::ActivityService do include UserActivitiesHelpers let(:user) { create(:user) } subject(:service) { described_class.new(user, 'type') } - describe '#execute', :redis do + describe '#execute', :clean_gitlab_redis_shared_state do context 'when last activity is nil' do before do service.execute @@ -41,8 +41,8 @@ describe Users::ActivityService, services: true do end def last_hour_user_ids - Gitlab::UserActivities.new. - select { |k, v| v >= 1.hour.ago.to_i.to_s }. - map { |k, _| k.to_i } + Gitlab::UserActivities.new + .select { |k, v| v >= 1.hour.ago.to_i.to_s } + .map { |k, _| k.to_i } end end diff --git a/spec/services/users/build_service_spec.rb b/spec/services/users/build_service_spec.rb index 2a6bfc1b3a0..677d4a622e1 100644 --- a/spec/services/users/build_service_spec.rb +++ b/spec/services/users/build_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Users::BuildService, services: true do +describe Users::BuildService do describe '#execute' do let(:params) do { name: 'John Doe', username: 'jduser', email: 'jd@example.com', password: 'mydummypass' } diff --git a/spec/services/users/create_service_spec.rb b/spec/services/users/create_service_spec.rb index 75746278573..24dac569678 100644 --- a/spec/services/users/create_service_spec.rb +++ b/spec/services/users/create_service_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -describe Users::CreateService, services: true do +describe Users::CreateService do describe '#execute' do let(:admin_user) { create(:admin) } diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb index 4bc30018ebd..a82567f6f43 100644 --- a/spec/services/users/destroy_service_spec.rb +++ b/spec/services/users/destroy_service_spec.rb @@ -1,11 +1,11 @@ require 'spec_helper' -describe Users::DestroyService, services: true do +describe Users::DestroyService do describe "Deletes a user and all their personal projects" do let!(:user) { create(:user) } let!(:admin) { create(:admin) } let!(:namespace) { create(:namespace, owner: user) } - let!(:project) { create(:empty_project, namespace: namespace) } + let!(:project) { create(:project, namespace: namespace) } let(:service) { described_class.new(admin) } context 'no options are given' do @@ -47,7 +47,7 @@ describe Users::DestroyService, services: true do end context "for an issue the user was assigned to" do - let!(:issue) { create(:issue, project: project, assignee: user) } + let!(:issue) { create(:issue, project: project, assignees: [user]) } before do service.execute(user) @@ -60,13 +60,13 @@ describe Users::DestroyService, services: true do it 'migrates the issue so that it is "Unassigned"' do migrated_issue = Issue.find_by_id(issue.id) - expect(migrated_issue.assignee).to be_nil + expect(migrated_issue.assignees).to be_empty end end end context "a deleted user's merge_requests" do - let(:project) { create(:project) } + let(:project) { create(:project, :repository) } before do project.add_developer(user) @@ -147,16 +147,22 @@ describe Users::DestroyService, services: true do end context "migrating associated records" do + let!(:issue) { create(:issue, author: user) } + it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do - expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once + expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original service.execute(user) + + expect(issue.reload.author).to be_ghost end it 'does not run `MigrateToGhostUser` if hard_delete option is given' do expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute) service.execute(user, hard_delete: true) + + expect(Issue.exists?(issue.id)).to be_falsy end end end diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb index 9e1edf1ac30..ac3a8738cac 100644 --- a/spec/services/users/migrate_to_ghost_user_service_spec.rb +++ b/spec/services/users/migrate_to_ghost_user_service_spec.rb @@ -1,22 +1,38 @@ require 'spec_helper' -describe Users::MigrateToGhostUserService, services: true do +describe Users::MigrateToGhostUserService do let!(:user) { create(:user) } - let!(:project) { create(:project) } + let!(:project) { create(:project, :repository) } let(:service) { described_class.new(user) } context "migrating a user's associated records to the ghost user" do context 'issues' do - include_examples "migrating a deleted user's associated records to the ghost user", Issue do - let(:created_record) { create(:issue, project: project, author: user) } - let(:assigned_record) { create(:issue, project: project, assignee: user) } + context 'deleted user is present as both author and edited_user' do + include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:author, :last_edited_by] do + let(:created_record) do + create(:issue, project: project, author: user, last_edited_by: user) + end + end + end + + context 'deleted user is present only as edited_user' do + include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:last_edited_by] do + let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) } + end end end context 'merge requests' do - include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest do - let(:created_record) { create(:merge_request, source_project: project, author: user, target_branch: "first") } - let(:assigned_record) { create(:merge_request, source_project: project, assignee: user, target_branch: 'second') } + context 'deleted user is present as both author and merge_user' do + include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:author, :merge_user] do + let(:created_record) { create(:merge_request, source_project: project, author: user, merge_user: user, target_branch: "first") } + end + end + + context 'deleted user is present only as both merge_user' do + include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:merge_user] do + let(:created_record) { create(:merge_request, source_project: project, merge_user: user, target_branch: "first") } + end end end @@ -33,9 +49,8 @@ describe Users::MigrateToGhostUserService, services: true do end context 'award emoji' do - include_examples "migrating a deleted user's associated records to the ghost user", AwardEmoji do + include_examples "migrating a deleted user's associated records to the ghost user", AwardEmoji, [:user] do let(:created_record) { create(:award_emoji, user: user) } - let(:author_alias) { :user } context "when the awardable already has an award emoji of the same name assigned to the ghost user" do let(:awardable) { create(:issue) } diff --git a/spec/services/users/refresh_authorized_projects_service_spec.rb b/spec/services/users/refresh_authorized_projects_service_spec.rb index b19374ef1a2..08fd26d67fd 100644 --- a/spec/services/users/refresh_authorized_projects_service_spec.rb +++ b/spec/services/users/refresh_authorized_projects_service_spec.rb @@ -1,22 +1,20 @@ require 'spec_helper' describe Users::RefreshAuthorizedProjectsService do - let(:project) { create(:empty_project) } + # We're using let! here so that any expectations for the service class are not + # triggered twice. + let!(:project) { create(:project) } + let(:user) { project.namespace.owner } let(:service) { described_class.new(user) } - def create_authorization(project, user, access_level = Gitlab::Access::MASTER) - ProjectAuthorization. - create!(project: project, user: user, access_level: access_level) - end - - describe '#execute', :redis do + describe '#execute', :clean_gitlab_redis_shared_state do it 'refreshes the authorizations using a lease' do - expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain). - and_return('foo') + expect_any_instance_of(Gitlab::ExclusiveLease).to receive(:try_obtain) + .and_return('foo') - expect(Gitlab::ExclusiveLease).to receive(:cancel). - with(an_instance_of(String), 'foo') + expect(Gitlab::ExclusiveLease).to receive(:cancel) + .with(an_instance_of(String), 'foo') expect(service).to receive(:execute_without_lease) @@ -30,20 +28,24 @@ describe Users::RefreshAuthorizedProjectsService do end it 'updates the authorized projects of the user' do - project2 = create(:empty_project) - to_remove = create_authorization(project2, user) + project2 = create(:project) + to_remove = user.project_authorizations + .create!(project: project2, access_level: Gitlab::Access::MASTER) - expect(service).to receive(:update_authorizations). - with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + expect(service).to receive(:update_authorizations) + .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute_without_lease end it 'sets the access level of a project to the highest available level' do - to_remove = create_authorization(project, user, Gitlab::Access::DEVELOPER) + user.project_authorizations.delete_all + + to_remove = user.project_authorizations + .create!(project: project, access_level: Gitlab::Access::DEVELOPER) - expect(service).to receive(:update_authorizations). - with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) + expect(service).to receive(:update_authorizations) + .with([to_remove.project_id], [[user.id, project.id, Gitlab::Access::MASTER]]) service.execute_without_lease end @@ -61,34 +63,10 @@ describe Users::RefreshAuthorizedProjectsService do service.update_authorizations([], []) end - - context 'when the authorized projects column is not set' do - before do - user.update!(authorized_projects_populated: nil) - end - - it 'populates the authorized projects column' do - service.update_authorizations([], []) - - expect(user.authorized_projects_populated).to eq true - end - end - - context 'when the authorized projects column is set' do - before do - user.update!(authorized_projects_populated: true) - end - - it 'does nothing' do - expect(user).not_to receive(:set_authorized_projects_column) - - service.update_authorizations([], []) - end - end end it 'removes authorizations that should be removed' do - authorization = create_authorization(project, user) + authorization = user.project_authorizations.find_by(project_id: project.id) service.update_authorizations([authorization.project_id]) @@ -96,6 +74,8 @@ describe Users::RefreshAuthorizedProjectsService do end it 'inserts authorizations that should be added' do + user.project_authorizations.delete_all + service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) authorizations = user.project_authorizations @@ -105,16 +85,6 @@ describe Users::RefreshAuthorizedProjectsService do expect(authorizations[0].project_id).to eq(project.id) expect(authorizations[0].access_level).to eq(Gitlab::Access::MASTER) end - - it 'populates the authorized projects column' do - # make sure we start with a nil value no matter what the default in the - # factory may be. - user.update!(authorized_projects_populated: nil) - - service.update_authorizations([], [[user.id, project.id, Gitlab::Access::MASTER]]) - - expect(user.authorized_projects_populated).to eq(true) - end end describe '#fresh_access_levels_per_project' do @@ -139,7 +109,7 @@ describe Users::RefreshAuthorizedProjectsService do end context 'projects the user is a member of' do - let!(:other_project) { create(:empty_project) } + let!(:other_project) { create(:project) } before do other_project.team.add_reporter(user) @@ -152,7 +122,7 @@ describe Users::RefreshAuthorizedProjectsService do context 'projects of groups the user is a member of' do let(:group) { create(:group) } - let!(:other_project) { create(:empty_project, group: group) } + let!(:other_project) { create(:project, group: group) } before do group.add_owner(user) @@ -163,10 +133,10 @@ describe Users::RefreshAuthorizedProjectsService do end end - context 'projects of subgroups of groups the user is a member of' do + context 'projects of subgroups of groups the user is a member of', :nested_groups do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } - let!(:other_project) { create(:empty_project, group: nested_group) } + let!(:other_project) { create(:project, group: nested_group) } before do group.add_master(user) @@ -179,7 +149,7 @@ describe Users::RefreshAuthorizedProjectsService do context 'projects shared with groups the user is a member of' do let(:group) { create(:group) } - let(:other_project) { create(:empty_project) } + let(:other_project) { create(:project) } let!(:project_group_link) { create(:project_group_link, project: other_project, group: group, group_access: Gitlab::Access::GUEST) } before do @@ -191,10 +161,10 @@ describe Users::RefreshAuthorizedProjectsService do end end - context 'projects shared with subgroups of groups the user is a member of' do + context 'projects shared with subgroups of groups the user is a member of', :nested_groups do let(:group) { create(:group) } let(:nested_group) { create(:group, parent: group) } - let(:other_project) { create(:empty_project) } + let(:other_project) { create(:project) } let!(:project_group_link) { create(:project_group_link, project: other_project, group: nested_group, group_access: Gitlab::Access::DEVELOPER) } before do @@ -208,8 +178,6 @@ describe Users::RefreshAuthorizedProjectsService do end describe '#current_authorizations_per_project' do - before { create_authorization(project, user) } - let(:hash) { service.current_authorizations_per_project } it 'returns a Hash' do @@ -233,13 +201,13 @@ describe Users::RefreshAuthorizedProjectsService do describe '#current_authorizations' do context 'without authorizations' do it 'returns an empty list' do + user.project_authorizations.delete_all + expect(service.current_authorizations.empty?).to eq(true) end end context 'with an authorization' do - before { create_authorization(project, user) } - let(:row) { service.current_authorizations.take } it 'returns the currently authorized projects' do diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb new file mode 100644 index 00000000000..343804e3de0 --- /dev/null +++ b/spec/services/users/update_service_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe Users::UpdateService do + let(:user) { create(:user) } + + describe '#execute' do + it 'updates the name' do + result = update_user(user, name: 'New Name') + + expect(result).to eq(status: :success) + expect(user.name).to eq('New Name') + end + + it 'returns an error result when record cannot be updated' do + expect do + update_user(user, { email: 'invalid' }) + end.not_to change { user.reload.email } + end + + def update_user(user, opts) + described_class.new(user, opts).execute + end + end + + describe '#execute!' do + it 'updates the name' do + result = update_user(user, name: 'New Name') + + expect(result).to be true + expect(user.name).to eq('New Name') + end + + it 'raises an error when record cannot be updated' do + expect do + update_user(user, email: 'invalid') + end.to raise_error(ActiveRecord::RecordInvalid) + end + + def update_user(user, opts) + described_class.new(user, opts).execute! + end + end +end diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb new file mode 100644 index 00000000000..0726e135b20 --- /dev/null +++ b/spec/services/web_hook_service_spec.rb @@ -0,0 +1,154 @@ +require 'spec_helper' + +describe WebHookService do + let(:project) { create(:project) } + let(:project_hook) { create(:project_hook) } + let(:headers) do + { + 'Content-Type' => 'application/json', + 'X-Gitlab-Event' => 'Push Hook' + } + end + let(:data) do + { before: 'oldrev', after: 'newrev', ref: 'ref' } + end + let(:service_instance) { described_class.new(project_hook, data, 'push_hooks') } + + describe '#execute' do + before do + project.hooks << [project_hook] + + WebMock.stub_request(:post, project_hook.url) + end + + context 'when token is defined' do + let(:project_hook) { create(:project_hook, :token) } + + it 'POSTs to the webhook URL' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers.merge({ 'X-Gitlab-Token' => project_hook.token }) + ).once + end + end + + it 'POSTs to the webhook URL' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers + ).once + end + + it 'POSTs the data as JSON' do + service_instance.execute + expect(WebMock).to have_requested(:post, project_hook.url).with( + headers: headers + ).once + end + + it 'catches exceptions' do + WebMock.stub_request(:post, project_hook.url).to_raise(StandardError.new('Some error')) + + expect { service_instance.execute }.to raise_error(StandardError) + end + + it 'handles exceptions' do + exceptions = [SocketError, OpenSSL::SSL::SSLError, Errno::ECONNRESET, Errno::ECONNREFUSED, Errno::EHOSTUNREACH, Net::OpenTimeout, Net::ReadTimeout] + exceptions.each do |exception_class| + exception = exception_class.new('Exception message') + + WebMock.stub_request(:post, project_hook.url).to_raise(exception) + expect(service_instance.execute).to eq({ status: :error, message: exception.message }) + expect { service_instance.execute }.not_to raise_error + end + end + + it 'handles 200 status code' do + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success') + + expect(service_instance.execute).to include({ status: :success, http_status: 200, message: 'Success' }) + end + + it 'handles 2xx status codes' do + WebMock.stub_request(:post, project_hook.url).to_return(status: 201, body: 'Success') + + expect(service_instance.execute).to include({ status: :success, http_status: 201, message: 'Success' }) + end + + context 'execution logging' do + let(:hook_log) { project_hook.web_hook_logs.last } + + context 'with success' do + before do + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: 'Success') + service_instance.execute + end + + it 'log successful execution' do + expect(hook_log.trigger).to eq('push_hooks') + expect(hook_log.url).to eq(project_hook.url) + expect(hook_log.request_headers).to eq(headers) + expect(hook_log.response_body).to eq('Success') + expect(hook_log.response_status).to eq('200') + expect(hook_log.execution_duration).to be > 0 + expect(hook_log.internal_error_message).to be_nil + end + end + + context 'with exception' do + before do + WebMock.stub_request(:post, project_hook.url).to_raise(SocketError.new('Some HTTP Post error')) + service_instance.execute + end + + it 'log failed execution' do + expect(hook_log.trigger).to eq('push_hooks') + expect(hook_log.url).to eq(project_hook.url) + expect(hook_log.request_headers).to eq(headers) + expect(hook_log.response_body).to eq('') + expect(hook_log.response_status).to eq('internal error') + expect(hook_log.execution_duration).to be > 0 + expect(hook_log.internal_error_message).to eq('Some HTTP Post error') + end + end + + context 'with unsafe response body' do + before do + WebMock.stub_request(:post, project_hook.url).to_return(status: 200, body: "\xBB") + service_instance.execute + end + + it 'log successful execution' do + expect(hook_log.trigger).to eq('push_hooks') + expect(hook_log.url).to eq(project_hook.url) + expect(hook_log.request_headers).to eq(headers) + expect(hook_log.response_body).to eq('') + expect(hook_log.response_status).to eq('200') + expect(hook_log.execution_duration).to be > 0 + expect(hook_log.internal_error_message).to be_nil + end + end + + context 'should not log ServiceHooks' do + let(:service_hook) { create(:service_hook) } + let(:service_instance) { described_class.new(service_hook, data, 'service_hook') } + + before do + WebMock.stub_request(:post, service_hook.url).to_return(status: 200, body: 'Success') + end + + it { expect { service_instance.execute }.not_to change(WebHookLog, :count) } + end + end + end + + describe '#async_execute' do + let(:system_hook) { create(:system_hook) } + + it 'enqueue WebHookWorker' do + expect(Sidekiq::Client).to receive(:enqueue).with(WebHookWorker, project_hook.id, data, 'push_hooks') + + described_class.new(project_hook, data, 'push_hooks').async_execute + end + end +end diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb index 5341ba3d261..b270194d9b8 100644 --- a/spec/services/wiki_pages/create_service_spec.rb +++ b/spec/services/wiki_pages/create_service_spec.rb @@ -1,8 +1,9 @@ require 'spec_helper' -describe WikiPages::CreateService, services: true do - let(:project) { create(:empty_project) } +describe WikiPages::CreateService do + let(:project) { create(:project) } let(:user) { create(:user) } + let(:opts) do { title: 'Title', @@ -10,27 +11,28 @@ describe WikiPages::CreateService, services: true do format: 'markdown' } end - let(:service) { described_class.new(project, user, opts) } + + subject(:service) { described_class.new(project, user, opts) } + + before do + project.add_developer(user) + end describe '#execute' do - context "valid params" do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end - - subject { service.execute } - - it 'creates a valid wiki page' do - is_expected.to be_valid - expect(subject.title).to eq(opts[:title]) - expect(subject.content).to eq(opts[:content]) - expect(subject.format).to eq(opts[:format].to_sym) - end - - it 'executes webhooks' do - expect(service).to have_received(:execute_hooks).once.with(subject, 'create') - end + it 'creates wiki page with valid attributes' do + page = service.execute + + expect(page).to be_valid + expect(page.title).to eq(opts[:title]) + expect(page.content).to eq(opts[:content]) + expect(page.format).to eq(opts[:format].to_sym) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'create') + + service.execute end end end diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb index a4b9a390fe2..2938126914b 100644 --- a/spec/services/wiki_pages/destroy_service_spec.rb +++ b/spec/services/wiki_pages/destroy_service_spec.rb @@ -1,21 +1,22 @@ require 'spec_helper' -describe WikiPages::DestroyService, services: true do - let(:project) { create(:empty_project) } +describe WikiPages::DestroyService do + let(:project) { create(:project) } let(:user) { create(:user) } - let(:wiki_page) { create(:wiki_page) } - let(:service) { described_class.new(project, user) } + let(:page) { create(:wiki_page) } - describe '#execute' do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end + subject(:service) { described_class.new(project, user) } + before do + project.add_developer(user) + end + + describe '#execute' do it 'executes webhooks' do - service.execute(wiki_page) + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'delete') - expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete') + service.execute(page) end end end diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb index 2bccca764d7..2399db7d3d4 100644 --- a/spec/services/wiki_pages/update_service_spec.rb +++ b/spec/services/wiki_pages/update_service_spec.rb @@ -1,37 +1,41 @@ require 'spec_helper' -describe WikiPages::UpdateService, services: true do - let(:project) { create(:empty_project) } +describe WikiPages::UpdateService do + let(:project) { create(:project) } let(:user) { create(:user) } - let(:wiki_page) { create(:wiki_page) } + let(:page) { create(:wiki_page) } + let(:opts) do { content: 'New content for wiki page', format: 'markdown', - message: 'New wiki message' + message: 'New wiki message', + title: 'New Title' } end - let(:service) { described_class.new(project, user, opts) } + + subject(:service) { described_class.new(project, user, opts) } + + before do + project.add_developer(user) + end describe '#execute' do - context "valid params" do - before do - allow(service).to receive(:execute_hooks) - project.add_master(user) - end - - subject { service.execute(wiki_page) } - - it 'updates the wiki page' do - is_expected.to be_valid - expect(subject.content).to eq(opts[:content]) - expect(subject.format).to eq(opts[:format].to_sym) - expect(subject.message).to eq(opts[:message]) - end - - it 'executes webhooks' do - expect(service).to have_received(:execute_hooks).once.with(subject, 'update') - end + it 'updates the wiki page' do + updated_page = service.execute(page) + + expect(updated_page).to be_valid + expect(updated_page.message).to eq(opts[:message]) + expect(updated_page.content).to eq(opts[:content]) + expect(updated_page.format).to eq(opts[:format].to_sym) + expect(updated_page.title).to eq(opts[:title]) + end + + it 'executes webhooks' do + expect(service).to receive(:execute_hooks).once + .with(instance_of(WikiPage), 'update') + + service.execute(page) end end end |