diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-02 15:08:02 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-12-02 15:08:02 +0300 |
commit | f62c9f693fbe0d9bfac43cbe24b86af3c35f6a17 (patch) | |
tree | 7bbeb4fdf92d78324a92ff8a1385ea807c78665a /spec | |
parent | 81745c5a7143999d492943a2dceb977ced0a766f (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
19 files changed, 525 insertions, 303 deletions
diff --git a/spec/features/issue_rebalancing_spec.rb b/spec/features/issue_rebalancing_spec.rb index 686aa5eb1b6..4fa29361c54 100644 --- a/spec/features/issue_rebalancing_spec.rb +++ b/spec/features/issue_rebalancing_spec.rb @@ -41,13 +41,13 @@ RSpec.describe 'Issue rebalancing' do it 'shows an alert in project issues list with manual sort', :js do visit project_issues_path(project, sort: 'relative_position') - expect(page).to have_selector('.flash-notice', text: alert_message_regex, count: 1) + expect(page).to have_selector('.gl-alert-danger', text: alert_message_regex, count: 1) end it 'shows an alert in group issues list with manual sort', :js do visit issues_group_path(group, sort: 'relative_position') - expect(page).to have_selector('.flash-notice', text: alert_message_regex, count: 1) + expect(page).to have_selector('.gl-alert-danger', text: alert_message_regex, count: 1) end it 'does not show an alert in project issues list with other sorts' do diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js index 3a2beb714e9..eddc916ed14 100644 --- a/spec/frontend/boards/board_list_spec.js +++ b/spec/frontend/boards/board_list_spec.js @@ -269,6 +269,10 @@ describe('Board list component', () => { it('Draggable is not used', () => { expect(findDraggable().exists()).toBe(false); }); + + it('Board card move to position is not visible', () => { + expect(findMoveToPositionComponent().exists()).toBe(false); + }); }); }); }); diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml index f5670376efc..29f4a0cd76d 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml @@ -16,3 +16,16 @@ cyclonedx not an array or string: paths: - foo - bar + +# invalid artifacts:when +artifacts-when-unknown: + artifacts: + when: unknown + +artifacts-when-array: + artifacts: + when: [always] + +artifacts-when-boolean: + artifacts: + when: true diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml index 3979c9ae2ac..9baed2a7922 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/cache.yml @@ -51,12 +51,39 @@ cache-untracked-string: cache: untracked: 'true' -when_integer: +# invalid cache:when +cache-when-integer: script: echo "This job uses a cache." cache: when: 0 -when_not_reserved_keyword: +cache-when-array: + script: echo "This job uses a cache." + cache: + when: [always] + +cache-when-boolean: + script: echo "This job uses a cache." + cache: + when: true + +cache-when-never: script: echo "This job uses a cache." cache: when: 'never' + +# invalid cache:policy +cache-policy-array: + script: echo "This job uses a cache." + cache: + policy: [push] + +cache-policy-boolean: + script: echo "This job uses a cache." + cache: + policy: true + +cache-when-unknown: + script: echo "This job uses a cache." + cache: + policy: unknown diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml index 20c1fc2c50f..a5c9153ee13 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml @@ -23,3 +23,13 @@ cylonedx mixed list of string paths and globs: cyclonedx: - ./foo - "bar/*.baz" + +# valid artifacts:when +artifacts-when-on-failure: + artifacts: + when: on_failure + +artifacts-no-when: + artifacts: + paths: + - binaries/ diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml index 75918cd2a1b..d50b74e1448 100644 --- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml +++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/cache.yml @@ -122,3 +122,20 @@ cache-untracked-false: script: test cache: untracked: false + +# valid cache:policy +cache-policy-push: + script: echo "This job uses a cache." + cache: + policy: push + +cache-policy-pull: + script: echo "This job uses a cache." + cache: + policy: pull + +cache-no-policy: + script: echo "This job uses a cache." + cache: + paths: + - binaries/ diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js index 4b09c92391c..f5624923b46 100644 --- a/spec/frontend/issues/list/components/issues_list_app_spec.js +++ b/spec/frontend/issues/list/components/issues_list_app_spec.js @@ -21,7 +21,7 @@ import { setSortPreferenceMutationResponseWithErrors, urlParams, } from 'jest/issues/list/mock_data'; -import createFlash, { FLASH_TYPES } from '~/flash'; +import { createAlert, FLASH_TYPES } from '~/flash'; import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils'; import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue'; import IssuableByEmail from '~/issuable/components/issuable_by_email.vue'; @@ -35,6 +35,7 @@ import { CREATED_DESC, RELATIVE_POSITION, RELATIVE_POSITION_ASC, + UPDATED_DESC, urlSortParams, } from '~/issues/list/constants'; import eventHub from '~/issues/list/eventhub'; @@ -399,7 +400,7 @@ describe('CE IssuesListApp component', () => { }); it('shows an alert to tell the user that manual reordering is disabled', () => { - expect(createFlash).toHaveBeenCalledWith({ + expect(createAlert).toHaveBeenCalledWith({ message: IssuesListApp.i18n.issueRepositioningMessage, type: FLASH_TYPES.NOTICE, }); @@ -439,7 +440,7 @@ describe('CE IssuesListApp component', () => { }); it('shows an alert to tell the user they must be signed in to search', () => { - expect(createFlash).toHaveBeenCalledWith({ + expect(createAlert).toHaveBeenCalledWith({ message: IssuesListApp.i18n.anonymousSearchingMessage, type: FLASH_TYPES.NOTICE, }); @@ -796,7 +797,11 @@ describe('CE IssuesListApp component', () => { it.each(Object.keys(urlSortParams))( 'updates to the new sort when payload is `%s`', async (sortKey) => { - wrapper = mountComponent(); + // Ensure initial sort key is different so we can trigger an update when emitting a sort key + wrapper = + sortKey === CREATED_DESC + ? mountComponent({ provide: { initialSort: UPDATED_DESC } }) + : mountComponent(); router.push = jest.fn(); findIssuableList().vm.$emit('sort', sortKey); @@ -826,7 +831,7 @@ describe('CE IssuesListApp component', () => { }); it('shows an alert to tell the user that manual reordering is disabled', () => { - expect(createFlash).toHaveBeenCalledWith({ + expect(createAlert).toHaveBeenCalledWith({ message: IssuesListApp.i18n.issueRepositioningMessage, type: FLASH_TYPES.NOTICE, }); @@ -838,9 +843,9 @@ describe('CE IssuesListApp component', () => { const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse); wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock }); - findIssuableList().vm.$emit('sort', CREATED_DESC); + findIssuableList().vm.$emit('sort', UPDATED_DESC); - expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: CREATED_DESC } }); + expect(mutationMock).toHaveBeenCalledWith({ input: { issuesSort: UPDATED_DESC } }); }); it('captures error when mutation response has errors', async () => { @@ -849,7 +854,7 @@ describe('CE IssuesListApp component', () => { .mockResolvedValue(setSortPreferenceMutationResponseWithErrors); wrapper = mountComponent({ sortPreferenceMutationResponse: mutationMock }); - findIssuableList().vm.$emit('sort', CREATED_DESC); + findIssuableList().vm.$emit('sort', UPDATED_DESC); await waitForPromises(); expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!')); @@ -913,7 +918,7 @@ describe('CE IssuesListApp component', () => { }); it('shows an alert to tell the user they must be signed in to search', () => { - expect(createFlash).toHaveBeenCalledWith({ + expect(createAlert).toHaveBeenCalledWith({ message: IssuesListApp.i18n.anonymousSearchingMessage, type: FLASH_TYPES.NOTICE, }); diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js index 293396c9de7..cc3ee41523f 100644 --- a/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js +++ b/spec/frontend/vue_shared/components/source_viewer/plugins/utils/go_sum_linker_spec.js @@ -3,9 +3,9 @@ import goSumLinker from '~/vue_shared/components/source_viewer/plugins/utils/go_ describe('Highlight.js plugin for linking go.sum dependencies', () => { it('mutates the input value by wrapping dependencies and tags in anchors', () => { const inputValue = - '<span class="">cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>'; + '<span class="">cloud.google.com/Go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>'; const outputValue = - '<span class=""><a href="https://pkg.go.dev/cloud.google.com/go/bigquery" target="_blank" rel="nofollow noreferrer noopener">cloud.google.com/go/bigquery</a> v1.0.1/go.mod h1:<a href="https://sum.golang.org/lookup/cloud.google.com/go/bigquery@v1.0.1" target="_blank" rel="nofollow noreferrer noopener">i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</a></span>'; + '<span class=""><a href="https://pkg.go.dev/cloud.google.com/go/bigquery" target="_blank" rel="nofollow noreferrer noopener">cloud.google.com/Go/bigquery</a> v1.0.1/go.mod h1:<a href="https://sum.golang.org/lookup/cloud.google.com/go/bigquery@v1.0.1" target="_blank" rel="nofollow noreferrer noopener">i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</a></span>'; const hljsResultMock = { value: inputValue }; const output = goSumLinker(hljsResultMock); diff --git a/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb new file mode 100644 index 00000000000..f5a8035b9b4 --- /dev/null +++ b/spec/lib/gitlab/analytics/cycle_analytics/stage_events_spec.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Gitlab::Analytics::CycleAnalytics::StageEvents, feature_category: :value_stream_management do + describe '#selectable_events' do + subject(:selectable_events) { described_class.selectable_events } + + it 'excludes internal events' do + expect(selectable_events).to include(Gitlab::Analytics::CycleAnalytics::StageEvents::MergeRequestCreated) + expect(selectable_events).to exclude(Gitlab::Analytics::CycleAnalytics::StageEvents::IssueStageEnd) + end + end +end diff --git a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb index 7476fc6c25f..6264e0c8e33 100644 --- a/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/artifacts_spec.rb @@ -142,6 +142,26 @@ RSpec.describe Gitlab::Ci::Config::Entry::Artifacts do end end + context 'when the `when` keyword is not a string' do + context 'when it is an array' do + let(:config) { { paths: %w[results.txt], when: ['always'] } } + + it 'returns error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'artifacts when should be a string' + end + end + + context 'when it is a boolean' do + let(:config) { { paths: %w[results.txt], when: true } } + + it 'returns error' do + expect(entry).not_to be_valid + expect(entry.errors).to include 'artifacts when should be a string' + end + end + end + describe 'excluded artifacts' do context 'when configuration is valid' do let(:config) { { untracked: true, exclude: ['some/directory/'] } } diff --git a/spec/lib/gitlab/ci/config/entry/cache_spec.rb b/spec/lib/gitlab/ci/config/entry/cache_spec.rb index 247f4b63910..414cbb169b9 100644 --- a/spec/lib/gitlab/ci/config/entry/cache_spec.rb +++ b/spec/lib/gitlab/ci/config/entry/cache_spec.rb @@ -163,22 +163,6 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do end end - context 'when policy is unknown' do - let(:config) { { policy: 'unknown' } } - - it 'reports error' do - is_expected.to include('cache policy should be pull-push, push, or pull') - end - end - - context 'when `when` is unknown' do - let(:config) { { when: 'unknown' } } - - it 'reports error' do - is_expected.to include('cache when should be on_success, on_failure or always') - end - end - context 'when descendants are invalid' do context 'with invalid keys' do let(:config) { { key: 1 } } @@ -228,6 +212,62 @@ RSpec.describe Gitlab::Ci::Config::Entry::Cache do is_expected.to include 'cache config contains unknown keys: invalid' end end + + context 'when the `when` keyword is not a valid string' do + context 'when `when` is unknown' do + let(:config) { { when: 'unknown' } } + + it 'returns error' do + is_expected.to include('cache when should be one of: on_success, on_failure, always') + end + end + + context 'when it is an array' do + let(:config) { { when: ['always'] } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache when should be a string') + end + end + + context 'when it is a boolean' do + let(:config) { { when: true } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache when should be a string') + end + end + end + + context 'when the `policy` keyword is not a valid string' do + context 'when `policy` is unknown' do + let(:config) { { policy: 'unknown' } } + + it 'returns error' do + is_expected.to include('cache policy should be one of: pull-push, push, pull') + end + end + + context 'when it is an array' do + let(:config) { { policy: ['pull-push'] } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache policy should be a string') + end + end + + context 'when it is a boolean' do + let(:config) { { policy: true } } + + it 'returns error' do + expect(entry).not_to be_valid + is_expected.to include('cache policy should be a string') + end + end + end end end end diff --git a/spec/lib/gitlab/ci/yaml_processor_spec.rb b/spec/lib/gitlab/ci/yaml_processor_spec.rb index 0d1deb863b1..ae98d2e0cad 100644 --- a/spec/lib/gitlab/ci/yaml_processor_spec.rb +++ b/spec/lib/gitlab/ci/yaml_processor_spec.rb @@ -2946,7 +2946,7 @@ module Gitlab context 'returns errors if job artifacts:when is not an a predefined value' do let(:config) { YAML.dump({ stages: %w(build test), rspec: { script: "test", artifacts: { when: 1 } } }) } - it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be on_success, on_failure or always' + it_behaves_like 'returns errors', 'jobs:rspec:artifacts when should be one of: on_success, on_failure, always' end context 'returns errors if job artifacts:expire_in is not an a string' do diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb index 918b091eef2..3c7542ea5f9 100644 --- a/spec/lib/gitlab/workhorse_spec.rb +++ b/spec/lib/gitlab/workhorse_spec.rb @@ -209,6 +209,16 @@ RSpec.describe Gitlab::Workhorse do describe '.git_http_ok' do let(:user) { create(:user) } + let(:gitaly_params) do + { + GitalyServer: { + call_metadata: call_metadata, + address: Gitlab::GitalyClient.address('default'), + token: Gitlab::GitalyClient.token('default') + } + } + end + let(:repo_path) { 'ignored but not allowed to be empty in gitlab-workhorse' } let(:action) { 'info_refs' } let(:params) do @@ -246,100 +256,79 @@ RSpec.describe Gitlab::Workhorse do it { expect(subject).to include(params) } end - context 'when Gitaly is enabled' do - let(:gitaly_params) do - { - GitalyServer: { - call_metadata: call_metadata, - address: Gitlab::GitalyClient.address('default'), - token: Gitlab::GitalyClient.token('default') - } - } - end + it 'includes a Repository param' do + repo_param = { + storage_name: 'default', + relative_path: project.disk_path + '.git', + gl_repository: "project-#{project.id}" + } - before do - allow(Gitlab.config.gitaly).to receive(:enabled).and_return(true) - end + expect(subject[:Repository]).to include(repo_param) + end - it 'includes a Repository param' do - repo_param = { - storage_name: 'default', - relative_path: project.disk_path + '.git', - gl_repository: "project-#{project.id}" - } + context "when git_upload_pack action is passed" do + let(:action) { 'git_upload_pack' } - expect(subject[:Repository]).to include(repo_param) - end + it { expect(subject).to include(gitaly_params) } - context "when git_upload_pack action is passed" do - let(:action) { 'git_upload_pack' } - let(:feature_flag) { :post_upload_pack } + context 'show_all_refs enabled' do + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } - it 'includes Gitaly params in the returned value' do - allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(true) + it { is_expected.to include(ShowAllRefs: true) } + end - expect(subject).to include(gitaly_params) + context 'when a feature flag is set for a single project' do + before do + stub_feature_flags(gitaly_mep_mep: project) end - context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } + it 'sets the flag to true for that project' do + response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) - it { is_expected.to include(ShowAllRefs: true) } + expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'true') end - context 'when a feature flag is set for a single project' do - before do - stub_feature_flags(gitaly_mep_mep: project) - end - - it 'sets the flag to true for that project' do - response = described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action) + it 'sets the flag to false for other projects' do + other_project = create(:project, :public, :repository) + response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action) - expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', - 'gitaly-feature-mep-mep' => 'true') - end - - it 'sets the flag to false for other projects' do - other_project = create(:project, :public, :repository) - response = described_class.git_http_ok(other_project.repository, Gitlab::GlRepository::PROJECT, user, action) - - expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', - 'gitaly-feature-mep-mep' => 'false') - end + expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'false') + end - it 'sets the flag to false when there is no project' do - snippet = create(:personal_snippet, :repository) - response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action) + it 'sets the flag to false when there is no project' do + snippet = create(:personal_snippet, :repository) + response = described_class.git_http_ok(snippet.repository, Gitlab::GlRepository::SNIPPET, user, action) - expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', - 'gitaly-feature-mep-mep' => 'false') - end + expect(response.dig(:GitalyServer, :call_metadata)).to include('gitaly-feature-enforce-requests-limits' => 'true', + 'gitaly-feature-mep-mep' => 'false') end end + end - context "when git_receive_pack action is passed" do - let(:action) { 'git_receive_pack' } + context "when git_receive_pack action is passed" do + let(:action) { 'git_receive_pack' } - it { expect(subject).to include(gitaly_params) } - end + it { expect(subject).to include(gitaly_params) } + end - context "when info_refs action is passed" do - let(:action) { 'info_refs' } + context "when info_refs action is passed" do + let(:action) { 'info_refs' } - it { expect(subject).to include(gitaly_params) } + it { expect(subject).to include(gitaly_params) } - context 'show_all_refs enabled' do - subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } + context 'show_all_refs enabled' do + subject { described_class.git_http_ok(repository, Gitlab::GlRepository::PROJECT, user, action, show_all_refs: true) } - it { is_expected.to include(ShowAllRefs: true) } - end + it { is_expected.to include(ShowAllRefs: true) } end + end - context 'when action passed is not supported by Gitaly' do - let(:action) { 'download' } + context 'when action passed is not supported by Gitaly' do + let(:action) { 'download' } - it { expect { subject }.to raise_exception('Unsupported action: download') } - end + it { expect { subject }.to raise_exception('Unsupported action: download') } end context 'when receive_max_input_size has been updated' do diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb index d7fc5486ad6..8cdb195f9c7 100644 --- a/spec/models/group_spec.rb +++ b/spec/models/group_spec.rb @@ -40,7 +40,7 @@ RSpec.describe Group do it { is_expected.to have_many(:bulk_import_exports).class_name('BulkImports::Export') } it { is_expected.to have_many(:contacts).class_name('CustomerRelations::Contact') } it { is_expected.to have_many(:organizations).class_name('CustomerRelations::Organization') } - it { is_expected.to have_many(:protected_branches) } + it { is_expected.to have_many(:protected_branches).inverse_of(:group).with_foreign_key(:namespace_id) } it { is_expected.to have_one(:crm_settings) } it { is_expected.to have_one(:group_feature) } it { is_expected.to have_one(:harbor_integration) } diff --git a/spec/services/protected_branches/api_service_spec.rb b/spec/services/protected_branches/api_service_spec.rb index 94484f5a7b9..c98e253267b 100644 --- a/spec/services/protected_branches/api_service_spec.rb +++ b/spec/services/protected_branches/api_service_spec.rb @@ -3,32 +3,55 @@ require 'spec_helper' RSpec.describe ProtectedBranches::ApiService do - let_it_be(:project) { create(:project) } - let_it_be(:user) { create(:user, maintainer_projects: [project]) } - - it 'creates a protected branch with prefilled defaults' do - expect(::ProtectedBranches::CreateService).to receive(:new).with( - project, user, hash_including( - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }] - ) - ).and_call_original - - expect(described_class.new(project, user, { name: 'new name' }).create).to be_valid + shared_examples 'execute with entity' do + it 'creates a protected branch with prefilled defaults' do + expect(::ProtectedBranches::CreateService).to receive(:new).with( + entity, user, hash_including( + push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }] + ) + ).and_call_original + + expect(described_class.new(entity, user, { name: 'new name' }).create).to be_valid + end + + it 'updates a protected branch without prefilled defaults' do + expect(::ProtectedBranches::UpdateService).to receive(:new).with( + entity, user, hash_including( + push_access_levels_attributes: [], + merge_access_levels_attributes: [] + ) + ).and_call_original + + expect do + expect(described_class.new(entity, user, { name: 'new name' }).update(protected_branch)).to be_valid + end.not_to change { protected_branch.reload.allow_force_push } + end + end + + context 'with entity project' do + let_it_be_with_reload(:entity) { create(:project) } + let_it_be_with_reload(:protected_branch) { create(:protected_branch, project: entity, allow_force_push: true) } + let(:user) { entity.first_owner } + + it_behaves_like 'execute with entity' end - it 'updates a protected branch without prefilled defaults' do - protected_branch = create(:protected_branch, project: project, allow_force_push: true) + context 'with entity group' do + let_it_be_with_reload(:entity) { create(:group) } + let_it_be_with_reload(:user) { create(:user) } + let_it_be_with_reload(:protected_branch) do + create(:protected_branch, group: entity, project: nil, allow_force_push: true) + end - expect(::ProtectedBranches::UpdateService).to receive(:new).with( - project, user, hash_including( - push_access_levels_attributes: [], - merge_access_levels_attributes: [] - ) - ).and_call_original + before do + allow(Ability).to receive(:allowed?).with(user, :update_protected_branch, protected_branch).and_return(true) + allow(Ability) + .to receive(:allowed?) + .with(user, :create_protected_branch, instance_of(ProtectedBranch)) + .and_return(true) + end - expect do - expect(described_class.new(project, user, { name: 'new name' }).update(protected_branch)).to be_valid - end.not_to change { protected_branch.reload.allow_force_push } + it_behaves_like 'execute with entity' end end diff --git a/spec/services/protected_branches/cache_service_spec.rb b/spec/services/protected_branches/cache_service_spec.rb index d7a3258160b..ea434922661 100644 --- a/spec/services/protected_branches/cache_service_spec.rb +++ b/spec/services/protected_branches/cache_service_spec.rb @@ -4,122 +4,150 @@ require 'spec_helper' RSpec.describe ProtectedBranches::CacheService, :clean_gitlab_redis_cache do - subject(:service) { described_class.new(project, user) } + shared_examples 'execute with entity' do + subject(:service) { described_class.new(entity, user) } - let_it_be(:project) { create(:project) } - let_it_be(:user) { project.first_owner } + let(:immediate_expiration) { 0 } - let(:immediate_expiration) { 0 } - - describe '#fetch' do - it 'caches the value' do - expect(service.fetch('main') { true }).to eq(true) - expect(service.fetch('not-found') { false }).to eq(false) - - # Uses cached values - expect(service.fetch('main') { false }).to eq(true) - expect(service.fetch('not-found') { true }).to eq(false) - end - - it 'sets expiry on the key' do - stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) - - expect(service.fetch('main') { true }).to eq(true) - expect(service.fetch('not-found') { false }).to eq(false) - - expect(service.fetch('main') { false }).to eq(false) - expect(service.fetch('not-found') { true }).to eq(true) - end - - it 'does not set an expiry on the key after the hash is already created' do - expect(service.fetch('main') { true }).to eq(true) + describe '#fetch' do + it 'caches the value' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) - stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) + # Uses cached values + expect(service.fetch('main') { false }).to eq(true) + expect(service.fetch('not-found') { true }).to eq(false) + end - expect(service.fetch('not-found') { false }).to eq(false) + it 'sets expiry on the key' do + stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) - expect(service.fetch('main') { false }).to eq(true) - expect(service.fetch('not-found') { true }).to eq(false) - end + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) - context 'when CACHE_LIMIT is exceeded' do - before do - stub_const("#{described_class.name}::CACHE_LIMIT", 2) + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) end - it 'recreates cache' do + it 'does not set an expiry on the key after the hash is already created' do expect(service.fetch('main') { true }).to eq(true) + + stub_const("#{described_class.name}::CACHE_EXPIRE_IN", immediate_expiration) + expect(service.fetch('not-found') { false }).to eq(false) - # Uses cached values expect(service.fetch('main') { false }).to eq(true) expect(service.fetch('not-found') { true }).to eq(false) + end - # Overflow - expect(service.fetch('new-branch') { true }).to eq(true) + context 'when CACHE_LIMIT is exceeded' do + before do + stub_const("#{described_class.name}::CACHE_LIMIT", 2) + end - # Refreshes values - expect(service.fetch('main') { false }).to eq(false) - expect(service.fetch('not-found') { true }).to eq(true) - end - end + it 'recreates cache' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) + + # Uses cached values + expect(service.fetch('main') { false }).to eq(true) + expect(service.fetch('not-found') { true }).to eq(false) - context 'when dry_run is on' do - it 'does not use cached value' do - expect(service.fetch('main', dry_run: true) { true }).to eq(true) - expect(service.fetch('main', dry_run: true) { false }).to eq(false) + # Overflow + expect(service.fetch('new-branch') { true }).to eq(true) + + # Refreshes values + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) + end end - context 'when cache mismatch' do - it 'logs an error' do + context 'when dry_run is on' do + it 'does not use cached value' do expect(service.fetch('main', dry_run: true) { true }).to eq(true) + expect(service.fetch('main', dry_run: true) { false }).to eq(false) + end + + context 'when cache mismatch' do + it 'logs an error' do + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + + expect(Gitlab::AppLogger).to receive(:error).with( + { + 'class' => described_class.name, + 'message' => /Cache mismatch/, + 'record_class' => entity.class.name, + 'record_id' => entity.id, + 'record_path' => entity.full_path + } + ) + + expect(service.fetch('main', dry_run: true) { false }).to eq(false) + end + end - expect(Gitlab::AppLogger).to receive(:error).with( - { - 'class' => described_class.name, - 'message' => /Cache mismatch/, - 'project_id' => project.id, - 'project_path' => project.full_path - } - ) + context 'when cache matches' do + it 'does not log an error' do + expect(service.fetch('main', dry_run: true) { true }).to eq(true) - expect(service.fetch('main', dry_run: true) { false }).to eq(false) + expect(Gitlab::AppLogger).not_to receive(:error) + + expect(service.fetch('main', dry_run: true) { true }).to eq(true) + end end end + end - context 'when cache matches' do - it 'does not log an error' do - expect(service.fetch('main', dry_run: true) { true }).to eq(true) + describe '#refresh' do + it 'clears cached values' do + expect(service.fetch('main') { true }).to eq(true) + expect(service.fetch('not-found') { false }).to eq(false) - expect(Gitlab::AppLogger).not_to receive(:error) + service.refresh - expect(service.fetch('main', dry_run: true) { true }).to eq(true) + # Recreates cache + expect(service.fetch('main') { false }).to eq(false) + expect(service.fetch('not-found') { true }).to eq(true) + end + end + + describe 'metrics' do + it 'records hit ratio metrics' do + expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics| + expect(metrics).to receive(:increment_cache_miss).once + expect(metrics).to receive(:increment_cache_hit).exactly(4).times end + + 5.times { service.fetch('main') { true } } end end end - describe '#refresh' do - it 'clears cached values' do - expect(service.fetch('main') { true }).to eq(true) - expect(service.fetch('not-found') { false }).to eq(false) + context 'with entity project' do + let_it_be_with_reload(:entity) { create(:project) } + let(:user) { entity.first_owner } - service.refresh + it_behaves_like 'execute with entity' + end + + context 'with entity group' do + let_it_be_with_reload(:entity) { create(:group) } + let_it_be_with_reload(:user) { create(:user) } - # Recreates cache - expect(service.fetch('main') { false }).to eq(false) - expect(service.fetch('not-found') { true }).to eq(true) + before do + entity.add_owner(user) end - end - describe 'metrics' do - it 'records hit ratio metrics' do - expect_next_instance_of(Gitlab::Cache::Metrics) do |metrics| - expect(metrics).to receive(:increment_cache_miss).once - expect(metrics).to receive(:increment_cache_hit).exactly(4).times + context 'when feature flag enabled' do + it_behaves_like 'execute with entity' + end + + context 'when feature flag disabled' do + before do + stub_feature_flags(group_protected_branches: false) end - 5.times { service.fetch('main') { true } } + it_behaves_like 'execute with entity' end end end diff --git a/spec/services/protected_branches/create_service_spec.rb b/spec/services/protected_branches/create_service_spec.rb index b42524e761c..9c8fe769ed8 100644 --- a/spec/services/protected_branches/create_service_spec.rb +++ b/spec/services/protected_branches/create_service_spec.rb @@ -3,70 +3,75 @@ require 'spec_helper' RSpec.describe ProtectedBranches::CreateService do - let_it_be_with_reload(:project) { create(:project) } - - let(:user) { project.first_owner } - let(:params) do - { - name: name, - merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], - push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }] - } - end - - subject(:service) { described_class.new(project, user, params) } - - describe '#execute' do - let(:name) { 'master' } - - it 'creates a new protected branch' do - expect { service.execute }.to change(ProtectedBranch, :count).by(1) - expect(project.protected_branches.last.push_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) - expect(project.protected_branches.last.merge_access_levels.map(&:access_level)).to eq([Gitlab::Access::MAINTAINER]) + shared_examples 'execute with entity' do + let(:params) do + { + name: name, + merge_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }], + push_access_levels_attributes: [{ access_level: Gitlab::Access::MAINTAINER }] + } end - it 'refreshes the cache' do - expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| - expect(cache_service).to receive(:refresh) - end + subject(:service) { described_class.new(entity, user, params) } - service.execute - end - - context 'when protecting a branch with a name that contains HTML tags' do - let(:name) { 'foo<b>bar<\b>' } + describe '#execute' do + let(:name) { 'master' } it 'creates a new protected branch' do expect { service.execute }.to change(ProtectedBranch, :count).by(1) - expect(project.protected_branches.last.name).to eq(name) + expect(entity.protected_branches.last.push_access_levels.map(&:access_level)).to match_array([Gitlab::Access::MAINTAINER]) + expect(entity.protected_branches.last.merge_access_levels.map(&:access_level)).to match_array([Gitlab::Access::MAINTAINER]) end - end - context 'when user does not have permission' do - let(:user) { create(:user) } + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end - before do - project.add_developer(user) + service.execute end - it 'creates a new protected branch if we skip authorization step' do - expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1) + context 'when protecting a branch with a name that contains HTML tags' do + let(:name) { 'foo<b>bar<\b>' } + + it 'creates a new protected branch' do + expect { service.execute }.to change(ProtectedBranch, :count).by(1) + expect(entity.protected_branches.last.name).to eq(name) + end end - it 'raises Gitlab::Access:AccessDeniedError' do - expect { service.execute }.to raise_error(Gitlab::Access::AccessDeniedError) + context 'when a policy restricts rule creation' do + it "prevents creation of the protected branch rule" do + disallow(:create_protected_branch, an_instance_of(ProtectedBranch)) + + expect do + service.execute + end.to raise_error(Gitlab::Access::AccessDeniedError) + end + + it 'creates a new protected branch if we skip authorization step' do + expect { service.execute(skip_authorization: true) }.to change(ProtectedBranch, :count).by(1) + end end end + end - context 'when a policy restricts rule creation' do - it "prevents creation of the protected branch rule" do - disallow(:create_protected_branch, an_instance_of(ProtectedBranch)) + context 'with entity project' do + let_it_be_with_reload(:entity) { create(:project) } + let(:user) { entity.first_owner } - expect do - service.execute - end.to raise_error(Gitlab::Access::AccessDeniedError) - end + it_behaves_like 'execute with entity' + end + + context 'with entity group' do + let_it_be_with_reload(:entity) { create(:group) } + let_it_be_with_reload(:user) { create(:user) } + + before do + allow(Ability).to receive(:allowed?).with(user, :create_protected_branch, instance_of(ProtectedBranch)).and_return(true) end + + it_behaves_like 'execute with entity' end def disallow(ability, protected_branch) diff --git a/spec/services/protected_branches/destroy_service_spec.rb b/spec/services/protected_branches/destroy_service_spec.rb index 123deeea005..421d4aae5bb 100644 --- a/spec/services/protected_branches/destroy_service_spec.rb +++ b/spec/services/protected_branches/destroy_service_spec.rb @@ -3,37 +3,54 @@ require 'spec_helper' RSpec.describe ProtectedBranches::DestroyService do - let_it_be_with_reload(:project) { create(:project) } + shared_examples 'execute with entity' do + subject(:service) { described_class.new(entity, user) } - let!(:protected_branch) { create(:protected_branch, project: project) } - let(:user) { project.first_owner } + describe '#execute' do + it 'destroys a protected branch' do + service.execute(protected_branch) - subject(:service) { described_class.new(project, user) } - - describe '#execute' do - it 'destroys a protected branch' do - service.execute(protected_branch) + expect(protected_branch).to be_destroyed + end - expect(protected_branch).to be_destroyed - end + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end - it 'refreshes the cache' do - expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| - expect(cache_service).to receive(:refresh) + service.execute(protected_branch) end - service.execute(protected_branch) + context 'when a policy restricts rule deletion' do + it "prevents deletion of the protected branch rule" do + disallow(:destroy_protected_branch, protected_branch) + + expect do + service.execute(protected_branch) + end.to raise_error(Gitlab::Access::AccessDeniedError) + end + end end + end - context 'when a policy restricts rule deletion' do - it "prevents deletion of the protected branch rule" do - disallow(:destroy_protected_branch, protected_branch) + context 'with entity project' do + let_it_be_with_reload(:entity) { create(:project) } + let!(:protected_branch) { create(:protected_branch, project: entity) } + let(:user) { entity.first_owner } - expect do - service.execute(protected_branch) - end.to raise_error(Gitlab::Access::AccessDeniedError) - end + it_behaves_like 'execute with entity' + end + + context 'with entity group' do + let_it_be_with_reload(:entity) { create(:group) } + let_it_be_with_reload(:user) { create(:user) } + let!(:protected_branch) { create(:protected_branch, group: entity, project: nil) } + + before do + allow(Ability).to receive(:allowed?).with(user, :destroy_protected_branch, protected_branch).and_return(true) end + + it_behaves_like 'execute with entity' end def disallow(ability, protected_branch) diff --git a/spec/services/protected_branches/update_service_spec.rb b/spec/services/protected_branches/update_service_spec.rb index 2ff6c3c489a..c70cc032a6a 100644 --- a/spec/services/protected_branches/update_service_spec.rb +++ b/spec/services/protected_branches/update_service_spec.rb @@ -3,54 +3,64 @@ require 'spec_helper' RSpec.describe ProtectedBranches::UpdateService do - let_it_be_with_reload(:project) { create(:project) } + shared_examples 'execute with entity' do + let(:params) { { name: new_name } } - let!(:protected_branch) { create(:protected_branch, project: project) } - let(:user) { project.first_owner } - let(:params) { { name: new_name } } + subject(:service) { described_class.new(entity, user, params) } - subject(:service) { described_class.new(project, user, params) } + describe '#execute' do + let(:new_name) { 'new protected branch name' } + let(:result) { service.execute(protected_branch) } - describe '#execute' do - let(:new_name) { 'new protected branch name' } - let(:result) { service.execute(protected_branch) } + it 'updates a protected branch' do + expect(result.reload.name).to eq(params[:name]) + end - it 'updates a protected branch' do - expect(result.reload.name).to eq(params[:name]) - end + it 'refreshes the cache' do + expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| + expect(cache_service).to receive(:refresh) + end - it 'refreshes the cache' do - expect_next_instance_of(ProtectedBranches::CacheService) do |cache_service| - expect(cache_service).to receive(:refresh) + result end - result - end - - context 'when updating name of a protected branch to one that contains HTML tags' do - let(:new_name) { 'foo<b>bar<\b>' } - let(:result) { service.execute(protected_branch) } + context 'when updating name of a protected branch to one that contains HTML tags' do + let(:new_name) { 'foo<b>bar<\b>' } + let(:result) { service.execute(protected_branch) } - it 'updates a protected branch' do - expect(result.reload.name).to eq(new_name) + it 'updates a protected branch' do + expect(result.reload.name).to eq(new_name) + end end - end - context 'without admin_project permissions' do - let(:user) { create(:user) } + context 'when a policy restricts rule update' do + it "prevents update of the protected branch rule" do + disallow(:update_protected_branch, protected_branch) - it "raises error" do - expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) + end end end + end - context 'when a policy restricts rule update' do - it "prevents update of the protected branch rule" do - disallow(:update_protected_branch, protected_branch) + context 'with entity project' do + let_it_be_with_reload(:entity) { create(:project) } + let!(:protected_branch) { create(:protected_branch, project: entity) } + let(:user) { entity.first_owner } - expect { service.execute(protected_branch) }.to raise_error(Gitlab::Access::AccessDeniedError) - end + it_behaves_like 'execute with entity' + end + + context 'with entity group' do + let_it_be_with_reload(:entity) { create(:group) } + let_it_be_with_reload(:user) { create(:user) } + let!(:protected_branch) { create(:protected_branch, group: entity, project: nil) } + + before do + allow(Ability).to receive(:allowed?).with(user, :update_protected_branch, protected_branch).and_return(true) end + + it_behaves_like 'execute with entity' end def disallow(ability, protected_branch) |