diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 15:17:18 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-04 15:17:18 +0300 |
commit | fb5d3cceb8d43f8c2dc22a5d8c74327e9397f2e8 (patch) | |
tree | dbd3a17217fa46cf279ed692b605e03222fca360 /spec | |
parent | 6cd4578a23ffe0fb94632f83a07a25d01f8d6821 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
43 files changed, 932 insertions, 225 deletions
diff --git a/spec/controllers/application_controller_spec.rb b/spec/controllers/application_controller_spec.rb index cdd088c2d5e..ce76be9f509 100644 --- a/spec/controllers/application_controller_spec.rb +++ b/spec/controllers/application_controller_spec.rb @@ -1117,4 +1117,28 @@ RSpec.describe ApplicationController, feature_category: :shared do end end end + + context 'when Gitlab::Git::ResourceExhaustedError exception is raised' do + before do + sign_in user + end + + controller(described_class) do + def index + raise Gitlab::Git::ResourceExhaustedError.new( + "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later", 50 + ) + end + end + + it 'returns a plaintext error response with 429 status' do + get :index + + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.body).to include( + "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later" + ) + expect(response.headers['Retry-After']).to eq(50) + end + end end diff --git a/spec/controllers/graphql_controller_spec.rb b/spec/controllers/graphql_controller_spec.rb index cd72dbe394a..4bc7e11ec6b 100644 --- a/spec/controllers/graphql_controller_spec.rb +++ b/spec/controllers/graphql_controller_spec.rb @@ -65,6 +65,22 @@ RSpec.describe GraphqlController, feature_category: :integrations do ) expect(response).to have_gitlab_http_status(:forbidden) end + + it 'handles Gitlab::Git::ResourceExhaustedError', :aggregate_failures do + allow(controller).to receive(:execute) do + raise Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50) + end + + post :execute + + expect(json_response).to include( + 'errors' => include( + a_hash_including('message' => 'Upstream Gitaly has been exhausted. Try again later') + ) + ) + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.headers['Retry-After']).to be(50) + end end describe 'POST #execute' do diff --git a/spec/features/abuse_report_spec.rb b/spec/features/abuse_report_spec.rb index 272656fb4ca..98c1f9baf12 100644 --- a/spec/features/abuse_report_spec.rb +++ b/spec/features/abuse_report_spec.rb @@ -12,6 +12,7 @@ RSpec.describe 'Abuse reports', :js, feature_category: :insider_threat do before do sign_in(reporter1) + stub_feature_flags(moved_mr_sidebar: false) end describe 'report abuse to administrator' do diff --git a/spec/features/ide/user_opens_merge_request_spec.rb b/spec/features/ide/user_opens_merge_request_spec.rb index 0074b4b1eb0..dc280133a20 100644 --- a/spec/features/ide/user_opens_merge_request_spec.rb +++ b/spec/features/ide/user_opens_merge_request_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do + include CookieHelper + let(:merge_request) { create(:merge_request, :simple, source_project: project) } let(:project) { create(:project, :public, :repository) } let(:user) { project.first_owner } @@ -12,6 +14,8 @@ RSpec.describe 'IDE merge request', :js, feature_category: :web_ide do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') + visit(merge_request_path(merge_request)) end diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb index 709919d0196..a166ff46177 100644 --- a/spec/features/incidents/incident_details_spec.rb +++ b/spec/features/incidents/incident_details_spec.rb @@ -94,6 +94,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d end it 'routes the user to the incident details page when the `issue_type` is set to incident' do + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) wait_for_requests @@ -113,6 +114,7 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d end it 'routes the user to the issue details page when the `issue_type` is set to issue' do + set_cookie('new-actions-popover-viewed', 'true') visit incident_project_issues_path(project, incident) wait_for_requests diff --git a/spec/features/issues/discussion_lock_spec.rb b/spec/features/issues/discussion_lock_spec.rb index 47865d2b6ba..fb9addff1a2 100644 --- a/spec/features/issues/discussion_lock_spec.rb +++ b/spec/features/issues/discussion_lock_spec.rb @@ -9,6 +9,7 @@ RSpec.describe 'Discussion Lock', :js, feature_category: :team_planning do before do sign_in(user) + stub_feature_flags(moved_mr_sidebar: false) end context 'when a user is a team member' do diff --git a/spec/features/issues/gfm_autocomplete_spec.rb b/spec/features/issues/gfm_autocomplete_spec.rb index 2bd5373b715..665c7307231 100644 --- a/spec/features/issues/gfm_autocomplete_spec.rb +++ b/spec/features/issues/gfm_autocomplete_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do + include CookieHelper + let_it_be(:user) { create(:user, name: '💃speciąl someone💃', username: 'someone.special') } let_it_be(:user2) { create(:user, name: 'Marge Simpson', username: 'msimpson') } @@ -45,6 +47,7 @@ RSpec.describe 'GFM autocomplete', :js, feature_category: :team_planning do before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue_to_edit) wait_for_requests diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index d5f90bb9260..29a61d584ee 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -98,6 +98,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do project.add_developer(user_to_be_deleted) sign_in(user_to_be_deleted) + stub_feature_flags(moved_mr_sidebar: false) visit project_issue_path(project, issue) wait_for_requests @@ -129,7 +130,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do describe 'when an issue `issue_type` is edited' do before do sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) wait_for_requests end @@ -163,7 +164,7 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do describe 'when an incident `issue_type` is edited' do before do sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, incident) wait_for_requests end diff --git a/spec/features/issues/issue_sidebar_spec.rb b/spec/features/issues/issue_sidebar_spec.rb index 2ae347d4f9e..ee71181fba2 100644 --- a/spec/features/issues/issue_sidebar_spec.rb +++ b/spec/features/issues/issue_sidebar_spec.rb @@ -5,6 +5,7 @@ require 'spec_helper' RSpec.describe 'Issue Sidebar', feature_category: :team_planning do include MobileHelpers include Features::InviteMembersModalHelpers + include CookieHelper let_it_be(:group) { create(:group, :nested) } let_it_be(:project) { create(:project, :public, namespace: group) } @@ -20,6 +21,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'when signed in' do before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end context 'when concerning the assignee', :js do @@ -205,6 +207,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'as an allowed user' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_developer(user) visit_issue(project, issue) end @@ -293,6 +296,7 @@ RSpec.describe 'Issue Sidebar', feature_category: :team_planning do context 'as a guest' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_guest(user) visit_issue(project, issue) end diff --git a/spec/features/issues/user_edits_issue_spec.rb b/spec/features/issues/user_edits_issue_spec.rb index c6cedbc83cd..4ef58918a2b 100644 --- a/spec/features/issues/user_edits_issue_spec.rb +++ b/spec/features/issues/user_edits_issue_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" RSpec.describe "Issues > User edits issue", :js, feature_category: :team_planning do + include CookieHelper + let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:project_with_milestones) { create(:project_empty_repo, :public) } let_it_be(:user) { create(:user) } @@ -18,6 +20,7 @@ RSpec.describe "Issues > User edits issue", :js, feature_category: :team_plannin project.add_developer(user) project_with_milestones.add_developer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end context "from edit page" do diff --git a/spec/features/issues/user_toggles_subscription_spec.rb b/spec/features/issues/user_toggles_subscription_spec.rb index 904fafdf56a..00b04c10d33 100644 --- a/spec/features/issues/user_toggles_subscription_spec.rb +++ b/spec/features/issues/user_toggles_subscription_spec.rb @@ -10,6 +10,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is not logged in' do before do + stub_feature_flags(moved_mr_sidebar: false) visit(project_issue_path(project, issue)) end @@ -20,9 +21,9 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is logged in' do before do + stub_feature_flags(moved_mr_sidebar: false) project.add_developer(user) sign_in(user) - visit(project_issue_path(project, issue)) end @@ -52,6 +53,7 @@ RSpec.describe "User toggles subscription", :js, feature_category: :team_plannin context 'user is logged in without edit permission' do before do + stub_feature_flags(moved_mr_sidebar: false) sign_in(user2) visit(project_issue_path(project, issue)) diff --git a/spec/features/merge_request/user_manages_subscription_spec.rb b/spec/features/merge_request/user_manages_subscription_spec.rb index d4ccc4a93b5..3bcc8255ab7 100644 --- a/spec/features/merge_request/user_manages_subscription_spec.rb +++ b/spec/features/merge_request/user_manages_subscription_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'User manages subscription', :js, feature_category: :code_review_workflow do + include CookieHelper + let(:project) { create(:project, :public, :repository) } let(:merge_request) { create(:merge_request, source_project: project, target_project: project) } let(:user) { create(:user) } @@ -10,7 +12,7 @@ RSpec.describe 'User manages subscription', :js, feature_category: :code_review_ before do stub_feature_flags(moved_mr_sidebar: moved_mr_sidebar_enabled) - + set_cookie('new-actions-popover-viewed', 'true') project.add_maintainer(user) sign_in(user) diff --git a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb index 7cb1c95f6dc..601310cbacf 100644 --- a/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb +++ b/spec/features/merge_request/user_opens_checkout_branch_modal_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_category: :code_review_workflow do include ProjectForksHelper + include CookieHelper let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } @@ -11,6 +12,7 @@ RSpec.describe 'Merge request > User opens checkout branch modal', :js, feature_ before do project.add_maintainer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end describe 'for fork' do diff --git a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb index ad2ceeb23e2..21c62b0d0d8 100644 --- a/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb +++ b/spec/features/merge_request/user_sees_check_out_branch_modal_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_category: :code_review_workflow do + include CookieHelper + let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } let(:merge_request) { create(:merge_request, source_project: project) } @@ -10,6 +12,7 @@ RSpec.describe 'Merge request > User sees check out branch modal', :js, feature_ before do sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') visit project_merge_request_path(project, merge_request) wait_for_requests diff --git a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb index 0de59ea21c5..dae28cbb05c 100644 --- a/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb +++ b/spec/features/merge_request/user_selects_branches_for_new_mr_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_category: :code_review_workflow do include ListboxHelpers + include CookieHelper let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } @@ -17,6 +18,7 @@ RSpec.describe 'Merge request > User selects branches for new MR', :js, feature_ before do project.add_maintainer(user) sign_in(user) + set_cookie('new-actions-popover-viewed', 'true') end it 'selects the source branch sha when a tag with the same name exists' do diff --git a/spec/features/profiles/user_uses_comment_template_spec.rb b/spec/features/profiles/user_uses_comment_template_spec.rb index b426e3fb433..704d02e94f4 100644 --- a/spec/features/profiles/user_uses_comment_template_spec.rb +++ b/spec/features/profiles/user_uses_comment_template_spec.rb @@ -18,7 +18,7 @@ RSpec.describe 'User uses comment template', :js, it 'applies comment template' do visit project_merge_request_path(merge_request.project, merge_request) - find('[data-testid="comment-template-dropdown-toggle"]').click + find('.js-comment-template-toggle').click wait_for_requests diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb index adf410ce6e8..77f88994bfb 100644 --- a/spec/features/projects/issuable_templates_spec.rb +++ b/spec/features/projects/issuable_templates_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe 'issuable templates', :js, feature_category: :projects do include ProjectForksHelper + include CookieHelper let(:user) { create(:user) } let(:project) { create(:project, :public, :repository) } @@ -12,6 +13,7 @@ RSpec.describe 'issuable templates', :js, feature_category: :projects do before do project.add_maintainer(user) sign_in user + set_cookie('new-actions-popover-viewed', 'true') end context 'user creates an issue using templates' do diff --git a/spec/features/reportable_note/issue_spec.rb b/spec/features/reportable_note/issue_spec.rb index 55e7f5897bc..a18cdf27294 100644 --- a/spec/features/reportable_note/issue_spec.rb +++ b/spec/features/reportable_note/issue_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning do + include CookieHelper + let(:user) { create(:user) } let(:project) { create(:project) } let(:issue) { create(:issue, project: project) } @@ -11,7 +13,7 @@ RSpec.describe 'Reportable note on issue', :js, feature_category: :team_planning before do project.add_maintainer(user) sign_in(user) - + set_cookie('new-actions-popover-viewed', 'true') visit project_issue_path(project, issue) end diff --git a/spec/finders/packages/conan/package_finder_spec.rb b/spec/finders/packages/conan/package_finder_spec.rb index f25a62225a8..787cb256486 100644 --- a/spec/finders/packages/conan/package_finder_spec.rb +++ b/spec/finders/packages/conan/package_finder_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true require 'spec_helper' -RSpec.describe ::Packages::Conan::PackageFinder do +RSpec.describe ::Packages::Conan::PackageFinder, feature_category: :package_registry do using RSpec::Parameterized::TableSyntax let_it_be_with_reload(:project) { create(:project) } @@ -15,7 +15,8 @@ RSpec.describe ::Packages::Conan::PackageFinder do describe '#execute' do let(:query) { "#{conan_package.name.split('/').first[0, 3]}%" } - let(:finder) { described_class.new(user, query: query) } + let(:finder) { described_class.new(user, params) } + let(:params) { { query: query } } subject { finder.execute } @@ -40,7 +41,7 @@ RSpec.describe ::Packages::Conan::PackageFinder do end with_them do - let(:expected_packages) { packages_visible ? [conan_package, conan_package2] : [] } + let(:expected_packages) { packages_visible ? [conan_package2, conan_package] : [] } let(:user) { role == :anonymous ? nil : super() } before do @@ -50,5 +51,23 @@ RSpec.describe ::Packages::Conan::PackageFinder do it { is_expected.to eq(expected_packages) } end + + context 'with project' do + subject { described_class.new(user, params, project: project).execute } + + it { is_expected.to match_array([conan_package2, conan_package]) } + + it 'respects the limit' do + stub_const("#{described_class}::MAX_PACKAGES_COUNT", 1) + + expect(subject).to match_array([conan_package2]) + end + + context 'with a different project' do + let_it_be(:project) { private_project } + + it { is_expected.to match_array([private_package]) } + end + end end end diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js index e92493315f7..5daa21fd618 100644 --- a/spec/frontend/group_settings/components/shared_runners_form_spec.js +++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js @@ -2,12 +2,17 @@ import { GlAlert } from '@gitlab/ui'; import { nextTick } from 'vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { updateGroup } from '~/api/groups_api'; +import SharedRunnersForm from '~/group_settings/components/shared_runners_form.vue'; +import { I18N_CONFIRM_MESSAGE } from '~/group_settings/constants'; + jest.mock('~/api/groups_api'); +jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'); const GROUP_ID = '99'; +const GROUP_NAME = 'My group'; const RUNNER_ENABLED_VALUE = 'enabled'; const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable'; const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_and_overridable'; @@ -19,6 +24,8 @@ describe('group_settings/components/shared_runners_form', () => { wrapper = shallowMountExtended(SharedRunnersForm, { provide: { groupId: GROUP_ID, + groupName: GROUP_NAME, + groupIsEmpty: false, sharedRunnersSetting: RUNNER_ENABLED_VALUE, parentSharedRunnersSetting: null, runnerEnabledValue: RUNNER_ENABLED_VALUE, @@ -41,10 +48,12 @@ describe('group_settings/components/shared_runners_form', () => { }; beforeEach(() => { + confirmAction.mockResolvedValue(true); updateGroup.mockResolvedValue({}); }); afterEach(() => { + confirmAction.mockReset(); updateGroup.mockReset(); }); @@ -110,8 +119,9 @@ describe('group_settings/components/shared_runners_form', () => { it('does not update settings while loading', async () => { findSharedRunnersToggle().vm.$emit('change', true); + await nextTick(); findSharedRunnersToggle().vm.$emit('change', false); - await waitForPromises(); + await nextTick(); expect(updateGroup).toHaveBeenCalledTimes(1); }); @@ -134,6 +144,8 @@ describe('group_settings/components/shared_runners_form', () => { findSharedRunnersToggle().vm.$emit('change', true); await waitForPromises(); + expect(confirmAction).not.toHaveBeenCalled(); + expect(getSharedRunnersSetting()).toEqual(RUNNER_ENABLED_VALUE); expect(findOverrideToggle().props('disabled')).toBe(true); }); @@ -142,17 +154,59 @@ describe('group_settings/components/shared_runners_form', () => { findSharedRunnersToggle().vm.$emit('change', false); await waitForPromises(); + expect(confirmAction).toHaveBeenCalledTimes(1); + expect(confirmAction).toHaveBeenCalledWith( + I18N_CONFIRM_MESSAGE, + expect.objectContaining({ + title: expect.stringContaining(GROUP_NAME), + }), + ); + expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE); expect(findOverrideToggle().props('disabled')).toBe(false); }); + + describe('when user cancels operation', () => { + beforeEach(() => { + confirmAction.mockResolvedValue(false); + }); + + it('sends no payload when turned off', async () => { + findSharedRunnersToggle().vm.$emit('change', false); + await waitForPromises(); + + expect(confirmAction).toHaveBeenCalledTimes(1); + expect(confirmAction).toHaveBeenCalledWith( + I18N_CONFIRM_MESSAGE, + expect.objectContaining({ + title: expect.stringContaining(GROUP_NAME), + }), + ); + + expect(updateGroup).not.toHaveBeenCalled(); + expect(findOverrideToggle().props('disabled')).toBe(true); + }); + }); + + describe('when group is empty', () => { + beforeEach(() => { + createComponent({ groupIsEmpty: true }); + }); + + it('confirmation is not shown when turned off', async () => { + findSharedRunnersToggle().vm.$emit('change', false); + await waitForPromises(); + + expect(confirmAction).not.toHaveBeenCalled(); + expect(getSharedRunnersSetting()).toEqual(RUNNER_DISABLED_VALUE); + }); + }); }); describe('"Override the group setting" toggle', () => { - beforeEach(() => { + it('enabling the override toggle sends correct payload', async () => { createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE }); - }); - it('enabling the override toggle sends correct payload', async () => { findOverrideToggle().vm.$emit('change', true); await waitForPromises(); @@ -160,6 +214,8 @@ describe('group_settings/components/shared_runners_form', () => { }); it('disabling the override toggle sends correct payload', async () => { + createComponent({ sharedRunnersSetting: RUNNER_ALLOW_OVERRIDE_VALUE }); + findOverrideToggle().vm.$emit('change', false); await waitForPromises(); diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js index db3435855f6..a5ba512434c 100644 --- a/spec/frontend/issues/show/components/header_actions_spec.js +++ b/spec/frontend/issues/show/components/header_actions_spec.js @@ -2,6 +2,8 @@ import Vue, { nextTick } from 'vue'; import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex'; +import VueApollo from 'vue-apollo'; +import waitForPromises from 'helpers/wait_for_promises'; import { mockTracking } from 'helpers/tracking_helper'; import { createAlert, VARIANT_SUCCESS } from '~/alert'; import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants'; @@ -14,17 +16,22 @@ import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutatio import * as urlUtility from '~/lib/utils/url_utility'; import eventHub from '~/notes/event_hub'; import createStore from '~/notes/stores'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql'; +import updateIssueMutation from '~/issues/show/queries/update_issue.mutation.graphql'; +import toast from '~/vue_shared/plugins/global_toast'; jest.mock('~/alert'); jest.mock('~/issues/show/event_hub', () => ({ $emit: jest.fn() })); +jest.mock('~/vue_shared/plugins/global_toast'); describe('HeaderActions component', () => { let dispatchEventSpy; - let mutateMock; let wrapper; let visitUrlSpy; Vue.use(Vuex); + Vue.use(VueApollo); const store = createStore(); @@ -45,15 +52,28 @@ describe('HeaderActions component', () => { reportedUserId: 1, reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32', submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam', + issuableEmailAddress: null, + fullPath: 'full-path', }; - const updateIssueMutationResponse = { data: { updateIssue: { errors: [] } } }; + const updateIssueMutationResponse = { + data: { + updateIssue: { + errors: [], + issuable: { + id: 'gid://gitlab/Issue/511', + state: STATUS_OPEN, + }, + }, + }, + }; const promoteToEpicMutationResponse = { data: { promoteToEpic: { errors: [], epic: { + id: 'gid://gitlab/Epic/1', webPath: '/groups/gitlab-org/-/epics/1', }, }, @@ -69,6 +89,20 @@ describe('HeaderActions component', () => { }, }; + const mockIssueReferenceData = { + data: { + workspace: { + id: 'gid://gitlab/Project/7', + issuable: { + id: 'gid://gitlab/Issue/511', + reference: 'flightjs/Flight#33', + __typename: 'Issue', + }, + __typename: 'Project', + }, + }, + }; + const findToggleIssueStateButton = () => wrapper.find(`[data-testid="toggle-button"]`); const findEditButton = () => wrapper.find(`[data-testid="edit-button"]`); @@ -77,33 +111,54 @@ describe('HeaderActions component', () => { const findDesktopDropdown = () => findDropdownBy('desktop-dropdown'); const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem); const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem); + const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); + const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`); + const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`); + const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`); + const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`); + const findCopyEmailItem = () => wrapper.find(`[data-testid="copy-email"]`); const findModal = () => wrapper.findComponent(GlModal); const findModalLinkAt = (index) => findModal().findAllComponents(GlLink).at(index); + const issueReferenceSuccessHandler = jest.fn().mockResolvedValue(mockIssueReferenceData); + const updateIssueMutationResponseHandler = jest + .fn() + .mockResolvedValue(updateIssueMutationResponse); + const promoteToEpicMutationSuccessResponseHandler = jest + .fn() + .mockResolvedValue(promoteToEpicMutationResponse); + const promoteToEpicMutationErrorHandler = jest + .fn() + .mockResolvedValue(promoteToEpicMutationErrorResponse); + const mountComponent = ({ props = {}, issueState = STATUS_OPEN, blockedByIssues = [], - mutateResponse = {}, + movedMrSidebarEnabled = false, + promoteToEpicHandler = promoteToEpicMutationSuccessResponseHandler, } = {}) => { - mutateMock = jest.fn().mockResolvedValue(mutateResponse); - store.dispatch('setNoteableData', { blocked_by_issues: blockedByIssues, state: issueState, }); + const handlers = [ + [issueReferenceQuery, issueReferenceSuccessHandler], + [updateIssueMutation, updateIssueMutationResponseHandler], + [promoteToEpicMutation, promoteToEpicHandler], + ]; + return shallowMount(HeaderActions, { + apolloProvider: createMockApollo(handlers), store, provide: { ...defaultProps, ...props, - }, - mocks: { - $apollo: { - mutate: mutateMock, + glFeatures: { + movedMrSidebar: movedMrSidebarEnabled, }, }, stubs: { @@ -138,7 +193,6 @@ describe('HeaderActions component', () => { wrapper = mountComponent({ props: { issueType }, issueState, - mutateResponse: updateIssueMutationResponse, }); }); @@ -149,23 +203,19 @@ describe('HeaderActions component', () => { it('calls apollo mutation', () => { findToggleIssueStateButton().vm.$emit('click'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - stateEvent: newIssueState, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + stateEvent: newIssueState, + }, + }); }); it('dispatches a custom event to update the issue page', async () => { findToggleIssueStateButton().vm.$emit('click'); - await nextTick(); + await waitForPromises(); expect(dispatchEventSpy).toHaveBeenCalledTimes(1); }); @@ -290,28 +340,25 @@ describe('HeaderActions component', () => { describe('when "Promote to epic" button is clicked', () => { describe('when response is successful', () => { - beforeEach(() => { + beforeEach(async () => { visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); wrapper = mountComponent({ - mutateResponse: promoteToEpicMutationResponse, + promoteToEpicHandler: promoteToEpicMutationSuccessResponseHandler, }); wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + + await waitForPromises(); }); it('invokes GraphQL mutation when clicked', () => { - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - mutation: promoteToEpicMutation, - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - }, - }, - }), - ); + expect(promoteToEpicMutationSuccessResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + }, + }); }); it('shows a success message and tells the user they are being redirected', () => { @@ -329,14 +376,16 @@ describe('HeaderActions component', () => { }); describe('when response contains errors', () => { - beforeEach(() => { + beforeEach(async () => { visitUrlSpy = jest.spyOn(urlUtility, 'visitUrl').mockReturnValue({}); wrapper = mountComponent({ - mutateResponse: promoteToEpicMutationErrorResponse, + promoteToEpicHandler: promoteToEpicMutationErrorHandler, }); wrapper.find('[data-testid="promote-button"]').vm.$emit('click'); + + await waitForPromises(); }); it('shows an error message', () => { @@ -349,21 +398,17 @@ describe('HeaderActions component', () => { describe('when `toggle.issuable.state` event is emitted', () => { it('invokes a method to toggle the issue state', () => { - wrapper = mountComponent({ mutateResponse: updateIssueMutationResponse }); + wrapper = mountComponent(); eventHub.$emit('toggle.issuable.state'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid, - projectPath: defaultProps.projectPath, - stateEvent: ISSUE_STATE_EVENT_CLOSE, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid, + projectPath: defaultProps.projectPath, + stateEvent: ISSUE_STATE_EVENT_CLOSE, + }, + }); }); }); @@ -392,17 +437,13 @@ describe('HeaderActions component', () => { it('calls apollo mutation when primary button is clicked', () => { findModal().vm.$emit('primary'); - expect(mutateMock).toHaveBeenCalledWith( - expect.objectContaining({ - variables: { - input: { - iid: defaultProps.iid.toString(), - projectPath: defaultProps.projectPath, - stateEvent: ISSUE_STATE_EVENT_CLOSE, - }, - }, - }), - ); + expect(updateIssueMutationResponseHandler).toHaveBeenCalledWith({ + input: { + iid: defaultProps.iid.toString(), + projectPath: defaultProps.projectPath, + stateEvent: ISSUE_STATE_EVENT_CLOSE, + }, + }); }); describe.each` @@ -434,8 +475,6 @@ describe('HeaderActions component', () => { }); describe('abuse category selector', () => { - const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector); - beforeEach(() => { wrapper = mountComponent({ props: { isIssueAuthor: false } }); }); @@ -445,7 +484,7 @@ describe('HeaderActions component', () => { }); it('opens the drawer', async () => { - findDesktopDropdownItems().at(2).vm.$emit('click'); + findReportAbuseSelectorItem().vm.$emit('click'); await nextTick(); @@ -453,10 +492,160 @@ describe('HeaderActions component', () => { }); it('closes the drawer', async () => { - await findDesktopDropdownItems().at(2).vm.$emit('click'); + await findReportAbuseSelectorItem().vm.$emit('click'); await findAbuseCategorySelector().vm.$emit('close-drawer'); expect(findAbuseCategorySelector().exists()).toEqual(false); }); }); + + describe('notification toggle', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${true} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`, + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Notification toggle`, () => { + expect(findNotificationWidget().exists()).toBe(visible); + }); + }, + ); + }); + }); + + describe('lock issue option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${false} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + `when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" with issue type "$issueType"`, + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Lock issue option`, () => { + expect(findLockIssueWidget().exists()).toBe(visible); + }); + }, + ); + }); + }); + + describe('copy reference option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | visible + ${true} | ${TYPE_ISSUE} | ${true} + ${true} | ${TYPE_INCIDENT} | ${true} + ${false} | ${TYPE_ISSUE} | ${false} + ${false} | ${TYPE_INCIDENT} | ${false} + `( + 'when movedMrSidebarFlagEnabled is "$movedMrSidebarEnabled" with issue type "$issueType"', + ({ movedMrSidebarEnabled, issueType, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Copy reference option`, () => { + expect(findCopyRefenceDropdownItem().exists()).toBe(visible); + }); + }, + ); + }); + + describe('clicking when visible', () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType: TYPE_ISSUE, + }, + movedMrSidebarEnabled: true, + }); + }); + + it('shows toast message', () => { + findCopyRefenceDropdownItem().vm.$emit('click'); + + expect(toast).toHaveBeenCalledWith('Reference copied'); + }); + }); + }); + + describe('copy email option', () => { + describe('visibility', () => { + describe.each` + movedMrSidebarEnabled | issueType | issuableEmailAddress | visible + ${true} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${true} + ${true} | ${TYPE_ISSUE} | ${''} | ${false} + ${true} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${true} + ${true} | ${TYPE_INCIDENT} | ${''} | ${false} + ${false} | ${TYPE_ISSUE} | ${'mock-email-address'} | ${false} + ${false} | ${TYPE_INCIDENT} | ${'mock-email-address'} | ${false} + `( + 'when movedMrSidebarEnabled flag is "$movedMrSidebarEnabled" issue type is "$issueType" and issuableEmailAddress="$issuableEmailAddress"', + ({ movedMrSidebarEnabled, issueType, issuableEmailAddress, visible }) => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType, + issuableEmailAddress, + }, + movedMrSidebarEnabled, + }); + }); + + it(`${visible ? 'shows' : 'hides'} Copy email option`, () => { + expect(findCopyEmailItem().exists()).toBe(visible); + }); + }, + ); + }); + + describe('clicking when visible', () => { + beforeEach(() => { + wrapper = mountComponent({ + props: { + issueType: TYPE_ISSUE, + issuableEmailAddress: 'mock-email-address', + }, + movedMrSidebarEnabled: true, + }); + }); + + it('shows toast message', () => { + findCopyEmailItem().vm.$emit('click'); + + expect(toast).toHaveBeenCalledWith('Email address copied'); + }); + }); + }); }); diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js new file mode 100644 index 00000000000..bf3e81c7d3a --- /dev/null +++ b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js @@ -0,0 +1,77 @@ +import { GlPopover } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue'; +import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants'; +import { TYPE_ISSUE } from '~/issues/constants'; +import * as utils from '~/lib/utils/common_utils'; + +describe('NewHeaderActionsPopover', () => { + let wrapper; + + const createComponent = ({ issueType = TYPE_ISSUE, movedMrSidebarEnabled = true }) => { + wrapper = shallowMountExtended(NewHeaderActionsPopover, { + propsData: { + issueType, + }, + stubs: { + GlPopover, + }, + provide: { + glFeatures: { + movedMrSidebar: movedMrSidebarEnabled, + }, + }, + }); + }; + + const findPopover = () => wrapper.findComponent(GlPopover); + const findConfirmButton = () => wrapper.findByTestId('confirm-button'); + + it('should not be visible when the feature flag :moved_mr_sidebar is disabled', () => { + createComponent({ movedMrSidebarEnabled: false }); + expect(findPopover().exists()).toBe(false); + }); + + describe('without the popover cookie', () => { + beforeEach(() => { + utils.setCookie = jest.fn(); + + createComponent({}); + }); + + it('renders the popover with correct text', () => { + expect(findPopover().exists()).toBe(true); + expect(findPopover().text()).toContain('issue actions'); + }); + + it('does not call setCookie', () => { + expect(utils.setCookie).not.toHaveBeenCalled(); + }); + + describe('when the confirm button is clicked', () => { + beforeEach(() => { + findConfirmButton().vm.$emit('click'); + }); + + it('sets the popover cookie', () => { + expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true); + }); + + it('hides the popover', () => { + expect(findPopover().exists()).toBe(false); + }); + }); + }); + + describe('with the popover cookie', () => { + beforeEach(() => { + jest.spyOn(utils, 'getCookie').mockReturnValue('true'); + + createComponent({}); + }); + + it('does not render the popover', () => { + expect(findPopover().exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js index 9b790e739fb..fab5a7b8844 100644 --- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_action_spec.js @@ -66,6 +66,7 @@ describe('confirmAction', () => { modalHtmlMessage: '<strong>Hello</strong>', title: 'title', hideCancel: true, + size: 'md', }; await renderRootComponent('', options); expect(modal.props()).toEqual( @@ -79,6 +80,7 @@ describe('confirmAction', () => { modalHtmlMessage: options.modalHtmlMessage, title: options.title, hideCancel: options.hideCancel, + size: 'md', }), ); }); diff --git a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js index c135180c9df..9dcb850076c 100644 --- a/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js +++ b/spec/frontend/lib/utils/confirm_via_gl_modal/confirm_modal_spec.js @@ -14,6 +14,7 @@ describe('Confirm Modal', () => { secondaryText, secondaryVariant, title, + size, hideCancel = false, } = {}) => { wrapper = mount(ConfirmModal, { @@ -24,6 +25,7 @@ describe('Confirm Modal', () => { secondaryVariant, hideCancel, title, + size, }, }); }; @@ -91,5 +93,17 @@ describe('Confirm Modal', () => { expect(findGlModal().props().title).toBe(title); }); + + it('should set modal size to `sm` by default', () => { + createComponent(); + + expect(findGlModal().props('size')).toBe('sm'); + }); + + it('should set modal size when `size` prop is set', () => { + createComponent({ size: 'md' }); + + expect(findGlModal().props('size')).toBe('md'); + }); }); }); diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js index d26ef7298ce..5e766e9a41c 100644 --- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js +++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js @@ -29,6 +29,7 @@ describe('IssuableLockForm', () => { const findEditForm = () => wrapper.findComponent(EditForm); const findSidebarLockStatusTooltip = () => getBinding(findSidebarCollapseIcon().element, 'gl-tooltip'); + const findIssuableLockClickable = () => wrapper.find('[data-testid="issuable-lock"]'); const initStore = (isLocked) => { if (issuableType === ISSUABLE_TYPE_ISSUE) { @@ -48,7 +49,7 @@ describe('IssuableLockForm', () => { store.getters.getNoteableData.discussion_locked = isLocked; }; - const createComponent = ({ props = {} }, movedMrSidebar = false) => { + const createComponent = ({ props = {}, movedMrSidebar = false }) => { wrapper = shallowMount(IssuableLockForm, { store, provide: { @@ -169,11 +170,27 @@ describe('IssuableLockForm', () => { `('displays $message when merge request is $locked', async ({ locked, message }) => { initStore(locked); - createComponent({}, true); + createComponent({ movedMrSidebar: true }); await wrapper.find('.dropdown-item').trigger('click'); expect(toast).toHaveBeenCalledWith(message); }); }); + + describe('moved_mr_sidebar flag', () => { + describe('when the flag is off', () => { + it('does not show the non editable lock status', () => { + createComponent({ movedMrSidebar: false }); + expect(findIssuableLockClickable().exists()).toBe(false); + }); + }); + + describe('when the flag is on', () => { + it('does not show the non editable lock status', () => { + createComponent({ movedMrSidebar: true }); + expect(findIssuableLockClickable().exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js index bfa9b7dd706..aea25abb324 100644 --- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js +++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js @@ -49,7 +49,7 @@ describe('Comment templates dropdown', () => { const mockApollo = createMockApolloProvider(savedRepliesResponse); wrapper = createComponent({ mockApollo }); - wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click'); + wrapper.find('.js-comment-template-toggle').trigger('click'); await waitForPromises(); @@ -60,7 +60,7 @@ describe('Comment templates dropdown', () => { const mockApollo = createMockApolloProvider(savedRepliesResponse); wrapper = createComponent({ mockApollo }); - wrapper.findByTestId('comment-template-dropdown-toggle').trigger('click'); + wrapper.find('.js-comment-template-toggle').trigger('click'); await waitForPromises(); diff --git a/spec/helpers/ci/runners_helper_spec.rb b/spec/helpers/ci/runners_helper_spec.rb index a01002a2a1e..ab3f0b193ac 100644 --- a/spec/helpers/ci/runners_helper_spec.rb +++ b/spec/helpers/ci/runners_helper_spec.rb @@ -106,6 +106,8 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do describe '#group_shared_runners_settings_data' do let_it_be(:parent) { create(:group) } let_it_be(:group) { create(:group, parent: parent, shared_runners_enabled: false) } + let_it_be(:group_with_project) { create(:group, parent: parent) } + let_it_be(:project) { create(:project, group: group_with_project) } let(:runner_constants) do { @@ -118,6 +120,8 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do it 'returns group data for top level group' do result = { group_id: parent.id, + group_name: parent.name, + group_is_empty: 'false', shared_runners_setting: Namespace::SR_ENABLED, parent_shared_runners_setting: nil }.merge(runner_constants) @@ -128,12 +132,26 @@ RSpec.describe Ci::RunnersHelper, feature_category: :runner_fleet do it 'returns group data for child group' do result = { group_id: group.id, + group_name: group.name, + group_is_empty: 'true', shared_runners_setting: Namespace::SR_DISABLED_AND_UNOVERRIDABLE, parent_shared_runners_setting: Namespace::SR_ENABLED }.merge(runner_constants) expect(helper.group_shared_runners_settings_data(group)).to eq result end + + it 'returns group data for child group with project' do + result = { + group_id: group_with_project.id, + group_name: group_with_project.name, + group_is_empty: 'false', + shared_runners_setting: Namespace::SR_ENABLED, + parent_shared_runners_setting: Namespace::SR_ENABLED + }.merge(runner_constants) + + expect(helper.group_shared_runners_settings_data(group_with_project)).to eq result + end end describe '#group_runners_data_attributes' do diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb index d940c696fb3..38cbb5a1d66 100644 --- a/spec/helpers/issues_helper_spec.rb +++ b/spec/helpers/issues_helper_spec.rb @@ -3,6 +3,8 @@ require 'spec_helper' RSpec.describe IssuesHelper do + include Features::MergeRequestHelpers + let_it_be(:project) { create(:project) } let_it_be_with_reload(:issue) { create(:issue, project: project) } @@ -235,10 +237,13 @@ RSpec.describe IssuesHelper do describe '#issue_header_actions_data' do let(:current_user) { create(:user) } + let(:merge_request) { create(:merge_request, :opened, source_project: project, author: current_user) } + let(:issuable_sidebar_issue) { serialize_issuable_sidebar(current_user, project, merge_request) } before do allow(helper).to receive(:current_user).and_return(current_user) allow(helper).to receive(:can?).and_return(true) + allow(helper).to receive(:issuable_sidebar).and_return(issuable_sidebar_issue) end it 'returns expected result' do @@ -257,10 +262,11 @@ RSpec.describe IssuesHelper do report_abuse_path: add_category_abuse_reports_path, reported_user_id: issue.author.id, reported_from_url: issue_url(issue), - submit_as_spam_path: mark_as_spam_project_issue_path(project, issue) + submit_as_spam_path: mark_as_spam_project_issue_path(project, issue), + issuable_email_address: issuable_sidebar_issue[:create_note_email] } - expect(helper.issue_header_actions_data(project, issue, current_user)).to include(expected) + expect(helper.issue_header_actions_data(project, issue, current_user, issuable_sidebar_issue)).to include(expected) end end diff --git a/spec/helpers/sessions_helper_spec.rb b/spec/helpers/sessions_helper_spec.rb index c7b8225b866..b8290fa2337 100644 --- a/spec/helpers/sessions_helper_spec.rb +++ b/spec/helpers/sessions_helper_spec.rb @@ -52,7 +52,7 @@ RSpec.describe SessionsHelper do end describe '#send_rate_limited?' do - let_it_be(:user) { build(:user) } + let(:user) { build_stubbed(:user) } subject { helper.send_rate_limited?(user) } @@ -77,30 +77,14 @@ RSpec.describe SessionsHelper do end describe '#obfuscated_email' do - subject { helper.obfuscated_email(email) } - - context 'when an email address is normal length' do - let(:email) { 'alex@gitlab.com' } - - it { is_expected.to eq('al**@g*****.com') } - end - - context 'when an email address contains multiple top level domains' do - let(:email) { 'alex@gl.co.uk' } + let(:email) { 'mail@example.com' } - it { is_expected.to eq('al**@g****.uk') } - end - - context 'when an email address is very short' do - let(:email) { 'a@b.c' } - - it { is_expected.to eq('a@b.c') } - end + subject { helper.obfuscated_email(email) } - context 'when an email address is even shorter' do - let(:email) { 'a@b' } + it 'delegates to Gitlab::Utils::Email.obfuscated_email' do + expect(Gitlab::Utils::Email).to receive(:obfuscated_email).with(email).and_call_original - it { is_expected.to eq('a@b') } + expect(subject).to eq('ma**@e******.com') end end end diff --git a/spec/lib/gitlab/avatar_cache_spec.rb b/spec/lib/gitlab/avatar_cache_spec.rb index ffe6f81b6e7..a57d811edaf 100644 --- a/spec/lib/gitlab/avatar_cache_spec.rb +++ b/spec/lib/gitlab/avatar_cache_spec.rb @@ -62,40 +62,52 @@ RSpec.describe Gitlab::AvatarCache, :clean_gitlab_redis_cache do end describe "#delete_by_email" do - subject { described_class.delete_by_email(*emails) } + shared_examples 'delete emails' do + subject { described_class.delete_by_email(*emails) } - before do - perform_fetch - end + before do + perform_fetch + end - context "no emails, somehow" do - let(:emails) { [] } + context "no emails, somehow" do + let(:emails) { [] } - it { is_expected.to eq(0) } - end + it { is_expected.to eq(0) } + end - context "single email" do - let(:emails) { "foo@bar.com" } + context "single email" do + let(:emails) { "foo@bar.com" } - it "removes the email" do - expect(read(key, "20:2:true")).to eq(avatar_path) + it "removes the email" do + expect(read(key, "20:2:true")).to eq(avatar_path) - expect(subject).to eq(1) + expect(subject).to eq(1) - expect(read(key, "20:2:true")).to eq(nil) + expect(read(key, "20:2:true")).to eq(nil) + end end - end - context "multiple emails" do - let(:emails) { ["foo@bar.com", "missing@baz.com"] } + context "multiple emails" do + let(:emails) { ["foo@bar.com", "missing@baz.com"] } - it "removes the emails it finds" do - expect(read(key, "20:2:true")).to eq(avatar_path) + it "removes the emails it finds" do + expect(read(key, "20:2:true")).to eq(avatar_path) - expect(subject).to eq(1) + expect(subject).to eq(1) - expect(read(key, "20:2:true")).to eq(nil) + expect(read(key, "20:2:true")).to eq(nil) + end + end + end + + context 'when feature flag disabled' do + before do + stub_feature_flags(use_pipeline_over_multikey: false) end + + it_behaves_like 'delete emails' end + + it_behaves_like 'delete emails' end end diff --git a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb index 9ccae754a92..82bba31193b 100644 --- a/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_outside_transaction_spec.rb @@ -61,12 +61,12 @@ RSpec.describe Gitlab::Database::WithLockRetriesOutsideTransaction, feature_cate context 'lock_fiber' do it 'acquires lock successfully' do - check_exclusive_lock_query = """ + check_exclusive_lock_query = <<~QUERY SELECT 1 FROM pg_locks l JOIN pg_class t ON l.relation = t.oid WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}' - """ + QUERY expect(connection.execute(check_exclusive_lock_query).to_a).to be_present end diff --git a/spec/lib/gitlab/database/with_lock_retries_spec.rb b/spec/lib/gitlab/database/with_lock_retries_spec.rb index 7fe6362634b..7e0435c815b 100644 --- a/spec/lib/gitlab/database/with_lock_retries_spec.rb +++ b/spec/lib/gitlab/database/with_lock_retries_spec.rb @@ -61,12 +61,12 @@ RSpec.describe Gitlab::Database::WithLockRetries, feature_category: :database do context 'lock_fiber' do it 'acquires lock successfully' do - check_exclusive_lock_query = """ + check_exclusive_lock_query = <<~QUERY SELECT 1 FROM pg_locks l JOIN pg_class t ON l.relation = t.oid WHERE t.relkind = 'r' AND l.mode = 'ExclusiveLock' AND t.relname = '#{Project.table_name}' - """ + QUERY expect(connection.execute(check_exclusive_lock_query).to_a).to be_present end diff --git a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb index 30981e4bd7d..0dc0f50b104 100644 --- a/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb +++ b/spec/lib/gitlab/discussions_diff/highlight_cache_spec.rb @@ -41,57 +41,81 @@ RSpec.describe Gitlab::DiscussionsDiff::HighlightCache, :clean_gitlab_redis_cach end describe '#read_multiple' do - it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do - described_class.write_multiple(mapping) + shared_examples 'read multiple keys' do + it 'reads multiple keys and serializes content into Gitlab::Diff::Line objects' do + described_class.write_multiple(mapping) - found = described_class.read_multiple(mapping.keys) + found = described_class.read_multiple(mapping.keys) - expect(found.size).to eq(2) - expect(found.first.size).to eq(2) - expect(found.first).to all(be_a(Gitlab::Diff::Line)) - end + expect(found.size).to eq(2) + expect(found.first.size).to eq(2) + expect(found.first).to all(be_a(Gitlab::Diff::Line)) + end - it 'returns nil when cached key is not found' do - described_class.write_multiple(mapping) + it 'returns nil when cached key is not found' do + described_class.write_multiple(mapping) - found = described_class.read_multiple([2, 3]) + found = described_class.read_multiple([2, 3]) - expect(found.size).to eq(2) + expect(found.size).to eq(2) - expect(found.first).to eq(nil) - expect(found.second.size).to eq(2) - expect(found.second).to all(be_a(Gitlab::Diff::Line)) - end + expect(found.first).to eq(nil) + expect(found.second.size).to eq(2) + expect(found.second).to all(be_a(Gitlab::Diff::Line)) + end - it 'returns lines which rich_text are HTML-safe' do - described_class.write_multiple(mapping) + it 'returns lines which rich_text are HTML-safe' do + described_class.write_multiple(mapping) + + found = described_class.read_multiple(mapping.keys) + rich_texts = found.flatten.map(&:rich_text) + + expect(rich_texts).to all(be_html_safe) + end + end - found = described_class.read_multiple(mapping.keys) - rich_texts = found.flatten.map(&:rich_text) + context 'when feature flag is disabled' do + before do + stub_feature_flags(use_pipeline_over_multikey: false) + end - expect(rich_texts).to all(be_html_safe) + it_behaves_like 'read multiple keys' end + + it_behaves_like 'read multiple keys' end describe '#clear_multiple' do - it 'removes all named keys' do - described_class.write_multiple(mapping) + shared_examples 'delete multiple keys' do + it 'removes all named keys' do + described_class.write_multiple(mapping) - described_class.clear_multiple(mapping.keys) + described_class.clear_multiple(mapping.keys) - expect(described_class.read_multiple(mapping.keys)).to all(be_nil) - end + expect(described_class.read_multiple(mapping.keys)).to all(be_nil) + end - it 'only removed named keys' do - to_clear, to_leave = mapping.keys + it 'only removed named keys' do + to_clear, to_leave = mapping.keys - described_class.write_multiple(mapping) - described_class.clear_multiple([to_clear]) + described_class.write_multiple(mapping) + described_class.clear_multiple([to_clear]) - cleared, left = described_class.read_multiple([to_clear, to_leave]) + cleared, left = described_class.read_multiple([to_clear, to_leave]) - expect(cleared).to be_nil - expect(left).to all(be_a(Gitlab::Diff::Line)) + expect(cleared).to be_nil + expect(left).to all(be_a(Gitlab::Diff::Line)) + end end + + context 'when feature flag is disabled' do + before do + stub_feature_flags(use_pipeline_over_multikey: false) + end + + it_behaves_like 'delete multiple keys' + end + + it_behaves_like 'delete multiple keys' end end diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index e78e01ae129..06904849ef5 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1858,8 +1858,8 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen context 'when Gitaly returns Internal error' do before do - expect(repository.gitaly_ref_client) - .to receive(:find_tag) + expect(Gitlab::GitalyClient) + .to receive(:call) .and_raise(GRPC::Internal, "tag not found") end @@ -1868,8 +1868,8 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen context 'when Gitaly returns tag_not_found error' do before do - expect(repository.gitaly_ref_client) - .to receive(:find_tag) + expect(Gitlab::GitalyClient) + .to receive(:call) .and_raise(new_detailed_error(GRPC::Core::StatusCodes::NOT_FOUND, "tag was not found", Gitaly::FindTagError.new(tag_not_found: Gitaly::ReferenceNotFoundError.new))) diff --git a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb index e551dfaa1c5..c321d4bbdb9 100644 --- a/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb +++ b/spec/lib/gitlab/git/wraps_gitaly_errors_spec.rb @@ -2,24 +2,81 @@ require 'spec_helper' -RSpec.describe Gitlab::Git::WrapsGitalyErrors do +RSpec.describe Gitlab::Git::WrapsGitalyErrors, feature_category: :gitaly do subject(:wrapper) do klazz = Class.new { include Gitlab::Git::WrapsGitalyErrors } klazz.new end describe "#wrapped_gitaly_errors" do - mapping = { - GRPC::NotFound => Gitlab::Git::Repository::NoRepository, - GRPC::InvalidArgument => ArgumentError, - GRPC::DeadlineExceeded => Gitlab::Git::CommandTimedOut, - GRPC::BadStatus => Gitlab::Git::CommandError - } - - mapping.each do |grpc_error, error| - it "wraps #{grpc_error} in a #{error}" do - expect { wrapper.wrapped_gitaly_errors { raise grpc_error, 'wrapped' } } - .to raise_error(error) + where(:original_error, :wrapped_error) do + [ + [GRPC::NotFound, Gitlab::Git::Repository::NoRepository], + [GRPC::InvalidArgument, ArgumentError], + [GRPC::DeadlineExceeded, Gitlab::Git::CommandTimedOut], + [GRPC::BadStatus, Gitlab::Git::CommandError] + ] + end + + with_them do + it "wraps #{params[:original_error]} in a #{params[:wrapped_error]}" do + expect { wrapper.wrapped_gitaly_errors { raise original_error, 'wrapped' } } + .to raise_error(wrapped_error) + end + end + + context 'when wrap GRPC::ResourceExhausted' do + context 'with Gitaly::LimitError detail' do + let(:original_error) do + new_detailed_error( + GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED, + 'resource exhausted', + Gitaly::LimitError.new( + error_message: "maximum time in concurrency queue reached", + retry_after: Google::Protobuf::Duration.new(seconds: 5, nanos: 1500) + ) + ) + end + + it "wraps in a Gitlab::Git::ResourceExhaustedError with error message" do + expect { wrapper.wrapped_gitaly_errors { raise original_error } }.to raise_error do |wrapped_error| + expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError) + expect(wrapped_error.message).to eql( + "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later" + ) + expect(wrapped_error.headers).to eql({ 'Retry-After' => 5 }) + end + end + end + + context 'with Gitaly::LimitError detail without retry after' do + let(:original_error) do + new_detailed_error( + GRPC::Core::StatusCodes::RESOURCE_EXHAUSTED, + 'resource exhausted', + Gitaly::LimitError.new(error_message: "maximum time in concurrency queue reached") + ) + end + + it "wraps in a Gitlab::Git::ResourceExhaustedError with error message" do + expect { wrapper.wrapped_gitaly_errors { raise original_error } }.to raise_error do |wrapped_error| + expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError) + expect(wrapped_error.message).to eql( + "Upstream Gitaly has been exhausted: maximum time in concurrency queue reached. Try again later" + ) + expect(wrapped_error.headers).to eql({}) + end + end + end + + context 'without Gitaly::LimitError detail' do + it("wraps in a Gitlab::Git::ResourceExhaustedError with default message") { + expect { wrapper.wrapped_gitaly_errors { raise GRPC::ResourceExhausted } }.to raise_error do |wrapped_error| + expect(wrapped_error).to be_a(Gitlab::Git::ResourceExhaustedError) + expect(wrapped_error.message).to eql("Upstream Gitaly has been exhausted. Try again later") + expect(wrapped_error.headers).to eql({}) + end + } end end diff --git a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb index 207ac1c0eaa..a78d15134fa 100644 --- a/spec/lib/gitlab/reactive_cache_set_cache_spec.rb +++ b/spec/lib/gitlab/reactive_cache_set_cache_spec.rb @@ -46,17 +46,29 @@ RSpec.describe Gitlab::ReactiveCacheSetCache, :clean_gitlab_redis_cache do end describe '#clear_cache!', :use_clean_rails_redis_caching do - it 'deletes the cached items' do - # Cached key and value - Rails.cache.write('test_item', 'test_value') - # Add key to set - cache.write(cache_prefix, 'test_item') + shared_examples 'clears cache' do + it 'deletes the cached items' do + # Cached key and value + Rails.cache.write('test_item', 'test_value') + # Add key to set + cache.write(cache_prefix, 'test_item') - expect(cache.read(cache_prefix)).to contain_exactly('test_item') - cache.clear_cache!(cache_prefix) + expect(cache.read(cache_prefix)).to contain_exactly('test_item') + cache.clear_cache!(cache_prefix) + + expect(cache.read(cache_prefix)).to be_empty + end + end - expect(cache.read(cache_prefix)).to be_empty + context 'when featuer flag disabled' do + before do + stub_feature_flags(use_pipeline_over_multikey: false) + end + + it_behaves_like 'clears cache' end + + it_behaves_like 'clears cache' end describe '#include?' do diff --git a/spec/lib/gitlab/repository_set_cache_spec.rb b/spec/lib/gitlab/repository_set_cache_spec.rb index c93fd884347..65a50b68c44 100644 --- a/spec/lib/gitlab/repository_set_cache_spec.rb +++ b/spec/lib/gitlab/repository_set_cache_spec.rb @@ -72,48 +72,60 @@ RSpec.describe Gitlab::RepositorySetCache, :clean_gitlab_redis_cache do end describe '#expire' do - subject { cache.expire(*keys) } + shared_examples 'expires varying amount of keys' do + subject { cache.expire(*keys) } - before do - cache.write(:foo, ['value']) - cache.write(:bar, ['value2']) - end + before do + cache.write(:foo, ['value']) + cache.write(:bar, ['value2']) + end - it 'actually wrote the values' do - expect(cache.read(:foo)).to contain_exactly('value') - expect(cache.read(:bar)).to contain_exactly('value2') - end + it 'actually wrote the values' do + expect(cache.read(:foo)).to contain_exactly('value') + expect(cache.read(:bar)).to contain_exactly('value2') + end - context 'single key' do - let(:keys) { %w(foo) } + context 'single key' do + let(:keys) { %w(foo) } - it { is_expected.to eq(1) } + it { is_expected.to eq(1) } - it 'deletes the given key from the cache' do - subject + it 'deletes the given key from the cache' do + subject - expect(cache.read(:foo)).to be_empty + expect(cache.read(:foo)).to be_empty + end end - end - context 'multiple keys' do - let(:keys) { %w(foo bar) } + context 'multiple keys' do + let(:keys) { %w(foo bar) } - it { is_expected.to eq(2) } + it { is_expected.to eq(2) } - it 'deletes the given keys from the cache' do - subject + it 'deletes the given keys from the cache' do + subject - expect(cache.read(:foo)).to be_empty - expect(cache.read(:bar)).to be_empty + expect(cache.read(:foo)).to be_empty + expect(cache.read(:bar)).to be_empty + end + end + + context 'no keys' do + let(:keys) { [] } + + it { is_expected.to eq(0) } end end - context 'no keys' do - let(:keys) { [] } + context 'when feature flag is disabled' do + before do + stub_feature_flags(use_pipeline_over_multikey: false) + end - it { is_expected.to eq(0) } + it_behaves_like 'expires varying amount of keys' end + + it_behaves_like 'expires varying amount of keys' end describe '#exist?' do diff --git a/spec/lib/gitlab/utils/email_spec.rb b/spec/lib/gitlab/utils/email_spec.rb index d7a881d8655..c81c2558f70 100644 --- a/spec/lib/gitlab/utils/email_spec.rb +++ b/spec/lib/gitlab/utils/email_spec.rb @@ -8,13 +8,20 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do describe '.obfuscated_email' do where(:input, :output) do - 'alex@gitlab.com' | 'al**@g*****.com' - 'alex@gl.co.uk' | 'al**@g****.uk' - 'a@b.c' | 'a@b.c' - 'q@example.com' | 'q@e******.com' - 'q@w.' | 'q@w.' - 'a@b' | 'a@b' - 'no mail' | 'no mail' + 'alex@gitlab.com' | 'al**@g*****.com' + 'alex@gl.co.uk' | 'al**@g****.uk' + 'a@b.c' | 'aa@b.c' + 'qqwweerrttyy@example.com' | 'qq**********@e******.com' + 'getsuperfancysupport@paywhatyouwant.accounting' | 'ge******************@p*************.accounting' + 'q@example.com' | 'qq@e******.com' + 'q@w.' | 'qq@w.' + 'a@b' | 'aa@b' + 'trun"@"e@example.com' | 'tr******@e******.com' + '@' | '@' + 'n' | 'n' + 'no mail' | 'n******' + 'truncated@exa' | 'tr*******@exa' + '' | '' end with_them do @@ -29,9 +36,14 @@ RSpec.describe Gitlab::Utils::Email, feature_category: :service_desk do 'qqwweerrttyy@example.com' | 'qq*****@e*****.c**' 'getsuperfancysupport@paywhatyouwant.accounting' | 'ge*****@p*****.a**' 'q@example.com' | 'qq*****@e*****.c**' - 'q@w.' | 'q@w.' - 'a@b' | 'a@b' - 'no mail' | 'no mail' + 'q@w.' | 'qq*****@w*****.' + 'a@b' | 'aa*****@b**' + 'trun"@"e@example.com' | 'tr*****@e*****.c**' + '@' | '@' + 'no mail' | 'n**' + 'n' | 'n**' + 'truncated@exa' | 'tr*****@e**' + '' | '' end with_them do diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb index fcf60f0559a..3291eba48d3 100644 --- a/spec/models/packages/package_spec.rb +++ b/spec/models/packages/package_spec.rb @@ -847,6 +847,14 @@ RSpec.describe Packages::Package, type: :model, feature_category: :package_regis is_expected.to match_array([package]) end end + + describe '.preload_conan_metadatum' do + subject { described_class.preload_conan_metadatum } + + it 'loads conan metadatum' do + expect(subject.first.association(:conan_metadatum)).to be_loaded + end + end end describe '.without_nuget_temporary_name' do diff --git a/spec/requests/api/api_spec.rb b/spec/requests/api/api_spec.rb index d5ad0779bd9..219c7dbdbc5 100644 --- a/spec/requests/api/api_spec.rb +++ b/spec/requests/api/api_spec.rb @@ -359,4 +359,26 @@ RSpec.describe API::API, feature_category: :system_access do end end end + + describe 'Handle Gitlab::Git::ResourceExhaustedError exception' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :repository, creator: user) } + + before do + project.add_maintainer(user) + allow(Gitlab::GitalyClient).to receive(:call).with(any_args).and_raise( + Gitlab::Git::ResourceExhaustedError.new("Upstream Gitaly has been exhausted. Try again later", 50) + ) + end + + it 'returns 429 status with exhausted' do + get api("/projects/#{project.id}/repository/commits", user) + + expect(response).to have_gitlab_http_status(:too_many_requests) + expect(response.headers['Retry-After']).to be(50) + expect(json_response).to eql( + 'message' => 'Upstream Gitaly has been exhausted. Try again later' + ) + end + end end diff --git a/spec/requests/api/conan_project_packages_spec.rb b/spec/requests/api/conan_project_packages_spec.rb index 814745f9e29..06f175233db 100644 --- a/spec/requests/api/conan_project_packages_spec.rb +++ b/spec/requests/api/conan_project_packages_spec.rb @@ -33,6 +33,29 @@ RSpec.describe API::ConanProjectPackages, feature_category: :package_registry do subject { get api(url), params: params } end + + context 'with access to package registry for everyone' do + before do + project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE) + project.project_feature.update!(package_registry_access_level: ProjectFeature::PUBLIC) + + get api(url), params: params + end + + subject { json_response['results'] } + + context 'with a matching name' do + let(:params) { { q: package.conan_recipe } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + + context 'with a * wildcard' do + let(:params) { { q: "#{package.name[0, 3]}*" } } + + it { is_expected.to contain_exactly(package.conan_recipe) } + end + end end describe 'GET /api/v4/projects/:id/packages/conan/v1/users/authenticate' do diff --git a/spec/services/packages/conan/search_service_spec.rb b/spec/services/packages/conan/search_service_spec.rb index 9e8be164d8c..83ece404d5f 100644 --- a/spec/services/packages/conan/search_service_spec.rb +++ b/spec/services/packages/conan/search_service_spec.rb @@ -9,7 +9,7 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist let!(:conan_package) { create(:conan_package, project: project) } let!(:conan_package2) { create(:conan_package, project: project) } - subject { described_class.new(user, query: query) } + subject { described_class.new(project, user, query: query) } before do project.add_developer(user) @@ -24,7 +24,7 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist result = subject.execute expect(result.status).to eq :success - expect(result.payload).to eq(results: [conan_package.conan_recipe, conan_package2.conan_recipe]) + expect(result.payload).to eq(results: [conan_package2.conan_recipe, conan_package.conan_recipe]) end end @@ -71,5 +71,29 @@ RSpec.describe Packages::Conan::SearchService, feature_category: :package_regist expect(result.payload).to eq(results: []) end end + + context 'for project' do + let_it_be(:project2) { create(:project, :public) } + let(:query) { conan_package.name } + let!(:conan_package3) { create(:conan_package, name: conan_package.name, project: project2) } + + context 'when passing a project' do + it 'returns only packages of the given project' do + result = subject.execute + + expect(result.status).to eq :success + expect(result[:results]).to match_array([conan_package.conan_recipe]) + end + end + + context 'when passing a project with nil' do + it 'returns all packages' do + result = described_class.new(nil, user, query: query).execute + + expect(result.status).to eq :success + expect(result[:results]).to eq([conan_package3.conan_recipe, conan_package.conan_recipe]) + end + end + end end end diff --git a/spec/services/packages/conan/single_package_search_service_spec.rb b/spec/services/packages/conan/single_package_search_service_spec.rb new file mode 100644 index 00000000000..1d95d1d4f64 --- /dev/null +++ b/spec/services/packages/conan/single_package_search_service_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Packages::Conan::SinglePackageSearchService, feature_category: :package_registry do + let_it_be(:user) { create(:user) } + let_it_be(:project) { create(:project, :public) } + + let!(:conan_package) { create(:conan_package, project: project) } + let!(:conan_package2) { create(:conan_package, project: project) } + + describe '#execute' do + context 'with a valid query and user with permissions' do + before do + allow_next_instance_of(described_class) do |service| + allow(service).to receive(:can_access_project_package?).and_return(true) + end + end + + it 'returns the correct package' do + [conan_package, conan_package2].each do |package| + result = described_class.new(package.conan_recipe, user).execute + + expect(result.status).to eq :success + expect(result[:results]).to match_array([package.conan_recipe]) + end + end + end + + context 'with a user without permissions' do + before do + allow_next_instance_of(described_class) do |service| + allow(service).to receive(:can_access_project_package?).and_return(false) + end + end + + it 'returns an empty array' do + result = described_class.new(conan_package.conan_recipe, user).execute + + expect(result.status).to eq :success + expect(result[:results]).to match_array([]) + end + end + end +end |