this is a title
', title_text: 'this is a title', @@ -21,3 +23,11 @@ export const secondRequest = { updated_by_path: '/other_user', lock_version: 2, }; + +export const descriptionProps = { + canUpdate: true, + descriptionHtml: 'test', + descriptionText: 'test', + taskStatus: '', + updateUrl: TEST_HOST, +}; diff --git a/spec/graphql/resolvers/merge_requests_resolver_spec.rb b/spec/graphql/resolvers/merge_requests_resolver_spec.rb index f9acbe46820..a548589d269 100644 --- a/spec/graphql/resolvers/merge_requests_resolver_spec.rb +++ b/spec/graphql/resolvers/merge_requests_resolver_spec.rb @@ -206,6 +206,33 @@ RSpec.describe Resolvers::MergeRequestsResolver do expect(result.compact).to contain_exactly(merge_request_4) end end + + describe 'sorting' do + context 'when sorting by created' do + it 'sorts merge requests ascending' do + expect(resolve_mr(project, sort: 'created_asc')).to eq [merge_request_1, merge_request_2, merge_request_3, merge_request_4, merge_request_5, merge_request_6, merge_request_with_milestone] + end + + it 'sorts merge requests descending' do + expect(resolve_mr(project, sort: 'created_desc')).to eq [merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_3, merge_request_2, merge_request_1] + end + end + + context 'when sorting by merged at' do + before do + merge_request_1.metrics.update!(merged_at: 10.days.ago) + merge_request_3.metrics.update!(merged_at: 5.days.ago) + end + + it 'sorts merge requests ascending' do + expect(resolve_mr(project, sort: :merged_at_asc)).to eq [merge_request_1, merge_request_3, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2] + end + + it 'sorts merge requests descending' do + expect(resolve_mr(project, sort: :merged_at_desc)).to eq [merge_request_3, merge_request_1, merge_request_with_milestone, merge_request_6, merge_request_5, merge_request_4, merge_request_2] + end + end + end end def resolve_mr_single(project, iid) diff --git a/spec/graphql/types/merge_request_sort_enum_spec.rb b/spec/graphql/types/merge_request_sort_enum_spec.rb new file mode 100644 index 00000000000..472eba1a50d --- /dev/null +++ b/spec/graphql/types/merge_request_sort_enum_spec.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe GitlabSchema.types['MergeRequestSort'] do + specify { expect(described_class.graphql_name).to eq('MergeRequestSort') } + + it_behaves_like 'common sort values' + + it 'exposes all the existing issue sort values' do + expect(described_class.values.keys).to include( + *%w[MERGED_AT_ASC MERGED_AT_DESC] + ) + end +end diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb index 95255d534af..44a89bfa35e 100644 --- a/spec/graphql/types/project_type_spec.rb +++ b/spec/graphql/types/project_type_spec.rb @@ -75,7 +75,8 @@ RSpec.describe GitlabSchema.types['Project'] do :merged_before, :author_username, :assignee_username, - :milestone_title + :milestone_title, + :sort ) end end diff --git a/spec/helpers/container_registry_helper_spec.rb b/spec/helpers/container_registry_helper_spec.rb new file mode 100644 index 00000000000..6e6e8137b3e --- /dev/null +++ b/spec/helpers/container_registry_helper_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe ContainerRegistryHelper do + using RSpec::Parameterized::TableSyntax + + describe '#limit_delete_tags_service?' do + subject { helper.limit_delete_tags_service? } + + where(:feature_flag_enabled, :client_support, :expected_result) do + true | true | true + true | false | false + false | true | false + false | false | false + end + + with_them do + before do + stub_feature_flags(container_registry_expiration_policies_throttling: feature_flag_enabled) + allow(ContainerRegistry::Client).to receive(:supports_tag_delete?).and_return(client_support) + end + + it { is_expected.to eq(expected_result) } + end + end +end diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb index 9b32758c053..8e83bd0ee9d 100644 --- a/spec/helpers/issuables_helper_spec.rb +++ b/spec/helpers/issuables_helper_spec.rb @@ -197,7 +197,8 @@ RSpec.describe IssuablesHelper do initialTitleText: issue.title, initialDescriptionHtml: 'issue text
', initialDescriptionText: 'issue text', - initialTaskStatus: '0 of 0 tasks completed' + initialTaskStatus: '0 of 0 tasks completed', + issueType: 'issue' } expect(helper.issuable_initial_data(issue)).to match(hash_including(expected_data)) end diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb index aa947329c33..4daf7375a40 100644 --- a/spec/lib/container_registry/client_spec.rb +++ b/spec/lib/container_registry/client_spec.rb @@ -289,4 +289,57 @@ RSpec.describe ContainerRegistry::Client do end end end + + describe '.supports_tag_delete?' do + let(:registry_enabled) { true } + let(:registry_api_url) { 'http://sandbox.local' } + let(:registry_tags_support_enabled) { true } + let(:is_on_dot_com) { false } + + subject { described_class.supports_tag_delete? } + + before do + allow(::Gitlab).to receive(:com?).and_return(is_on_dot_com) + stub_container_registry_config(enabled: registry_enabled, api_url: registry_api_url, key: 'spec/fixtures/x509_certificate_pk.key') + stub_registry_tags_support(registry_tags_support_enabled) + end + + context 'with the registry enabled' do + it { is_expected.to be true } + + context 'without an api url' do + let(:registry_api_url) { '' } + + it { is_expected.to be false } + end + + context 'on .com' do + let(:is_on_dot_com) { true } + + it { is_expected.to be true } + end + + context 'when registry server does not support tag deletion' do + let(:registry_tags_support_enabled) { false } + + it { is_expected.to be false } + end + end + + context 'with the registry disabled' do + let(:registry_enabled) { false } + + it { is_expected.to be false } + end + + def stub_registry_tags_support(supported = true) + status_code = supported ? 200 : 404 + stub_request(:options, "#{registry_api_url}/v2/name/tags/reference/tag") + .to_return( + status: status_code, + body: '', + headers: { 'Allow' => 'DELETE' } + ) + end + end end diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb index ab25608e2f0..0c4318013e3 100644 --- a/spec/models/application_setting_spec.rb +++ b/spec/models/application_setting_spec.rb @@ -71,6 +71,8 @@ RSpec.describe ApplicationSetting do it { is_expected.not_to allow_value('three').for(:push_event_activities_limit) } it { is_expected.not_to allow_value(nil).for(:push_event_activities_limit) } + it { is_expected.to validate_numericality_of(:container_registry_delete_tags_service_timeout).only_integer.is_greater_than_or_equal_to(0) } + it { is_expected.to validate_numericality_of(:snippet_size_limit).only_integer.is_greater_than(0) } it { is_expected.to validate_numericality_of(:wiki_page_max_content_bytes).only_integer.is_greater_than_or_equal_to(1024) } it { is_expected.to validate_presence_of(:max_artifacts_size) } diff --git a/spec/models/atlassian/identity_spec.rb b/spec/models/atlassian/identity_spec.rb new file mode 100644 index 00000000000..a1dfe5b0e51 --- /dev/null +++ b/spec/models/atlassian/identity_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Atlassian::Identity do + describe 'associations' do + it { is_expected.to belong_to(:user) } + end + + describe 'validations' do + subject { create(:atlassian_identity) } + + it { is_expected.to validate_presence_of(:extern_uid) } + it { is_expected.to validate_uniqueness_of(:extern_uid) } + it { is_expected.to validate_presence_of(:user) } + it { is_expected.to validate_uniqueness_of(:user) } + end + + describe 'encrypted tokens' do + let(:token) { SecureRandom.alphanumeric(1254) } + let(:refresh_token) { SecureRandom.alphanumeric(45) } + let(:identity) { create(:atlassian_identity, token: token, refresh_token: refresh_token) } + + it 'saves the encrypted token, refresh token and corresponding ivs' do + expect(identity.encrypted_token).not_to be_nil + expect(identity.encrypted_token_iv).not_to be_nil + expect(identity.encrypted_refresh_token).not_to be_nil + expect(identity.encrypted_refresh_token_iv).not_to be_nil + + expect(identity.token).to eq(token) + expect(identity.refresh_token).to eq(refresh_token) + end + end +end diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb index 052afc28ef7..63c2b6fbf7b 100644 --- a/spec/models/merge_request_spec.rb +++ b/spec/models/merge_request_spec.rb @@ -61,6 +61,24 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '.order_merged_at_asc' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'returns MRs ordered by merged_at ascending' do + expect(described_class.order_merged_at_asc).to eq([older_mr, newer_mr]) + end + end + + describe '.order_merged_at_desc' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'returns MRs ordered by merged_at descending' do + expect(described_class.order_merged_at_desc).to eq([newer_mr, older_mr]) + end + end + describe '#squash_in_progress?' do let(:repo_path) do Gitlab::GitalyClient::StorageSettings.allow_disk_access do @@ -431,6 +449,23 @@ RSpec.describe MergeRequest, factory_default: :keep do end end + describe '.sort_by_attribute' do + context 'merged_at' do + let_it_be(:older_mr) { create(:merge_request, :with_merged_metrics) } + let_it_be(:newer_mr) { create(:merge_request, :with_merged_metrics) } + + it 'sorts asc' do + merge_requests = described_class.sort_by_attribute(:merged_at_asc) + expect(merge_requests).to eq([older_mr, newer_mr]) + end + + it 'sorts desc' do + merge_requests = described_class.sort_by_attribute(:merged_at_desc) + expect(merge_requests).to eq([newer_mr, older_mr]) + end + end + end + describe '#target_branch_sha' do let(:project) { create(:project, :repository) } diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 0c356fc5118..bdd13a77d7f 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -171,6 +171,16 @@ RSpec.describe Service do it { is_expected.to be_falsey } end end + + context 'when group-level service' do + Service.available_services_types.each do |service_type| + let(:service) do + service_type.constantize.new(group_id: group.id) + end + + it { is_expected.to be_falsey } + end + end end describe '#test' do diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb index 3f9c6981de1..248957a43e0 100644 --- a/spec/models/snippet_spec.rb +++ b/spec/models/snippet_spec.rb @@ -787,4 +787,26 @@ RSpec.describe Snippet do end end end + + describe '#multiple_files?' do + subject { snippet.multiple_files? } + + context 'when snippet has multiple files' do + let(:snippet) { create(:snippet, :repository) } + + it { is_expected.to be_truthy } + end + + context 'when snippet does not have multiple files' do + let(:snippet) { create(:snippet, :empty_repo) } + + it { is_expected.to be_falsey } + end + + context 'when the snippet does not have a repository' do + let(:snippet) { build(:snippet) } + + it { is_expected.to be_falsey } + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 9cd666e541f..98f1c5bb2cc 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -68,6 +68,7 @@ RSpec.describe User do it { is_expected.to have_one(:namespace) } it { is_expected.to have_one(:status) } it { is_expected.to have_one(:user_detail) } + it { is_expected.to have_one(:atlassian_identity) } it { is_expected.to have_one(:user_highest_role) } it { is_expected.to have_many(:snippets).dependent(:destroy) } it { is_expected.to have_many(:members) } diff --git a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb index 9a9c7107b20..2189ae3c519 100644 --- a/spec/requests/api/graphql/mutations/design_management/upload_spec.rb +++ b/spec/requests/api/graphql/mutations/design_management/upload_spec.rb @@ -12,11 +12,11 @@ RSpec.describe "uploading designs" do let(:files) { [fixture_file_upload("spec/fixtures/dk.png")] } let(:variables) { {} } - let(:mutation) do + def mutation input = { project_path: project.full_path, iid: issue.iid, - files: files + files: files.dup }.merge(variables) graphql_mutation(:design_management_upload, input) end @@ -30,31 +30,15 @@ RSpec.describe "uploading designs" do end it "returns an error if the user is not allowed to upload designs" do - post_graphql_mutation(mutation, current_user: create(:user)) + post_graphql_mutation_with_uploads(mutation, current_user: create(:user)) expect(graphql_errors).to be_present end - it "succeeds (backward compatibility)" do - post_graphql_mutation(mutation, current_user: current_user) + it "succeeds, and responds with the created designs" do + post_graphql_mutation_with_uploads(mutation, current_user: current_user) expect(graphql_errors).not_to be_present - end - - it 'succeeds' do - file_path_in_params = ['designManagementUploadInput', 'files', 0] - params = mutation_to_apollo_uploads_param(mutation, files: [file_path_in_params]) - - workhorse_post_with_file(api('/', current_user, version: 'graphql'), - params: params, - file_key: '1' - ) - - expect(graphql_errors).not_to be_present - end - - it "responds with the created designs" do - post_graphql_mutation(mutation, current_user: current_user) expect(mutation_response).to include( "designs" => a_collection_containing_exactly( @@ -65,7 +49,7 @@ RSpec.describe "uploading designs" do it "can respond with skipped designs" do 2.times do - post_graphql_mutation(mutation, current_user: current_user) + post_graphql_mutation_with_uploads(mutation, current_user: current_user) files.each(&:rewind) end @@ -80,7 +64,7 @@ RSpec.describe "uploading designs" do let(:variables) { { iid: "123" } } it "returns an error" do - post_graphql_mutation(mutation, current_user: create(:user)) + post_graphql_mutation_with_uploads(mutation, current_user: create(:user)) expect(graphql_errors).not_to be_empty end @@ -92,7 +76,7 @@ RSpec.describe "uploading designs" do expect(service).to receive(:execute).and_return({ status: :error, message: "Something went wrong" }) end - post_graphql_mutation(mutation, current_user: current_user) + post_graphql_mutation_with_uploads(mutation, current_user: current_user) expect(mutation_response["errors"].first).to eq("Something went wrong") end end diff --git a/spec/requests/api/graphql/project/merge_requests_spec.rb b/spec/requests/api/graphql/project/merge_requests_spec.rb index bb63a5994b0..9446e10c01d 100644 --- a/spec/requests/api/graphql/project/merge_requests_spec.rb +++ b/spec/requests/api/graphql/project/merge_requests_spec.rb @@ -210,4 +210,48 @@ RSpec.describe 'getting merge request listings nested in a project' do include_examples 'N+1 query check' end end + describe 'sorting and pagination' do + let(:data_path) { [:project, :mergeRequests] } + + def pagination_query(params, page_info) + graphql_query_for( + :project, + { full_path: project.full_path }, + <<~QUERY + mergeRequests(#{params}) { + #{page_info} edges { + node { + id + } + } + } + QUERY + ) + end + + def pagination_results_data(data) + data.map { |project| project.dig('node', 'id') } + end + + context 'when sorting by merged_at DESC' do + it_behaves_like 'sorted paginated query' do + let(:sort_param) { 'MERGED_AT_DESC' } + let(:first_param) { 2 } + + let(:expected_results) do + [ + merge_request_b, + merge_request_c, + merge_request_d, + merge_request_a + ].map(&:to_gid).map(&:to_s) + end + + before do + merge_request_c.metrics.update!(merged_at: 5.days.ago) + merge_request_b.metrics.update!(merged_at: 1.day.ago) + end + end + end + end end diff --git a/spec/requests/api/snippets_spec.rb b/spec/requests/api/snippets_spec.rb index 4e2f6e108eb..81dd9022657 100644 --- a/spec/requests/api/snippets_spec.rb +++ b/spec/requests/api/snippets_spec.rb @@ -391,21 +391,98 @@ RSpec.describe API::Snippets do create(:personal_snippet, :repository, author: user, visibility_level: visibility_level) end - shared_examples 'snippet updates' do - it 'updates a snippet' do - new_content = 'New content' - new_description = 'New description' + let(:create_action) { { action: 'create', file_path: 'foo.txt', content: 'bar' } } + let(:update_action) { { action: 'update', file_path: 'CHANGELOG', content: 'bar' } } + let(:move_action) { { action: 'move', file_path: '.old-gitattributes', previous_path: '.gitattributes' } } + let(:delete_action) { { action: 'delete', file_path: 'CONTRIBUTING.md' } } + let(:bad_file_path) { { action: 'create', file_path: '../../etc/passwd', content: 'bar' } } + let(:bad_previous_path) { { action: 'create', previous_path: '../../etc/passwd', file_path: 'CHANGELOG', content: 'bar' } } + let(:invalid_move) { { action: 'move', file_path: 'missing_previous_path.txt' } } - update_snippet(params: { content: new_content, description: new_description, visibility: 'internal' }) + context 'with snippet file changes' do + using RSpec::Parameterized::TableSyntax - expect(response).to have_gitlab_http_status(:ok) - snippet.reload - expect(snippet.content).to eq(new_content) - expect(snippet.description).to eq(new_description) - expect(snippet.visibility).to eq('internal') + where(:is_multi_file, :file_name, :content, :files, :status) do + true | nil | nil | [create_action] | :success + true | nil | nil | [update_action] | :success + true | nil | nil | [move_action] | :success + true | nil | nil | [delete_action] | :success + true | nil | nil | [create_action, update_action] | :success + true | 'foo.txt' | 'bar' | [create_action] | :bad_request + true | 'foo.txt' | 'bar' | nil | :bad_request + true | nil | nil | nil | :bad_request + true | 'foo.txt' | nil | [create_action] | :bad_request + true | nil | 'bar' | [create_action] | :bad_request + true | '' | nil | [create_action] | :bad_request + true | nil | '' | [create_action] | :bad_request + true | nil | nil | [bad_file_path] | :bad_request + true | nil | nil | [bad_previous_path] | :bad_request + true | nil | nil | [invalid_move] | :forbidden + + false | 'foo.txt' | 'bar' | nil | :success + false | 'foo.txt' | nil | nil | :success + false | nil | 'bar' | nil | :success + false | 'foo.txt' | 'bar' | [create_action] | :bad_request + false | nil | nil | nil | :bad_request + false | nil | '' | nil | :bad_request + false | nil | nil | [bad_file_path] | :bad_request + false | nil | nil | [bad_previous_path] | :bad_request + end + + with_them do + before do + allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(is_multi_file) + end + + it 'has the correct response' do + update_params = {}.tap do |params| + params[:files] = files if files + params[:file_name] = file_name if file_name + params[:content] = content if content + end + + update_snippet(params: update_params) + + expect(response).to have_gitlab_http_status(status) + end + end + + context 'when save fails due to a repository commit error' do + before do + allow_next_instance_of(Repository) do |instance| + allow(instance).to receive(:multi_action).and_raise(Gitlab::Git::CommitError) + end + + update_snippet(params: { files: [create_action] }) + end + + it 'returns a bad request response' do + expect(response).to have_gitlab_http_status(:bad_request) + end end end + shared_examples 'snippet non-file updates' do + it 'updates a snippet non-file attributes' do + new_description = 'New description' + new_title = 'New title' + new_visibility = 'internal' + + update_snippet(params: { title: new_title, description: new_description, visibility: new_visibility }) + + snippet.reload + + aggregate_failures do + expect(response).to have_gitlab_http_status(:ok) + expect(snippet.description).to eq(new_description) + expect(snippet.visibility).to eq(new_visibility) + expect(snippet.title).to eq(new_title) + end + end + end + + it_behaves_like 'snippet non-file updates' + context 'with restricted visibility settings' do before do stub_application_setting(restricted_visibility_levels: @@ -413,11 +490,9 @@ RSpec.describe API::Snippets do Gitlab::VisibilityLevel::PRIVATE]) end - it_behaves_like 'snippet updates' + it_behaves_like 'snippet non-file updates' end - it_behaves_like 'snippet updates' - it 'returns 404 for invalid snippet id' do update_snippet(snippet_id: non_existing_record_id, params: { title: 'Foo' }) @@ -438,13 +513,6 @@ RSpec.describe API::Snippets do expect(response).to have_gitlab_http_status(:bad_request) end - it 'returns 400 if content is blank' do - update_snippet(params: { content: '' }) - - expect(response).to have_gitlab_http_status(:bad_request) - expect(json_response['error']).to eq 'content is empty' - end - it 'returns 400 if title is blank' do update_snippet(params: { title: '' }) diff --git a/spec/services/ci/parse_dotenv_artifact_service_spec.rb b/spec/services/ci/parse_dotenv_artifact_service_spec.rb index a5f01187a83..91b81af9fd1 100644 --- a/spec/services/ci/parse_dotenv_artifact_service_spec.rb +++ b/spec/services/ci/parse_dotenv_artifact_service_spec.rb @@ -66,12 +66,13 @@ RSpec.describe Ci::ParseDotenvArtifactService do end context 'when multiple key/value pairs exist in one line' do - let(:blob) { 'KEY1=VAR1KEY2=VAR1' } + let(:blob) { 'KEY=VARCONTAINING=EQLS' } - it 'returns error' do - expect(subject[:status]).to eq(:error) - expect(subject[:message]).to eq("Validation failed: Key can contain only letters, digits and '_'.") - expect(subject[:http_status]).to eq(:bad_request) + it 'parses the dotenv data' do + subject + + expect(build.job_variables.as_json).to contain_exactly( + hash_including('key' => 'KEY', 'value' => 'VARCONTAINING=EQLS')) end end diff --git a/spec/services/projects/container_repository/delete_tags_service_spec.rb b/spec/services/projects/container_repository/delete_tags_service_spec.rb index 3014ccbd7ba..5116427dad2 100644 --- a/spec/services/projects/container_repository/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/delete_tags_service_spec.rb @@ -90,6 +90,10 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do subject { service.execute(repository) } + before do + stub_feature_flags(container_registry_expiration_policies_throttling: false) + end + context 'without permissions' do it { is_expected.to include(status: :error) } end @@ -119,6 +123,18 @@ RSpec.describe Projects::ContainerRepository::DeleteTagsService do it_behaves_like 'logging a success response' end + + context 'with a timeout error' do + before do + expect_next_instance_of(::Projects::ContainerRepository::Gitlab::DeleteTagsService) do |delete_service| + expect(delete_service).to receive(:delete_tags).and_raise(::Projects::ContainerRepository::Gitlab::DeleteTagsService::TimeoutError) + end + end + + it { is_expected.to include(status: :error, message: 'timeout while deleting tags') } + + it_behaves_like 'logging an error response', message: 'timeout while deleting tags' + end end context 'and the feature is disabled' do diff --git a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb index 68c232e5d83..3bbcec8775e 100644 --- a/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb +++ b/spec/services/projects/container_repository/gitlab/delete_tags_service_spec.rb @@ -12,13 +12,21 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do subject { service.execute } - context 'with tags to delete' do + before do + stub_feature_flags(container_registry_expiration_policies_throttling: false) + end + + RSpec.shared_examples 'deleting tags' do it 'deletes the tags by name' do stub_delete_reference_requests(tags) expect_delete_tag_by_names(tags) is_expected.to eq(status: :success, deleted: tags) end + end + + context 'with tags to delete' do + it_behaves_like 'deleting tags' it 'succeeds when tag delete returns 404' do stub_delete_reference_requests('A' => 200, 'Ba' => 404) @@ -41,6 +49,47 @@ RSpec.describe Projects::ContainerRepository::Gitlab::DeleteTagsService do it { is_expected.to eq(status: :error, message: 'could not delete tags') } end end + + context 'with throttling enabled' do + let(:timeout) { 10 } + + before do + stub_feature_flags(container_registry_expiration_policies_throttling: true) + stub_application_setting(container_registry_delete_tags_service_timeout: timeout) + end + + it_behaves_like 'deleting tags' + + context 'with timeout' do + context 'set to a valid value' do + before do + allow(Time.zone).to receive(:now).and_return(10, 15, 25) # third call to Time.zone.now will be triggering the timeout + stub_delete_reference_requests('A' => 200) + end + + it { is_expected.to include(status: :error, message: 'timeout while deleting tags') } + + it 'tracks the exception' do + expect(::Gitlab::ErrorTracking) + .to receive(:track_exception).with(::Projects::ContainerRepository::Gitlab::DeleteTagsService::TimeoutError, tags_count: tags.size, container_repository_id: repository.id) + + subject + end + end + + context 'set to 0' do + let(:timeout) { 0 } + + it_behaves_like 'deleting tags' + end + + context 'set to nil' do + let(:timeout) { nil } + + it_behaves_like 'deleting tags' + end + end + end end context 'with empty tags' do diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb index 87525734490..5635ba3df05 100644 --- a/spec/support/helpers/graphql_helpers.rb +++ b/spec/support/helpers/graphql_helpers.rb @@ -241,6 +241,39 @@ module GraphqlHelpers post_graphql(mutation.query, current_user: current_user, variables: mutation.variables) end + def post_graphql_mutation_with_uploads(mutation, current_user: nil) + file_paths = file_paths_in_mutation(mutation) + params = mutation_to_apollo_uploads_param(mutation, files: file_paths) + + workhorse_post_with_file(api('/', current_user, version: 'graphql'), + params: params, + file_key: '1' + ) + end + + def file_paths_in_mutation(mutation) + paths = [] + find_uploads(paths, [], mutation.variables) + + paths + end + + # Depth first search for UploadedFile values + def find_uploads(paths, path, value) + case value + when Rack::Test::UploadedFile + paths << path + when Hash + value.each do |k, v| + find_uploads(paths, path + [k], v) + end + when Array + value.each_with_index do |v, i| + find_uploads(paths, path + [i], v) + end + end + end + # this implements GraphQL multipart request v2 # https://github.com/jaydenseric/graphql-multipart-request-spec/tree/v2.0.0-alpha.2 # this is simplified and do not support file deduplication diff --git a/spec/support/shared_examples/requests/snippet_shared_examples.rb b/spec/support/shared_examples/requests/snippet_shared_examples.rb index a17163328f4..84ef7723b9b 100644 --- a/spec/support/shared_examples/requests/snippet_shared_examples.rb +++ b/spec/support/shared_examples/requests/snippet_shared_examples.rb @@ -2,6 +2,10 @@ RSpec.shared_examples 'update with repository actions' do context 'when the repository exists' do + before do + allow_any_instance_of(Snippet).to receive(:multiple_files?).and_return(false) + end + it 'commits the changes to the repository' do existing_blob = snippet.blobs.first new_file_name = existing_blob.path + '_new'