diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 11:33:21 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-17 11:33:21 +0000 |
commit | 7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0 (patch) | |
tree | 5bdc2229f5198d516781f8d24eace62fc7e589e9 /spec/lib/gitlab/gitaly_client | |
parent | 185b095e93520f96e9cfc31d9c3e69b498cdab7c (diff) | |
download | gitlab-ce-7021455bd1ed7b125c55eb1b33c5a01f2bc55ee0.tar.gz |
Add latest changes from gitlab-org/gitlab@15-6-stable-eev15.6.0-rc42
Diffstat (limited to 'spec/lib/gitlab/gitaly_client')
4 files changed, 525 insertions, 30 deletions
diff --git a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb index 9c3bc935acc..baf7076c718 100644 --- a/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/object_pool_service_spec.rb @@ -4,7 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::GitalyClient::ObjectPoolService do let(:pool_repository) { create(:pool_repository) } - let(:project) { create(:project, :repository) } + let(:project) { pool_repository.source_project } let(:raw_repository) { project.repository.raw } let(:object_pool) { pool_repository.object_pool } @@ -45,21 +45,32 @@ RSpec.describe Gitlab::GitalyClient::ObjectPoolService do end describe '#fetch' do - before do - subject.delete + context 'without changes' do + it 'fetches changes' do + expect(subject.fetch(project.repository)).to eq(Gitaly::FetchIntoObjectPoolResponse.new) + end end - it 'restores the pool repository objects' do - subject.fetch(project.repository) + context 'with new reference in source repository' do + let(:branch) { 'ref-to-be-fetched' } + let(:source_ref) { "refs/heads/#{branch}" } + let(:pool_ref) { "refs/remotes/origin/heads/#{branch}" } - expect(object_pool.repository.exists?).to be(true) - end + before do + # Create a new reference in the source repository that we can fetch. + project.repository.write_ref(source_ref, 'refs/heads/master') + end - context 'when called twice' do - it "doesn't raise an error" do - subject.delete + it 'fetches changes' do + # Sanity-check to verify that the reference only exists in the source repository now, but not in the + # object pool. + expect(project.repository.ref_exists?(source_ref)).to be(true) + expect(object_pool.repository.ref_exists?(pool_ref)).to be(false) + + subject.fetch(project.repository) - expect { subject.fetch(project.repository) }.not_to raise_error + # The fetch should've created the reference in the object pool. + expect(object_pool.repository.ref_exists?(pool_ref)).to be(true) end end end diff --git a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb index 7e8aaa3cdf4..604feeea325 100644 --- a/spec/lib/gitlab/gitaly_client/operation_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/operation_service_spec.rb @@ -830,32 +830,225 @@ RSpec.describe Gitlab::GitalyClient::OperationService do 'master', repository) end - 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 '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") } + 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") + it 'raises a PreReceiveError' do + expect { subject }.to raise_error(Gitlab::Git::PreReceiveError, "something failed") + end end - end - context 'when an index_error is present' do - let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") } + context 'when an index_error is present' do + let(:response) { Gitaly::UserCommitFilesResponse.new(index_error: "something failed") } - it 'raises a PreReceiveError' do - expect { subject }.to raise_error(Gitlab::Git::Index::IndexError, "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 'when branch_update is nil' do - let(:response) { Gitaly::UserCommitFilesResponse.new } + 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 - it { expect(subject).to be_nil } + 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 a PreReceiveError' 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) { "File not found: README.md" } + 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) { "Directory already exists: dir1" } + 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) { "File already exists: README.md" } + 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) { "Directory traversal in path escapes repository: ../../../../etc/shadow" } + 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) { "Received empty 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 + let(:permission_error) do + GRPC::PermissionDenied.new + end + + it 'raises PermissionDenied' do + expect_any_instance_of(Gitaly::OperationService::Stub) + .to receive(:user_commit_files).with(kind_of(Enumerator), kind_of(Hash)) + .and_raise(permission_error) + + expect { subject }.to raise_error(GRPC::PermissionDenied) + 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 diff --git a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb index 5ce88b06241..bd96e9baf1d 100644 --- a/spec/lib/gitlab/gitaly_client/ref_service_spec.rb +++ b/spec/lib/gitlab/gitaly_client/ref_service_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe Gitlab::GitalyClient::RefService do - let_it_be(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository, create_tag: 'test') } let(:storage_name) { project.repository_storage } let(:relative_path) { project.disk_path + '.git' } @@ -438,12 +438,28 @@ RSpec.describe Gitlab::GitalyClient::RefService do it 'sends a find_refs_by_oid message' do expect_any_instance_of(Gitaly::RefService::Stub) .to receive(:find_refs_by_oid) - .with(gitaly_request_with_params(sort_field: 'refname', oid: oid, limit: 1), kind_of(Hash)) + .with(gitaly_request_with_params(sort_field: 'refname', + oid: oid, + limit: 1), kind_of(Hash)) .and_call_original refs = client.find_refs_by_oid(oid: oid, limit: 1) expect(refs.to_a).to eq([Gitlab::Git::BRANCH_REF_PREFIX + project.repository.root_ref]) end + + it 'filters by ref_patterns' do + expect_any_instance_of(Gitaly::RefService::Stub) + .to receive(:find_refs_by_oid) + .with(gitaly_request_with_params(sort_field: 'refname', + oid: oid, + limit: 1, + ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]), kind_of(Hash)) + .and_call_original + + refs = client.find_refs_by_oid(oid: oid, limit: 1, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) + + expect(refs.to_a).to eq([Gitlab::Git::TAG_REF_PREFIX + 'test']) + end end end diff --git a/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb new file mode 100644 index 00000000000..41dce5d76dd --- /dev/null +++ b/spec/lib/gitlab/gitaly_client/with_feature_flag_actors_spec.rb @@ -0,0 +1,275 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::GitalyClient::WithFeatureFlagActors do + let(:user) { create(:user) } + let(:service) do + Class.new do + include Gitlab::GitalyClient::WithFeatureFlagActors + end.new + end + + describe '#user_actor' do + context 'when user is not available in ApplicationContext' do + it 'returns nil' do + expect(service.user_actor).to be(nil) + end + end + + context 'when user is available in ApplicationContext' do + around do |example| + ::Gitlab::ApplicationContext.with_context(user: user) { example.run } + end + + it 'returns corresponding user record' do + expect(service.user_actor.flipper_id).to eql(user.flipper_id) + end + end + + context 'when user does not exist' do + around do |example| + ::Gitlab::ApplicationContext.with_context(user: SecureRandom.uuid) { example.run } + end + + it 'returns corresponding user record' do + expect(service.user_actor).to be(nil) + end + end + end + + describe '#repository, #project_actor, #group_actor' do + context 'when normal project repository' do + let_it_be(:project) { create(:project, group: create(:group)) } + let(:expected_project) { project } + let(:expected_group) { Feature::Gitaly::ActorWrapper.new(::Group, project.group.id) } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(project.repository) } + end + end + + context 'when project wiki repository' do + let_it_be(:project) { create(:project, :wiki_repo, group: create(:group)) } + let(:expected_project) { nil } + let(:expected_group) { nil } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.wiki.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.wiki.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(project.wiki.repository) } + end + end + + context 'when repository of project in user namespace' do + let_it_be(:project) { create(:project, namespace: create(:user).namespace) } + let(:expected_project) { project } + let(:expected_group) { Feature::Gitaly::ActorWrapper.new(::Group, project.namespace_id) } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { project.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(project.repository) } + end + end + + context 'when personal snippet' do + let(:snippet) { create(:personal_snippet) } + let(:expected_project) { nil } + let(:expected_group) { nil } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { snippet.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { snippet.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(snippet.repository) } + end + end + + context 'when project snippet' do + let_it_be(:project) { create(:project, group: create(:group)) } + let(:snippet) { create(:project_snippet, project: project) } + let(:expected_project) { nil } + let(:expected_group) { nil } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { snippet.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { snippet.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(snippet.repository) } + end + end + + context 'when project design' do + let_it_be(:project) { create(:project, group: create(:group)) } + let(:issue) { create(:issue, project: project) } + let(:design) { create(:design, issue: issue) } + + let(:expected_project) { project } + let(:expected_group) { project.group } + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { design.repository } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { design.repository.raw } + end + + it_behaves_like 'Gitaly feature flag actors are inferred from repository' do + let(:repository) { raw_repo_without_container(design.repository) } + end + end + end + + describe '#gitaly_client_call' do + let(:call_arg_1) { double } + let(:call_arg_2) { double } + let(:call_arg_3) { double } + let(:call_result) { double } + + before do + allow(Gitlab::GitalyClient).to receive(:call).and_return(call_result) + end + + context 'when actors_aware_gitaly_calls flag is enabled' do + let(:repository_actor) { instance_double(::Repository) } + let(:user_actor) { instance_double(::User) } + let(:project_actor) { instance_double(Project) } + let(:group_actor) { instance_double(Group) } + + before do + stub_feature_flags(actors_aware_gitaly_calls: true) + + allow(service).to receive(:user_actor).and_return(user_actor) + allow(service).to receive(:repository_actor).and_return(repository_actor) + allow(service).to receive(:project_actor).and_return(project_actor) + allow(service).to receive(:group_actor).and_return(group_actor) + allow(Gitlab::GitalyClient).to receive(:with_feature_flag_actors).and_call_original + end + + it 'triggers client call with feature flag actors' do + result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3) + + expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3) + expect(Gitlab::GitalyClient).to have_received(:with_feature_flag_actors).with( + repository: repository_actor, + user: user_actor, + project: project_actor, + group: group_actor + ) + expect(result).to be(call_result) + end + + context 'when call without repository_actor' do + before do + allow(service).to receive(:repository_actor).and_return(nil) + allow(Gitlab::ErrorTracking).to receive(:track_and_raise_for_dev_exception).and_call_original + end + + it 'calls error tracking track_and_raise_for_dev_exception' do + expect do + service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3) + end.to raise_error /gitaly_client_call called without setting repository_actor/ + + expect(Gitlab::ErrorTracking).to have_received(:track_and_raise_for_dev_exception).with( + be_a(Feature::InvalidFeatureFlagError) + ) + end + end + end + + context 'when actors_aware_gitaly_calls not enabled' do + before do + stub_feature_flags(actors_aware_gitaly_calls: false) + end + + it 'triggers client call without feature flag actors' do + expect(Gitlab::GitalyClient).not_to receive(:with_feature_flag_actors) + + result = service.gitaly_client_call(call_arg_1, call_arg_2, karg: call_arg_3) + + expect(Gitlab::GitalyClient).to have_received(:call).with(call_arg_1, call_arg_2, karg: call_arg_3) + expect(result).to be(call_result) + end + end + + describe '#gitaly_feature_flag_actors' do + let_it_be(:project) { create(:project) } + let(:repository_actor) { project.repository } + + context 'when actors_aware_gitaly_calls flag is enabled' do + let(:user_actor) { instance_double(::User) } + let(:project_actor) { instance_double(Project) } + let(:group_actor) { instance_double(Group) } + + before do + stub_feature_flags(actors_aware_gitaly_calls: true) + + allow(Feature::Gitaly).to receive(:user_actor).and_return(user_actor) + allow(Feature::Gitaly).to receive(:project_actor).with(project).and_return(project_actor) + allow(Feature::Gitaly).to receive(:group_actor).with(project).and_return(group_actor) + end + + it 'returns a hash with collected feature flag actors' do + result = service.gitaly_feature_flag_actors(repository_actor) + expect(result).to eql( + repository: repository_actor, + user: user_actor, + project: project_actor, + group: group_actor + ) + + expect(Feature::Gitaly).to have_received(:user_actor).with(no_args) + expect(Feature::Gitaly).to have_received(:project_actor).with(project) + expect(Feature::Gitaly).to have_received(:group_actor).with(project) + end + end + + context 'when actors_aware_gitaly_calls not enabled' do + before do + stub_feature_flags(actors_aware_gitaly_calls: false) + end + + it 'returns an empty hash' do + expect(Feature::Gitaly).not_to receive(:user_actor) + expect(Feature::Gitaly).not_to receive(:project_actor) + expect(Feature::Gitaly).not_to receive(:group_actor) + + result = service.gitaly_feature_flag_actors(repository_actor) + expect(result).to eql({}) + end + end + end + end +end |