diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-05-19 10:33:21 +0300 |
commit | 36a59d088eca61b834191dacea009677a96c052f (patch) | |
tree | e4f33972dab5d8ef79e3944a9f403035fceea43f /spec/support/shared_examples | |
parent | a1761f15ec2cae7c7f7bbda39a75494add0dfd6f (diff) |
Add latest changes from gitlab-org/gitlab@15-0-stable-eev15.0.0-rc42
Diffstat (limited to 'spec/support/shared_examples')
45 files changed, 1186 insertions, 538 deletions
diff --git a/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb new file mode 100644 index 00000000000..db724dcfe99 --- /dev/null +++ b/spec/support/shared_examples/ci/log_downstream_pipeline_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'logs downstream pipeline creation' do + def record_downstream_pipeline_logs + logs = [] + allow(::Gitlab::AppLogger).to receive(:info) do |args| + logs << args + end + + yield + + logs.find { |log| log[:message] == "downstream pipeline created" } + end + + it 'logs details' do + pipeline = nil + + log_entry = record_downstream_pipeline_logs do + pipeline = subject + end + + expect(log_entry).to be_present + expect(log_entry).to eq( + message: "downstream pipeline created", + class: described_class.name, + root_pipeline_id: expected_root_pipeline.id, + downstream_pipeline_id: pipeline.id, + downstream_pipeline_relationship: expected_downstream_relationship, + hierarchy_size: expected_hierarchy_size, + root_pipeline_plan: expected_root_pipeline.project.actual_plan_name, + root_pipeline_namespace_path: expected_root_pipeline.project.namespace.full_path, + root_pipeline_project_path: expected_root_pipeline.project.full_path) + end +end diff --git a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb index c6e880635aa..a79b94209f3 100644 --- a/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/environments_controller_shared_examples.rb @@ -65,3 +65,20 @@ RSpec.shared_examples 'failed response for #cancel_auto_stop' do end end end + +RSpec.shared_examples 'avoids N+1 queries on environment detail page' do + render_views + + before do + create_deployment_with_associations(sequence: 0) + end + + it 'avoids N+1 queries' do + control = ActiveRecord::QueryRecorder.new { get :show, params: environment_params } + + create_deployment_with_associations(sequence: 1) + create_deployment_with_associations(sequence: 2) + + expect { get :show, params: environment_params }.not_to exceed_query_limit(control.count).with_threshold(34) + end +end diff --git a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb index fadf428125a..9cf35325202 100644 --- a/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb +++ b/spec/support/shared_examples/controllers/repository_lfs_file_load_shared_examples.rb @@ -44,8 +44,10 @@ RSpec.shared_examples 'a controller that can serve LFS files' do |options = {}| expect(controller).to receive(:send_file) .with( File.join(lfs_uploader.root, lfs_uploader.store_dir, lfs_uploader.filename), - filename: filename, - disposition: 'attachment') + { + filename: filename, + disposition: 'attachment' + }) subject diff --git a/spec/support/shared_examples/features/content_editor_shared_examples.rb b/spec/support/shared_examples/features/content_editor_shared_examples.rb index 5c44cb7f04b..c93d8e3d511 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -23,6 +23,8 @@ RSpec.shared_examples 'edits content using the content editor' do describe 'code block bubble menu' do it 'shows a code block bubble menu for a code block' do + find(content_editor_testid).send_keys [:enter, :enter] + find(content_editor_testid).send_keys '```js ' # trigger input rule find(content_editor_testid).send_keys 'var a = 0' find(content_editor_testid).send_keys [:shift, :left] @@ -32,6 +34,8 @@ RSpec.shared_examples 'edits content using the content editor' do end it 'sets code block type to "javascript" for `js`' do + find(content_editor_testid).send_keys [:enter, :enter] + find(content_editor_testid).send_keys '```js ' find(content_editor_testid).send_keys 'var a = 0' @@ -39,6 +43,8 @@ RSpec.shared_examples 'edits content using the content editor' do end it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do + find(content_editor_testid).send_keys [:enter, :enter] + find(content_editor_testid).send_keys '```nomnoml ' find(content_editor_testid).send_keys 'test' diff --git a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb index 5d1488502d2..6fd844f0e5f 100644 --- a/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb +++ b/spec/support/shared_examples/features/dependency_proxy_shared_examples.rb @@ -17,7 +17,7 @@ end RSpec.shared_examples 'a successful manifest pull' do it 'sends a file' do - expect(controller).to receive(:send_file).with(manifest.file.path, type: manifest.content_type) + expect(controller).to receive(:send_file).with(manifest.file.path, { type: manifest.content_type }) subject end diff --git a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb index ccd063faac4..2fff4137934 100644 --- a/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb +++ b/spec/support/shared_examples/features/editable_merge_request_shared_examples.rb @@ -48,7 +48,7 @@ RSpec.shared_examples 'an editable merge request' do end page.within '.reviewer' do - expect(page).to have_content user.username + expect(page).to have_content user.name end page.within '.milestone' do diff --git a/spec/support/shared_examples/features/inviting_groups_shared_examples.rb b/spec/support/shared_examples/features/inviting_groups_shared_examples.rb new file mode 100644 index 00000000000..4921676a065 --- /dev/null +++ b/spec/support/shared_examples/features/inviting_groups_shared_examples.rb @@ -0,0 +1,144 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'inviting groups search results' do + context 'with instance admin considerations' do + let_it_be(:group_to_invite) { create(:group) } + + context 'when user is an admin' do + let_it_be(:admin) { create(:admin) } + + before do + sign_in(admin) + gitlab_enable_admin_mode_sign_in(admin) + end + + it 'shows groups where the admin has no direct membership' do + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_to_have_group(group_to_invite) + expect_not_to_have_group(group) + end + end + + it 'shows groups where the admin has at least guest level membership' do + group_to_invite.add_guest(admin) + + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_to_have_group(group_to_invite) + expect_not_to_have_group(group) + end + end + end + + context 'when user is not an admin' do + before do + group.add_owner(user) + sign_in(user) + end + + it 'does not show groups where the user has no direct membership' do + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_not_to_have_group(group_to_invite) + expect_not_to_have_group(group) + end + end + + it 'shows groups where the user has at least guest level membership' do + group_to_invite.add_guest(user) + + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_to_have_group(group_to_invite) + expect_not_to_have_group(group) + end + end + end + end + + context 'when user is not an admin and there are hierarchy considerations' do + let_it_be(:group_outside_hierarchy) { create(:group) } + + before_all do + group.add_owner(user) + group_within_hierarchy.add_owner(user) + group_outside_hierarchy.add_owner(user) + end + + before do + sign_in(user) + end + + it 'does not show self or ancestors', :aggregate_failures do + group_sibling = create(:group, parent: group) + group_sibling.add_owner(user) + + visit members_page_path_within_hierarchy + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_to_have_group(group_outside_hierarchy) + expect_to_have_group(group_sibling) + expect_not_to_have_group(group) + expect_not_to_have_group(group_within_hierarchy) + end + end + + context 'when sharing with groups outside the hierarchy is enabled' do + it 'shows groups within and outside the hierarchy in search results' do + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + wait_for_requests + + page.within(group_dropdown_selector) do + expect_to_have_group(group_within_hierarchy) + expect_to_have_group(group_outside_hierarchy) + end + end + end + + context 'when sharing with groups outside the hierarchy is disabled' do + before do + group.update!(prevent_sharing_groups_outside_hierarchy: true) + end + + it 'shows only groups within the hierarchy in search results' do + visit members_page_path + + click_on 'Invite a group' + click_on 'Select a group' + + page.within(group_dropdown_selector) do + expect_to_have_group(group_within_hierarchy) + expect_not_to_have_group(group_outside_hierarchy) + end + end + end + end +end diff --git a/spec/support/shared_examples/features/manage_applications_shared_examples.rb b/spec/support/shared_examples/features/manage_applications_shared_examples.rb index 3a8267b21da..442264e7ae4 100644 --- a/spec/support/shared_examples/features/manage_applications_shared_examples.rb +++ b/spec/support/shared_examples/features/manage_applications_shared_examples.rb @@ -9,11 +9,9 @@ RSpec.shared_examples 'manage applications' do visit new_application_path expect(page).to have_content 'Add new application' - expect(find('#doorkeeper_application_expire_access_tokens')).to be_checked fill_in :doorkeeper_application_name, with: application_name fill_in :doorkeeper_application_redirect_uri, with: application_redirect_uri - uncheck :doorkeeper_application_expire_access_tokens check :doorkeeper_application_scopes_read_user click_on 'Save application' @@ -25,8 +23,6 @@ RSpec.shared_examples 'manage applications' do click_on 'Edit' - expect(find('#doorkeeper_application_expire_access_tokens')).not_to be_checked - application_name_changed = "#{application_name} changed" fill_in :doorkeeper_application_name, with: application_name_changed diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb index 9d023d9514a..4565108b5e4 100644 --- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button it "#{action} a MR with multiple assignees", :js do find('.js-assignee-search').click page.within '.dropdown-menu-user' do - click_link user.name + click_link user.name unless action == 'creates' click_link user2.name end diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb index bbde448a1a1..a44a699c878 100644 --- a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees widget merge request' do |action, save it "#{action} a MR with multiple assignees", :js do find('.js-assignee-search').click page.within '.dropdown-menu-user' do - click_link user.name + click_link user.name unless action == 'creates' click_link user2.name end diff --git a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb index ad6ca3e1900..48cde90bd9b 100644 --- a/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_reviewers_mr_shared_examples.rb @@ -40,7 +40,7 @@ RSpec.shared_examples 'multiple reviewers merge request' do |action, save_button # Closing dropdown to persist click_link 'Edit' - expect(page).to have_content user2.username + expect(page).to have_content user2.name end end end diff --git a/spec/support/shared_examples/features/packages_shared_examples.rb b/spec/support/shared_examples/features/packages_shared_examples.rb index ded30f32314..323bd4f5171 100644 --- a/spec/support/shared_examples/features/packages_shared_examples.rb +++ b/spec/support/shared_examples/features/packages_shared_examples.rb @@ -97,9 +97,9 @@ def click_sort_option(option, ascending) wait_for_requests end - find('button.gl-dropdown-toggle').click + find('[data-testid="registry-sort-dropdown"]').click - page.within('.dropdown-menu') do + page.within('[data-testid="registry-sort-dropdown"] .dropdown-menu') do click_button option end diff --git a/spec/support/shared_examples/features/sidebar_shared_examples.rb b/spec/support/shared_examples/features/sidebar_shared_examples.rb index 11d216ff4b6..af3ea0600a2 100644 --- a/spec/support/shared_examples/features/sidebar_shared_examples.rb +++ b/spec/support/shared_examples/features/sidebar_shared_examples.rb @@ -108,7 +108,11 @@ RSpec.shared_examples 'issue boards sidebar' do wait_for_requests - expect(page).to have_content('This issue is confidential') + expect(page).to have_content( + _('Only project members with at least' \ + ' Reporter role can view or be' \ + ' notified about this issue.') + ) end end end diff --git a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb index 41b1964cff0..8081c51577a 100644 --- a/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_creates_wiki_page_shared_examples.rb @@ -233,6 +233,23 @@ RSpec.shared_examples 'User creates wiki page' do .and have_content("Last edited by #{user.name}") .and have_content("My awesome wiki!") end + + context 'when a server side validation error is returned' do + it "still displays edit form", :js do + click_link("New page") + + page.within(".wiki-form") do + fill_in(:wiki_title, with: "home") + fill_in(:wiki_content, with: "My awesome home page!") + end + + # Submits page with a name already in use to trigger a validation error + click_button("Create page") + + expect(page).to have_field(:wiki_title) + expect(page).to have_field(:wiki_content) + end + end end it "shows the emoji autocompletion dropdown", :js do diff --git a/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb new file mode 100644 index 00000000000..b989dbc6524 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/incident_management_timeline_events_shared_examples.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# Requres: +# * subject with a 'resolve' name +# * Defined expected timeline event via `let(:expected_timeline_event) { instance_double(...) }` +RSpec.shared_examples 'creating an incident timeline event' do + it 'creates a timeline event' do + expect { resolve }.to change(IncidentManagement::TimelineEvent, :count).by(1) + end + + it 'responds with a timeline event', :aggregate_failures do + response = resolve + timeline_event = IncidentManagement::TimelineEvent.last! + + expect(response).to match(timeline_event: timeline_event, errors: be_empty) + + expect(timeline_event.promoted_from_note).to eq(expected_timeline_event.promoted_from_note) + expect(timeline_event.note).to eq(expected_timeline_event.note) + expect(timeline_event.occurred_at.to_s).to eq(expected_timeline_event.occurred_at) + expect(timeline_event.incident).to eq(expected_timeline_event.incident) + expect(timeline_event.author).to eq(expected_timeline_event.author) + end +end + +# Requres +# * subject with a 'resolve' name +# * a user factory with a 'current_user' name +RSpec.shared_examples 'failing to create an incident timeline event' do + context 'when a user has no permissions to create timeline event' do + before do + project.add_guest(current_user) + end + + it 'raises an error' do + expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) + end + end +end + +# Requres: +# * subject with a 'resolve' name +RSpec.shared_examples 'responding with an incident timeline errors' do |errors:| + it 'returns errors' do + expect(resolve).to eq(timeline_event: nil, errors: errors) + end +end diff --git a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb index 3d6fec85490..da8562161e7 100644 --- a/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb +++ b/spec/support/shared_examples/graphql/resolvers/packages_resolvers_shared_examples.rb @@ -4,7 +4,11 @@ RSpec.shared_examples 'group and projects packages resolver' do context 'without sort' do let_it_be(:npm_package) { create(:package, project: project) } - it { is_expected.to contain_exactly(npm_package) } + it 'returns the proper packages' do + expect(::Packages::Package).not_to receive(:preload_pipelines) + + expect(subject).to contain_exactly(npm_package) + end end context 'with sorting and filtering' do diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index 37a805902a9..6d6e7b761f6 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -101,7 +101,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| context 'when sorting' do it 'sorts correctly' do - expect(results).to eq all_records + expect(results).to match all_records end context 'when paginating' do @@ -110,17 +110,17 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| let(:rest) { all_records.drop(first_param) } it 'paginates correctly' do - expect(results).to eq first_page + expect(results).to match first_page fwds = pagination_query(sort_argument.merge(after: end_cursor)) post_graphql(fwds, current_user: current_user) - expect(results).to eq rest + expect(results).to match rest bwds = pagination_query(sort_argument.merge(before: start_cursor)) post_graphql(bwds, current_user: current_user) - expect(results).to eq first_page + expect(results).to match first_page end end @@ -130,7 +130,7 @@ RSpec.shared_examples 'sorted paginated query' do |conditions = {}| it 'fetches last elements without error' do post_graphql(pagination_query(params), current_user: current_user) - expect(results.first).to eq(all_records.last) + expect(results.first).to match all_records.last end end end diff --git a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb index 3caf153c2fa..cf9c36fafe8 100644 --- a/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb +++ b/spec/support/shared_examples/graphql/types/gitlab_style_deprecations_shared_examples.rb @@ -6,7 +6,7 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect { subject(deprecation_reason: 'foo') }.to raise_error( ArgumentError, 'Use `deprecated` property instead of `deprecation_reason`. ' \ - 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-fields-arguments-and-enum-values' + 'See https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#deprecating-schema-items' ) end diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index a3c67210a4a..e886ec65b02 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -796,8 +796,8 @@ RSpec.shared_examples 'trace with enabled live trace feature' do end end - describe '#archived_trace_exist?' do - subject { trace.archived_trace_exist? } + describe '#archived?' do + subject { trace.archived? } context 'when trace does not exist' do it { is_expected.to be_falsy } diff --git a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb index bea7cca2744..beec072e474 100644 --- a/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb +++ b/spec/support/shared_examples/lib/gitlab/cycle_analytics/deployment_metrics.rb @@ -62,8 +62,8 @@ shared_examples 'deployment metrics examples' do describe '#deployment_frequency' do subject { stage_summary.fourth[:value] } - it 'includes the unit: `per day`' do - expect(stage_summary.fourth[:unit]).to eq _('per day') + it 'includes the unit: `/day`' do + expect(stage_summary.fourth[:unit]).to eq _('/day') end before do diff --git a/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb new file mode 100644 index 00000000000..67d739b79ab --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/database/reestablished_connection_stack_shared_examples.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +RSpec.shared_context 'reconfigures connection stack' do |db_config_name| + before do + skip_if_multiple_databases_not_setup + + # Due to lib/gitlab/database/load_balancing/configuration.rb:92 requiring RequestStore + # we cannot use stub_feature_flags(force_no_sharing_primary_model: true) + Gitlab::Database.database_base_models.each do |_, model_class| + allow(model_class.load_balancer.configuration).to receive(:use_dedicated_connection?).and_return(true) + end + + ActiveRecord::Base.establish_connection(db_config_name.to_sym) # rubocop:disable Database/EstablishConnection + + expect(Gitlab::Database.db_config_name(ActiveRecord::Base.connection)) # rubocop:disable Database/MultipleDatabases + .to eq(db_config_name) + end + + around do |example| + with_reestablished_active_record_base do + example.run + end + end + + def validate_connections! + model_connections = Gitlab::Database.database_base_models.to_h do |db_config_name, model_class| + [model_class, Gitlab::Database.db_config_name(model_class.connection)] + end + + expect(model_connections).to eq(Gitlab::Database.database_base_models.invert) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb deleted file mode 100644 index 2633a89eeee..00000000000 --- a/spec/support/shared_examples/lib/gitlab/kubernetes/network_policy_common_shared_examples.rb +++ /dev/null @@ -1,162 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'network policy common specs' do - let(:name) { 'example-name' } - let(:namespace) { 'example-namespace' } - let(:labels) { nil } - - describe '#generate' do - subject { policy.generate } - - it { is_expected.to eq(Kubeclient::Resource.new(policy.resource)) } - end - - describe 'as_json' do - let(:json_policy) do - { - name: name, - namespace: namespace, - creation_timestamp: nil, - manifest: YAML.dump(policy.resource.deep_stringify_keys), - is_autodevops: false, - is_enabled: true, - environment_ids: [] - } - end - - subject { policy.as_json } - - it { is_expected.to eq(json_policy) } - end - - describe 'autodevops?' do - subject { policy.autodevops? } - - let(:labels) { { chart: chart } } - let(:chart) { nil } - - it { is_expected.to be false } - - context 'with non-autodevops chart' do - let(:chart) { 'foo' } - - it { is_expected.to be false } - end - - context 'with autodevops chart' do - let(:chart) { 'auto-deploy-app-0.6.0' } - - it { is_expected.to be true } - end - end - - describe 'enabled?' do - subject { policy.enabled? } - - let(:selector) { nil } - - it { is_expected.to be true } - - context 'with empty selector' do - let(:selector) { {} } - - it { is_expected.to be true } - end - - context 'with nil matchLabels in selector' do - let(:selector) { { matchLabels: nil } } - - it { is_expected.to be true } - end - - context 'with empty matchLabels in selector' do - let(:selector) { { matchLabels: {} } } - - it { is_expected.to be true } - end - - context 'with disabled_by label in matchLabels in selector' do - let(:selector) do - { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } - end - - it { is_expected.to be false } - end - end - - describe 'enable' do - subject { policy.enabled? } - - let(:selector) { nil } - - before do - policy.enable - end - - it { is_expected.to be true } - - context 'with empty selector' do - let(:selector) { {} } - - it { is_expected.to be true } - end - - context 'with nil matchLabels in selector' do - let(:selector) { { matchLabels: nil } } - - it { is_expected.to be true } - end - - context 'with empty matchLabels in selector' do - let(:selector) { { matchLabels: {} } } - - it { is_expected.to be true } - end - - context 'with disabled_by label in matchLabels in selector' do - let(:selector) do - { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } - end - - it { is_expected.to be true } - end - end - - describe 'disable' do - subject { policy.enabled? } - - let(:selector) { nil } - - before do - policy.disable - end - - it { is_expected.to be false } - - context 'with empty selector' do - let(:selector) { {} } - - it { is_expected.to be false } - end - - context 'with nil matchLabels in selector' do - let(:selector) { { matchLabels: nil } } - - it { is_expected.to be false } - end - - context 'with empty matchLabels in selector' do - let(:selector) { { matchLabels: {} } } - - it { is_expected.to be false } - end - - context 'with disabled_by label in matchLabels in selector' do - let(:selector) do - { matchLabels: { Gitlab::Kubernetes::NetworkPolicyCommon::DISABLED_BY_LABEL => 'gitlab' } } - end - - it { is_expected.to be false } - end - end -end diff --git a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb new file mode 100644 index 00000000000..d4986975f03 --- /dev/null +++ b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'merge request author auto assign' do + it 'populates merge request author as assignee' do + expect(find('.js-assignee-search')).to have_content(user.name) + expect(page).not_to have_content 'Assign yourself' + end +end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index e6b270c6188..fa10b03fa90 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -199,7 +199,7 @@ RSpec.shared_examples "chat integration" do |integration_name| { title: "Awesome wiki_page", content: "Some text describing some thing or another", - format: "md", + format: :markdown, message: "user created page: Awesome wiki_page" } end diff --git a/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb new file mode 100644 index 00000000000..873f858e432 --- /dev/null +++ b/spec/support/shared_examples/models/concerns/integrations/reset_secret_fields_shared_examples.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +RSpec.shared_examples Integrations::ResetSecretFields do + describe '#exposing_secrets_fields' do + it 'returns an array of strings' do + expect(integration.exposing_secrets_fields).to be_a(Array) + expect(integration.exposing_secrets_fields).to all(be_a(String)) + end + end + + describe '#reset_secret_fields?' do + let(:exposing_fields) { integration.exposing_secrets_fields } + + it 'returns false if no exposing field has changed' do + exposing_fields.each do |field| + allow(integration).to receive("#{field}_changed?").and_return(false) + end + + expect(integration.send(:reset_secret_fields?)).to be(false) + end + + it 'returns true if any exposing field has changed' do + exposing_fields.each do |field| + allow(integration).to receive("#{field}_changed?").and_return(true) + + other_exposing_fields = exposing_fields.without(field) + other_exposing_fields.each do |other_field| + allow(integration).to receive("#{other_field}_changed?").and_return(false) + end + + expect(integration.send(:reset_secret_fields?)).to be(true) + end + end + end + + describe 'validation callback' do + before do + # Store a value in each password field + integration.secret_fields.each do |field| + integration.public_send("#{field}=", 'old value') + end + + # Treat values as persisted + integration.reset_updated_properties + integration.instance_variable_set('@old_data_fields', nil) if integration.supports_data_fields? + end + + context 'when an exposing field has changed' do + let(:exposing_field) { integration.exposing_secrets_fields.first } + + before do + integration.public_send("#{exposing_field}=", 'new value') + end + + it 'clears all secret fields' do + integration.valid? + + integration.secret_fields.each do |field| + expect(integration.public_send(field)).to be_nil + expect(integration.properties[field]).to be_nil if integration.properties.present? + expect(integration.data_fields[field]).to be_nil if integration.supports_data_fields? + end + end + + context 'when a secret field has been updated' do + let(:secret_field) { integration.secret_fields.first } + let(:other_secret_fields) { integration.secret_fields.without(secret_field) } + let(:new_value) { 'new value' } + + before do + integration.public_send("#{secret_field}=", new_value) + end + + it 'does not clear this secret field' do + integration.valid? + + expect(integration.public_send(secret_field)).to eq('new value') + + other_secret_fields.each do |field| + expect(integration.public_send(field)).to be_nil + end + end + + context 'when a secret field has been updated with the same value' do + let(:new_value) { 'old value' } + + it 'does not clear this secret field' do + integration.valid? + + expect(integration.public_send(secret_field)).to eq('old value') + + other_secret_fields.each do |field| + expect(integration.public_send(field)).to be_nil + end + end + end + end + end + + context 'when no exposing field has changed' do + it 'does not clear any secret fields' do + integration.valid? + + integration.secret_fields.each do |field| + expect(integration.public_send(field)).to eq('old value') + end + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index da5c35c970a..2e062cda4e9 100644 --- a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb @@ -45,9 +45,33 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end it "notifies about #{event_type} events" do + expect(chat_integration).not_to receive(:log_error) + chat_integration.execute(data) + expect(WebMock).to have_requested(:post, stubbed_resolved_hostname) end + + context 'when the response is not successful' do + let!(:stubbed_resolved_hostname) do + stub_full_request(webhook_url, method: :post) + .to_return(status: 409, body: 'error message') + .request_pattern.uri_pattern.to_s + end + + it 'logs an error' do + expect(chat_integration).to receive(:log_error).with( + 'SlackMattermostNotifier HTTP error response', + request_host: 'example.gitlab.com', + response_code: 409, + response_body: 'error message' + ) + + chat_integration.execute(data) + + expect(WebMock).to have_requested(:post, stubbed_resolved_hostname) + end + end end shared_examples "untriggered #{integration_name} integration" do |event_type: nil, branches_to_be_notified: nil| @@ -59,8 +83,9 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name stub_full_request(webhook_url, method: :post).request_pattern.uri_pattern.to_s end - it "notifies about #{event_type} events" do + it "does not notify about #{event_type} events" do chat_integration.execute(data) + expect(WebMock).not_to have_requested(:post, stubbed_resolved_hostname) end end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index a329a6dca91..e293d10964b 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -77,312 +77,309 @@ RSpec.shared_examples '#valid_level_roles' do |entity_name| end RSpec.shared_examples_for "member creation" do - let_it_be(:user) { create(:user) } let_it_be(:admin) { create(:admin) } - describe '#execute' do - it 'returns a Member object', :aggregate_failures do - member = described_class.new(source, user, :maintainer).execute - - expect(member).to be_a member_type - expect(member).to be_persisted - end + it 'returns a Member object', :aggregate_failures do + member = described_class.new(source, user, :maintainer).execute - context 'when adding a project_bot' do - let_it_be(:project_bot) { create(:user, :project_bot) } - - before_all do - source.add_owner(user) - end + expect(member).to be_a member_type + expect(member).to be_persisted + end - context 'when project_bot is already a member' do - before do - source.add_developer(project_bot) - end + context 'when adding a project_bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } - it 'does not update the member' do - member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + before_all do + source.add_owner(user) + end - expect(source.users.reload).to include(project_bot) - expect(member).to be_persisted - expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) - expect(member.errors.full_messages).to include(/not authorized to update member/) - end + context 'when project_bot is already a member' do + before do + source.add_developer(project_bot) end - context 'when project_bot is not already a member' do - it 'adds the member' do - member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + it 'does not update the member' do + member = described_class.new(source, project_bot, :maintainer, current_user: user).execute - expect(source.users.reload).to include(project_bot) - expect(member).to be_persisted - end + expect(source.users.reload).to include(project_bot) + expect(member).to be_persisted + expect(member.access_level).to eq(Gitlab::Access::DEVELOPER) + expect(member.errors.full_messages).to include(/not authorized to update member/) end end - context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do - it 'sets members.created_by to the given admin current_user' do - member = described_class.new(source, user, :maintainer, current_user: admin).execute + context 'when project_bot is not already a member' do + it 'adds the member' do + member = described_class.new(source, project_bot, :maintainer, current_user: user).execute + expect(source.users.reload).to include(project_bot) expect(member).to be_persisted - expect(source.users.reload).to include(user) - expect(member.created_by).to eq(admin) end end + end - context 'when admin mode is disabled' do - it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do - member = described_class.new(source, user, :maintainer, current_user: admin).execute + context 'when admin mode is enabled', :enable_admin_mode, :aggregate_failures do + it 'sets members.created_by to the given admin current_user' do + member = described_class.new(source, user, :maintainer, current_user: admin).execute - expect(member).not_to be_persisted - expect(source.users.reload).not_to include(user) - expect(member.errors.full_messages).to include(/not authorized to create member/) - end + expect(member).to be_persisted + expect(source.users.reload).to include(user) + expect(member.created_by).to eq(admin) end + end - it 'sets members.expires_at to the given expires_at' do - member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute + context 'when admin mode is disabled' do + it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do + member = described_class.new(source, user, :maintainer, current_user: admin).execute - expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + expect(member).not_to be_persisted + expect(source.users.reload).not_to include(user) + expect(member.errors.full_messages).to include(/not authorized to create member/) end + end - described_class.access_levels.each do |sym_key, int_access_level| - it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do - expect(source.users).not_to include(user) + it 'sets members.expires_at to the given expires_at' do + member = described_class.new(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)).execute - member = described_class.new(source, user.id, sym_key).execute + expect(member.expires_at).to eq(Date.new(2016, 9, 22)) + end - expect(member.access_level).to eq(int_access_level) - expect(source.users.reload).to include(user) - end + described_class.access_levels.each do |sym_key, int_access_level| + it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, sym_key).execute - it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + + it "accepts the #{int_access_level} integer as access level", :aggregate_failures do + expect(source.users).not_to include(user) + + member = described_class.new(source, user.id, int_access_level).execute + + expect(member.access_level).to eq(int_access_level) + expect(source.users.reload).to include(user) + end + end + + context 'with no current_user' do + context 'when called with a known user id' do + it 'adds the user as a member' do expect(source.users).not_to include(user) - member = described_class.new(source, user.id, int_access_level).execute + described_class.new(source, user.id, :maintainer).execute - expect(member.access_level).to eq(int_access_level) expect(source.users.reload).to include(user) end end - context 'with no current_user' do - context 'when called with a known user id' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user id' do + it 'does not add the user as a member' do + expect(source.users).not_to include(user) - described_class.new(source, user.id, :maintainer).execute + described_class.new(source, non_existing_record_id, :maintainer).execute - expect(source.users.reload).to include(user) - end + expect(source.users.reload).not_to include(user) end + end - context 'when called with an unknown user id' do - it 'does not add the user as a member' do - expect(source.users).not_to include(user) + context 'when called with a user object' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) - described_class.new(source, non_existing_record_id, :maintainer).execute + described_class.new(source, user, :maintainer).execute - expect(source.users.reload).not_to include(user) - end + expect(source.users.reload).to include(user) + end + end + + context 'when called with a requester user object' do + before do + source.request_access(user) end - context 'when called with a user object' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + it 'adds the requester as a member', :aggregate_failures do + expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy + expect do described_class.new(source, user, :maintainer).execute + end.to raise_error(Gitlab::Access::AccessDeniedError) - expect(source.users.reload).to include(user) - end + expect(source.users.reload).not_to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_truthy end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end - - it 'adds the requester as a member', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + context 'when called with a known user email' do + it 'adds the user as a member' do + expect(source.users).not_to include(user) - expect do - described_class.new(source, user, :maintainer).execute - end.to raise_error(Gitlab::Access::AccessDeniedError) + described_class.new(source, user.email, :maintainer).execute - expect(source.users.reload).not_to include(user) - expect(source.requesters.reload.exists?(user_id: user)).to be_truthy - end + expect(source.users.reload).to include(user) end + end - context 'when called with a known user email' do - it 'adds the user as a member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user email' do + it 'creates an invited member' do + expect(source.users).not_to include(user) - described_class.new(source, user.email, :maintainer).execute + described_class.new(source, 'user@example.com', :maintainer).execute - expect(source.users.reload).to include(user) - end + expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') end + end - context 'when called with an unknown user email' do - it 'creates an invited member' do - expect(source.users).not_to include(user) + context 'when called with an unknown user email starting with a number' do + it 'creates an invited member', :aggregate_failures do + email_starting_with_number = "#{user.id}_email@example.com" - described_class.new(source, 'user@example.com', :maintainer).execute + described_class.new(source, email_starting_with_number, :maintainer).execute - expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') - end + expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) + expect(source.users.reload).not_to include(user) end + end + end - context 'when called with an unknown user email starting with a number' do - it 'creates an invited member', :aggregate_failures do - email_starting_with_number = "#{user.id}_email@example.com" + context 'when current_user can update member', :enable_admin_mode do + it 'creates the member' do + expect(source.users).not_to include(user) - described_class.new(source, email_starting_with_number, :maintainer).execute + described_class.new(source, user, :maintainer, current_user: admin).execute - expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) - expect(source.users.reload).not_to include(user) - end - end + expect(source.users.reload).to include(user) end - context 'when current_user can update member', :enable_admin_mode do - it 'creates the member' do + context 'when called with a requester user object' do + before do + source.request_access(user) + end + + it 'adds the requester as a member', :aggregate_failures do expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy described_class.new(source, user, :maintainer, current_user: admin).execute expect(source.users.reload).to include(user) + expect(source.requesters.reload.exists?(user_id: user)).to be_falsy end + end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end + context 'when current_user cannot update member' do + it 'does not create the member', :aggregate_failures do + expect(source.users).not_to include(user) - it 'adds the requester as a member', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + member = described_class.new(source, user, :maintainer, current_user: user).execute - described_class.new(source, user, :maintainer, current_user: admin).execute + expect(source.users.reload).not_to include(user) + expect(member).not_to be_persisted + end - expect(source.users.reload).to include(user) - expect(source.requesters.reload.exists?(user_id: user)).to be_falsy - end + context 'when called with a requester user object' do + before do + source.request_access(user) end - end - context 'when current_user cannot update member' do - it 'does not create the member', :aggregate_failures do + it 'does not destroy the requester', :aggregate_failures do expect(source.users).not_to include(user) + expect(source.requesters.exists?(user_id: user)).to be_truthy - member = described_class.new(source, user, :maintainer, current_user: user).execute + described_class.new(source, user, :maintainer, current_user: user).execute expect(source.users.reload).not_to include(user) - expect(member).not_to be_persisted + expect(source.requesters.exists?(user_id: user)).to be_truthy end + end + end - context 'when called with a requester user object' do - before do - source.request_access(user) - end + context 'when member already exists' do + before do + source.add_user(user, :developer) + end - it 'does not destroy the requester', :aggregate_failures do - expect(source.users).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer, current_user: user).execute + described_class.new(source, user, :maintainer).execute - expect(source.users.reload).not_to include(user) - expect(source.requesters.exists?(user_id: user)).to be_truthy - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) end end - context 'when member already exists' do - before do - source.add_user(user, :developer) - end - - context 'with no current_user' do - it 'updates the member' do - expect(source.users).to include(user) + context 'when current_user can update member', :enable_admin_mode do + it 'updates the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer).execute + described_class.new(source, user, :maintainer, current_user: admin).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) end + end - context 'when current_user can update member', :enable_admin_mode do - it 'updates the member' do - expect(source.users).to include(user) + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) - described_class.new(source, user, :maintainer, current_user: admin).execute + described_class.new(source, user, :maintainer, current_user: user).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) - end + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) end + end + end - context 'when current_user cannot update member' do - it 'does not update the member' do - expect(source.users).to include(user) + context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do + let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source } - described_class.new(source, user, :maintainer, current_user: user).execute + it 'creates a member_task with the correct attributes', :aggregate_failures do + described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) - end - end - end + member = source.members.last - context 'when `tasks_to_be_done` and `tasks_project_id` are passed' do - let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source } + expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect(member.member_task.project).to eq(task_project) + end - it 'creates a member_task with the correct attributes', :aggregate_failures do - described_class.new(source, user, :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id).execute + context 'with an already existing member' do + before do + source.add_user(user, :developer) + end - member = source.members.last + it 'does not update tasks to be done if tasks already exist', :aggregate_failures do + member = source.members.find_by(user_id: user.id) + create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) - expect(member.tasks_to_be_done).to match_array([:ci, :code]) + expect do + described_class.new(source, + user, + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id).execute + end.not_to change(MemberTask, :count) + + member.reset + expect(member.tasks_to_be_done).to match_array([:code, :ci]) expect(member.member_task.project).to eq(task_project) end - context 'with an already existing member' do - before do - source.add_user(user, :developer) - end - - it 'does not update tasks to be done if tasks already exist', :aggregate_failures do - member = source.members.find_by(user_id: user.id) - create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) - - expect do - described_class.new(source, - user, - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id).execute - end.not_to change(MemberTask, :count) - - member.reset - expect(member.tasks_to_be_done).to match_array([:code, :ci]) - expect(member.member_task.project).to eq(task_project) - end - - it 'adds tasks to be done if they do not exist', :aggregate_failures do - expect do - described_class.new(source, - user, - :developer, - tasks_to_be_done: %w(issues), - tasks_project_id: task_project.id).execute - end.to change(MemberTask, :count).by(1) - - member = source.members.find_by(user_id: user.id) - expect(member.tasks_to_be_done).to match_array([:issues]) - expect(member.member_task.project).to eq(task_project) - end + it 'adds tasks to be done if they do not exist', :aggregate_failures do + expect do + described_class.new(source, + user, + :developer, + tasks_to_be_done: %w(issues), + tasks_project_id: task_project.id).execute + end.to change(MemberTask, :count).by(1) + + member = source.members.find_by(user_id: user.id) + expect(member.tasks_to_be_done).to match_array([:issues]) + expect(member.member_task.project).to eq(task_project) end end end diff --git a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb b/spec/support/shared_examples/models/reviewer_state_shared_examples.rb deleted file mode 100644 index f1392768b06..00000000000 --- a/spec/support/shared_examples/models/reviewer_state_shared_examples.rb +++ /dev/null @@ -1,15 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'having reviewer state' do - describe 'mr_attention_requests feature flag is disabled' do - before do - stub_feature_flags(mr_attention_requests: false) - end - - it { is_expected.to have_attributes(state: 'unreviewed') } - end - - describe 'mr_attention_requests feature flag is enabled' do - it { is_expected.to have_attributes(state: 'attention_requested') } - end -end diff --git a/spec/support/shared_examples/models/wiki_shared_examples.rb b/spec/support/shared_examples/models/wiki_shared_examples.rb index 03e9dd65e33..6f17231a040 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -392,41 +392,161 @@ RSpec.shared_examples 'wiki model' do end describe '#create_page' do - it 'creates a new wiki page' do - expect(subject.create_page('test page', 'this is content')).not_to eq(false) - expect(subject.list_pages.count).to eq(1) - end + shared_examples 'create_page tests' do + it 'creates a new wiki page' do + expect(subject.create_page('test page', 'this is content')).not_to eq(false) + expect(subject.list_pages.count).to eq(1) + end - it 'returns false when a duplicate page exists' do - subject.create_page('test page', 'content') + it 'returns false when a duplicate page exists' do + subject.create_page('test page', 'content') - expect(subject.create_page('test page', 'content')).to eq(false) - end + expect(subject.create_page('test page', 'content')).to eq(false) + end - it 'stores an error message when a duplicate page exists' do - 2.times { subject.create_page('test page', 'content') } + it 'stores an error message when a duplicate page exists' do + 2.times { subject.create_page('test page', 'content') } - expect(subject.error_message).to match(/Duplicate page:/) - end + expect(subject.error_message).to match(/Duplicate page:/) + end + + it 'sets the correct commit message' do + subject.create_page('test page', 'some content', :markdown, 'commit message') + + expect(subject.list_pages.first.page.version.message).to eq('commit message') + end + + it 'sets the correct commit email' do + subject.create_page('test page', 'content') + + expect(user.commit_email).not_to eq(user.email) + expect(commit.author_email).to eq(user.commit_email) + expect(commit.committer_email).to eq(user.commit_email) + end + + it 'runs after_wiki_activity callbacks' do + expect(subject).to receive(:after_wiki_activity) - it 'sets the correct commit message' do - subject.create_page('test page', 'some content', :markdown, 'commit message') + subject.create_page('Test Page', 'This is content') + end + + it 'cannot create two pages with the same title but different format' do + subject.create_page('test page', 'content', :markdown) + subject.create_page('test page', 'content', :rdoc) + + expect(subject.error_message).to match(/Duplicate page:/) + end + + it 'cannot create two pages with the same title but different capitalization' do + subject.create_page('test page', 'content') + subject.create_page('Test page', 'content') + + expect(subject.error_message).to match(/Duplicate page:/) + end - expect(subject.list_pages.first.page.version.message).to eq('commit message') + it 'cannot create two pages with the same title, different capitalization, and different format' do + subject.create_page('test page', 'content') + subject.create_page('Test page', 'content', :rdoc) + + expect(subject.error_message).to match(/Duplicate page:/) + end end - it 'sets the correct commit email' do - subject.create_page('test page', 'content') + it_behaves_like 'create_page tests' do + it 'returns false if a page exists already in the repository', :aggregate_failures do + subject.create_page('test page', 'content') - expect(user.commit_email).not_to eq(user.email) - expect(commit.author_email).to eq(user.commit_email) - expect(commit.committer_email).to eq(user.commit_email) + allow(subject).to receive(:file_exists_by_regex?).and_return(false) + + expect(subject.create_page('test page', 'content')).to eq false + expect(subject.error_message).to match(/Duplicate page:/) + end + + it 'returns false if it has an invalid format', :aggregate_failures do + expect(subject.create_page('test page', 'content', :foobar)).to eq false + expect(subject.error_message).to match(/Invalid format selected/) + end + + using RSpec::Parameterized::TableSyntax + + where(:new_file, :format, :existing_repo_files, :success) do + 'foo' | :markdown | [] | true + 'foo' | :rdoc | [] | true + 'foo' | :asciidoc | [] | true + 'foo' | :org | [] | true + 'foo' | :textile | [] | false + 'foo' | :creole | [] | false + 'foo' | :rest | [] | false + 'foo' | :mediawiki | [] | false + 'foo' | :pod | [] | false + 'foo' | :plaintext | [] | false + 'foo' | :markdown | ['foo.md'] | false + 'foo' | :markdown | ['foO.md'] | false + 'foO' | :markdown | ['foo.md'] | false + 'foo' | :markdown | ['foo.mdfoo'] | true + 'foo' | :markdown | ['foo.markdown'] | false + 'foo' | :markdown | ['foo.mkd'] | false + 'foo' | :markdown | ['foo.mkdn'] | false + 'foo' | :markdown | ['foo.mdown'] | false + 'foo' | :markdown | ['foo.adoc'] | false + 'foo' | :markdown | ['foo.asciidoc'] | false + 'foo' | :markdown | ['foo.org'] | false + 'foo' | :markdown | ['foo.rdoc'] | false + 'foo' | :markdown | ['foo.textile'] | false + 'foo' | :markdown | ['foo.creole'] | false + 'foo' | :markdown | ['foo.rest'] | false + 'foo' | :markdown | ['foo.rest.txt'] | false + 'foo' | :markdown | ['foo.rst'] | false + 'foo' | :markdown | ['foo.rst.txt'] | false + 'foo' | :markdown | ['foo.rst.txtfoo'] | true + 'foo' | :markdown | ['foo.mediawiki'] | false + 'foo' | :markdown | ['foo.wiki'] | false + 'foo' | :markdown | ['foo.pod'] | false + 'foo' | :markdown | ['foo.txt'] | false + 'foo' | :markdown | ['foo.Md'] | false + 'foo' | :markdown | ['foo.jpg'] | true + 'foo' | :rdoc | ['foo.md'] | false + 'foo' | :rdoc | ['foO.md'] | false + 'foO' | :rdoc | ['foo.md'] | false + 'foo' | :asciidoc | ['foo.md'] | false + 'foo' | :org | ['foo.md'] | false + 'foo' | :markdown | ['dir/foo.md'] | true + '/foo' | :markdown | ['foo.md'] | false + './foo' | :markdown | ['foo.md'] | false + '../foo' | :markdown | ['foo.md'] | false + '../../foo' | :markdown | ['foo.md'] | false + '../../foo' | :markdown | ['dir/foo.md'] | true + 'dir/foo' | :markdown | ['foo.md'] | true + 'dir/foo' | :markdown | ['dir/foo.md'] | false + 'dir/foo' | :markdown | ['dir/foo.rdoc'] | false + '/dir/foo' | :markdown | ['dir/foo.rdoc'] | false + './dir/foo' | :markdown | ['dir/foo.rdoc'] | false + '../dir/foo' | :markdown | ['dir/foo.rdoc'] | false + '../dir/../foo' | :markdown | ['dir/foo.rdoc'] | true + '../dir/../foo' | :markdown | ['foo.rdoc'] | false + '../dir/../dir/foo' | :markdown | ['dir/foo.rdoc'] | false + '../dir/../another/foo' | :markdown | ['dir/foo.rdoc'] | true + 'another/dir/foo' | :markdown | ['dir/foo.md'] | true + 'foo bar' | :markdown | ['foo-bar.md'] | false + 'foo bar' | :markdown | ['foo-bar.md'] | true + 'föö'.encode('ISO-8859-1') | :markdown | ['f��.md'] | false + end + + with_them do + specify do + allow(subject.repository).to receive(:ls_files).and_return(existing_repo_files) + + expect(subject.create_page(new_file, 'content', format)).to eq success + end + end end - it 'runs after_wiki_activity callbacks' do - expect(subject).to receive(:after_wiki_activity) + context 'when feature flag :gitaly_replace_wiki_create_page is disabled' do + before do + stub_feature_flags(gitaly_replace_wiki_create_page: false) + end - subject.create_page('Test Page', 'This is content') + it_behaves_like 'create_page tests' end end @@ -452,7 +572,7 @@ RSpec.shared_examples 'wiki model' do expect(subject).to receive(:after_wiki_activity) expect(update_page).to eq true - page = subject.find_page(updated_title.presence || original_title) + page = subject.find_page(expected_title) expect(page.raw_content).to eq(updated_content) expect(page.path).to eq(expected_path) @@ -467,23 +587,25 @@ RSpec.shared_examples 'wiki model' do shared_context 'common examples' do using RSpec::Parameterized::TableSyntax - where(:original_title, :original_format, :updated_title, :updated_format, :expected_path) do - 'test page' | :markdown | 'new test page' | :markdown | 'new-test-page.md' - 'test page' | :markdown | 'test page' | :markdown | 'test-page.md' - 'test page' | :markdown | 'test page' | :asciidoc | 'test-page.asciidoc' + where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do + 'test page' | :markdown | 'new test page' | :markdown | 'new test page' | 'new-test-page.md' + 'test page' | :markdown | 'test page' | :markdown | 'test page' | 'test-page.md' + 'test page' | :markdown | 'test page' | :asciidoc | 'test page' | 'test-page.asciidoc' + + 'test page' | :markdown | 'new dir/new test page' | :markdown | 'new dir/new test page' | 'new-dir/new-test-page.md' + 'test page' | :markdown | 'new dir/test page' | :markdown | 'new dir/test page' | 'new-dir/test-page.md' - 'test page' | :markdown | 'new dir/new test page' | :markdown | 'new-dir/new-test-page.md' - 'test page' | :markdown | 'new dir/test page' | :markdown | 'new-dir/test-page.md' + 'test dir/test page' | :markdown | 'new dir/new test page' | :markdown | 'new dir/new test page' | 'new-dir/new-test-page.md' + 'test dir/test page' | :markdown | 'test dir/test page' | :markdown | 'test dir/test page' | 'test-dir/test-page.md' + 'test dir/test page' | :markdown | 'test dir/test page' | :asciidoc | 'test dir/test page' | 'test-dir/test-page.asciidoc' - 'test dir/test page' | :markdown | 'new dir/new test page' | :markdown | 'new-dir/new-test-page.md' - 'test dir/test page' | :markdown | 'test dir/test page' | :markdown | 'test-dir/test-page.md' - 'test dir/test page' | :markdown | 'test dir/test page' | :asciidoc | 'test-dir/test-page.asciidoc' + 'test dir/test page' | :markdown | 'new test page' | :markdown | 'new test page' | 'new-test-page.md' + 'test dir/test page' | :markdown | 'test page' | :markdown | 'test page' | 'test-page.md' - 'test dir/test page' | :markdown | 'new test page' | :markdown | 'new-test-page.md' - 'test dir/test page' | :markdown | 'test page' | :markdown | 'test-page.md' + 'test page' | :markdown | nil | :markdown | 'test page' | 'test-page.md' + 'test.page' | :markdown | nil | :markdown | 'test.page' | 'test.page.md' - 'test page' | :markdown | nil | :markdown | 'test-page.md' - 'test.page' | :markdown | nil | :markdown | 'test.page.md' + 'testpage' | :markdown | './testpage' | :markdown | 'testpage' | 'testpage.md' end end @@ -497,16 +619,23 @@ RSpec.shared_examples 'wiki model' do shared_context 'extended examples' do using RSpec::Parameterized::TableSyntax - where(:original_title, :original_format, :updated_title, :updated_format, :expected_path) do - 'test page' | :markdown | 'new test page' | :asciidoc | 'new-test-page.asciidoc' - 'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new-dir/new-test-page.asciidoc' - 'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new-dir/new-test-page.asciidoc' - 'test dir/test page' | :markdown | 'new test page' | :asciidoc | 'new-test-page.asciidoc' - 'test page' | :markdown | nil | :asciidoc | 'test-page.asciidoc' - 'test dir/test page' | :markdown | nil | :asciidoc | 'test-dir/test-page.asciidoc' - 'test dir/test page' | :markdown | nil | :markdown | 'test-dir/test-page.md' - 'test page' | :markdown | '' | :markdown | 'test-page.md' - 'test.page' | :markdown | '' | :markdown | 'test.page.md' + where(:original_title, :original_format, :updated_title, :updated_format, :expected_title, :expected_path) do + 'test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc' + 'test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc' + 'test dir/test page' | :markdown | 'new dir/new test page' | :asciidoc | 'new dir/new test page' | 'new-dir/new-test-page.asciidoc' + 'test dir/test page' | :markdown | 'new test page' | :asciidoc | 'new test page' | 'new-test-page.asciidoc' + 'test page' | :markdown | nil | :asciidoc | 'test page' | 'test-page.asciidoc' + 'test dir/test page' | :markdown | nil | :asciidoc | 'test dir/test page' | 'test-dir/test-page.asciidoc' + 'test dir/test page' | :markdown | nil | :markdown | 'test dir/test page' | 'test-dir/test-page.md' + 'test page' | :markdown | '' | :markdown | 'test page' | 'test-page.md' + 'test.page' | :markdown | '' | :markdown | 'test.page' | 'test.page.md' + 'testpage' | :markdown | '../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | 'dir/../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | './dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/../testpage' | :markdown | 'testpage' | 'testpage.md' + 'dir/testpage' | :markdown | '../dir/../dir/testpage' | :markdown | 'dir/testpage' | 'dir/testpage.md' + 'dir/testpage' | :markdown | '../dir/../another/testpage' | :markdown | 'another/testpage' | 'another/testpage.md' end end @@ -547,16 +676,6 @@ RSpec.shared_examples 'wiki model' do end end end - - context 'when feature flag :gitaly_replace_wiki_update_page is disabled' do - before do - stub_feature_flags(gitaly_replace_wiki_update_page: false) - end - - it_behaves_like 'update_page tests' do - include_context 'common examples' - end - end end describe '#delete_page' do diff --git a/spec/support/shared_examples/nav_sidebar_shared_examples.rb b/spec/support/shared_examples/nav_sidebar_shared_examples.rb index 3e500683712..4b815988bc5 100644 --- a/spec/support/shared_examples/nav_sidebar_shared_examples.rb +++ b/spec/support/shared_examples/nav_sidebar_shared_examples.rb @@ -27,7 +27,7 @@ end RSpec.shared_examples 'sidebar includes snowplow attributes' do |track_action, track_label, track_property| specify do - allow(view).to receive(:tracking_enabled?).and_return(true) + stub_application_setting(snowplow_enabled: true) render diff --git a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb index c1eccafa987..f5c41416763 100644 --- a/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/container_repositories_shared_examples.rb @@ -21,6 +21,7 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc expect(json_response.map { |repository| repository['id'] }).to contain_exactly( root_repository.id, test_repository.id) expect(response.body).not_to include('tags') + expect(response.body).not_to include('tags_count') end it 'returns a matching schema' do @@ -29,7 +30,11 @@ RSpec.shared_examples 'returns repositories for allowed users' do |user_type, sc expect(response).to have_gitlab_http_status(:ok) expect(response).to match_response_schema('registry/repositories') end + end +end +RSpec.shared_examples 'returns tags for allowed users' do |user_type, scope| + context "for #{user_type}" do context 'with tags param' do let(:url) { "/#{scope}s/#{object.id}/registry/repositories?tags=true" } @@ -169,10 +174,12 @@ RSpec.shared_examples 'reconciling migration_state' do end end - context 'import_failed response' do - let(:status) { 'import_failed' } + %w[import_canceled import_failed].each do |status| + context "#{status} response" do + let(:status) { status } - it_behaves_like 'retrying the import' + it_behaves_like 'retrying the import' + end end context 'pre_import_in_progress response' do @@ -192,17 +199,11 @@ RSpec.shared_examples 'reconciling migration_state' do end end - context 'pre_import_failed response' do - let(:status) { 'pre_import_failed' } - - it_behaves_like 'retrying the pre_import' - end - - %w[pre_import_canceled import_canceled].each do |canceled_status| - context "#{canceled_status} response" do - let(:status) { canceled_status } + %w[pre_import_canceled pre_import_failed].each do |status| + context "#{status} response" do + let(:status) { status } - it_behaves_like 'enforcing states coherence to', 'import_skipped' + it_behaves_like 'retrying the pre_import' end end end diff --git a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb index da9d254039b..e534a02e562 100644 --- a/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/group_and_project_boards_query_shared_examples.rb @@ -67,11 +67,15 @@ RSpec.shared_examples 'group and project boards query' do let(:sort_param) { } let(:first_param) { 2 } + def pagination_results_data(nodes) + nodes + end + let(:all_records) do if board_parent.multiple_issue_boards_available? - boards.map { |board| global_id_of(board) } + boards.map { |board| a_graphql_entity_for(board) } else - [global_id_of(boards.first)] + [a_graphql_entity_for(boards.first)] end end end diff --git a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb index 7e1f4500779..9033a8b4d3a 100644 --- a/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/noteable_shared_examples.rb @@ -12,9 +12,9 @@ RSpec.shared_examples 'a noteable graphql type we can query' do def expected noteable.discussions.map do |discussion| - include( - 'id' => global_id_of(discussion), - 'replyId' => global_id_of(discussion, id: discussion.reply_id), + a_graphql_entity_for( + discussion, + 'replyId' => global_id_of(discussion, id: discussion.reply_id).to_s, 'createdAt' => discussion.created_at.iso8601, 'notes' => include( 'nodes' => have_attributes(size: discussion.notes.size) @@ -50,8 +50,8 @@ RSpec.shared_examples 'a noteable graphql type we can query' do post_graphql(query(fields), current_user: current_user) - data = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable, :id) - expect(data[0]).to eq(global_id_of(noteable)) + entities = graphql_data_at(*path_to_noteable, :discussions, :nodes, :noteable) + expect(entities).to all(match(a_graphql_entity_for(noteable))) end end @@ -62,10 +62,10 @@ RSpec.shared_examples 'a noteable graphql type we can query' do def expected noteable.notes.map do |note| - include( - 'id' => global_id_of(note), - 'project' => include('id' => global_id_of(project)), - 'author' => include('id' => global_id_of(note.author)), + a_graphql_entity_for( + note, + 'project' => a_graphql_entity_for(project), + 'author' => a_graphql_entity_for(note.author), 'createdAt' => note.created_at.iso8601, 'body' => eq(note.note) ) diff --git a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb index 127b1a6d4c4..9f7ec6e90e9 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/group_and_project_packages_list_shared_examples.rb @@ -104,7 +104,7 @@ RSpec.shared_examples 'group and project packages query' do } end - let(:expected_packages) { sorted_packages.map { |package| global_id_of(package) } } + let(:expected_packages) { sorted_packages.map { |package| global_id_of(package).to_s } } let(:data_path) { [resource_type, :packages] } @@ -191,4 +191,91 @@ RSpec.shared_examples 'group and project packages query' do it { is_expected.to include({ "name" => versionless_package.name }) } end end + + context 'when reading pipelines' do + let(:npm_pipelines) { create_list(:ci_pipeline, 6, project: project1) } + let(:npm_pipeline_gids) { npm_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse } + let(:composer_pipelines) { create_list(:ci_pipeline, 6, project: project2) } + let(:composer_pipeline_gids) { composer_pipelines.sort_by(&:id).map(&:to_gid).map(&:to_s).reverse } + let(:npm_end_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'endCursor') } + let(:npm_start_cursor) { graphql_data_npm_package.dig('pipelines', 'pageInfo', 'startCursor') } + let(:pipelines_nodes) do + <<~QUERY + nodes { + id + } + pageInfo { + startCursor + endCursor + } + QUERY + end + + before do + resource.add_maintainer(current_user) + + npm_pipelines.each do |pipeline| + create(:package_build_info, package: npm_package, pipeline: pipeline) + end + + composer_pipelines.each do |pipeline| + create(:package_build_info, package: composer_package, pipeline: pipeline) + end + end + + it 'loads the second page with pagination first correctly' do + run_query(first: 2) + expect(npm_pipeline_ids).to eq(npm_pipeline_gids[0..1]) + expect(composer_pipeline_ids).to eq(composer_pipeline_gids[0..1]) + + run_query(first: 2, after: npm_end_cursor) + expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3]) + expect(composer_pipeline_ids).to be_empty + end + + it 'loads the second page with pagination last correctly' do + run_query(last: 2) + expect(npm_pipeline_ids).to eq(npm_pipeline_gids[4..5]) + expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5]) + + run_query(last: 2, before: npm_start_cursor) + expect(npm_pipeline_ids).to eq(npm_pipeline_gids[2..3]) + expect(composer_pipeline_ids).to eq(composer_pipeline_gids[4..5]) + end + + def run_query(args) + pipelines_field = query_graphql_field('pipelines', args, pipelines_nodes) + + packages_nodes = <<~QUERY + nodes { + id + #{pipelines_field} + } + QUERY + + query = graphql_query_for( + resource_type, + { 'fullPath' => resource.full_path }, + query_graphql_field('packages', {}, packages_nodes) + ) + + post_graphql(query, current_user: current_user) + end + + def npm_pipeline_ids + graphql_data_npm_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] } + end + + def composer_pipeline_ids + graphql_data_composer_package.dig('pipelines', 'nodes').map { |pipeline| pipeline['id'] } + end + + def graphql_data_npm_package + graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == npm_package.to_gid.to_s } + end + + def graphql_data_composer_package + graphql_data_at(resource_type, :packages, :nodes).find { |pkg| pkg['id'] == composer_package.to_gid.to_s } + end + end end diff --git a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb index ab93f54111b..b4019d7c232 100644 --- a/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/packages/package_details_shared_examples.rb @@ -28,14 +28,10 @@ RSpec.shared_examples 'a package with files' do end it 'has the basic package files data' do - expect(first_file_response).to include( - 'id' => global_id_of(first_file), - 'fileName' => first_file.file_name, - 'size' => first_file.size.to_s, - 'downloadPath' => first_file.download_path, - 'fileSha1' => first_file.file_sha1, - 'fileMd5' => first_file.file_md5, - 'fileSha256' => first_file.file_sha256 + expect(first_file_response).to match a_graphql_entity_for( + first_file, + :file_name, :download_path, :file_sha1, :file_md5, :file_sha256, + 'size' => first_file.size.to_s ) end diff --git a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb index c134f7d1839..3c5f25baaa1 100644 --- a/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/projects/alert_management/integrations_shared_examples.rb @@ -30,14 +30,12 @@ RSpec.shared_examples 'GraphQL query with several integrations requested' do |gr it 'returns the correct properties of the integrations', :aggregate_failures do post_graphql(multi_selection_query, current_user: current_user) - expect(graphql_data.dig('project', 'ai', 'nodes')).to include( - 'id' => global_id_of(active_http_integration), - 'name' => active_http_integration.name + expect(graphql_data.dig('project', 'ai', 'nodes')).to match a_graphql_entity_for( + active_http_integration, :name ) - expect(graphql_data.dig('project', 'ii', 'nodes')).to include( - 'id' => global_id_of(inactive_http_integration), - 'name' => inactive_http_integration.name + expect(graphql_data.dig('project', 'ii', 'nodes')).to match a_graphql_entity_for( + inactive_http_integration, :name ) end diff --git a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb index 249a7b7cdac..1ea11ba3d7c 100644 --- a/spec/support/shared_examples/requests/api/milestones_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/milestones_shared_examples.rb @@ -203,16 +203,16 @@ RSpec.shared_examples 'group and project milestones' do |route_definition| end describe "DELETE #{route_definition}/:milestone_id" do - it "rejects a member with reporter access from deleting a milestone" do - reporter = create(:user) - milestone.resource_parent.add_reporter(reporter) + it "rejects a member with guest access from deleting a milestone" do + guest = create(:user) + milestone.resource_parent.add_guest(guest) - delete api(resource_route, reporter) + delete api(resource_route, guest) expect(response).to have_gitlab_http_status(:forbidden) end - it 'deletes the milestone when the user has developer access to the project' do + it 'deletes the milestone when the user has reporter access to the project' do delete api(resource_route, user) expect(project.milestones.find_by_id(milestone.id)).to be_nil diff --git a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb index 68cb91d7414..d4417b23a5f 100644 --- a/spec/support/shared_examples/requests/rack_attack_shared_examples.rb +++ b/spec/support/shared_examples/requests/rack_attack_shared_examples.rb @@ -149,6 +149,7 @@ RSpec.shared_examples 'rate-limited token requests' do arguments = a_hash_including({ message: 'Rack_Attack', + status: 429, env: :throttle, remote_ip: '127.0.0.1', request_method: request_method, @@ -314,6 +315,7 @@ RSpec.shared_examples 'rate-limited web authenticated requests' do arguments = a_hash_including({ message: 'Rack_Attack', + status: 429, env: :throttle, remote_ip: '127.0.0.1', request_method: request_method, @@ -391,14 +393,16 @@ RSpec.shared_examples 'tracking when dry-run mode is set' do end it 'logs RackAttack info into structured logs' do - arguments = a_hash_including({ - message: 'Rack_Attack', - env: :track, - remote_ip: '127.0.0.1', - matched: throttle_name - }) + expect(Gitlab::AuthLogger).to receive(:error) do |arguments| + expect(arguments).to include( + message: 'Rack_Attack', + env: :track, + remote_ip: '127.0.0.1', + matched: throttle_name + ) - expect(Gitlab::AuthLogger).to receive(:error).with(arguments) + expect(arguments).not_to have_key(:status) + end (1 + requests_per_period).times do do_request @@ -576,6 +580,7 @@ RSpec.shared_examples 'rate-limited unauthenticated requests' do arguments = a_hash_including({ message: 'Rack_Attack', + status: 429, env: :throttle, remote_ip: '127.0.0.1', request_method: 'GET', diff --git a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb index fcd52cdf7fa..e1baa594f3c 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| +RSpec.shared_examples 'avoid N+1 on environments serialization' do it 'avoids N+1 database queries with grouping', :request_store do create_environment_with_associations(project) diff --git a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb index e776c098fa0..31571b1ffb9 100644 --- a/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable/destroy_service_shared_examples.rb @@ -1,21 +1,33 @@ # frozen_string_literal: true -shared_examples_for 'service deleting todos' do +shared_examples_for 'service scheduling async deletes' do it 'destroys associated todos asynchronously' do - expect(TodosDestroyer::DestroyedIssuableWorker) + expect(worker_class) .to receive(:perform_async) .with(issuable.id, issuable.class.name) subject.execute(issuable) end -end -shared_examples_for 'service deleting label links' do - it 'destroys associated label links asynchronously' do - expect(Issuable::LabelLinksDestroyWorker) + it 'works inside a transaction' do + expect(worker_class) .to receive(:perform_async) .with(issuable.id, issuable.class.name) - subject.execute(issuable) + ApplicationRecord.transaction do + subject.execute(issuable) + end + end +end + +shared_examples_for 'service deleting todos' do + it_behaves_like 'service scheduling async deletes' do + let(:worker_class) { TodosDestroyer::DestroyedIssuableWorker } + end +end + +shared_examples_for 'service deleting label links' do + it_behaves_like 'service scheduling async deletes' do + let(:worker_class) { Issuable::LabelLinksDestroyWorker } end end diff --git a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb index c4f6273b46c..5e49bdd706c 100644 --- a/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb +++ b/spec/support/shared_examples/services/jira/requests/base_shared_examples.rb @@ -66,18 +66,12 @@ RSpec.shared_examples 'a service that handles Jira API errors' do it 'logs the error' do stub_client_and_raise(Timeout::Error, 'foo') - expect(Gitlab::ProjectServiceLogger).to receive(:error).with( - hash_including( - client_url: be_present, - message: 'Error sending message', - service_class: described_class.name, - error: hash_including( - exception_class: Timeout::Error.name, - exception_message: 'foo', - exception_backtrace: be_present - ) - ) + expect(jira_integration).to receive(:log_exception).with( + kind_of(Timeout::Error), + message: 'Error sending message', + client_url: jira_integration.url ) + expect(subject).to be_error end diff --git a/spec/support/shared_examples/work_item_base_types_importer.rb b/spec/support/shared_examples/work_item_base_types_importer.rb index 68e37171ea2..593670ac4b8 100644 --- a/spec/support/shared_examples/work_item_base_types_importer.rb +++ b/spec/support/shared_examples/work_item_base_types_importer.rb @@ -1,10 +1,48 @@ # frozen_string_literal: true RSpec.shared_examples 'work item base types importer' do - it 'creates all base work item types' do - # Fixtures need to run on a pristine DB, but the test suite preloads the base types before(:suite) + it "creates all base work item types if they don't exist" do WorkItems::Type.delete_all expect { subject }.to change(WorkItems::Type, :count).from(0).to(WorkItems::Type::BASE_TYPES.count) + + types_in_db = WorkItems::Type.all.map { |type| type.slice(:base_type, :icon_name, :name).symbolize_keys } + expected_types = WorkItems::Type::BASE_TYPES.map do |type, attributes| + attributes.slice(:icon_name, :name).merge(base_type: type.to_s) + end + + expect(types_in_db).to match_array(expected_types) + expect(WorkItems::Type.all).to all(be_valid) + end + + it 'upserts base work item types if they already exist' do + first_type = WorkItems::Type.first + original_name = first_type.name + + first_type.update!(name: original_name.upcase) + + expect do + subject + first_type.reload + end.to not_change(WorkItems::Type, :count).and( + change(first_type, :name).from(original_name.upcase).to(original_name) + ) + end + + it 'executes a single INSERT query' do + expect { subject }.to make_queries_matching(/INSERT/, 1) + end + + context 'when some base types exist' do + before do + WorkItems::Type.limit(1).delete_all + end + + it 'inserts all types and does nothing if some already existed' do + expect { subject }.to make_queries_matching(/INSERT/, 1).and( + change(WorkItems::Type, :count).by(1) + ) + expect(WorkItems::Type.count).to eq(WorkItems::Type::BASE_TYPES.count) + end end end diff --git a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb index 26731f34ed6..3d4e840fe2d 100644 --- a/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb +++ b/spec/support/shared_examples/workers/batched_background_migration_worker_shared_examples.rb @@ -205,4 +205,123 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end end + + describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do + include Gitlab::Database::DynamicModelHelpers + + let(:migration_class) do + Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do + def perform(matching_status) + each_sub_batch( + operation_name: :update_all, + batching_scope: -> (relation) { relation.where(status: matching_status) } + ) do |sub_batch| + sub_batch.update_all(some_column: 0) + end + end + end + end + + let!(:migration) do + create( + :batched_background_migration, + :active, + table_name: table_name, + column_name: :id, + max_value: migration_records, + batch_size: batch_size, + sub_batch_size: sub_batch_size, + job_class_name: 'ExampleDataMigration', + job_arguments: [1] + ) + end + + let(:table_name) { 'example_data' } + let(:batch_size) { 5 } + let(:sub_batch_size) { 2 } + let(:number_of_batches) { 10 } + let(:migration_records) { batch_size * number_of_batches } + + let(:connection) { Gitlab::Database.database_base_models[tracking_database].connection } + let(:example_data) { define_batchable_model(table_name, connection: connection) } + + around do |example| + Gitlab::Database::SharedModel.using_connection(connection) do + example.run + end + end + + before do + # Create example table populated with test data to migrate. + # + # Test data should have two records that won't be updated: + # - one record beyond the migration's range + # - one record that doesn't match the migration job's batch condition + connection.execute(<<~SQL) + CREATE TABLE #{table_name} ( + id integer primary key, + some_column integer, + status smallint); + + INSERT INTO #{table_name} (id, some_column, status) + SELECT generate_series, generate_series, 1 + FROM generate_series(1, #{migration_records + 1}); + + UPDATE #{table_name} + SET status = 0 + WHERE some_column = #{migration_records - 5}; + SQL + + stub_feature_flags(execute_batched_migrations_on_schedule: true) + + stub_const('Gitlab::BackgroundMigration::ExampleDataMigration', migration_class) + end + + subject(:full_migration_run) do + # process all batches, then do an extra execution to mark the job as finished + (number_of_batches + 1).times do + described_class.new.perform + + travel_to((migration.interval + described_class::INTERVAL_VARIANCE).seconds.from_now) + end + end + + it 'marks the migration record as finished' do + expect { full_migration_run }.to change { migration.reload.status }.from(1).to(3) # active -> finished + end + + it 'creates job records for each processed batch', :aggregate_failures do + expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0) + + final_min_value = migration.batched_jobs.reduce(1) do |next_min_value, batched_job| + expect(batched_job.min_value).to eq(next_min_value) + + batched_job.max_value + 1 + end + + final_max_value = final_min_value - 1 + expect(final_max_value).to eq(migration_records) + end + + it 'marks all job records as succeeded', :aggregate_failures do + expect { full_migration_run }.to change { migration.reload.batched_jobs.count }.from(0) + + expect(migration.batched_jobs).to all(be_succeeded) + end + + it 'updates matching records in the range', :aggregate_failures do + expect { full_migration_run } + .to change { example_data.where('status = 1 AND some_column <> 0').count } + .from(migration_records).to(1) + + record_outside_range = example_data.last + + expect(record_outside_range.status).to eq(1) + expect(record_outside_range.some_column).not_to eq(0) + end + + it 'does not update non-matching records in the range' do + expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count } + end + end end diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb index 4751d91efde..77c4a3431e2 100644 --- a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb @@ -202,6 +202,8 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| before do expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid) + + statistics_keys.delete(:repository_size) end it_behaves_like 'it calls Gitaly' diff --git a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb index d9105981b4b..2741b2a9de7 100644 --- a/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb +++ b/spec/support/shared_examples/workers/concerns/reenqueuer_shared_examples.rb @@ -19,7 +19,13 @@ RSpec.shared_examples 'reenqueuer' do describe '#perform' do it 'tries to obtain a lease' do - expect_to_obtain_exclusive_lease(subject.lease_key) + lease_key = if subject.respond_to?(:set_custom_lease_key) + subject.set_custom_lease_key(*job_args) + else + subject.lease_key + end + + expect_to_obtain_exclusive_lease(lease_key) subject_perform end |