diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 10:00:54 +0000 |
commit | 3cccd102ba543e02725d247893729e5c73b38295 (patch) | |
tree | f36a04ec38517f5deaaacb5acc7d949688d1e187 /spec/support | |
parent | 205943281328046ef7b4528031b90fbda70c75ac (diff) | |
download | gitlab-ce-3cccd102ba543e02725d247893729e5c73b38295.tar.gz |
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'spec/support')
68 files changed, 1360 insertions, 379 deletions
diff --git a/spec/support/database_cleaner.rb b/spec/support/database_cleaner.rb index 8f706fdebc9..f8ddf3e66a5 100644 --- a/spec/support/database_cleaner.rb +++ b/spec/support/database_cleaner.rb @@ -20,6 +20,9 @@ RSpec.configure do |config| # We drop and recreate the database if any table has more than 1200 columns, just to be safe. if any_connection_class_with_more_than_allowed_columns? recreate_all_databases! + + # Seed required data as recreating DBs will delete it + TestEnv.seed_db end end diff --git a/spec/support/fips.rb b/spec/support/fips.rb new file mode 100644 index 00000000000..1d278dcdf60 --- /dev/null +++ b/spec/support/fips.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true +# rubocop: disable RSpec/EnvAssignment + +RSpec.configure do |config| + config.around(:each, :fips_mode) do |example| + set_fips_mode(true) do + example.run + end + end + + config.around(:each, fips_mode: false) do |example| + set_fips_mode(false) do + example.run + end + end + + def set_fips_mode(value) + prior_value = ENV["FIPS_MODE"] + ENV["FIPS_MODE"] = value.to_s + + yield + + ENV["FIPS_MODE"] = prior_value + end +end + +# rubocop: enable RSpec/EnvAssignment diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml index 52ae36229a6..b1533879e32 100644 --- a/spec/support/gitlab_stubs/gitlab_ci.yml +++ b/spec/support/gitlab_stubs/gitlab_ci.yml @@ -1,4 +1,4 @@ -image: ruby:2.6 +image: image:1.0 services: - postgres diff --git a/spec/support/helpers/cycle_analytics_helpers.rb b/spec/support/helpers/cycle_analytics_helpers.rb index 70b794f7d82..044ec56b1cc 100644 --- a/spec/support/helpers/cycle_analytics_helpers.rb +++ b/spec/support/helpers/cycle_analytics_helpers.rb @@ -86,6 +86,25 @@ module CycleAnalyticsHelpers wait_for_stages_to_load(ready_selector) end + def select_value_stream(value_stream_name) + toggle_value_stream_dropdown + + page.find('[data-testid="dropdown-value-streams"]').all('li button').find { |item| item.text == value_stream_name.to_s }.click + wait_for_requests + end + + def create_value_stream_group_aggregation(group) + aggregation = Analytics::CycleAnalytics::Aggregation.safe_create_for_group(group) + Analytics::CycleAnalytics::AggregatorService.new(aggregation: aggregation).execute + end + + def select_group_and_custom_value_stream(group, custom_value_stream_name) + create_value_stream_group_aggregation(group) + + select_group(group) + select_value_stream(custom_value_stream_name) + end + def toggle_dropdown(field) page.within("[data-testid*='#{field}']") do find('.dropdown-toggle').click diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 2a4f78ca57f..7ed64615020 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -5,19 +5,22 @@ module Spec module Helpers module Features module InviteMembersModalHelper - def invite_member(name, role: 'Guest', expires_at: nil) + def invite_member(names, role: 'Guest', expires_at: nil, refresh: true) click_on 'Invite members' - page.within '[data-testid="invite-modal"]' do - find('[data-testid="members-token-select-input"]').set(name) + page.within invite_modal_selector do + Array.wrap(names).each do |name| + find(member_dropdown_selector).set(name) + + wait_for_requests + click_button name + end - wait_for_requests - click_button name choose_options(role, expires_at) click_button 'Invite' - page.refresh + page.refresh if refresh end end @@ -43,6 +46,31 @@ module Spec fill_in 'YYYY-MM-DD', with: expires_at.strftime('%Y-%m-%d') if expires_at end + + def click_groups_tab + expect(page).to have_link 'Groups' + click_link "Groups" + end + + def group_dropdown_selector + '[data-testid="group-select-dropdown"]' + end + + def member_dropdown_selector + '[data-testid="members-token-select-input"]' + end + + def invite_modal_selector + '[data-testid="invite-modal"]' + end + + def expect_to_have_group(group) + expect(page).to have_selector("[entity-id='#{group.id}']") + end + + def expect_not_to_have_group(group) + expect(page).not_to have_selector("[entity-id='#{group.id}']") + end end end end diff --git a/spec/support/helpers/features/runner_helpers.rb b/spec/support/helpers/features/runner_helpers.rb new file mode 100644 index 00000000000..63fc628358c --- /dev/null +++ b/spec/support/helpers/features/runner_helpers.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module Spec + module Support + module Helpers + module Features + module RunnersHelpers + def within_runner_row(runner_id) + within "[data-testid='runner-row-#{runner_id}']" do + yield + end + end + + def search_bar_selector + '[data-testid="runners-filtered-search"]' + end + + # The filters must be clicked first to be able to receive events + # See: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1493 + def focus_filtered_search + page.within(search_bar_selector) do + page.find('.gl-filtered-search-term-token').click + end + end + + def input_filtered_search_keys(search_term) + focus_filtered_search + + page.within(search_bar_selector) do + page.find('input').send_keys(search_term) + click_on 'Search' + end + + wait_for_requests + end + + def open_filtered_search_suggestions(filter) + focus_filtered_search + + page.within(search_bar_selector) do + click_on filter + end + + wait_for_requests + end + + def input_filtered_search_filter_is_only(filter, value) + focus_filtered_search + + page.within(search_bar_selector) do + click_on filter + + # For OPERATOR_IS_ONLY, clicking the filter + # immediately preselects "=" operator + + page.find('input').send_keys(value) + page.find('input').send_keys(:enter) + + click_on 'Search' + end + + wait_for_requests + end + end + end + end + end +end diff --git a/spec/support/helpers/gitaly_setup.rb b/spec/support/helpers/gitaly_setup.rb index a4ee618457d..0ad83bdeeb2 100644 --- a/spec/support/helpers/gitaly_setup.rb +++ b/spec/support/helpers/gitaly_setup.rb @@ -267,7 +267,7 @@ module GitalySetup { 'default' => repos_path }, force: true, options: { - internal_socket_dir: File.join(gitaly_dir, "internal_gitaly2"), + runtime_dir: File.join(gitaly_dir, "run2"), gitaly_socket: "gitaly2.socket", config_filename: "gitaly2.config.toml" } diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb index 7666d71f13c..29b1bb260f2 100644 --- a/spec/support/helpers/login_helpers.rb +++ b/spec/support/helpers/login_helpers.rb @@ -99,7 +99,7 @@ module LoginHelpers fill_in "user_password", with: (password || "12345678") check 'user_remember_me' if remember - click_button "Sign in" + find('[data-testid="sign-in-button"]:enabled').click if two_factor_auth fill_in "user_otp_attempt", with: user.reload.current_otp diff --git a/spec/support/helpers/navbar_structure_helper.rb b/spec/support/helpers/navbar_structure_helper.rb index fb06ebfdae2..315303401cc 100644 --- a/spec/support/helpers/navbar_structure_helper.rb +++ b/spec/support/helpers/navbar_structure_helper.rb @@ -92,4 +92,16 @@ module NavbarStructureHelper new_sub_nav_item_name: _('Google Cloud') ) end + + def analytics_sub_nav_item + [ + _('Value stream'), + _('CI/CD'), + (_('Code review') if Gitlab.ee?), + (_('Merge request') if Gitlab.ee?), + _('Repository') + ] + end end + +NavbarStructureHelper.prepend_mod diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb index f5a1a97a1d0..581ef07752e 100644 --- a/spec/support/helpers/search_helpers.rb +++ b/spec/support/helpers/search_helpers.rb @@ -2,9 +2,12 @@ module SearchHelpers def fill_in_search(text) - page.within('.search-input-wrap') do + # Once the `new_header_search` feature flag has been removed + # We can remove the `.search-input-wrap` selector + # https://gitlab.com/gitlab-org/gitlab/-/issues/339348 + page.within('.header-search-new') do find('#search').click - fill_in('search', with: text) + fill_in 'search', with: text end wait_for_all_requests diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 587d4e22828..d81d0d436a1 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -41,7 +41,6 @@ module TestEnv 'pages-deploy-target' => '7975be0', 'audio' => 'c3c21fd', 'video' => '8879059', - 'add-balsamiq-file' => 'b89b56d', 'crlf-diff' => '5938907', 'conflict-start' => '824be60', 'conflict-resolvable' => '1450cd6', @@ -81,7 +80,9 @@ module TestEnv 'compare-with-merge-head-source' => 'f20a03d', 'compare-with-merge-head-target' => '2f1e176', 'trailers' => 'f0a5ed6', - 'add_commit_with_5mb_subject' => '8cf8e80' + 'add_commit_with_5mb_subject' => '8cf8e80', + 'blame-on-renamed' => '32c33da', + 'with-executables' => '6b8dc4a' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index b9f90b11a69..50d1b14cf56 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -26,7 +26,6 @@ module UsageDataHelpers COUNTS_KEYS = %i( assignee_lists - boards ci_builds ci_internal_pipelines ci_external_pipelines diff --git a/spec/support/matchers/graphql_matchers.rb b/spec/support/matchers/graphql_matchers.rb index dcaec176687..3ba88c3ae71 100644 --- a/spec/support/matchers/graphql_matchers.rb +++ b/spec/support/matchers/graphql_matchers.rb @@ -7,14 +7,14 @@ RSpec::Matchers.define :require_graphql_authorizations do |*expected| if klass.respond_to?(:required_permissions) klass.required_permissions else - [klass.to_graphql.metadata[:authorize]] + Array.wrap(klass.authorize) end end match do |klass| actual = permissions_for(klass) - expect(actual).to match_array(expected) + expect(actual).to match_array(expected.compact) end failure_message do |klass| @@ -213,16 +213,16 @@ RSpec::Matchers.define :have_graphql_resolver do |expected| match do |field| case expected when Method - expect(field.to_graphql.metadata[:type_class].resolve_proc).to eq(expected) + expect(field.type_class.resolve_proc).to eq(expected) else - expect(field.to_graphql.metadata[:type_class].resolver).to eq(expected) + expect(field.type_class.resolver).to eq(expected) end end end RSpec::Matchers.define :have_graphql_extension do |expected| match do |field| - expect(field.to_graphql.metadata[:type_class].extensions).to include(expected) + expect(field.type_class.extensions).to include(expected) end end diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb index f01c4075eeb..1932f78506f 100644 --- a/spec/support/matchers/markdown_matchers.rb +++ b/spec/support/matchers/markdown_matchers.rb @@ -270,7 +270,7 @@ module MarkdownMatchers set_default_markdown_messages match do |actual| - expect(actual).to have_link(href: 'http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjligUAMhEd0g==') + expect(actual).to have_link(href: 'http://localhost:8000/nomnoml/svg/eNqLDsgsSixJrUmtTHXOL80rsVLwzCupKUrMTNHQtC7IzMlJTE_V0KzhUlCITkpNLEqJ1dWNLkgsKsoviUUSs7KLTssvzVHIzS8tyYjliuUCAE_tHdw=') end end end diff --git a/spec/support/matchers/project_namespace_matcher.rb b/spec/support/matchers/project_namespace_matcher.rb index 95aa5429679..8666a605276 100644 --- a/spec/support/matchers/project_namespace_matcher.rb +++ b/spec/support/matchers/project_namespace_matcher.rb @@ -10,7 +10,7 @@ RSpec::Matchers.define :be_in_sync_with_project do |project| project_namespace.present? && project.name == project_namespace.name && project.path == project_namespace.path && - project.namespace == project_namespace.parent && + project.namespace_id == project_namespace.parent_id && project.visibility_level == project_namespace.visibility_level && project.shared_runners_enabled == project_namespace.shared_runners_enabled end diff --git a/spec/support/services/deploy_token_shared_examples.rb b/spec/support/services/deploy_token_shared_examples.rb index adc5ea0fcdc..d322b3fc81d 100644 --- a/spec/support/services/deploy_token_shared_examples.rb +++ b/spec/support/services/deploy_token_shared_examples.rb @@ -19,6 +19,10 @@ RSpec.shared_examples 'a deploy token creation service' do it 'returns a DeployToken' do expect(subject[:deploy_token]).to be_an_instance_of DeployToken end + + it 'sets the creator_id as the id of the current_user' do + expect(subject[:deploy_token].read_attribute(:creator_id)).to eq(user.id) + end end context 'when expires at date is not passed' do diff --git a/spec/support/services/issuable_update_service_shared_examples.rb b/spec/support/services/issuable_update_service_shared_examples.rb index 4d2843af1c4..c168df7a7d2 100644 --- a/spec/support/services/issuable_update_service_shared_examples.rb +++ b/spec/support/services/issuable_update_service_shared_examples.rb @@ -23,3 +23,47 @@ RSpec.shared_examples 'issuable update service' do end end end + +RSpec.shared_examples 'keeps issuable labels sorted after update' do + before do + update_issuable(label_ids: [label_b.id]) + end + + context 'when label is changed' do + it 'keeps the labels sorted by title ASC' do + update_issuable({ add_label_ids: [label_a.id] }) + + expect(issuable.labels).to eq([label_a, label_b]) + end + end +end + +RSpec.shared_examples 'broadcasting issuable labels updates' do + before do + update_issuable(label_ids: [label_a.id]) + end + + context 'when label is added' do + it 'triggers the GraphQL subscription' do + expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable) + + update_issuable({ add_label_ids: [label_b.id] }) + end + end + + context 'when label is removed' do + it 'triggers the GraphQL subscription' do + expect(GraphqlTriggers).to receive(:issuable_labels_updated).with(issuable) + + update_issuable({ remove_label_ids: [label_a.id] }) + end + end + + context 'when label is unchanged' do + it 'does not trigger the GraphQL subscription' do + expect(GraphqlTriggers).not_to receive(:issuable_labels_updated).with(issuable) + + update_issuable({ label_ids: [label_a.id] }) + end + end +end diff --git a/spec/support/shared_contexts/container_repositories_shared_context.rb b/spec/support/shared_contexts/container_repositories_shared_context.rb index 9a9f80a3cbd..a74b09d38bd 100644 --- a/spec/support/shared_contexts/container_repositories_shared_context.rb +++ b/spec/support/shared_contexts/container_repositories_shared_context.rb @@ -1,13 +1,10 @@ # frozen_string_literal: true RSpec.shared_context 'importable repositories' do - let_it_be(:root_group) { create(:group) } - let_it_be(:group) { create(:group, parent_id: root_group.id) } - let_it_be(:project) { create(:project, namespace: group) } - let_it_be(:valid_container_repository) { create(:container_repository, project: project, created_at: 2.days.ago) } - let_it_be(:valid_container_repository2) { create(:container_repository, project: project, created_at: 1.year.ago) } - let_it_be(:importing_container_repository) { create(:container_repository, :importing, project: project, created_at: 2.days.ago) } - let_it_be(:new_container_repository) { create(:container_repository, project: project) } + let_it_be(:valid_container_repository) { create(:container_repository, created_at: 2.days.ago, migration_plan: 'free') } + let_it_be(:valid_container_repository2) { create(:container_repository, created_at: 1.year.ago, migration_plan: 'free') } + let_it_be(:importing_container_repository) { create(:container_repository, :importing, created_at: 2.days.ago, migration_plan: 'free') } + let_it_be(:new_container_repository) { create(:container_repository, migration_plan: 'free') } let_it_be(:denied_root_group) { create(:group) } let_it_be(:denied_group) { create(:group, parent_id: denied_root_group.id) } @@ -18,7 +15,8 @@ RSpec.shared_context 'importable repositories' do stub_application_setting(container_registry_import_created_before: 1.day.ago) stub_feature_flags( container_registry_phase_2_deny_list: false, - container_registry_migration_limit_gitlab_org: false + container_registry_migration_limit_gitlab_org: false, + container_registry_migration_phase2_all_plans: false ) Feature::FlipperGate.create!( diff --git a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb index 6a09497a497..ef1c01f72f9 100644 --- a/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb +++ b/spec/support/shared_contexts/finders/users_finder_shared_contexts.rb @@ -3,8 +3,10 @@ RSpec.shared_context 'UsersFinder#execute filter by project context' do let_it_be(:normal_user) { create(:user, username: 'johndoe') } let_it_be(:admin_user) { create(:user, :admin, username: 'iamadmin') } + let_it_be(:banned_user) { create(:user, :banned, username: 'iambanned') } let_it_be(:blocked_user) { create(:user, :blocked, username: 'notsorandom') } let_it_be(:external_user) { create(:user, :external) } + let_it_be(:unconfirmed_user) { create(:user, confirmed_at: nil) } let_it_be(:omniauth_user) { create(:omniauth_user, provider: 'twitter', extern_uid: '123456') } - let_it_be(:internal_user) { User.alert_bot } + let_it_be(:internal_user) { User.alert_bot.tap { |u| u.confirm } } end diff --git a/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb b/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb index d857e683aa2..196173d4a63 100644 --- a/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb +++ b/spec/support/shared_contexts/lib/container_registry/client_stubs_shared_context.rb @@ -8,8 +8,8 @@ RSpec.shared_context 'container registry client stubs' do end end - def stub_container_registry_gitlab_api_repository_details(client, path:, size_bytes:) - allow(client).to receive(:repository_details).with(path, with_size: true).and_return('size_bytes' => size_bytes) + def stub_container_registry_gitlab_api_repository_details(client, path:, size_bytes:, sizing: :self) + allow(client).to receive(:repository_details).with(path, sizing: sizing).and_return('size_bytes' => size_bytes) end def stub_container_registry_gitlab_api_network_error(client_method: :supports_gitlab_api?) diff --git a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb index d0915bbf158..dea03af2248 100644 --- a/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb +++ b/spec/support/shared_contexts/markdown_golden_master_shared_examples.rb @@ -64,6 +64,9 @@ RSpec.shared_context 'API::Markdown Golden Master shared context' do |markdown_y let(:substitutions) { markdown_example.fetch(:substitutions, {}) } it "verifies conversion of GFM to HTML", :unlimited_max_formatted_output_length do + stub_application_setting(plantuml_enabled: true, plantuml_url: 'http://localhost:8080') + stub_application_setting(kroki_enabled: true, kroki_url: 'http://localhost:8000') + pending pending_reason if pending_reason normalized_example_html = normalize_html(example_html, substitutions) diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index b4a71f52092..65c7f63cf6e 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec.shared_context 'project navbar structure' do + include NavbarStructureHelper + let(:security_and_compliance_nav_item) do { nav_item: _('Security & Compliance'), @@ -93,13 +95,7 @@ RSpec.shared_context 'project navbar structure' do }, { nav_item: _('Analytics'), - nav_sub_items: [ - _('Value stream'), - _('CI/CD'), - (_('Code review') if Gitlab.ee?), - (_('Merge request') if Gitlab.ee?), - _('Repository') - ] + nav_sub_items: analytics_sub_nav_item }, { nav_item: _('Wiki'), diff --git a/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb b/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb index fce78957eba..efd5d344a28 100644 --- a/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb +++ b/spec/support/shared_contexts/serializers/group_group_link_shared_context.rb @@ -1,10 +1,10 @@ # frozen_string_literal: true RSpec.shared_context 'group_group_link' do - let(:shared_with_group) { create(:group) } - let(:shared_group) { create(:group) } + let_it_be(:shared_with_group) { create(:group) } + let_it_be(:shared_group) { create(:group) } - let!(:group_group_link) do + let_it_be(:group_group_link) do create( :group_group_link, { diff --git a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb index e1d864213b5..37d410a35bf 100644 --- a/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb +++ b/spec/support/shared_contexts/services/service_ping/stubbed_service_ping_metrics_definitions_shared_context.rb @@ -21,7 +21,7 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do let(:optional_metrics) do [ - metric_attributes('counts.boards', 'optional', 'number'), + metric_attributes('counts.boards', 'optional', 'number', 'CountBoardsMetric'), metric_attributes('gitaly.filesystems', '').except('data_category'), metric_attributes('usage_activity_by_stage.monitor.projects_with_enabled_alert_integrations_histogram', 'optional', 'object'), metric_attributes('topology', 'optional', 'object') @@ -43,11 +43,14 @@ RSpec.shared_context 'stubbed service ping metrics definitions' do Gitlab::Usage::MetricDefinition.instance_variable_set(:@all, nil) end - def metric_attributes(key_path, category, value_type = 'string') + def metric_attributes(key_path, category, value_type = 'string', instrumentation_class = '') { 'key_path' => key_path, 'data_category' => category, - 'value_type' => value_type + 'value_type' => value_type, + 'status' => 'active', + 'instrumentation_class' => instrumentation_class, + 'time_frame' => 'all' } end end diff --git a/spec/support/shared_contexts/url_shared_context.rb b/spec/support/shared_contexts/url_shared_context.rb index da1d6e0049c..0e1534bf6c7 100644 --- a/spec/support/shared_contexts/url_shared_context.rb +++ b/spec/support/shared_contexts/url_shared_context.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true RSpec.shared_context 'valid urls with CRLF' do - let(:valid_urls_with_CRLF) do + let(:valid_urls_with_crlf) do [ "http://example.com/pa%0dth", "http://example.com/pa%0ath", @@ -16,7 +16,7 @@ RSpec.shared_context 'valid urls with CRLF' do end RSpec.shared_context 'invalid urls' do - let(:urls_with_CRLF) do + let(:urls_with_crlf) do [ "git://example.com/pa%0dth", "git://example.com/pa%0ath", diff --git a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb index 1e303197990..15590fd10dc 100644 --- a/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb +++ b/spec/support/shared_examples/boards/multiple_issue_boards_shared_examples.rb @@ -138,7 +138,7 @@ RSpec.shared_examples 'multiple issue boards' do wait_for_requests - dropdown_selector = '.js-boards-selector .dropdown-menu' + dropdown_selector = '[data-testid="boards-selector"] .dropdown-menu' page.within(dropdown_selector) do yield end diff --git a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb index 46fc2cbdc9b..2ea98002de1 100644 --- a/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb +++ b/spec/support/shared_examples/controllers/githubish_import_controller_shared_examples.rb @@ -184,6 +184,41 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET status' do expect(json_response.dig("provider_repos").count).to eq(1) end end + + context 'when namespace_id query param is provided' do + let_it_be(:current_user) { create(:user) } + + let(:namespace) { create(:namespace) } + + before do + allow(controller).to receive(:current_user).and_return(current_user) + end + + context 'when user is allowed to create projects in this namespace' do + before do + allow(current_user).to receive(:can?).and_return(true) + end + + it 'provides namespace to the template' do + get :status, params: { namespace_id: namespace.id }, format: :html + + expect(response).to have_gitlab_http_status :ok + expect(assigns(:namespace)).to eq(namespace) + end + end + + context 'when user is not allowed to create projects in this namespace' do + before do + allow(current_user).to receive(:can?).and_return(false) + end + + it 'renders 404' do + get :status, params: { namespace_id: namespace.id }, format: :html + + expect(response).to have_gitlab_http_status :not_found + end + end + end end end @@ -515,7 +550,7 @@ RSpec.shared_examples 'a GitHub-ish import controller: GET realtime_changes' do get :realtime_changes - expect(json_response).to eq([{ "id" => project.id, "import_status" => project.import_status }]) + expect(json_response).to match([a_hash_including({ "id" => project.id, "import_status" => project.import_status })]) expect(Integer(response.headers['Poll-Interval'])).to be > -1 end end diff --git a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb index bf26922d9c5..885c0229038 100644 --- a/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb +++ b/spec/support/shared_examples/controllers/wiki_actions_shared_examples.rb @@ -150,6 +150,7 @@ RSpec.shared_examples 'wiki controller actions' do expect(response).to render_template('shared/wikis/diff') expect(assigns(:diffs)).to be_a(Gitlab::Diff::FileCollection::Base) expect(assigns(:diff_notes_disabled)).to be(true) + expect(assigns(:page).content).to be_empty end end @@ -475,9 +476,13 @@ RSpec.shared_examples 'wiki controller actions' do context 'when page exists' do shared_examples 'deletes the page' do specify do - expect do - request - end.to change { wiki.list_pages.size }.by(-1) + aggregate_failures do + expect do + request + end.to change { wiki.list_pages.size }.by(-1) + + expect(assigns(:page).content).to be_empty + end end end diff --git a/spec/support/shared_examples/features/access_tokens_shared_examples.rb b/spec/support/shared_examples/features/access_tokens_shared_examples.rb index ae246a87bb6..215d9d3e5a8 100644 --- a/spec/support/shared_examples/features/access_tokens_shared_examples.rb +++ b/spec/support/shared_examples/features/access_tokens_shared_examples.rb @@ -29,15 +29,15 @@ RSpec.shared_examples 'resource access tokens creation' do |resource_type| click_on '1' # Scopes - check 'api' check 'read_api' + check 'read_repository' click_on "Create #{resource_type} access token" expect(active_resource_access_tokens).to have_text(name) expect(active_resource_access_tokens).to have_text('in') - expect(active_resource_access_tokens).to have_text('api') expect(active_resource_access_tokens).to have_text('read_api') + expect(active_resource_access_tokens).to have_text('read_repository') expect(active_resource_access_tokens).to have_text('Maintainer') expect(created_resource_access_token).not_to be_empty end 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 2332285540a..5c44cb7f04b 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -1,14 +1,48 @@ # frozen_string_literal: true RSpec.shared_examples 'edits content using the content editor' do - it 'formats text as bold using bubble menu' do - content_editor_testid = '[data-testid="content-editor"] [contenteditable]' + content_editor_testid = '[data-testid="content-editor"] [contenteditable].ProseMirror' - expect(page).to have_css(content_editor_testid) + describe 'formatting bubble menu' do + it 'shows a formatting bubble menu for a regular paragraph' do + expect(page).to have_css(content_editor_testid) - find(content_editor_testid).send_keys 'Typing text in the content editor' - find(content_editor_testid).send_keys [:shift, :left] + find(content_editor_testid).send_keys 'Typing text in the content editor' + find(content_editor_testid).send_keys [:shift, :left] - expect(page).to have_css('[data-testid="formatting-bubble-menu"]') + expect(page).to have_css('[data-testid="formatting-bubble-menu"]') + end + + it 'does not show a formatting bubble menu for code' do + find(content_editor_testid).send_keys 'This is a `code`' + find(content_editor_testid).send_keys [:shift, :left] + + expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') + end + end + + describe 'code block bubble menu' do + it 'shows a code block bubble menu for a code block' do + 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] + + expect(page).not_to have_css('[data-testid="formatting-bubble-menu"]') + expect(page).to have_css('[data-testid="code-block-bubble-menu"]') + end + + it 'sets code block type to "javascript" for `js`' do + find(content_editor_testid).send_keys '```js ' + find(content_editor_testid).send_keys 'var a = 0' + + expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Javascript') + end + + it 'sets code block type to "Custom (nomnoml)" for `nomnoml`' do + find(content_editor_testid).send_keys '```nomnoml ' + find(content_editor_testid).send_keys 'test' + + expect(find('[data-testid="code-block-bubble-menu"]')).to have_text('Custom (nomnoml)') + end end end diff --git a/spec/support/shared_examples/features/inviting_members_shared_examples.rb b/spec/support/shared_examples/features/inviting_members_shared_examples.rb new file mode 100644 index 00000000000..58357b262f5 --- /dev/null +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -0,0 +1,175 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'inviting members' do |snowplow_invite_label| + before_all do + group.add_owner(user1) + end + + it 'adds user as member', :js, :snowplow, :aggregate_failures do + visit members_page_path + + invite_member(user2.name, role: 'Reporter') + + page.within find_member_row(user2) do + expect(page).to have_button('Reporter') + end + + expect_snowplow_event( + category: 'Members::InviteService', + action: 'create_member', + label: snowplow_invite_label, + property: 'existing_user', + user: user1 + ) + end + + it 'invites user by email', :js, :snowplow, :aggregate_failures do + visit members_page_path + + invite_member('test@example.com', role: 'Reporter') + + click_link 'Invited' + + page.within find_invited_member_row('test@example.com') do + expect(page).to have_button('Reporter') + end + + expect_snowplow_event( + category: 'Members::InviteService', + action: 'create_member', + label: snowplow_invite_label, + property: 'net_new_user', + user: user1 + ) + end + + it 'invites user by username and invites user by email', :js, :aggregate_failures do + visit members_page_path + + invite_member([user2.name, 'test@example.com'], role: 'Reporter') + + page.within find_member_row(user2) do + expect(page).to have_button('Reporter') + end + + click_link 'Invited' + + page.within find_invited_member_row('test@example.com') do + expect(page).to have_button('Reporter') + end + end + + context 'when member is already a member by username' do + it 'updates the member for that user', :js do + visit members_page_path + + invite_member(user2.name, role: 'Developer') + + invite_member(user2.name, role: 'Reporter', refresh: false) + + expect(page).not_to have_selector(invite_modal_selector) + + page.refresh + + page.within find_invited_member_row(user2.name) do + expect(page).to have_button('Reporter') + end + end + end + + context 'when member is already a member by email' do + it 'fails with an error', :js do + visit members_page_path + + invite_member('test@example.com', role: 'Developer') + + invite_member('test@example.com', role: 'Reporter', refresh: false) + + expect(page).to have_selector(invite_modal_selector) + expect(page).to have_content("The member's email address has already been taken") + + page.refresh + + click_link 'Invited' + + page.within find_invited_member_row('test@example.com') do + expect(page).to have_button('Developer') + end + end + end + + context 'when inviting a parent group member to the sub-entity' do + before_all do + group.add_owner(user1) + group.add_developer(user2) + end + + context 'when role is higher than parent group membership' do + let(:role) { 'Maintainer' } + + it 'adds the user as a member on sub-entity with higher access level', :js do + visit subentity_members_page_path + + invite_member(user2.name, role: role, refresh: false) + + expect(page).not_to have_selector(invite_modal_selector) + + page.refresh + + page.within find_invited_member_row(user2.name) do + expect(page).to have_button(role) + end + end + end + + context 'when role is lower than parent group membership' do + let(:role) { 'Reporter' } + + it 'fails with an error', :js do + visit subentity_members_page_path + + invite_member(user2.name, role: role, refresh: false) + + expect(page).to have_selector(invite_modal_selector) + expect(page).to have_content "Access level should be greater than or equal to Developer inherited membership " \ + "from group #{group.name}" + + page.refresh + + page.within find_invited_member_row(user2.name) do + expect(page).to have_content('Developer') + expect(page).not_to have_button('Developer') + end + end + + context 'when there are multiple users invited with errors' do + let_it_be(:user3) { create(:user) } + + before do + group.add_maintainer(user3) + end + + it 'only shows the first user error', :js do + visit subentity_members_page_path + + invite_member([user2.name, user3.name], role: role, refresh: false) + + expect(page).to have_selector(invite_modal_selector) + expect(page).to have_text("Access level should be greater than or equal to", count: 1) + + page.refresh + + page.within find_invited_member_row(user2.name) do + expect(page).to have_content('Developer') + expect(page).not_to have_button('Developer') + end + + page.within find_invited_member_row(user3.name) do + expect(page).to have_content('Maintainer') + expect(page).not_to have_button('Maintainer') + end + end + end + end + end +end diff --git a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb index 066c3e17a09..0a5ad5a59c0 100644 --- a/spec/support/shared_examples/features/project_upload_files_shared_examples.rb +++ b/spec/support/shared_examples/features/project_upload_files_shared_examples.rb @@ -62,7 +62,7 @@ RSpec.shared_examples 'it uploads and commits a new image file' do |drop: false| visit(project_blob_path(project, 'upload_image/logo_sample.svg')) - expect(page).to have_css('.file-content img') + expect(page).to have_css('.file-holder img') end end diff --git a/spec/support/shared_examples/features/runners_shared_examples.rb b/spec/support/shared_examples/features/runners_shared_examples.rb new file mode 100644 index 00000000000..d9460c7b8f1 --- /dev/null +++ b/spec/support/shared_examples/features/runners_shared_examples.rb @@ -0,0 +1,141 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'shows and resets runner registration token' do + include Spec::Support::Helpers::ModalHelpers + include Spec::Support::Helpers::Features::RunnersHelpers + + before do + click_on dropdown_text + end + + describe 'shows registration instructions' do + before do + click_on 'Show runner installation and registration instructions' + + wait_for_requests + end + + it 'opens runner installation modal', :aggregate_failures do + within_modal do + expect(page).to have_text "Install a runner" + expect(page).to have_text "Environment" + expect(page).to have_text "Architecture" + expect(page).to have_text "Download and install binary" + end + end + + it 'dismisses runner installation modal' do + within_modal do + click_button('Close', match: :first) + end + + expect(page).not_to have_text "Install a runner" + end + end + + it 'has a registration token' do + click_on 'Click to reveal' + expect(page.find('[data-testid="token-value"] input').value).to have_content(registration_token) + end + + describe 'reset registration token' do + let!(:old_registration_token) { find('[data-testid="token-value"] input').value } + + before do + click_on 'Reset registration token' + + within_modal do + click_button('Reset token', match: :first) + end + + wait_for_requests + end + + it 'changes registration token' do + expect(find('.gl-toast')).to have_content('New registration token generated!') + + click_on dropdown_text + click_on 'Click to reveal' + + expect(old_registration_token).not_to eq registration_token + end + end +end + +RSpec.shared_examples 'shows no runners' do + it 'shows counts with 0' do + expect(page).to have_text "Online runners 0" + expect(page).to have_text "Offline runners 0" + expect(page).to have_text "Stale runners 0" + end + + it 'shows "no runners" message' do + expect(page).to have_text 'No runners found' + end +end + +RSpec.shared_examples 'shows runner in list' do + it 'does not show empty state' do + expect(page).not_to have_content 'No runners found' + end + + it 'shows runner row' do + within_runner_row(runner.id) do + expect(page).to have_text "##{runner.id}" + expect(page).to have_text runner.short_sha + expect(page).to have_text runner.description + end + end +end + +RSpec.shared_examples 'pauses, resumes and deletes a runner' do + include Spec::Support::Helpers::ModalHelpers + + it 'pauses and resumes runner' do + within_runner_row(runner.id) do + click_button "Pause" + + expect(page).to have_text 'paused' + expect(page).to have_button 'Resume' + expect(page).not_to have_button 'Pause' + + click_button "Resume" + + expect(page).not_to have_text 'paused' + expect(page).not_to have_button 'Resume' + expect(page).to have_button 'Pause' + end + end + + describe 'deletes runner' do + before do + within_runner_row(runner.id) do + click_on 'Delete runner' + end + end + + it 'shows a confirmation modal' do + expect(page).to have_text "Delete runner ##{runner.id} (#{runner.short_sha})?" + expect(page).to have_text "Are you sure you want to continue?" + end + + it 'deletes a runner' do + within_modal do + click_on 'Delete runner' + end + + expect(page.find('.gl-toast')).to have_text(/Runner .+ deleted/) + expect(page).not_to have_content runner.description + end + + it 'cancels runner deletion' do + within_modal do + click_on 'Cancel' + end + + wait_for_requests + + expect(page).to have_content runner.description + end + end +end diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb index bb5460e2a6f..095c48cade8 100644 --- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb +++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb @@ -11,6 +11,7 @@ RSpec.shared_examples 'search timeouts' do |scope| end it 'renders timeout information' do + # expect(page).to have_content('This endpoint has been requested too many times.') expect(page).to have_content('Your search timed out') 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 f676b6aa60d..41b1964cff0 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 @@ -20,6 +20,12 @@ RSpec.shared_examples 'User creates wiki page' do click_link "Create your first page" end + it 'shows all available formats in the dropdown' do + Wiki::VALID_USER_MARKUPS.each do |key, markup| + expect(page).to have_css("#wiki_format option[value=#{key}]", text: markup[:name]) + end + end + it "disables the submit button", :js do page.within(".wiki-form") do fill_in(:wiki_content, with: "") diff --git a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb index 85490bffc0e..12a4c6d7583 100644 --- a/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb +++ b/spec/support/shared_examples/features/wiki/user_updates_wiki_page_shared_examples.rb @@ -145,19 +145,6 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'edits content using the content editor' end - - context 'with feature flag off' do - before do - stub_feature_flags(wiki_switch_between_content_editor_raw_markdown: false) - visit(wiki_path(wiki)) - - click_link('Edit') - - click_button 'Use the new editor' - end - - it_behaves_like 'edits content using the content editor' - end end end diff --git a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb index 2e3a3ce6b41..04bb2fb69bb 100644 --- a/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb +++ b/spec/support/shared_examples/graphql/mutations/boards_create_shared_examples.rb @@ -3,16 +3,6 @@ RSpec.shared_examples 'boards create mutation' do include GraphqlHelpers - let_it_be(:current_user, reload: true) { create(:user) } - let(:name) { 'board name' } - let(:mutation) { graphql_mutation(:create_board, params) } - - subject { post_graphql_mutation(mutation, current_user: current_user) } - - def mutation_response - graphql_mutation_response(:create_board) - end - context 'when the user does not have permission' do it_behaves_like 'a mutation that returns a top-level access error' diff --git a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb index 56b6dc682eb..2c6118779e6 100644 --- a/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb +++ b/spec/support/shared_examples/graphql/notes_creation_shared_examples.rb @@ -85,3 +85,14 @@ RSpec.shared_examples 'a Note mutation when there are rate limit validation erro end end end + +RSpec.shared_examples 'a Note mutation with confidential notes' do + it_behaves_like 'a Note mutation that creates a Note' + + it 'returns a Note with confidentiality enabled' do + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response).to have_key('note') + expect(mutation_response['note']['confidential']).to eq(true) + 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 efb2c466f70..3caf153c2fa 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 @@ -62,9 +62,9 @@ RSpec.shared_examples 'Gitlab-style deprecations' do expect(deprecable.deprecation_reason).to include 'This was renamed.' end - it 'supports named reasons: discouraged' do - deprecable = subject(deprecated: { milestone: '1.10', reason: :discouraged }) + it 'supports named reasons: alpha' do + deprecable = subject(deprecated: { milestone: '1.10', reason: :alpha }) - expect(deprecable.deprecation_reason).to include 'Use of this is not recommended.' + expect(deprecable.deprecation_reason).to include 'This feature is in Alpha' end end diff --git a/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb b/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb new file mode 100644 index 00000000000..c2c27fb65ca --- /dev/null +++ b/spec/support/shared_examples/helpers/wiki_helpers_shared_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'wiki endpoint helpers' do + let(:resource_path) { page.wiki.container.class.to_s.pluralize.downcase } + let(:url) { "/api/v4/#{resource_path}/#{page.wiki.container.id}/wikis/#{page.slug}?version=#{page.version.id}"} + + it 'returns the full endpoint url' do + expect(helper.wiki_page_render_api_endpoint(page)).to end_with(url) + end + + context 'when relative url is set' do + let(:relative_url) { "/gitlab#{url}" } + + it 'returns the full endpoint url with the relative path' do + stub_config_setting(relative_url_root: '/gitlab') + + expect(helper.wiki_page_render_api_endpoint(page)).to end_with(relative_url) + end + end +end diff --git a/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb b/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb new file mode 100644 index 00000000000..050fdc3fff7 --- /dev/null +++ b/spec/support/shared_examples/incident_management/issuable_escalation_statuses/build_examples.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'initializes new escalation status with expected attributes' do |attributes = {}| + let(:expected_attributes) { attributes } + + specify do + expect { execute }.to change { incident.escalation_status } + .from(nil) + .to(instance_of(::IncidentManagement::IssuableEscalationStatus)) + + expect(incident.escalation_status).to have_attributes( + id: nil, + issue_id: incident.id, + policy_id: nil, + escalations_started_at: nil, + status_event: nil, + **expected_attributes + ) + end +end diff --git a/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb new file mode 100644 index 00000000000..4fc15cacab4 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/event_store_shared_examples.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'subscribes to event' do + include AfterNextHelpers + + it 'consumes the published event', :sidekiq_inline do + expect_next(described_class) + .to receive(:handle_event) + .with(instance_of(event.class)) + .and_call_original + + ::Gitlab::EventStore.publish(event) + end +end + +def consume_event(subscriber:, event:) + subscriber.new.perform(event.class.name, event.data) +end diff --git a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb index 4b956c2b566..b5d93aec1bf 100644 --- a/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/usage_data_counters/issuable_activity_shared_examples.rb @@ -5,7 +5,7 @@ RSpec.shared_examples 'a daily tracked issuable event' do stub_application_setting(usage_ping_enabled: true) end - def count_unique(date_from:, date_to:) + def count_unique(date_from: 1.minute.ago, date_to: 1.minute.from_now) Gitlab::UsageDataCounters::HLLRedisCounter.unique_events(event_names: action, start_date: date_from, end_date: date_to) end @@ -14,6 +14,7 @@ RSpec.shared_examples 'a daily tracked issuable event' do expect(track_action(author: user1)).to be_truthy expect(track_action(author: user1)).to be_truthy expect(track_action(author: user2)).to be_truthy + expect(count_unique).to eq(2) end end diff --git a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb index b4c438771ce..d816754f328 100644 --- a/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb +++ b/spec/support/shared_examples/lib/sidebars/projects/menus/zentao_menu_shared_examples.rb @@ -34,8 +34,16 @@ RSpec.shared_examples 'ZenTao menu with CE version' do expect(subject.link).to eq zentao_integration.url end - it 'contains only open ZenTao item' do - expect(subject.renderable_items.map(&:item_id)).to match_array [:open_zentao] + it 'renders external-link icon' do + expect(subject.sprite_icon).to eq 'external-link' + end + + it 'renders ZenTao menu' do + expect(subject.title).to eq s_('ZentaoIntegration|ZenTao') + end + + it 'does not contain items' do + expect(subject.renderable_items.count).to eq 0 end end end diff --git a/spec/support/shared_examples/lib/wikis_api_examples.rb b/spec/support/shared_examples/lib/wikis_api_examples.rb index f068a7676ad..c57ac328a60 100644 --- a/spec/support/shared_examples/lib/wikis_api_examples.rb +++ b/spec/support/shared_examples/lib/wikis_api_examples.rb @@ -80,6 +80,8 @@ RSpec.shared_examples_for 'wikis API returns wiki page' do context 'when wiki page has versions' do let(:new_content) { 'New content' } + let(:old_content) { page.content } + let(:old_version_id) { page.version.id } before do wiki.update_page(page.page, content: new_content, message: 'updated page') @@ -96,10 +98,10 @@ RSpec.shared_examples_for 'wikis API returns wiki page' do end context 'when version param is set' do - let(:params) { { version: page.version.id } } + let(:params) { { version: old_version_id } } it 'retrieves the specific page version' do - expect(json_response['content']).to eq(page.content) + expect(json_response['content']).to eq(old_content) end context 'when version param is not valid or inexistent' do diff --git a/spec/support/shared_examples/models/application_setting_shared_examples.rb b/spec/support/shared_examples/models/application_setting_shared_examples.rb index 38f5c7be393..74ec6474e80 100644 --- a/spec/support/shared_examples/models/application_setting_shared_examples.rb +++ b/spec/support/shared_examples/models/application_setting_shared_examples.rb @@ -239,7 +239,7 @@ RSpec.shared_examples 'application settings examples' do describe '#allowed_key_types' do it 'includes all key types by default' do - expect(setting.allowed_key_types).to contain_exactly(*described_class::SUPPORTED_KEY_TYPES) + expect(setting.allowed_key_types).to contain_exactly(*Gitlab::SSHPublicKey.supported_types) end it 'excludes disabled key types' do diff --git a/spec/support/shared_examples/models/concerns/bulk_users_by_email_load_shared_examples.rb b/spec/support/shared_examples/models/concerns/bulk_users_by_email_load_shared_examples.rb new file mode 100644 index 00000000000..c3e9ff5c91a --- /dev/null +++ b/spec/support/shared_examples/models/concerns/bulk_users_by_email_load_shared_examples.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a BulkUsersByEmailLoad model' do + describe '#users_by_emails' do + let_it_be(:user1) { create(:user, emails: [create(:email, email: 'user1@example.com')]) } + let_it_be(:user2) { create(:user, emails: [create(:email, email: 'user2@example.com')]) } + + subject(:model) { described_class.new(id: non_existing_record_id) } + + context 'when nothing is loaded' do + let(:passed_emails) { [user1.emails.first.email, user2.email] } + + it 'preforms the yielded query and supplies the data with only emails desired' do + expect(model.users_by_emails(passed_emails).keys).to contain_exactly(*passed_emails) + end + end + + context 'when store is preloaded', :request_store do + let(:passed_emails) { [user1.emails.first.email, user2.email, user1.email] } + let(:resource_data) do + { + user1.emails.first.email => instance_double('User'), + user2.email => instance_double('User') + } + end + + before do + Gitlab::SafeRequestStore["user_by_email_for_users:#{model.class.name}:#{model.id}"] = resource_data + end + + it 'passes back loaded data and does not update the items that already exist' do + users_by_emails = model.users_by_emails(passed_emails) + + expect(users_by_emails.keys).to contain_exactly(*passed_emails) + expect(users_by_emails).to include(resource_data.merge(user1.email => user1)) + end + end + end +end diff --git a/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb index 6b208c0024d..e625ba785d2 100644 --- a/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/from_set_operator_shared_examples.rb @@ -20,6 +20,12 @@ RSpec.shared_examples 'from set operator' do |sql_klass| expect(query.to_sql).to match(/FROM \(\(SELECT.+\)\n#{operator_keyword}\n\(SELECT.+\)\) users/m) end + it "returns empty set when passing empty array" do + query = model.public_send(operator_method, []) + + expect(query.to_sql).to match(/WHERE \(1=0\)/m) + end + it 'supports the use of a custom alias for the sub query' do query = model.public_send(operator_method, [model.where(id: 1), model.where(id: 2)], 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 d6415e98289..da5c35c970a 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 @@ -227,9 +227,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name end context 'for confidential notes' do - before_all do - issue_note.update!(confidential: true) - end + let_it_be(:issue_note) { create(:note_on_issue, project: project, note: "issue note", confidential: true) } it 'falls back to note channel' do expect(::Slack::Messenger).to execute_with_options(channel: ['random']) diff --git a/spec/support/shared_examples/models/group_shared_examples.rb b/spec/support/shared_examples/models/group_shared_examples.rb new file mode 100644 index 00000000000..9f3359ba4ab --- /dev/null +++ b/spec/support/shared_examples/models/group_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'checks self and root ancestor feature flag' do + let_it_be(:root_group) { create(:group) } + let_it_be(:group) { create(:group, parent: root_group) } + let_it_be(:project) { create(:project, group: group) } + + subject { group.public_send(feature_flag_method) } + + context 'when FF is enabled for the root group' do + before do + stub_feature_flags(feature_flag => root_group) + end + + it { is_expected.to be_truthy } + end + + context 'when FF is enabled for the group' do + before do + stub_feature_flags(feature_flag => group) + end + + it { is_expected.to be_truthy } + + context 'when root_group is the actor' do + it 'is not enabled if the FF is enabled for a child' do + expect(root_group.public_send(feature_flag_method)).to be_falsey + end + end + end + + context 'when FF is disabled globally' do + before do + stub_feature_flags(feature_flag => false) + end + + it { is_expected.to be_falsey } + end + + context 'when FF is enabled globally' do + it { is_expected.to be_truthy } + end +end diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb deleted file mode 100644 index 13ffc1b7f87..00000000000 --- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb +++ /dev/null @@ -1,61 +0,0 @@ -# frozen_string_literal: true - -# This shared example requires a `builder` and `user` variable -RSpec.shared_examples 'issuable hook data' do |kind| - let(:data) { builder.build(user: user) } - - include_examples 'project hook data' do - let(:project) { builder.issuable.project } - end - - include_examples 'deprecated repository hook data' - - context "with a #{kind}" do - it 'contains issuable data' do - expect(data[:object_kind]).to eq(kind) - expect(data[:user]).to eq(user.hook_attrs) - expect(data[:project]).to eq(builder.issuable.project.hook_attrs) - expect(data[:object_attributes]).to eq(builder.issuable.hook_attrs) - expect(data[:changes]).to eq({}) - expect(data[:repository]).to eq(builder.issuable.project.hook_attrs.slice(:name, :url, :description, :homepage)) - end - - it 'does not contain certain keys' do - expect(data).not_to have_key(:assignees) - expect(data).not_to have_key(:assignee) - end - - describe 'changes are given' do - let(:changes) do - { - cached_markdown_version: %w[foo bar], - description: ['A description', 'A cool description'], - description_html: %w[foo bar], - in_progress_merge_commit_sha: %w[foo bar], - lock_version: %w[foo bar], - merge_jid: %w[foo bar], - title: ['A title', 'Hello World'], - title_html: %w[foo bar] - } - end - - let(:data) { builder.build(user: user, changes: changes) } - - it 'populates the :changes hash' do - expect(data[:changes]).to match(hash_including({ - title: { previous: 'A title', current: 'Hello World' }, - description: { previous: 'A description', current: 'A cool description' } - })) - end - - it 'does not contain certain keys' do - expect(data[:changes]).not_to have_key('cached_markdown_version') - expect(data[:changes]).not_to have_key('description_html') - expect(data[:changes]).not_to have_key('lock_version') - expect(data[:changes]).not_to have_key('title_html') - expect(data[:changes]).not_to have_key('in_progress_merge_commit_sha') - expect(data[:changes]).not_to have_key('merge_jid') - end - end - end -end diff --git a/spec/support/shared_examples/models/issuable_link_shared_examples.rb b/spec/support/shared_examples/models/issuable_link_shared_examples.rb index ca98c2597a2..9892e66b582 100644 --- a/spec/support/shared_examples/models/issuable_link_shared_examples.rb +++ b/spec/support/shared_examples/models/issuable_link_shared_examples.rb @@ -55,6 +55,19 @@ RSpec.shared_examples 'issuable link' do end end + describe 'scopes' do + describe '.for_source_or_target' do + it 'returns only links where id is either source or target id' do + link1 = create(issuable_link_factory, source: issuable_link.source) + link2 = create(issuable_link_factory, target: issuable_link.source) + # unrelated link, should not be included in result list + create(issuable_link_factory) # rubocop: disable Rails/SaveBang + + expect(described_class.for_source_or_target(issuable_link.source_id)).to match_array([issuable_link, link1, link2]) + end + end + end + describe '.link_type' do it { is_expected.to define_enum_for(:link_type).with_values(relates_to: 0, blocks: 1) } diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 17026f085bb..a329a6dca91 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -88,19 +88,55 @@ RSpec.shared_examples_for "member creation" do expect(member).to be_persisted end - context 'when admin mode is enabled', :enable_admin_mode do + context 'when adding a project_bot' do + let_it_be(:project_bot) { create(:user, :project_bot) } + + before_all do + source.add_owner(user) + end + + context 'when project_bot is already a member' do + before do + source.add_developer(project_bot) + end + + 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 + 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 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 + end + 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 + expect(member).to be_persisted + expect(source.users.reload).to include(user) expect(member.created_by).to eq(admin) end end context 'when admin mode is disabled' do - it 'rejects setting members.created_by to the given admin current_user' 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.created_by).to be_nil + 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 @@ -142,7 +178,7 @@ RSpec.shared_examples_for "member creation" do end context 'when called with an unknown user id' do - it 'adds the user as a member' do + it 'does not add the user as a member' do expect(source.users).not_to include(user) described_class.new(source, non_existing_record_id, :maintainer).execute @@ -410,6 +446,22 @@ RSpec.shared_examples_for "bulk member creation" do end end + it 'with the same user sent more than once by user and by email' do + members = described_class.add_users(source, [user1, user1.email], :maintainer) + + expect(members.map(&:user)).to contain_exactly(user1) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + + it 'with the same user sent more than once by user id and by email' do + members = described_class.add_users(source, [user1.id, user1.email], :maintainer) + + expect(members.map(&:user)).to contain_exactly(user1) + expect(members).to all(be_a(member_type)) + expect(members).to all(be_persisted) + end + context 'when a member already exists' do before do source.add_user(user1, :developer) diff --git a/spec/support/shared_examples/models/project_shared_examples.rb b/spec/support/shared_examples/models/project_shared_examples.rb new file mode 100644 index 00000000000..475ac1da04b --- /dev/null +++ b/spec/support/shared_examples/models/project_shared_examples.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'returns true if project is inactive' do + using RSpec::Parameterized::TableSyntax + + where(:storage_size, :last_activity_at, :expected_result) do + 1.megabyte | 1.month.ago | false + 1.megabyte | 3.years.ago | false + 8.megabytes | 1.month.ago | false + 8.megabytes | 3.years.ago | true + end + + with_them do + before do + stub_application_setting(inactive_projects_min_size_mb: 5) + stub_application_setting(inactive_projects_send_warning_email_after_months: 24) + + project.statistics.storage_size = storage_size + project.last_activity_at = last_activity_at + project.save! + end + + it 'returns expected result' do + expect(project.inactive?).to eq(expected_result) + end + 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 b3f79d9fe6e..03e9dd65e33 100644 --- a/spec/support/shared_examples/models/wiki_shared_examples.rb +++ b/spec/support/shared_examples/models/wiki_shared_examples.rb @@ -11,6 +11,10 @@ RSpec.shared_examples 'wiki model' do subject { wiki } + it 'VALID_USER_MARKUPS contains all valid markups' do + expect(described_class::VALID_USER_MARKUPS.keys).to match_array(%i(markdown rdoc asciidoc org)) + end + it 'container class includes HasWiki' do # NOTE: This is not enforced at runtime, since we also need to support Geo::DeletedProject expect(wiki_container).to be_kind_of(HasWiki) @@ -427,45 +431,131 @@ RSpec.shared_examples 'wiki model' do end describe '#update_page' do - let(:page) { create(:wiki_page, wiki: subject, title: 'update-page') } + shared_examples 'update_page tests' do + with_them do + let!(:page) { create(:wiki_page, wiki: subject, title: original_title, format: original_format, content: 'original content') } + + let(:message) { 'updated page' } + let(:updated_content) { 'updated content' } + + def update_page + subject.update_page( + page.page, + content: updated_content, + title: updated_title, + format: updated_format, + message: message + ) + end + + specify :aggregate_failures do + expect(subject).to receive(:after_wiki_activity) + expect(update_page).to eq true + + page = subject.find_page(updated_title.presence || original_title) - def update_page - subject.update_page( - page.page, - content: 'some other content', - format: :markdown, - message: 'updated page' - ) + expect(page.raw_content).to eq(updated_content) + expect(page.path).to eq(expected_path) + expect(page.version.message).to eq(message) + 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 + end end - it 'updates the content of the page' do - update_page - page = subject.find_page('update-page') + 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' + + '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' - expect(page.raw_content).to eq('some other content') + '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.md' + 'test dir/test page' | :markdown | 'test page' | :markdown | 'test-page.md' + + 'test page' | :markdown | nil | :markdown | 'test-page.md' + 'test.page' | :markdown | nil | :markdown | 'test.page.md' + end end - it 'sets the correct commit message' do - update_page - page = subject.find_page('update-page') + # There are two bugs in Gollum. THe first one is when the title and the format are updated + # at the same time https://gitlab.com/gitlab-org/gitlab/-/issues/243519. + # The second one is when the wiki page is within a dir and the `title` argument + # we pass to the update method is `nil`. Gollum will remove the dir and move the page. + # + # We can include this context into the former once it is fixed + # or when Gollum is removed since the Gitaly approach already fixes it. + 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' + end + end - expect(page.version.message).to eq('updated page') + it_behaves_like 'update_page tests' do + include_context 'common examples' + include_context 'extended examples' end - it 'sets the correct commit email' do - update_page + context 'when format is invalid' do + let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } - 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) + it 'returns false and sets error message' do + expect(subject.update_page(page.page, content: 'new content', format: :foobar)).to eq false + expect(subject.error_message).to match(/Invalid format selected/) + end end - it 'runs after_wiki_activity callbacks' do - page + context 'when format is not allowed' do + let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } - expect(subject).to receive(:after_wiki_activity) + it 'returns false and sets error message' do + expect(subject.update_page(page.page, content: 'new content', format: :creole)).to eq false + expect(subject.error_message).to match(/Invalid format selected/) + end + end + + context 'when page path does not have a default extension' do + let!(:page) { create(:wiki_page, wiki: subject, title: 'test page') } + + context 'when format is not different' do + it 'does not change the default extension' do + path = 'test-page.markdown' + page.page.instance_variable_set(:@path, path) - update_page + expect(subject.repository).to receive(:update_file).with(user, path, anything, anything) + + subject.update_page(page.page, content: 'new content', format: :markdown) + 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 diff --git a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb index 58822f4309b..991d6289373 100644 --- a/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb +++ b/spec/support/shared_examples/policies/wiki_policies_shared_examples.rb @@ -107,10 +107,4 @@ RSpec.shared_examples 'model with wiki policies' do expect_disallowed(*disallowed_permissions) end end - - # TODO: Remove this helper once we implement group features - # https://gitlab.com/gitlab-org/gitlab/-/issues/208412 - def set_access_level(access_level) - raise NotImplementedError - end end diff --git a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb index 9f4fdcf7ba1..dc2c4f890b1 100644 --- a/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/composer_packages_shared_examples.rb @@ -163,11 +163,11 @@ RSpec.shared_examples 'rejects Composer access with unknown project id' do let(:project) { double(id: non_existing_record_id) } context 'as anonymous' do - it_behaves_like 'process Composer api request', :anonymous, :not_found + it_behaves_like 'process Composer api request', :anonymous, :unauthorized end context 'as authenticated user' do - subject { get api(url), headers: basic_auth_header(user.username, personal_access_token.token) } + subject { get api(url), params: params, headers: basic_auth_header(user.username, personal_access_token.token) } it_behaves_like 'process Composer api request', :anonymous, :not_found end 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 e1e75be2494..c1eccafa987 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 @@ -116,3 +116,93 @@ RSpec.shared_examples 'not hitting graphql network errors with the container reg expect_graphql_errors_to_be_empty end end + +RSpec.shared_examples 'reconciling migration_state' do + shared_examples 'enforcing states coherence to' do |expected_migration_state| + it 'leaves the repository in the expected migration_state' do + expect(repository.gitlab_api_client).not_to receive(:pre_import_repository) + expect(repository.gitlab_api_client).not_to receive(:import_repository) + + subject + + expect(repository.reload.migration_state).to eq(expected_migration_state) + end + end + + shared_examples 'retrying the pre_import' do + it 'retries the pre_import' do + expect(repository).to receive(:migration_pre_import).and_return(:ok) + + expect { subject }.to change { repository.reload.migration_state }.to('pre_importing') + end + end + + shared_examples 'retrying the import' do + it 'retries the import' do + expect(repository).to receive(:migration_import).and_return(:ok) + + expect { subject }.to change { repository.reload.migration_state }.to('importing') + end + end + + context 'native response' do + let(:status) { 'native' } + + it 'finishes the import' do + expect { subject } + .to change { repository.reload.migration_state }.to('import_done') + .and change { repository.reload.migration_skipped_reason }.to('native_import') + end + end + + context 'import_in_progress response' do + let(:status) { 'import_in_progress' } + + it_behaves_like 'enforcing states coherence to', 'importing' + end + + context 'import_complete response' do + let(:status) { 'import_complete' } + + it 'finishes the import' do + expect { subject }.to change { repository.reload.migration_state }.to('import_done') + end + end + + context 'import_failed response' do + let(:status) { 'import_failed' } + + it_behaves_like 'retrying the import' + end + + context 'pre_import_in_progress response' do + let(:status) { 'pre_import_in_progress' } + + it_behaves_like 'enforcing states coherence to', 'pre_importing' + end + + context 'pre_import_complete response' do + let(:status) { 'pre_import_complete' } + + it 'finishes the pre_import and starts the import' do + expect(repository).to receive(:finish_pre_import).and_call_original + expect(repository).to receive(:migration_import).and_return(:ok) + + expect { subject }.to change { repository.reload.migration_state }.to('importing') + 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 } + + it_behaves_like 'enforcing states coherence to', 'import_skipped' + 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 01ed6c26576..da9d254039b 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 @@ -54,11 +54,13 @@ RSpec.shared_examples 'group and project boards query' do end context 'when using default sorting' do + # rubocop:disable RSpec/VariableName let!(:board_B) { create(:board, resource_parent: board_parent, name: 'B') } let!(:board_C) { create(:board, resource_parent: board_parent, name: 'C') } let!(:board_a) { create(:board, resource_parent: board_parent, name: 'a') } let!(:board_A) { create(:board, resource_parent: board_parent, name: 'A') } let(:boards) { [board_a, board_A, board_B, board_C] } + # rubocop:enable RSpec/VariableName context 'when ascending' do it_behaves_like 'sorted paginated query' do diff --git a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb index c5e5803c0a7..673d7741017 100644 --- a/spec/support/shared_examples/requests/api/issuable_participants_examples.rb +++ b/spec/support/shared_examples/requests/api/issuable_participants_examples.rb @@ -28,34 +28,4 @@ RSpec.shared_examples 'issuable participants endpoint' do expect(response).to have_gitlab_http_status(:not_found) end - - context 'with a confidential note' do - let!(:note) do - create( - :note, - :confidential, - project: project, - noteable: entity, - author: create(:user) - ) - end - - it 'returns a full list of participants' do - get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", user) - - expect(response).to have_gitlab_http_status(:ok) - participant_ids = json_response.map { |el| el['id'] } - expect(participant_ids).to match_array([entity.author_id, note.author_id]) - end - - context 'when user cannot see a confidential note' do - it 'returns a limited list of participants' do - get api("/projects/#{project.id}/#{area}/#{entity.iid}/participants", create(:user)) - - expect(response).to have_gitlab_http_status(:ok) - participant_ids = json_response.map { |el| el['id'] } - expect(participant_ids).to match_array([entity.author_id]) - end - end - end end diff --git a/spec/support/shared_examples/requests/api/notes_shared_examples.rb b/spec/support/shared_examples/requests/api/notes_shared_examples.rb index 2a157f6e855..e7e30665b08 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -142,15 +142,6 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| expect(json_response['author']['username']).to eq(user.username) end - it "creates a confidential note if confidential is set to true" do - post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: { body: 'hi!', confidential: true } - - expect(response).to have_gitlab_http_status(:created) - expect(json_response['body']).to eq('hi!') - expect(json_response['confidential']).to be_truthy - expect(json_response['author']['username']).to eq(user.username) - end - it "returns a 400 bad request error if body not given" do post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user) @@ -306,52 +297,31 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end describe "PUT /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes/:note_id" do - let(:params) { { body: 'Hello!', confidential: false } } + let(:params) { { body: 'Hello!' } } subject do put api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes/#{note.id}", user), params: params end - context 'when eveything is ok' do - before do - note.update!(confidential: true) - end + context 'when only body param is present' do + let(:params) { { body: 'Hello!' } } - context 'with multiple params present' do - before do - subject - end - - it 'returns modified note' do - expect(response).to have_gitlab_http_status(:ok) - expect(json_response['body']).to eq('Hello!') - expect(json_response['confidential']).to be_falsey - end - - it 'updates the note' do - expect(note.reload.note).to eq('Hello!') - expect(note.confidential).to be_falsey - end - end - - context 'when only body param is present' do - let(:params) { { body: 'Hello!' } } - - it 'updates only the note text' do - expect { subject }.not_to change { note.reload.confidential } + it 'updates the note text' do + subject - expect(note.note).to eq('Hello!') - end + expect(note.reload.note).to eq('Hello!') + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['body']).to eq('Hello!') end + end - context 'when only confidential param is present' do - let(:params) { { confidential: false } } + context 'when confidential param is present' do + let(:params) { { confidential: true } } - it 'updates only the note text' do - expect { subject }.not_to change { note.reload.note } + it 'does not allow to change confidentiality' do + expect { subject }.not_to change { note.reload.note } - expect(note.confidential).to be_falsey - end + expect(response).to have_gitlab_http_status(:bad_request) end end @@ -393,3 +363,24 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| end end end + +RSpec.shared_examples 'noteable API with confidential notes' do |parent_type, noteable_type, id_name| + it_behaves_like 'noteable API', parent_type, noteable_type, id_name + + describe "POST /#{parent_type}/:id/#{noteable_type}/:noteable_id/notes" do + let(:params) { { body: 'hi!' } } + + subject do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params + end + + it "creates a confidential note if confidential is set to true" do + post api("/#{parent_type}/#{parent.id}/#{noteable_type}/#{noteable[id_name]}/notes", user), params: params.merge(confidential: true) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response['body']).to eq('hi!') + expect(json_response['confidential']).to be_truthy + expect(json_response['author']['username']).to eq(user.username) + end + end +end 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 87a33060435..fcd52cdf7fa 100644 --- a/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb +++ b/spec/support/shared_examples/serializers/environment_serializer_shared_examples.rb @@ -1,8 +1,5 @@ # frozen_string_literal: true RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| - # Investigating in https://gitlab.com/gitlab-org/gitlab/-/issues/353209 - let(:query_threshold) { 1 + (ee ? 4 : 0) } - it 'avoids N+1 database queries with grouping', :request_store do create_environment_with_associations(project) @@ -11,9 +8,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| create_environment_with_associations(project) create_environment_with_associations(project) - expect { serialize(grouping: true) } - .not_to exceed_query_limit(control.count) - .with_threshold(query_threshold) + # Fix N+1 queries introduced by multi stop_actions for environment. + # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780 + relax_count = 14 + + expect { serialize(grouping: true) }.not_to exceed_query_limit(control.count + relax_count) end it 'avoids N+1 database queries without grouping', :request_store do @@ -24,9 +23,11 @@ RSpec.shared_examples 'avoid N+1 on environments serialization' do |ee: false| create_environment_with_associations(project) create_environment_with_associations(project) - expect { serialize(grouping: false) } - .not_to exceed_query_limit(control.count) - .with_threshold(query_threshold) + # Fix N+1 queries introduced by multi stop_actions for environment. + # Tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/358780 + relax_count = 14 + + expect { serialize(grouping: false) }.not_to exceed_query_limit(control.count + relax_count) end it 'does not preload for environments that does not exist in the page', :request_store do diff --git a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb index 0e2bddc19ab..fd832d4484d 100644 --- a/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/boards_list_service_shared_examples.rb @@ -13,10 +13,12 @@ RSpec.shared_examples 'boards list service' do end RSpec.shared_examples 'multiple boards list service' do + # rubocop:disable RSpec/VariableName let(:service) { described_class.new(parent, double) } let!(:board_B) { create(:board, resource_parent: parent, name: 'B-board') } let!(:board_c) { create(:board, resource_parent: parent, name: 'c-board') } let!(:board_a) { create(:board, resource_parent: parent, name: 'a-board') } + # rubocop:enable RSpec/VariableName describe '#execute' do it 'returns all issue boards' do diff --git a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb index a780952d51b..7677e5d8cb2 100644 --- a/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb +++ b/spec/support/shared_examples/services/container_registry_auth_service_shared_examples.rb @@ -154,6 +154,30 @@ RSpec.shared_examples 'logs an auth warning' do |requested_actions| end end +RSpec.shared_examples 'allowed to delete container repository images' do + let(:authentication_abilities) do + [:admin_container_image] + end + + it_behaves_like 'a valid token' + + context 'allow to delete images' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:*"] } + end + + it_behaves_like 'a deletable' + end + + context 'allow to delete images since registry 2.7' do + let(:current_params) do + { scopes: ["repository:#{project.full_path}:delete"] } + end + + it_behaves_like 'a deletable since registry 2.7' + end +end + RSpec.shared_examples 'a container registry auth service' do include_context 'container registry auth service context' @@ -204,6 +228,46 @@ RSpec.shared_examples 'a container registry auth service' do it_behaves_like 'not a container repository factory' end + describe '.pull_nested_repositories_access_token' do + let_it_be(:project) { create(:project) } + + let(:token) { described_class.pull_nested_repositories_access_token(project.full_path) } + let(:access) do + [ + { + 'type' => 'repository', + 'name' => project.full_path, + 'actions' => ['pull'] + }, + { + 'type' => 'repository', + 'name' => "#{project.full_path}/*", + 'actions' => ['pull'] + } + ] + end + + subject { { token: token } } + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end + + it_behaves_like 'a valid token' + it_behaves_like 'not a container repository factory' + + context 'with path ending with a slash' do + let(:token) { described_class.pull_nested_repositories_access_token("#{project.full_path}/") } + + it 'has the correct scope' do + expect(payload).to include('access' => access) + end + + it_behaves_like 'a valid token' + it_behaves_like 'not a container repository factory' + end + end + context 'user authorization' do let_it_be(:current_user) { create(:user) } @@ -504,38 +568,14 @@ RSpec.shared_examples 'a container registry auth service' do end context 'delete authorized as maintainer' do - let_it_be(:current_project) { create(:project) } + let_it_be(:project) { create(:project) } let_it_be(:current_user) { create(:user) } - let(:authentication_abilities) do - [:admin_container_image] - end - before_all do - current_project.add_maintainer(current_user) - end - - it_behaves_like 'a valid token' - - context 'allow to delete images' do - let(:current_params) do - { scopes: ["repository:#{current_project.full_path}:*"] } - end - - it_behaves_like 'a deletable' do - let(:project) { current_project } - end + project.add_maintainer(current_user) end - context 'allow to delete images since registry 2.7' do - let(:current_params) do - { scopes: ["repository:#{current_project.full_path}:delete"] } - end - - it_behaves_like 'a deletable since registry 2.7' do - let(:project) { current_project } - end - end + it_behaves_like 'allowed to delete container repository images' end context 'build authorized as user' do diff --git a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb index 6146aae6b9b..9610cdd18a3 100644 --- a/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb +++ b/spec/support/shared_examples/services/issuable_links/create_links_shared_examples.rb @@ -70,8 +70,10 @@ shared_examples 'issuable link creation' do expect(issuable_link_class.find_by!(target: issuable3)).to have_attributes(source: issuable, link_type: 'relates_to') end - it 'returns success status' do - is_expected.to eq(status: :success) + it 'returns success status and created links', :aggregate_failures do + expect(subject.keys).to match_array([:status, :created_references]) + expect(subject[:status]).to eq(:success) + expect(subject[:created_references].map(&:target_id)).to match_array([issuable2.id, issuable3.id]) end it 'creates notes' do diff --git a/spec/support/shared_examples/views/milestone_shared_examples.rb b/spec/support/shared_examples/views/milestone_shared_examples.rb new file mode 100644 index 00000000000..b6f4d0db0e9 --- /dev/null +++ b/spec/support/shared_examples/views/milestone_shared_examples.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'milestone empty states' do + include Devise::Test::ControllerHelpers + + let_it_be(:user) { build(:user) } + let(:empty_state) { 'Use milestones to track issues and merge requests over a fixed period of time' } + + before do + assign(:projects, []) + allow(view).to receive(:current_user).and_return(user) + end + + context 'with no milestones' do + before do + assign(:milestones, []) + assign(:milestone_states, { opened: 0, closed: 0, all: 0 }) + render + end + + it 'shows empty state' do + expect(rendered).to have_content(empty_state) + end + + it 'does not show tabs or searchbar' do + expect(rendered).not_to have_link('Open') + expect(rendered).not_to have_link('Closed') + expect(rendered).not_to have_link('All') + end + end + + context 'with no open milestones' do + before do + allow(view).to receive(:milestone_path).and_return("/milestones/1") + assign(:milestones, []) + assign(:milestone_states, { opened: 0, closed: 1, all: 1 }) + end + + it 'shows tabs and searchbar', :aggregate_failures do + render + + expect(rendered).not_to have_content(empty_state) + expect(rendered).to have_link('Open') + expect(rendered).to have_link('Closed') + expect(rendered).to have_link('All') + end + + it 'shows empty state' do + render + + expect(rendered).to have_content('There are no open milestones') + end + end + + context 'with no closed milestones' do + before do + allow(view).to receive(:milestone_path).and_return("/milestones/1") + allow(view).to receive(:params).and_return(state: 'closed') + assign(:milestones, []) + assign(:milestone_states, { opened: 1, closed: 0, all: 1 }) + end + + it 'shows tabs and searchbar', :aggregate_failures do + render + + expect(rendered).not_to have_content(empty_state) + expect(rendered).to have_link('Open') + expect(rendered).to have_link('Closed') + expect(rendered).to have_link('All') + end + + it 'shows empty state on closed milestones' do + render + + expect(rendered).to have_content('There are no closed milestones') + 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 d202c4e00f0..26731f34ed6 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 @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database| +RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_database, feature_flag:| include ExclusiveLeaseHelpers describe 'defining the job attributes' do @@ -39,6 +39,16 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d end end + describe '.enabled?' do + it 'does not raise an error' do + expect { described_class.enabled? }.not_to raise_error + end + + it 'returns true' do + expect(described_class.enabled?).to be_truthy + end + end + describe '#perform' do subject(:worker) { described_class.new } @@ -76,7 +86,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d context 'when the feature flag is disabled' do before do - stub_feature_flags(execute_batched_migrations_on_schedule: false) + stub_feature_flags(feature_flag => false) end it 'does nothing' do @@ -89,7 +99,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d context 'when the feature flag is enabled' do before do - stub_feature_flags(execute_batched_migrations_on_schedule: true) + stub_feature_flags(feature_flag => true) allow(Gitlab::Database::BackgroundMigration::BatchedMigration).to receive(:active_migration).and_return(nil) 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 202606c6aa6..4751d91efde 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 @@ -230,76 +230,6 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| stub_feature_flags(optimized_housekeeping: false) end - it 'incremental repack adds a new packfile' do - create_objects(resource) - before_packs = packs(resource) - - expect(before_packs.count).to be >= 1 - - subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid) - after_packs = packs(resource) - - # Exactly one new pack should have been created - expect(after_packs.count).to eq(before_packs.count + 1) - - # Previously existing packs are still around - expect(before_packs & after_packs).to eq(before_packs) - end - - it 'full repack consolidates into 1 packfile' do - create_objects(resource) - subject.perform(resource.id, 'incremental_repack', lease_key, lease_uuid) - before_packs = packs(resource) - - expect(before_packs.count).to be >= 2 - - subject.perform(resource.id, 'full_repack', lease_key, lease_uuid) - after_packs = packs(resource) - - expect(after_packs.count).to eq(1) - - # Previously existing packs should be gone now - expect(after_packs - before_packs).to eq(after_packs) - - expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) - end - - it 'gc consolidates into 1 packfile and updates packed-refs' do - create_objects(resource) - before_packs = packs(resource) - before_packed_refs = packed_refs(resource) - - expect(before_packs.count).to be >= 1 - - # It's quite difficult to use `expect_next_instance_of` in this place - # because the RepositoryService is instantiated several times to do - # some repository calls like `exists?`, `create_repository`, ... . - # Therefore, since we're instantiating the object several times, - # RSpec has troubles figuring out which instance is the next and which - # one we want to mock. - # Besides, at this point, we actually want to perform the call to Gitaly, - # otherwise we would just use `instance_double` like in other parts of the - # spec file. - expect_any_instance_of(Gitlab::GitalyClient::RepositoryService) # rubocop:disable RSpec/AnyInstanceOf - .to receive(:garbage_collect) - .with(bitmaps_enabled, prune: false) - .and_call_original - - subject.perform(resource.id, 'gc', lease_key, lease_uuid) - after_packed_refs = packed_refs(resource) - after_packs = packs(resource) - - expect(after_packs.count).to eq(1) - - # Previously existing packs should be gone now - expect(after_packs - before_packs).to eq(after_packs) - - # The packed-refs file should have been updated during 'git gc' - expect(before_packed_refs).not_to eq(after_packed_refs) - - expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled) - end - it 'cleans up repository after finishing' do expect(resource).to receive(:cleanup).and_call_original |