# frozen_string_literal: true require 'spec_helper' RSpec.describe Gitlab::GitalyClient::OperationService, feature_category: :source_code_management do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :repository) } let(:repository) { project.repository.raw } let(:client) { described_class.new(repository) } let(:gitaly_user) { Gitlab::Git::User.from_gitlab(user).to_gitaly } describe '#user_create_branch' do let(:branch_name) { 'new' } let(:start_point) { 'master' } let(:request) do Gitaly::UserCreateBranchRequest.new( repository: repository.gitaly_repository, branch_name: branch_name, start_point: start_point, user: gitaly_user ) end let(:gitaly_commit) { build(:gitaly_commit) } let(:commit_id) { gitaly_commit.id } let(:gitaly_branch) do Gitaly::Branch.new(name: branch_name, target_commit: gitaly_commit) end let(:response) { Gitaly::UserCreateBranchResponse.new(branch: gitaly_branch) } let(:commit) { Gitlab::Git::Commit.new(repository, gitaly_commit) } subject { client.user_create_branch(branch_name, user, start_point) } it 'sends a user_create_branch message and returns a Gitlab::git::Branch' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_create_branch).with(request, kind_of(Hash)) .and_return(response) expect(subject.name).to eq(branch_name) expect(subject.dereferenced_target).to eq(commit) end context 'with structured errors' do context 'with CustomHookError' do let(:stdout) { nil } let(:stderr) { nil } let(:error_message) { "error_message" } let(:custom_hook_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, error_message, Gitaly::UserCreateBranchError.new( custom_hook: Gitaly::CustomHookError.new( stdout: stdout, stderr: stderr, hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE ))) end shared_examples 'failed branch creation' do it 'raised a PreRecieveError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_create_branch) .and_raise(custom_hook_error) expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq(expected_message) expect(error.raw_message).to eq(expected_raw_message) end end end context 'when details contain stderr without prefix' do let(:stderr) { "something" } let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } let(:expected_message) { error_message } let(:expected_raw_message) { stderr } it_behaves_like 'failed branch creation' end context 'when details contain stderr with prefix' do let(:stderr) { "GL-HOOK-ERR: something" } let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } let(:expected_message) { "something" } let(:expected_raw_message) { stderr } it_behaves_like 'failed branch creation' end context 'when details contain stdout without prefix' do let(:stderr) { " \n" } let(:stdout) { "something" } let(:expected_message) { error_message } let(:expected_raw_message) { stdout } it_behaves_like 'failed branch creation' end context 'when details contain stdout with prefix' do let(:stderr) { " \n" } let(:stdout) { "GL-HOOK-ERR: something" } let(:expected_message) { "something" } let(:expected_raw_message) { stdout } it_behaves_like 'failed branch creation' end context 'when details contain no stderr or stdout' do let(:stderr) { " \n" } let(:stdout) { "\n \n" } let(:expected_message) { error_message } let(:expected_raw_message) { "\n \n" } it_behaves_like 'failed branch creation' end end end end describe '#user_update_branch' do let(:branch_name) { 'my-branch' } let(:newrev) { '01e' } let(:oldrev) { '01d' } let(:request) do Gitaly::UserUpdateBranchRequest.new( repository: repository.gitaly_repository, branch_name: branch_name, newrev: newrev, oldrev: oldrev, user: gitaly_user ) end let(:response) { Gitaly::UserUpdateBranchResponse.new } subject { client.user_update_branch(branch_name, user, newrev, oldrev) } it 'sends a user_update_branch message' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_update_branch).with(request, kind_of(Hash)) .and_return(response) subject end context "when pre_receive_error is present" do let(:response) do Gitaly::UserUpdateBranchResponse.new(pre_receive_error: "GitLab: something failed") end it "throws a PreReceive exception" do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_update_branch).with(request, kind_of(Hash)) .and_return(response) expect { subject }.to raise_error( Gitlab::Git::PreReceiveError, "something failed") end end end describe '#user_merge_to_ref' do let(:first_parent_ref) { 'refs/heads/my-branch' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:ref) { 'refs/merge-requests/x/merge' } let(:message) { 'validaciĆ³n' } let(:response) { Gitaly::UserMergeToRefResponse.new(commit_id: 'new-commit-id') } let(:payload) do { source_sha: source_sha, branch: 'branch', target_ref: ref, message: message, first_parent_ref: first_parent_ref, allow_conflicts: true } end it 'sends a user_merge_to_ref message' do freeze_time do expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_merge_to_ref) do |_, request, options| expect(options).to be_kind_of(Hash) expect(request.to_h).to eq( payload.merge({ repository: repository.gitaly_repository.to_h, message: message.dup.force_encoding(Encoding::ASCII_8BIT), user: Gitlab::Git::User.from_gitlab(user).to_gitaly.to_h, timestamp: { nanos: 0, seconds: Time.current.to_i } }) ) end.and_return(response) client.user_merge_to_ref(user, **payload) end end end describe '#user_delete_branch' do let(:branch_name) { 'my-branch' } let(:request) do Gitaly::UserDeleteBranchRequest.new( repository: repository.gitaly_repository, branch_name: branch_name, user: gitaly_user ) end let(:response) { Gitaly::UserDeleteBranchResponse.new } subject { client.user_delete_branch(branch_name, user) } it 'sends a user_delete_branch message' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_delete_branch).with(request, kind_of(Hash)) .and_return(response) subject end context 'with a custom hook error' do let(:stdout) { nil } let(:stderr) { nil } let(:error_message) { "error_message" } let(:custom_hook_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, error_message, Gitaly::UserDeleteBranchError.new( custom_hook: Gitaly::CustomHookError.new( stdout: stdout, stderr: stderr, hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE ))) end shared_examples 'a failed branch deletion' do it 'raises a PreReceiveError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_delete_branch).with(request, kind_of(Hash)) .and_raise(custom_hook_error) expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq(expected_message) expect(error.raw_message).to eq(expected_raw_message) end end end context 'when details contain stderr' do let(:stderr) { "something" } let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } let(:expected_message) { error_message } let(:expected_raw_message) { stderr } it_behaves_like 'a failed branch deletion' end context 'when details contain stdout' do let(:stderr) { " \n" } let(:stdout) { "something" } let(:expected_message) { error_message } let(:expected_raw_message) { stdout } it_behaves_like 'a failed branch deletion' end end context 'with a non-detailed error' do it 'raises a GRPC error' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_delete_branch).with(request, kind_of(Hash)) .and_raise(GRPC::Internal.new('non-detailed error')) expect { subject }.to raise_error(GRPC::Internal) end end end describe '#user_merge_branch' do let(:target_branch) { 'master' } let(:target_sha) { repository.commit(target_branch).sha } let(:source_sha) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } let(:message) { 'Merge a branch' } subject do client.user_merge_branch(user, source_sha: source_sha, target_branch: target_branch, target_sha: target_sha, message: message ) {} end it 'sends a user_merge_branch message', :freeze_time do first_request = Gitaly::UserMergeBranchRequest.new( repository: repository.gitaly_repository, user: gitaly_user, commit_id: source_sha, branch: target_branch, expected_old_oid: target_sha, message: message, timestamp: Google::Protobuf::Timestamp.new(seconds: Time.now.utc.to_i) ) second_request = Gitaly::UserMergeBranchRequest.new(apply: true) expect_next_instance_of(Gitlab::GitalyClient::QueueEnumerator) do |instance| expect(instance).to receive(:push).with(first_request).and_call_original expect(instance).to receive(:push).with(second_request).and_call_original expect(instance).to receive(:close) end expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) expect(subject.newrev).to be_present expect(subject.repo_created).to be(false) expect(subject.branch_created).to be(false) end context 'with an exception with the UserMergeBranchError' do let(:permission_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, "GitLab: You are not allowed to push code to this project.", Gitaly::UserMergeBranchError.new( access_check: Gitaly::AccessCheckError.new( error_message: "You are not allowed to push code to this project.", protocol: "web", user_id: "user-15", changes: "df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/heads/master\n" ))) end it 'raises PreRecieveError with the error message' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(permission_error) expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq("You are not allowed to push code to this project.") end end end context 'with a custom hook error' do let(:stdout) { nil } let(:stderr) { nil } let(:error_message) { "error_message" } let(:custom_hook_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, error_message, Gitaly::UserMergeBranchError.new( custom_hook: Gitaly::CustomHookError.new( stdout: stdout, stderr: stderr, hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE ))) end shared_examples 'a failed merge' do it 'raises a PreReceiveError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(custom_hook_error) expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq(expected_message) expect(error.raw_message).to eq(expected_raw_message) end end end context 'when details contain stderr without prefix' do let(:stderr) { "something" } let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } let(:expected_message) { error_message } let(:expected_raw_message) { stderr } it_behaves_like 'a failed merge' end context 'when details contain stderr with prefix' do let(:stderr) { "GL-HOOK-ERR: something" } let(:stdout) { "GL-HOOK-ERR: stdout is overridden by stderr" } let(:expected_message) { "something" } let(:expected_raw_message) { stderr } it_behaves_like 'a failed merge' end context 'when details contain stdout without prefix' do let(:stderr) { " \n" } let(:stdout) { "something" } let(:expected_message) { error_message } let(:expected_raw_message) { stdout } it_behaves_like 'a failed merge' end context 'when details contain stdout with prefix' do let(:stderr) { " \n" } let(:stdout) { "GL-HOOK-ERR: something" } let(:expected_message) { "something" } let(:expected_raw_message) { stdout } it_behaves_like 'a failed merge' end context 'when details contain no stderr or stdout' do let(:stderr) { " \n" } let(:stdout) { "\n \n" } let(:expected_message) { error_message } let(:expected_raw_message) { "\n \n" } it_behaves_like 'a failed merge' end end context 'with an exception without the detailed error' do let(:permission_error) do GRPC::PermissionDenied.new end it 'raises PermissionDenied' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(permission_error) expect { subject }.to raise_error(GRPC::PermissionDenied) end end context 'with ReferenceUpdateError' do let(:reference_update_error) do new_detailed_error(GRPC::Core::StatusCodes::FAILED_PRECONDITION, "some ignored error message", Gitaly::UserMergeBranchError.new( reference_update: Gitaly::ReferenceUpdateError.new( reference_name: "refs/heads/something", old_oid: "1234", new_oid: "6789" ))) end it 'returns nil' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_merge_branch).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(reference_update_error) expect(subject).to be_nil end end end describe '#user_ff_branch' do let(:target_branch) { 'my-branch' } let(:target_sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' } let(:source_sha) { 'cfe32cf61b73a0d5e9f13e774abde7ff789b1660' } let(:request) do Gitaly::UserFFBranchRequest.new( repository: repository.gitaly_repository, branch: target_branch, commit_id: source_sha, expected_old_oid: target_sha, user: gitaly_user ) end let(:branch_update) do Gitaly::OperationBranchUpdate.new( commit_id: source_sha, repo_created: false, branch_created: false ) end let(:response) { Gitaly::UserFFBranchResponse.new(branch_update: branch_update) } before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_ff_branch).with(request, kind_of(Hash)) .and_return(response) end subject do client.user_ff_branch(user, source_sha: source_sha, target_branch: target_branch, target_sha: target_sha ) end it 'sends a user_ff_branch message and returns a BranchUpdate object' do expect(subject).to be_a(Gitlab::Git::OperationService::BranchUpdate) expect(subject.newrev).to eq(source_sha) expect(subject.repo_created).to be(false) expect(subject.branch_created).to be(false) end context 'when the response has no branch_update' do let(:response) { Gitaly::UserFFBranchResponse.new } it { expect(subject).to be_nil } end context "when the pre-receive hook fails" do let(:response) do Gitaly::UserFFBranchResponse.new( branch_update: nil, pre_receive_error: "pre-receive hook error message\n" ) end it "raises the error" do # the PreReceiveError class strips the GL-HOOK-ERR prefix from this error expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "pre-receive hook failed.") end end end shared_examples 'cherry pick and revert errors' do context 'when a pre_receive_error is present' do let(:response) { response_class.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") end end context 'when a commit_error is present' do let(:response) { response_class.new(commit_error: "something failed") } it 'raises a CommitError' do expect { subject }.to raise_error(Gitlab::Git::CommitError, "something failed") end end context 'when a create_tree_error is present' do let(:response) { response_class.new(create_tree_error: "something failed", create_tree_error_code: 'EMPTY') } it 'raises a CreateTreeError' do expect { subject }.to raise_error(Gitlab::Git::Repository::CreateTreeError) do |error| expect(error.error_code).to eq(:empty) end end end context 'when branch_update is nil' do let(:response) { response_class.new } it { expect(subject).to be_nil } end end shared_examples '#user_cherry_pick with a gRPC error' do it 'raises an exception' do expect_any_instance_of(Gitaly::OperationService::Stub).to receive(:user_cherry_pick) .and_raise(raised_error) expect { subject }.to raise_error(expected_error, expected_error_message) end end describe '#user_cherry_pick' do let(:response_class) { Gitaly::UserCherryPickResponse } subject do client.user_cherry_pick( user: user, commit: repository.commit, branch_name: 'master', message: 'Cherry-pick message', start_branch_name: 'master', start_repository: repository ) end context 'when AccessCheckError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::INTERNAL, 'something failed', Gitaly::UserCherryPickError.new( access_check: Gitaly::AccessCheckError.new( error_message: 'something went wrong' ))) end let(:expected_error) { Gitlab::Git::PreReceiveError } let(:expected_error_message) { "something went wrong" } it_behaves_like '#user_cherry_pick with a gRPC error' end context 'when NotAncestorError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::FAILED_PRECONDITION, 'Branch diverged', Gitaly::UserCherryPickError.new( target_branch_diverged: Gitaly::NotAncestorError.new ) ) end let(:expected_error) { Gitlab::Git::CommitError } let(:expected_error_message) { 'branch diverged' } it_behaves_like '#user_cherry_pick with a gRPC error' end context 'when MergeConflictError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::FAILED_PRECONDITION, 'Conflict', Gitaly::UserCherryPickError.new( cherry_pick_conflict: Gitaly::MergeConflictError.new ) ) end let(:expected_error) { Gitlab::Git::Repository::CreateTreeError } let(:expected_error_message) {} it_behaves_like '#user_cherry_pick with a gRPC error' end context 'when a non-detailed gRPC error is raised' do let(:raised_error) { GRPC::Internal.new('non-detailed error') } let(:expected_error) { GRPC::Internal } let(:expected_error_message) {} it_behaves_like '#user_cherry_pick with a gRPC error' end end describe '#user_revert' do let(:response_class) { Gitaly::UserRevertResponse } subject do client.user_revert( user: user, commit: repository.commit, branch_name: 'master', message: 'Revert message', start_branch_name: 'master', start_repository: repository ) end before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_revert).with(kind_of(Gitaly::UserRevertRequest), kind_of(Hash)) .and_return(response) end it_behaves_like 'cherry pick and revert errors' end describe '#rebase' do let(:response) { Gitaly::UserRebaseConfirmableResponse.new } subject do client.rebase( user, '', branch: 'master', branch_sha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0', remote_repository: repository, remote_branch: 'master' ) end shared_examples '#rebase with an error' do it 'raises a GitError exception' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_rebase_confirmable) .and_raise(raised_error) expect { subject }.to raise_error(expected_error) end end context 'when AccessError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::INTERNAL, 'something failed', Gitaly::UserRebaseConfirmableError.new( access_check: Gitaly::AccessCheckError.new( error_message: 'something went wrong' ))) end let(:expected_error) { Gitlab::Git::PreReceiveError } it_behaves_like '#rebase with an error' end context 'when RebaseConflictError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::INTERNAL, 'something failed', Gitaly::UserSquashError.new( rebase_conflict: Gitaly::MergeConflictError.new( conflicting_files: ['conflicting-file'] ))) end let(:expected_error) { Gitlab::Git::Repository::GitError } it_behaves_like '#rebase with an error' end context 'when non-detailed gRPC error is raised' do let(:raised_error) do GRPC::Internal.new('non-detailed error') end let(:expected_error) { GRPC::Internal } it_behaves_like '#rebase with an error' end end describe '#user_squash' do let(:start_sha) { 'b83d6e391c22777fca1ed3012fce84f633d7fed0' } let(:end_sha) { '54cec5282aa9f21856362fe321c800c236a61615' } let(:commit_message) { 'Squash message' } let(:time) do Time.now.utc end let(:request) do Gitaly::UserSquashRequest.new( repository: repository.gitaly_repository, user: gitaly_user, start_sha: start_sha, end_sha: end_sha, author: gitaly_user, commit_message: commit_message, timestamp: Google::Protobuf::Timestamp.new(seconds: time.to_i) ) end let(:squash_sha) { 'f00' } let(:response) { Gitaly::UserSquashResponse.new(squash_sha: squash_sha) } subject do client.user_squash(user, start_sha, end_sha, user, commit_message, time) end it 'sends a user_squash message and returns the squash sha' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_squash).with(request, kind_of(Hash)) .and_return(response) expect(subject).to eq(squash_sha) end shared_examples '#user_squash with an error' do it 'raises a GitError exception' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_squash).with(request, kind_of(Hash)) .and_raise(raised_error) expect { subject }.to raise_error(expected_error) end end context 'when ResolveRevisionError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::INVALID_ARGUMENT, 'something failed', Gitaly::UserSquashError.new( resolve_revision: Gitaly::ResolveRevisionError.new( revision: start_sha ))) end let(:expected_error) { Gitlab::Git::Repository::GitError } it_behaves_like '#user_squash with an error' end context 'when RebaseConflictError is raised' do let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::INTERNAL, 'something failed', Gitaly::UserSquashError.new( rebase_conflict: Gitaly::MergeConflictError.new( conflicting_files: ['conflicting-file'] ))) end let(:expected_error) { Gitlab::Git::Repository::GitError } it_behaves_like '#user_squash with an error' end context 'when non-detailed gRPC error is raised' do let(:raised_error) do GRPC::Internal.new('non-detailed error') end let(:expected_error) { GRPC::Internal } it_behaves_like '#user_squash with an error' end end describe '#user_commit_files' do subject do client.user_commit_files( gitaly_user, 'my-branch', 'Commit files message', [], 'janedoe@example.com', 'Jane Doe', 'master', repository) end context 'with unstructured errors' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) .and_return(response) end context 'when a pre_receive_error is present' do let(:response) { Gitaly::UserCommitFilesResponse.new(pre_receive_error: "GitLab: something failed") } it 'raises a PreReceiveError' do expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") end end context 'when an index_error is present' do let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") } it 'raises an IndexError' do expect { subject }.to raise_error(Gitlab::Git::Index::IndexError, "something failed") end end context 'when branch_update is nil' do let(:response) { Gitaly::UserCommitFilesResponse.new } it { expect(subject).to be_nil } end end context 'with structured errors' do context 'with AccessCheckError' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(raised_error) end let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, "error updating file", Gitaly::UserCommitFilesError.new( access_check: Gitaly::AccessCheckError.new( error_message: "something went wrong" ))) end it 'raises a PreReceiveError' do expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq("something went wrong") end end end context 'with IndexError' do let(:status_code) { nil } let(:expected_error) { nil } let(:structured_error) do new_detailed_error( status_code, "unused error message", expected_error) end shared_examples '#user_commit_files failure' do it 'raises an IndexError' do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(structured_error) expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::Index::IndexError) expect(error.message).to eq(expected_message) end end end context 'with missing file' do let(:status_code) { GRPC::Core::StatusCodes::NOT_FOUND } let(:expected_message) { "A file with this name doesn't exist" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "README.md", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_FILE_NOT_FOUND )) end it_behaves_like '#user_commit_files failure' end context 'with existing directory' do let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS } let(:expected_message) { "A directory with this name already exists" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "dir1", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_DIRECTORY_EXISTS )) end it_behaves_like '#user_commit_files failure' end context 'with existing file' do let(:status_code) { GRPC::Core::StatusCodes::ALREADY_EXISTS } let(:expected_message) { "A file with this name already exists" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "README.md", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_FILE_EXISTS )) end it_behaves_like '#user_commit_files failure' end context 'with invalid path' do let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT } let(:expected_message) { "invalid path: 'invalid://file/name'" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "invalid://file/name", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_INVALID_PATH )) end it_behaves_like '#user_commit_files failure' end context 'with directory traversal' do let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT } let(:expected_message) { "Path cannot include directory traversal" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "../../../../etc/shadow", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_DIRECTORY_TRAVERSAL )) end it_behaves_like '#user_commit_files failure' end context 'with empty path' do let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT } let(:expected_message) { "You must provide a file path" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_EMPTY_PATH )) end it_behaves_like '#user_commit_files failure' end context 'with unspecified error' do let(:status_code) { GRPC::Core::StatusCodes::INVALID_ARGUMENT } let(:expected_message) { "Unknown error performing git operation" } let(:expected_error) do Gitaly::UserCommitFilesError.new( index_update: Gitaly::IndexError.new( path: "", error_type: Gitaly::IndexError::ErrorType::ERROR_TYPE_UNSPECIFIED )) end it_behaves_like '#user_commit_files failure' end context 'with an exception without the detailed error' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(raised_error) end context 'with an index error from libgit2' do let(:raised_error) do GRPC::Internal.new('invalid path: .git/foo') end it 'raises IndexError' do expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::Index::IndexError) expect(error.message).to eq('invalid path: .git/foo') end end end context 'with a generic error' do let(:raised_error) do GRPC::PermissionDenied.new end it 'raises PermissionDenied' do expect { subject }.to raise_error(GRPC::PermissionDenied) end end end end context 'with CustomHookError' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) .and_raise(raised_error) end let(:raised_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, "error updating file", Gitaly::UserCommitFilesError.new( custom_hook: Gitaly::CustomHookError.new( stdout: "some stdout", stderr: "GitLab: some custom hook error message", hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE ))) end it 'raises a PreReceiveError' do expect { subject }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq("some custom hook error message") end end end end end describe '#user_commit_patches' do let(:patches_folder) { Rails.root.join('spec/fixtures/patchfiles') } let(:patch_content) do patch_names.map { |name| File.read(File.join(patches_folder, name)) }.join("\n") end let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) } let(:branch_name) { 'branch-with-patches' } subject(:commit_patches) do client.user_commit_patches(user, branch_name, patch_content) end it 'applies the patch correctly' do branch_update = commit_patches expect(branch_update).to be_branch_created commit = repository.commit(branch_update.newrev) expect(commit.author_email).to eq('patchuser@gitlab.org') expect(commit.committer_email).to eq(user.email) expect(commit.message.chomp).to eq('This does not apply to the `feature` branch') end context 'when the patch could not be applied' do let(:patch_names) { %w(0001-This-does-not-apply-to-the-feature-branch.patch) } let(:branch_name) { 'feature' } it 'raises the correct error' do expect { commit_patches }.to raise_error(GRPC::FailedPrecondition) end end end describe '#add_tag' do let(:tag_name) { 'some-tag' } let(:tag_message) { nil } let(:target) { 'master' } subject(:add_tag) do client.add_tag(tag_name, user, target, tag_message) end context 'without tag message' do let(:tag_name) { 'lightweight-tag' } it 'creates a lightweight tag' do tag = add_tag expect(tag.name).to eq(tag_name) expect(tag.message).to eq('') end end context 'with tag message' do let(:tag_name) { 'annotated-tag' } let(:tag_message) { "tag message" } it 'creates an annotated tag' do tag = add_tag expect(tag.name).to eq(tag_name) expect(tag.message).to eq(tag_message) end end context 'with preexisting tag' do let(:tag_name) { 'v1.0.0' } it 'raises a TagExistsError' do expect { add_tag }.to raise_error(Gitlab::Git::Repository::TagExistsError) end end context 'with invalid target' do let(:target) { 'refs/heads/does-not-exist' } it 'raises an InvalidRef error' do expect { add_tag }.to raise_error(Gitlab::Git::Repository::InvalidRef) end end context 'with internal error' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_create_tag) .and_raise(GRPC::Internal.new('undetailed internal error')) end it 'raises an Internal error' do expect { add_tag }.to raise_error do |error| expect(error).to be_a(GRPC::Internal) expect(error.details).to eq('undetailed internal error') end end end context 'with structured errors' do before do expect_any_instance_of(Gitaly::OperationService::Stub) .to receive(:user_create_tag) .and_raise(structured_error) end context 'with ReferenceExistsError' do let(:structured_error) do new_detailed_error( GRPC::Core::StatusCodes::ALREADY_EXISTS, 'tag exists already', Gitaly::UserCreateTagError.new( reference_exists: Gitaly::ReferenceExistsError.new( reference_name: tag_name, oid: 'something' ))) end it 'raises a TagExistsError' do expect { add_tag }.to raise_error(Gitlab::Git::Repository::TagExistsError) end end context 'with AccessCheckError' do let(:structured_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, "error creating tag", Gitaly::UserCreateTagError.new( access_check: Gitaly::AccessCheckError.new( error_message: "You are not allowed to create this tag.", protocol: "web", user_id: "user-15", changes: "df15b32277d2c55c6c595845a87109b09c913c556 5d6e0f935ad9240655f64e883cd98fad6f9a17ee refs/tags/v1.0.0\n" ))) end it 'raises a PreReceiveError' do expect { add_tag }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq("You are not allowed to create this tag.") end end end context 'with CustomHookError' do let(:structured_error) do new_detailed_error( GRPC::Core::StatusCodes::PERMISSION_DENIED, "custom hook error", Gitaly::UserCreateTagError.new( custom_hook: Gitaly::CustomHookError.new( stdout: "some stdout", stderr: "GitLab: some custom hook error message", hook_type: Gitaly::CustomHookError::HookType::HOOK_TYPE_PRERECEIVE ))) end it 'raises a PreReceiveError' do expect { add_tag }.to raise_error do |error| expect(error).to be_a(Gitlab::Git::PreReceiveError) expect(error.message).to eq("some custom hook error message") end end end end end end