diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-26 15:10:19 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-01-26 15:10:19 +0300 |
commit | a1c0b634f78f51389fd3ec390a1803afa3de49a2 (patch) | |
tree | f57fc3000a6e7f2bd271a63018427c8fb9c06c08 /spec | |
parent | db950c5706cdf47f7ccc2e02a4ffd691432ac5dc (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
14 files changed, 421 insertions, 99 deletions
diff --git a/spec/db/schema_spec.rb b/spec/db/schema_spec.rb index 9b839873fbb..327f162f57a 100644 --- a/spec/db/schema_spec.rb +++ b/spec/db/schema_spec.rb @@ -33,9 +33,9 @@ RSpec.describe 'Database schema', feature_category: :database do chat_names: %w[chat_id team_id user_id integration_id], chat_teams: %w[team_id], ci_build_needs: %w[partition_id], - ci_build_pending_states: %w[partition_id], + ci_build_pending_states: %w[partition_id build_id], ci_build_report_results: %w[partition_id], - ci_build_trace_chunks: %w[partition_id], + ci_build_trace_chunks: %w[partition_id build_id], ci_build_trace_metadata: %w[partition_id], ci_builds: %w[erased_by_id trigger_request_id partition_id], ci_builds_runner_session: %w[partition_id build_id], @@ -52,7 +52,7 @@ RSpec.describe 'Database schema', feature_category: :database do ci_sources_pipelines: %w[partition_id source_partition_id], ci_stages: %w[partition_id], ci_trigger_requests: %w[commit_id], - ci_unit_test_failures: %w[partition_id], + ci_unit_test_failures: %w[partition_id build_id], cluster_providers_aws: %w[security_group_id vpc_id access_key_id], cluster_providers_gcp: %w[gcp_project_id operation_id], compliance_management_frameworks: %w[group_id], diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js index b9d68587fa5..6fd106502c4 100644 --- a/spec/frontend/api_spec.js +++ b/spec/frontend/api_spec.js @@ -206,7 +206,7 @@ describe('Api', () => { expires_at: undefined, }; - mock.onPost(expectedUrl).reply(200, { + mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, { status: 'success', }); @@ -478,7 +478,7 @@ describe('Api', () => { jest.spyOn(axios, 'post'); - mock.onPost(expectedUrl).reply(200, { + mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, { status: 'success', }); @@ -494,7 +494,7 @@ describe('Api', () => { const projectId = 1; const options = { state: 'active' }; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`; - mock.onGet(expectedUrl).reply(200, [ + mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [ { id: 1, title: 'milestone1', @@ -514,7 +514,7 @@ describe('Api', () => { const projectId = 1; const issueIid = 11; const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/issues/11/todo`; - mock.onPost(expectedUrl).reply(200, { + mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, { id: 112, project: { id: 1, @@ -541,7 +541,7 @@ describe('Api', () => { expires_at: undefined, }; - mock.onPost(expectedUrl).reply(200, { + mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, { status: 'success', }); @@ -644,7 +644,7 @@ describe('Api', () => { jest.spyOn(axios, 'post'); - mock.onPost(expectedUrl).reply(200, { + mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, { status: 'success', }); @@ -958,7 +958,7 @@ describe('Api', () => { jest.spyOn(axios, 'post'); - mock.onPost(expectedUrl).replyOnce(200, [ + mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, [ { id: 'abcdefghijklmnop', short_id: 'abcdefg', @@ -984,7 +984,9 @@ describe('Api', () => { mock .onGet(expectedUrl) - .replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]); + .replyOnce(HTTP_STATUS_OK, [ + { id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }, + ]); return Api.allContextCommits(projectPath, mergeRequestId).then(({ data }) => { expect(data[0].title).toBe('Dummy commit title'); diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js index 21636017f10..e2a14a9102f 100644 --- a/spec/frontend/jira_connect/subscriptions/api_spec.js +++ b/spec/frontend/jira_connect/subscriptions/api_spec.js @@ -104,9 +104,11 @@ describe('JiraConnect API', () => { response = await makeRequest(); expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, { + headers: {}, params: { page: mockPage, per_page: mockPerPage, + search: undefined, }, }); expect(response.data).toEqual(mockResponse); diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js index f1fc5e4d90b..97038a2a231 100644 --- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js +++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js @@ -27,6 +27,7 @@ jest.mock('~/jira_connect/subscriptions/api', () => { }); const mockGroupsPath = '/groups'; +const mockAccessToken = '123'; describe('GroupsList', () => { let wrapper; @@ -39,6 +40,9 @@ describe('GroupsList', () => { provide: { groupsPath: mockGroupsPath, }, + computed: { + accessToken: () => mockAccessToken, + }, ...options, }), ); @@ -148,11 +152,15 @@ describe('GroupsList', () => { }); it('calls `fetchGroups` with search term', () => { - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: 1, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: mockSearchTeam, - }); + expect(fetchGroups).toHaveBeenLastCalledWith( + mockGroupsPath, + { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: mockSearchTeam, + }, + mockAccessToken, + ); }); it('disables GroupListItems', () => { @@ -222,11 +230,15 @@ describe('GroupsList', () => { findSearchBox().vm.$emit('input', newSearch); if (shouldSearch) { - expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, { - page: 1, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: expectedSearchValue, - }); + expect(fetchGroups).toHaveBeenCalledWith( + mockGroupsPath, + { + page: 1, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: expectedSearchValue, + }, + mockAccessToken, + ); } else { expect(fetchGroups).not.toHaveBeenCalled(); } @@ -257,11 +269,15 @@ describe('GroupsList', () => { }); it('should load results for page 2', () => { - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: 2, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: '', - }); + expect(fetchGroups).toHaveBeenLastCalledWith( + mockGroupsPath, + { + page: 2, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: '', + }, + mockAccessToken, + ); }); it.each` @@ -274,11 +290,15 @@ describe('GroupsList', () => { const searchBox = findSearchBox(); searchBox.vm.$emit('input', searchTerm); - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: expectedPage, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: expectedSearchTerm, - }); + expect(fetchGroups).toHaveBeenLastCalledWith( + mockGroupsPath, + { + page: expectedPage, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: expectedSearchTerm, + }, + mockAccessToken, + ); }, ); }); @@ -324,11 +344,15 @@ describe('GroupsList', () => { const paginationEl = findPagination(); paginationEl.vm.$emit('input', 2); - expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, { - page: 2, - perPage: DEFAULT_GROUPS_PER_PAGE, - search: '', - }); + expect(fetchGroups).toHaveBeenLastCalledWith( + mockGroupsPath, + { + page: 2, + perPage: DEFAULT_GROUPS_PER_PAGE, + search: '', + }, + mockAccessToken, + ); }); }); }); diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js index 4d0250fffbf..7f708f13eaa 100644 --- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js +++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js @@ -18,12 +18,14 @@ describe('generateRefDestinationPath', () => { ${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`} `('generates the correct destination path for $currentPath', ({ currentPath, result }) => { setWindowLocation(currentPath); - expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result); + expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(result); }); it('encodes the selected ref', () => { const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`; - expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result); + expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe( + result, + ); }); }); diff --git a/spec/graphql/resolvers/ci/variables_resolver_spec.rb b/spec/graphql/resolvers/ci/variables_resolver_spec.rb new file mode 100644 index 00000000000..16b72e8cb7f --- /dev/null +++ b/spec/graphql/resolvers/ci/variables_resolver_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Resolvers::Ci::VariablesResolver, feature_category: :pipeline_authoring do + include GraphqlHelpers + + describe '#resolve' do + let_it_be(:user) { create(:user) } + let_it_be(:args) { {} } + let_it_be(:obj) { nil } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project) } + + let_it_be(:ci_instance_variables) do + [ + create(:ci_instance_variable, key: 'a'), + create(:ci_instance_variable, key: 'b') + ] + end + + let_it_be(:ci_group_variables) do + [ + create(:ci_group_variable, group: group, key: 'a'), + create(:ci_group_variable, group: group, key: 'b') + ] + end + + let_it_be(:ci_project_variables) do + [ + create(:ci_variable, project: project, key: 'a'), + create(:ci_variable, project: project, key: 'b') + ] + end + + subject(:resolve_variables) { resolve(described_class, obj: obj, ctx: { current_user: user }, args: args) } + + context 'when parent object is nil' do + context 'when user is authorized', :enable_admin_mode do + let_it_be(:user) { create(:admin) } + + it "returns the instance's variables" do + expect(resolve_variables.items.to_a).to match_array(ci_instance_variables) + end + end + + context 'when user is not authorized' do + it "returns nil" do + expect(resolve_variables).to be_nil + end + end + end + + context 'when parent object is a Group' do + let_it_be(:obj) { group } + + it "returns the group's variables" do + expect(resolve_variables.items.to_a).to match_array(ci_group_variables) + end + end + + context 'when parent object is a Project' do + let_it_be(:obj) { project } + + it "returns the project's variables" do + expect(resolve_variables.items.to_a).to match_array(ci_project_variables) + end + end + end +end diff --git a/spec/graphql/types/ci/variable_sort_enum_spec.rb b/spec/graphql/types/ci/variable_sort_enum_spec.rb new file mode 100644 index 00000000000..1702360a21f --- /dev/null +++ b/spec/graphql/types/ci/variable_sort_enum_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Types::Ci::VariableSortEnum, feature_category: :pipeline_authoring do + it 'exposes the available order methods' do + expect(described_class.values).to match( + 'KEY_ASC' => have_attributes(value: :key_asc), + 'KEY_DESC' => have_attributes(value: :key_desc) + ) + end +end diff --git a/spec/models/concerns/ci/has_variable_spec.rb b/spec/models/concerns/ci/has_variable_spec.rb index 861d8f3b974..d7d0cabd4ae 100644 --- a/spec/models/concerns/ci/has_variable_spec.rb +++ b/spec/models/concerns/ci/has_variable_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Ci::HasVariable do +RSpec.describe Ci::HasVariable, feature_category: :continuous_integration do subject { build(:ci_variable) } it { is_expected.to validate_presence_of(:key) } @@ -113,4 +113,36 @@ RSpec.describe Ci::HasVariable do end end end + + describe '.order_by' do + let_it_be(:relation) { Ci::Variable.all } + + it 'supports ordering by key ascending' do + expect(relation).to receive(:reorder).with({ key: :asc }) + + relation.order_by('key_asc') + end + + it 'supports ordering by key descending' do + expect(relation).to receive(:reorder).with({ key: :desc }) + + relation.order_by('key_desc') + end + + context 'when order method is unknown' do + it 'does not call reorder' do + expect(relation).not_to receive(:reorder) + + relation.order_by('unknown') + end + end + + context 'when order method is nil' do + it 'does not call reorder' do + expect(relation).not_to receive(:reorder) + + relation.order_by(nil) + end + end + end end diff --git a/spec/requests/api/graphql/ci/group_variables_spec.rb b/spec/requests/api/graphql/ci/group_variables_spec.rb index 51cbb4719f7..e45ddbb1585 100644 --- a/spec/requests/api/graphql/ci/group_variables_spec.rb +++ b/spec/requests/api/graphql/ci/group_variables_spec.rb @@ -72,4 +72,32 @@ RSpec.describe 'Query.group(fullPath).ciVariables', feature_category: :pipeline_ expect(graphql_data.dig('group', 'ciVariables')).to be_nil end end + + describe 'sorting and pagination' do + let_it_be(:current_user) { user } + let_it_be(:data_path) { [:group, :ci_variables] } + let_it_be(:variables) do + [ + create(:ci_group_variable, group: group, key: 'd'), + create(:ci_group_variable, group: group, key: 'a'), + create(:ci_group_variable, group: group, key: 'c'), + create(:ci_group_variable, group: group, key: 'e'), + create(:ci_group_variable, group: group, key: 'b') + ] + end + + def pagination_query(params) + graphql_query_for( + :group, + { fullPath: group.full_path }, + query_graphql_field('ciVariables', params, "#{page_info} nodes { id }") + ) + end + + before do + group.add_owner(current_user) + end + + it_behaves_like 'sorted paginated variables' + end end diff --git a/spec/requests/api/graphql/ci/instance_variables_spec.rb b/spec/requests/api/graphql/ci/instance_variables_spec.rb index e0397e17923..5b65ae88426 100644 --- a/spec/requests/api/graphql/ci/instance_variables_spec.rb +++ b/spec/requests/api/graphql/ci/instance_variables_spec.rb @@ -69,4 +69,28 @@ RSpec.describe 'Query.ciVariables', feature_category: :pipeline_authoring do expect(graphql_data.dig('ciVariables')).to be_nil end end + + describe 'sorting and pagination' do + let_it_be(:current_user) { create(:admin) } + let_it_be(:data_path) { [:ci_variables] } + let_it_be(:variables) do + [ + create(:ci_instance_variable, key: 'd'), + create(:ci_instance_variable, key: 'a'), + create(:ci_instance_variable, key: 'c'), + create(:ci_instance_variable, key: 'e'), + create(:ci_instance_variable, key: 'b') + ] + end + + def pagination_query(params) + graphql_query_for( + :ci_variables, + params, + "#{page_info} nodes { id }" + ) + end + + it_behaves_like 'sorted paginated variables' + end end diff --git a/spec/requests/api/graphql/ci/project_variables_spec.rb b/spec/requests/api/graphql/ci/project_variables_spec.rb index 0338b58a0ea..f9a32b42b4d 100644 --- a/spec/requests/api/graphql/ci/project_variables_spec.rb +++ b/spec/requests/api/graphql/ci/project_variables_spec.rb @@ -66,4 +66,32 @@ RSpec.describe 'Query.project(fullPath).ciVariables', feature_category: :pipelin expect(graphql_data.dig('project', 'ciVariables')).to be_nil end end + + describe 'sorting and pagination' do + let_it_be(:current_user) { user } + let_it_be(:data_path) { [:project, :ci_variables] } + let_it_be(:variables) do + [ + create(:ci_variable, project: project, key: 'd'), + create(:ci_variable, project: project, key: 'a'), + create(:ci_variable, project: project, key: 'c'), + create(:ci_variable, project: project, key: 'e'), + create(:ci_variable, project: project, key: 'b') + ] + end + + def pagination_query(params) + graphql_query_for( + :project, + { fullPath: project.full_path }, + query_graphql_field('ciVariables', params, "#{page_info} nodes { id }") + ) + end + + before do + project.add_maintainer(current_user) + end + + it_behaves_like 'sorted paginated variables' + end end diff --git a/spec/services/projects/container_repository/destroy_service_spec.rb b/spec/services/projects/container_repository/destroy_service_spec.rb index 0ec0aecaa04..fed1d13daa5 100644 --- a/spec/services/projects/container_repository/destroy_service_spec.rb +++ b/spec/services/projects/container_repository/destroy_service_spec.rb @@ -5,86 +5,131 @@ require 'spec_helper' RSpec.describe Projects::ContainerRepository::DestroyService do let_it_be(:user) { create(:user) } let_it_be(:project) { create(:project, :private) } + let_it_be(:params) { {} } - subject { described_class.new(project, user) } + subject { described_class.new(project, user, params) } before do stub_container_registry_config(enabled: true) end - context 'when user does not have access to registry' do - let!(:repository) { create(:container_repository, :root, project: project) } + shared_examples 'returning an error status with message' do |error_message| + it 'returns an error status' do + response = subject.execute(repository) - it 'does not delete a repository' do - expect { subject.execute(repository) }.not_to change { ContainerRepository.count } + expect(response).to include(status: :error, message: error_message) end end - context 'when user has access to registry' do + shared_examples 'executing with permissions' do + let_it_be_with_refind(:repository) { create(:container_repository, :root, project: project) } + before do - project.add_developer(user) + stub_container_registry_tags(repository: :any, tags: %w[latest stable]) end - context 'when root container repository exists' do - let!(:repository) { create(:container_repository, :root, project: project) } + it 'deletes the repository' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) + expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + end + + it 'sends disable_timeout = true as part of the params as default' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true) + expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + end + + it 'sends disable_timeout = false as part of the params if it is set to false' do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false) + expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1) + end + context 'when deleting the tags fails' do before do - stub_container_registry_tags(repository: :any, tags: %w[latest stable]) + expect_cleanup_tags_service_with(container_repository: repository, return_status: :error) + allow(Gitlab::AppLogger).to receive(:error).and_call_original end - it 'deletes the repository' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) - expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) - end + it 'sets status as deleted_failed' do + subject.execute(repository) - it 'sends disable_timeout = true as part of the params as default' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: true) - expect { subject.execute(repository) }.to change { ContainerRepository.count }.by(-1) + expect(repository).to be_delete_failed end - it 'sends disable_timeout = false as part of the params if it is set to false' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success, disable_timeout: false) - expect { subject.execute(repository, disable_timeout: false) }.to change { ContainerRepository.count }.by(-1) - end + it 'logs the error' do + subject.execute(repository) - context 'when deleting the tags fails' do - it 'sets status as deleted_failed' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :error) - allow(Gitlab::AppLogger).to receive(:error).and_call_original + expect(Gitlab::AppLogger).to have_received(:error) + .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags") + end - subject.execute(repository) + it_behaves_like 'returning an error status with message', 'Deletion failed for container repository' + end - expect(repository).to be_delete_failed - expect(Gitlab::AppLogger).to have_received(:error) - .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: error in deleting tags") - end + context 'when destroying the repository fails' do + before do + expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) + allow(repository).to receive(:destroy).and_return(false) + allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2']) + allow(Gitlab::AppLogger).to receive(:error).and_call_original end - context 'when destroying the repository fails' do - it 'sets status as deleted_failed' do - expect_cleanup_tags_service_with(container_repository: repository, return_status: :success) - allow(repository).to receive(:destroy).and_return(false) - allow(repository.errors).to receive(:full_messages).and_return(['Error 1', 'Error 2']) - allow(Gitlab::AppLogger).to receive(:error).and_call_original + it 'sets status as deleted_failed' do + subject.execute(repository) + + expect(repository).to be_delete_failed + end - subject.execute(repository) + it 'logs the error' do + subject.execute(repository) - expect(repository).to be_delete_failed - expect(Gitlab::AppLogger).to have_received(:error) - .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2") - end + expect(Gitlab::AppLogger).to have_received(:error) + .with("Container repository with ID: #{repository.id} and path: #{repository.path} failed with message: Error 1. Error 2") end - def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true) - delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService) + it_behaves_like 'returning an error status with message', 'Deletion failed for container repository' + end + end + + context 'when user has access to registry' do + before do + project.add_developer(user) + end - expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with( - container_repository: container_repository, - params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout) - ).and_return(delete_tags_service) + it_behaves_like 'executing with permissions' + end - expect(delete_tags_service).to receive(:execute).and_return(status: return_status) - end + context 'when user does not have access to registry' do + let_it_be(:repository) { create(:container_repository, :root, project: project) } + + it 'does not delete a repository' do + expect { subject.execute(repository) }.not_to change { ContainerRepository.count } end + + it_behaves_like 'returning an error status with message', 'Unauthorized access' + end + + context 'when called during project deletion' do + let(:user) { nil } + let(:params) { { skip_permission_check: true } } + + it_behaves_like 'executing with permissions' + end + + context 'when there is no user' do + let(:user) { nil } + let(:repository) { create(:container_repository, :root, project: project) } + + it_behaves_like 'returning an error status with message', 'Unauthorized access' + end + + def expect_cleanup_tags_service_with(container_repository:, return_status:, disable_timeout: true) + delete_tags_service = instance_double(Projects::ContainerRepository::CleanupTagsService) + + expect(Projects::ContainerRepository::CleanupTagsService).to receive(:new).with( + container_repository: container_repository, + params: described_class::CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout) + ).and_return(delete_tags_service) + + expect(delete_tags_service).to receive(:execute).and_return(status: return_status) end end diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb index 0ef0ecc1099..05f699ed357 100644 --- a/spec/services/projects/destroy_service_spec.rb +++ b/spec/services/projects/destroy_service_spec.rb @@ -343,18 +343,30 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi end context 'when image repository deletion succeeds' do - it 'removes tags' do - expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService) - .to receive(:execute).and_return({ status: :success }) + it 'returns true' do + expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |instance| + expect(instance).to receive(:execute).and_return(status: :success) + end - destroy_project(project, user) + expect(destroy_project(project, user)).to be true + end + end + + context 'when image repository deletion raises an error' do + it 'returns false' do + expect_next_instance_of(Projects::ContainerRepository::CleanupTagsService) do |service| + expect(service).to receive(:execute).and_raise(RuntimeError) + end + + expect(destroy_project(project, user)).to be false end end context 'when image repository deletion fails' do - it 'raises an exception' do - expect_any_instance_of(Projects::ContainerRepository::CleanupTagsService) - .to receive(:execute).and_raise(RuntimeError) + it 'returns false' do + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :error }) + end expect(destroy_project(project, user)).to be false end @@ -381,8 +393,9 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi context 'when image repository tags deletion succeeds' do it 'removes tags' do - expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(true) + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :sucess }) + end destroy_project(project, user) end @@ -390,13 +403,27 @@ RSpec.describe Projects::DestroyService, :aggregate_failures, :event_store_publi context 'when image repository tags deletion fails' do it 'raises an exception' do - expect_any_instance_of(ContainerRepository) - .to receive(:delete_tags!).and_return(false) + expect_next_instance_of(Projects::ContainerRepository::DestroyService) do |service| + expect(service).to receive(:execute).and_return({ status: :error }) + end expect(destroy_project(project, user)).to be false end end end + + context 'when there are no tags for legacy root repository' do + before do + stub_container_registry_tags(repository: project.full_path, + tags: []) + end + + it 'does not try to destroy the repository' do + expect(Projects::ContainerRepository::DestroyService).not_to receive(:new) + + destroy_project(project, user) + end + end end context 'for a forked project with LFS objects' do diff --git a/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb new file mode 100644 index 00000000000..306310c9e9c --- /dev/null +++ b/spec/support/shared_examples/requests/api/graphql/ci/sorted_paginated_variables_shared_examples.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# Requires `current_user`, `data_path`, `variables`, and `pagination_query(params)` bindings +RSpec.shared_examples 'sorted paginated variables' do + subject(:expected_ordered_variables) { ordered_variables.map { |var| var.to_global_id.to_s } } + + context 'when sorted by key ascending' do + let(:ordered_variables) { variables.sort_by(&:key) } + + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :KEY_ASC } + let(:first_param) { 2 } + let(:all_records) { expected_ordered_variables } + end + end + + context 'when sorted by key descending' do + let(:ordered_variables) { variables.sort_by(&:key).reverse } + + it_behaves_like 'sorted paginated query' do + let(:sort_param) { :KEY_DESC } + let(:first_param) { 2 } + let(:all_records) { expected_ordered_variables } + end + end +end |