diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-07-20 15:40:28 +0000 |
commit | b595cb0c1dec83de5bdee18284abe86614bed33b (patch) | |
tree | 8c3d4540f193c5ff98019352f554e921b3a41a72 /spec/support | |
parent | 2f9104a328fc8a4bddeaa4627b595166d24671d0 (diff) | |
download | gitlab-ce-b595cb0c1dec83de5bdee18284abe86614bed33b.tar.gz |
Add latest changes from gitlab-org/gitlab@15-2-stable-eev15.2.0-rc42
Diffstat (limited to 'spec/support')
83 files changed, 1999 insertions, 540 deletions
diff --git a/spec/support/finder_collection.rb b/spec/support/finder_collection.rb new file mode 100644 index 00000000000..494dd4bdca1 --- /dev/null +++ b/spec/support/finder_collection.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'set' + +module Support + # Ensure that finders' `execute` method always returns + # `ActiveRecord::Relation`. + # + # See https://gitlab.com/gitlab-org/gitlab/-/issues/298771 + module FinderCollection + def self.install_check(finder_class) + return unless check?(finder_class) + + finder_class.prepend CheckResult + end + + ALLOWLIST_YAML = File.join(__dir__, 'finder_collection_allowlist.yml') + + def self.check?(finder_class) + @allowlist ||= YAML.load_file(ALLOWLIST_YAML).to_set + + @allowlist.exclude?(finder_class.name) + end + + module CheckResult + def execute(...) + result = super + + unless result.is_a?(ActiveRecord::Relation) + raise <<~MESSAGE + #{self.class}#execute returned `#{result.class}` instead of `ActiveRecord::Relation`. + All finder classes are expected to return `ActiveRecord::Relation`. + + Read more at https://docs.gitlab.com/ee/development/reusing_abstractions.html#finders + MESSAGE + end + + result + end + end + end +end + +RSpec.configure do |config| + config.before(:all, type: :finder) do + Support::FinderCollection.install_check(described_class) + end +end diff --git a/spec/support/finder_collection_allowlist.yml b/spec/support/finder_collection_allowlist.yml new file mode 100644 index 00000000000..8f09153afec --- /dev/null +++ b/spec/support/finder_collection_allowlist.yml @@ -0,0 +1,66 @@ +# Allow list for spec/support/finder_collection.rb + +# Permenant excludes +# For example: +# FooFinder # Reason: It uses a memory backend + +# Temporary excludes (aka TODOs) +# For example: +# BarFinder # See <ISSUE_URL> +- AccessRequestsFinder +- Admin::PlansFinder +- Analytics::CycleAnalytics::StageFinder +- ApplicationsFinder +- Autocomplete::GroupFinder +- Autocomplete::ProjectFinder +- Autocomplete::UsersFinder +- BilledUsersFinder +- Boards::BoardsFinder +- Boards::VisitsFinder +- BranchesFinder +- Ci::AuthJobFinder +- Ci::CommitStatusesFinder +- Ci::DailyBuildGroupReportResultsFinder +- ClusterAncestorsFinder +- Clusters::AgentAuthorizationsFinder +- Clusters::KubernetesNamespaceFinder +- ComplianceManagement::MergeRequests::ComplianceViolationsFinder +- ContainerRepositoriesFinder +- ContextCommitsFinder +- Environments::EnvironmentNamesFinder +- Environments::EnvironmentsByDeploymentsFinder +- EventsFinder +- GroupDescendantsFinder +- Groups::ProjectsRequiringAuthorizationsRefresh::OnDirectMembershipFinder +- Groups::ProjectsRequiringAuthorizationsRefresh::OnTransferFinder +- KeysFinder +- LfsPointersFinder +- LicenseTemplateFinder +- MergeRequests::OldestPerCommitFinder +- NotesFinder +- Packages::BuildInfosFinder +- Packages::Conan::PackageFileFinder +- Packages::Go::ModuleFinder +- Packages::Go::PackageFinder +- Packages::Go::VersionFinder +- Packages::PackageFileFinder +- Packages::PackageFinder +- Packages::Pypi::PackageFinder +- Projects::Integrations::Jira::ByIdsFinder +- Projects::Integrations::Jira::IssuesFinder +- Releases::EvidencePipelineFinder +- Repositories::BranchNamesFinder +- Repositories::ChangelogTagFinder +- Repositories::TreeFinder +- Security::FindingsFinder +- Security::PipelineVulnerabilitiesFinder +- Security::ScanExecutionPoliciesFinder +- Security::TrainingProviders::BaseUrlFinder +- Security::TrainingUrlsFinder +- SentryIssueFinder +- ServerlessDomainFinder +- TagsFinder +- TemplateFinder +- UploaderFinder +- UserGroupNotificationSettingsFinder +- UserGroupsCounter diff --git a/spec/support/gitlab_experiment.rb b/spec/support/gitlab_experiment.rb index 823aab0436e..4236091ca6c 100644 --- a/spec/support/gitlab_experiment.rb +++ b/spec/support/gitlab_experiment.rb @@ -2,7 +2,6 @@ # Require the provided spec helper and matchers. require 'gitlab/experiment/rspec' -require_relative 'stub_snowplow' RSpec.configure do |config| config.include StubSnowplow, :experiment diff --git a/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml b/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml new file mode 100644 index 00000000000..583d44c452e --- /dev/null +++ b/spec/support/gitlab_stubs/gitlab_ci_dast_includes.yml @@ -0,0 +1,10 @@ +dast: + stage: dast + image: + name: "$SECURE_ANALYZERS_PREFIX/dast:$DAST_VERSION" + variables: + GIT_STRATEGY: none + allow_failure: true + dast_configuration: + site_profile: "site_profile_name_included" + scanner_profile: "scanner_profile_name_included"
\ No newline at end of file diff --git a/spec/support/graphql/arguments.rb b/spec/support/graphql/arguments.rb index a5bb01c31a3..478a460a0f6 100644 --- a/spec/support/graphql/arguments.rb +++ b/spec/support/graphql/arguments.rb @@ -5,7 +5,7 @@ module Graphql delegate :blank?, :empty?, to: :to_h def initialize(values) - @values = values.compact + @values = values end def to_h @@ -42,7 +42,7 @@ module Graphql when Integer, Float, Symbol then value.to_s when String, GlobalID then "\"#{value.to_s.gsub(/"/, '\\"')}\"" when Time, Date then "\"#{value.iso8601}\"" - when nil then 'null' + when NilClass then 'null' when true then 'true' when false then 'false' else diff --git a/spec/support/helpers/database/database_helpers.rb b/spec/support/helpers/database/database_helpers.rb index db093bcef85..f3b2a2a6147 100644 --- a/spec/support/helpers/database/database_helpers.rb +++ b/spec/support/helpers/database/database_helpers.rb @@ -4,8 +4,10 @@ module Database module DatabaseHelpers # In order to directly work with views using factories, # we can swapout the view for a table of identical structure. - def swapout_view_for_table(view) - ActiveRecord::Base.connection.execute(<<~SQL.squish) + def swapout_view_for_table(view, connection: nil) + connection ||= ActiveRecord::Base.connection + + connection.execute(<<~SQL.squish) CREATE TABLE #{view}_copy (LIKE #{view}); DROP VIEW #{view}; ALTER TABLE #{view}_copy RENAME TO #{view}; diff --git a/spec/support/helpers/detailed_error_helpers.rb b/spec/support/helpers/detailed_error_helpers.rb new file mode 100644 index 00000000000..2da53a6bffd --- /dev/null +++ b/spec/support/helpers/detailed_error_helpers.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'google/rpc/status_pb' +require 'google/protobuf/well_known_types' + +module DetailedErrorHelpers + def new_detailed_error(error_code, error_message, details) + status_error = Google::Rpc::Status.new( + code: error_code, + message: error_message, + details: [Google::Protobuf::Any.pack(details)] + ) + + GRPC::BadStatus.new( + error_code, + error_message, + { "grpc-status-details-bin" => Google::Rpc::Status.encode(status_error) }) + end +end diff --git a/spec/support/helpers/features/invite_members_modal_helper.rb b/spec/support/helpers/features/invite_members_modal_helper.rb index 7ed64615020..b56ac5b32c6 100644 --- a/spec/support/helpers/features/invite_members_modal_helper.rb +++ b/spec/support/helpers/features/invite_members_modal_helper.rb @@ -9,18 +9,28 @@ module Spec click_on 'Invite members' page.within invite_modal_selector do - Array.wrap(names).each do |name| - find(member_dropdown_selector).set(name) + select_members(names) + choose_options(role, expires_at) + click_button 'Invite' + end - wait_for_requests - click_button name - end + page.refresh if refresh + end - choose_options(role, expires_at) + def input_invites(names) + click_on 'Invite members' - click_button 'Invite' + page.within invite_modal_selector do + select_members(names) + end + end + + def select_members(names) + Array.wrap(names).each do |name| + find(member_dropdown_selector).set(name) - page.refresh if refresh + wait_for_requests + click_button name end end @@ -64,6 +74,24 @@ module Spec '[data-testid="invite-modal"]' end + def member_token_error_selector(id) + "[data-testid='error-icon-#{id}']" + end + + def member_token_avatar_selector + "[data-testid='token-avatar']" + end + + def member_token_selector(id) + "[data-token-id='#{id}']" + end + + def remove_token(id) + page.within member_token_selector(id) do + find('[data-testid="close-icon"]').click + end + end + def expect_to_have_group(group) expect(page).to have_selector("[entity-id='#{group.id}']") end diff --git a/spec/support/helpers/features/source_editor_spec_helpers.rb b/spec/support/helpers/features/source_editor_spec_helpers.rb index 57057b47fbb..cdc59f9cbe1 100644 --- a/spec/support/helpers/features/source_editor_spec_helpers.rb +++ b/spec/support/helpers/features/source_editor_spec_helpers.rb @@ -15,13 +15,6 @@ module Spec execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')") end - - def editor_get_value - editor = find('.monaco-editor') - uri = editor['data-uri'] - - evaluate_script("monaco.editor.getModel('#{uri}').getValue()") - end end end end diff --git a/spec/support/helpers/features/web_ide_spec_helpers.rb b/spec/support/helpers/features/web_ide_spec_helpers.rb index 358bfacce05..70dedc3ac50 100644 --- a/spec/support/helpers/features/web_ide_spec_helpers.rb +++ b/spec/support/helpers/features/web_ide_spec_helpers.rb @@ -12,7 +12,7 @@ # ide_commit # module WebIdeSpecHelpers - include ActionView::Helpers::JavaScriptHelper + include Spec::Support::Helpers::Features::SourceEditorSpecHelpers def ide_visit(project) visit project_path(project) @@ -97,17 +97,7 @@ module WebIdeSpecHelpers end def ide_set_editor_value(value) - editor = find('.monaco-editor') - uri = editor['data-uri'] - - execute_script("monaco.editor.getModel('#{uri}').setValue('#{escape_javascript(value)}')") - end - - def ide_editor_value - editor = find('.monaco-editor') - uri = editor['data-uri'] - - evaluate_script("monaco.editor.getModel('#{uri}').getValue()") + editor_set_value(value) end def ide_commit_tab_selector diff --git a/spec/support/helpers/harbor_helper.rb b/spec/support/helpers/harbor_helper.rb new file mode 100644 index 00000000000..3f13710ede6 --- /dev/null +++ b/spec/support/helpers/harbor_helper.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module HarborHelper + def harbor_repository_url(container, *args) + if container.is_a?(Project) + project_harbor_repositories_path(container, *args) + else + group_harbor_repositories_path(container, *args) + end + end + + def harbor_artifact_url(container, *args) + if container.is_a?(Project) + project_harbor_repository_artifacts_path(container, *args) + else + group_harbor_repository_artifacts_path(container, *args) + end + end + + def harbor_tag_url(container, *args) + if container.is_a?(Project) + project_harbor_repository_artifact_tags_path(container, *args) + else + group_harbor_repository_artifact_tags_path(container, *args) + end + end +end diff --git a/spec/support/helpers/kubernetes_helpers.rb b/spec/support/helpers/kubernetes_helpers.rb index 29064f01913..dd210f02ae7 100644 --- a/spec/support/helpers/kubernetes_helpers.rb +++ b/spec/support/helpers/kubernetes_helpers.rb @@ -126,24 +126,6 @@ module KubernetesHelpers WebMock.stub_request(:get, pod_url).to_return(response || kube_pod_response) end - def stub_kubeclient_logs(pod_name, namespace, container: nil, status: nil, message: nil) - stub_kubeclient_discover(service.api_url) - - if container - container_query_param = "container=#{container}&" - end - - logs_url = service.api_url + "/api/v1/namespaces/#{namespace}/pods/#{pod_name}" \ - "/log?#{container_query_param}tailLines=#{::PodLogs::KubernetesService::LOGS_LIMIT}×tamps=true" - - if status - response = { status: status } - response[:body] = { message: message }.to_json if message - end - - WebMock.stub_request(:get, logs_url).to_return(response || kube_logs_response) - end - def stub_kubeclient_deployments(namespace, status: nil) stub_kubeclient_discover(service.api_url) deployments_url = service.api_url + "/apis/apps/v1/namespaces/#{namespace}/deployments" diff --git a/spec/support/helpers/project_forks_helper.rb b/spec/support/helpers/project_forks_helper.rb index 84b5dbc1d23..1504625bb00 100644 --- a/spec/support/helpers/project_forks_helper.rb +++ b/spec/support/helpers/project_forks_helper.rb @@ -70,12 +70,9 @@ module ProjectForksHelper def fork_project_with_submodules(project, user = nil, params = {}) Gitlab::GitalyClient.allow_n_plus_1_calls do forked_project = fork_project_direct(project, user, params) - TestEnv.copy_repo( - forked_project, - bare_repo: TestEnv.forked_repo_path_bare, - refs: TestEnv::FORKED_BRANCH_SHA - ) - forked_project.repository.expire_content_cache + repo = Gitlab::GlRepository::PROJECT.repository_for(forked_project) + repo.create_from_bundle(TestEnv.forked_repo_bundle_path) + repo.expire_content_cache forked_project end diff --git a/spec/support/helpers/project_helpers.rb b/spec/support/helpers/project_helpers.rb index ef8947ab340..2427ed2bcc9 100644 --- a/spec/support/helpers/project_helpers.rb +++ b/spec/support/helpers/project_helpers.rb @@ -13,7 +13,7 @@ module ProjectHelpers when :admin create(:user, :admin, name: 'admin') else - create(:user, name: membership).tap { |u| target.add_user(u, membership) } + create(:user, name: membership).tap { |u| target.add_member(u, membership) } end end diff --git a/spec/support/helpers/prometheus_helpers.rb b/spec/support/helpers/prometheus_helpers.rb index d49abbf3f19..e1f5e6dee14 100644 --- a/spec/support/helpers/prometheus_helpers.rb +++ b/spec/support/helpers/prometheus_helpers.rb @@ -267,6 +267,13 @@ module PrometheusHelpers } end + def prometheus_alert_payload_fingerprint(prometheus_alert) + # timestamp is hard-coded in #prometheus_map_alert_payload + fingerprint = "#{prometheus_alert.prometheus_metric_id}/2018-09-24T08:57:31.095725221Z" + + Gitlab::AlertManagement::Fingerprint.generate(fingerprint) + end + private def prometheus_map_alert_payload(status, alert) diff --git a/spec/support/helpers/repo_helpers.rb b/spec/support/helpers/repo_helpers.rb index f275be39dc4..e76a1dd5a74 100644 --- a/spec/support/helpers/repo_helpers.rb +++ b/spec/support/helpers/repo_helpers.rb @@ -137,80 +137,4 @@ eos file_content: content ).execute end - - def commit_options(repo, index, target, ref, message) - options = {} - options[:tree] = index.write_tree(repo) - options[:author] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:committer] = { - email: "test@example.com", - name: "Test Author", - time: Time.gm(2014, "mar", 3, 20, 15, 1) - } - options[:message] ||= message - options[:parents] = repo.empty? ? [] : [target].compact - options[:update_ref] = ref - - options - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of CHANGELOG with a single new line of text. - def new_commit_edit_old_file(repo) - oid = repo.write("I replaced the changelog with this text", :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "CHANGELOG", oid: oid, mode: 0100644) - - options = commit_options( - repo, - index, - repo.head.target, - "HEAD", - "Edit CHANGELOG in its original location" - ) - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of the specified file_path with new text. - def new_commit_edit_new_file(repo, file_path, commit_message, text, branch = repo.head) - oid = repo.write(text, :blob) - index = repo.index - index.read_tree(branch.target.tree) - index.add(path: file_path, oid: oid, mode: 0100644) - options = commit_options(repo, index, branch.target, branch.canonical_name, commit_message) - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Replaces the - # contents of encoding/CHANGELOG with new text. - def new_commit_edit_new_file_on_branch(repo, file_path, branch_name, commit_message, text) - branch = repo.branches[branch_name] - new_commit_edit_new_file(repo, file_path, commit_message, text, branch) - end - - # Writes a new commit to the repo and returns a Rugged::Commit. Moves the - # CHANGELOG file to the encoding/ directory. - def new_commit_move_file(repo) - blob_oid = repo.head.target.tree.detect { |i| i[:name] == "CHANGELOG" }[:oid] - file_content = repo.lookup(blob_oid).content - oid = repo.write(file_content, :blob) - index = repo.index - index.read_tree(repo.head.target.tree) - index.add(path: "encoding/CHANGELOG", oid: oid, mode: 0100644) - index.remove("CHANGELOG") - - options = commit_options(repo, index, repo.head.target, "HEAD", "Move CHANGELOG to encoding/") - - sha = Rugged::Commit.create(repo, options) - repo.lookup(sha) - end end diff --git a/spec/support/helpers/seed_helper.rb b/spec/support/helpers/seed_helper.rb index f65993efa05..59723583cbc 100644 --- a/spec/support/helpers/seed_helper.rb +++ b/spec/support/helpers/seed_helper.rb @@ -9,7 +9,6 @@ TEST_REPO_PATH = 'gitlab-git-test.git' TEST_NORMAL_REPO_PATH = 'not-bare-repo.git' TEST_MUTABLE_REPO_PATH = 'mutable-repo.git' TEST_BROKEN_REPO_PATH = 'broken-repo.git' -TEST_GITATTRIBUTES_REPO_PATH = 'with-git-attributes.git' module SeedHelper GITLAB_GIT_TEST_REPO_URL = File.expand_path('../gitlab-git-test.git', __dir__) @@ -25,8 +24,6 @@ module SeedHelper create_normal_seeds create_mutable_seeds create_broken_seeds - create_git_attributes - create_invalid_git_attributes end def create_bare_seeds @@ -67,48 +64,4 @@ module SeedHelper FileUtils.rm_r(refs_path) end - - def create_git_attributes - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --bare #{TEST_REPO_PATH} #{TEST_GITATTRIBUTES_REPO_PATH}), - chdir: SEED_STORAGE_PATH, - out: '/dev/null', - err: '/dev/null') - - dir = File.join(SEED_STORAGE_PATH, 'with-git-attributes.git', 'info') - - FileUtils.mkdir_p(dir) - - File.open(File.join(dir, 'attributes'), 'w') do |handle| - handle.write <<-EOF.strip -# This is a comment, it should be ignored. - -*.txt text -*.jpg -text -*.sh eol=lf gitlab-language=shell -*.haml.* gitlab-language=haml -foo/bar.* foo -*.cgi key=value?p1=v1&p2=v2 -/*.png gitlab-language=png -*.binary binary -/custom-highlighting/*.gitlab-custom gitlab-language=ruby -/custom-highlighting/*.gitlab-cgi gitlab-language=erb?parent=json - -# This uses a tab instead of spaces to ensure the parser also supports this. -*.md\tgitlab-language=markdown -bla/bla.txt - EOF - end - end - - def create_invalid_git_attributes - dir = File.join(SEED_STORAGE_PATH, 'with-invalid-git-attributes.git', 'info') - - FileUtils.mkdir_p(dir) - - enc = Encoding::UTF_16 - - File.open(File.join(dir, 'attributes'), 'w', encoding: enc) do |handle| - handle.write('# hello'.encode(enc)) - end - end end diff --git a/spec/support/stub_snowplow.rb b/spec/support/helpers/stub_snowplow.rb index c6e3b40972f..85c605efea3 100644 --- a/spec/support/stub_snowplow.rb +++ b/spec/support/helpers/stub_snowplow.rb @@ -13,7 +13,7 @@ module StubSnowplow .and_return(SnowplowTracker::Emitter.new(host, buffer_size: buffer_size)) # rubocop:enable RSpec/AnyInstanceOf - stub_application_setting(snowplow_enabled: true) + stub_application_setting(snowplow_enabled: true, snowplow_collector_hostname: host) allow(SnowplowTracker::SelfDescribingJson).to receive(:new).and_call_original allow(Gitlab::Tracking).to receive(:event).and_call_original # rubocop:disable RSpec/ExpectGitlabTracking diff --git a/spec/support/helpers/test_env.rb b/spec/support/helpers/test_env.rb index 7c865dd7e11..03e9ad1a08e 100644 --- a/spec/support/helpers/test_env.rb +++ b/spec/support/helpers/test_env.rb @@ -82,7 +82,13 @@ module TestEnv 'trailers' => 'f0a5ed6', 'add_commit_with_5mb_subject' => '8cf8e80', 'blame-on-renamed' => '32c33da', - 'with-executables' => '6b8dc4a' + 'with-executables' => '6b8dc4a', + 'spooky-stuff' => 'ba3343b', + 'few-commits' => '0031876', + 'two-commits' => '304d257', + 'utf-16' => 'f05a987', + 'gitaly-rename-test' => '94bb47c', + 'smime-signed-commits' => 'ed775cc' }.freeze # gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily @@ -259,43 +265,35 @@ module TestEnv # Create repository for FactoryBot.create(:project) def setup_factory_repo - setup_repo(factory_repo_path, factory_repo_path_bare, factory_repo_name, BRANCH_SHA) + setup_repo(factory_repo_path, factory_repo_bundle_path, factory_repo_name, BRANCH_SHA) end # Create repository for FactoryBot.create(:forked_project_with_submodules) # This repo has a submodule commit that is not present in the main test # repository. def setup_forked_repo - setup_repo(forked_repo_path, forked_repo_path_bare, forked_repo_name, FORKED_BRANCH_SHA) + setup_repo(forked_repo_path, forked_repo_bundle_path, forked_repo_name, FORKED_BRANCH_SHA) end - def setup_repo(repo_path, repo_path_bare, repo_name, refs) + def setup_repo(repo_path, repo_bundle_path, repo_name, refs) clone_url = "https://gitlab.com/gitlab-org/#{repo_name}.git" unless File.directory?(repo_path) start = Time.now system(*%W(#{Gitlab.config.git.bin_path} clone --quiet -- #{clone_url} #{repo_path})) + system(*%W(#{Gitlab.config.git.bin_path} -C #{repo_path} remote remove origin)) puts "==> #{repo_path} set up in #{Time.now - start} seconds...\n" end set_repo_refs(repo_path, refs) - unless File.directory?(repo_path_bare) + unless File.file?(repo_bundle_path) start = Time.now - # We must copy bare repositories because we will push to them. - system(git_env, *%W(#{Gitlab.config.git.bin_path} clone --quiet --bare -- #{repo_path} #{repo_path_bare})) - puts "==> #{repo_path_bare} set up in #{Time.now - start} seconds...\n" + system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{repo_path} bundle create #{repo_bundle_path} --all)) + puts "==> #{repo_bundle_path} generated in #{Time.now - start} seconds...\n" end end - def copy_repo(subject, bare_repo:, refs:) - target_repo_path = File.expand_path(repos_path + "/#{subject.disk_path}.git") - - FileUtils.mkdir_p(target_repo_path) - FileUtils.cp_r("#{File.expand_path(bare_repo)}/.", target_repo_path) - FileUtils.chmod_R 0755, target_repo_path - end - def rm_storage_dir(storage, dir) Gitlab::GitalyClient::StorageSettings.allow_disk_access do target_repo_refs_path = File.join(GitalySetup.repos_path(storage), dir) @@ -310,14 +308,6 @@ module TestEnv end end - def create_bare_repository(path) - FileUtils.mkdir_p(path) - - system(git_env, *%W(#{Gitlab.config.git.bin_path} -C #{path} init --bare), - out: '/dev/null', - err: '/dev/null') - end - def repos_path @repos_path ||= GitalySetup.repos_path end @@ -357,20 +347,12 @@ module TestEnv Capybara.current_session.visit '/' end - def factory_repo_path_bare - "#{factory_repo_path}_bare" - end - - def forked_repo_path_bare - "#{forked_repo_path}_bare" + def factory_repo_bundle_path + "#{factory_repo_path}.bundle" end - def with_empty_bare_repository(name = nil) - path = Rails.root.join('tmp/tests', name || 'empty-bare-repository').to_s - - yield(Rugged::Repository.init_at(path, :bare)) - ensure - FileUtils.rm_rf(path) + def forked_repo_bundle_path + "#{forked_repo_path}.bundle" end def seed_db @@ -386,9 +368,9 @@ module TestEnv gitaly gitlab-shell gitlab-test - gitlab-test_bare + gitlab-test.bundle gitlab-test-fork - gitlab-test-fork_bare + gitlab-test-fork.bundle gitlab-workhorse gitlab_workhorse_secret ] diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb index 50d1b14cf56..2a9144614d0 100644 --- a/spec/support/helpers/usage_data_helpers.rb +++ b/spec/support/helpers/usage_data_helpers.rb @@ -53,7 +53,6 @@ module UsageDataHelpers clusters_platforms_eks clusters_platforms_gke clusters_platforms_user - clusters_integrations_elastic_stack clusters_integrations_prometheus clusters_management_project in_review_folder @@ -91,7 +90,6 @@ module UsageDataHelpers projects_with_repositories_enabled projects_with_error_tracking_enabled projects_with_enabled_alert_integrations - projects_with_tracing_enabled projects_with_expiration_policy_enabled projects_with_expiration_policy_disabled projects_with_expiration_policy_enabled_with_keep_n_unset diff --git a/spec/support/matchers/background_migrations_matchers.rb b/spec/support/matchers/background_migrations_matchers.rb index c5b3e140585..9f39f576b95 100644 --- a/spec/support/matchers/background_migrations_matchers.rb +++ b/spec/support/matchers/background_migrations_matchers.rb @@ -74,6 +74,13 @@ RSpec::Matchers.define :have_scheduled_batched_migration do |gitlab_schema: :git .for_configuration(gitlab_schema, migration, table_name, column_name, job_arguments) expect(batched_migrations.count).to be(1) + + # the :batch_min_value & :batch_max_value attribute argument values get applied to the + # :min_value & :max_value columns on the database. Here we change the attribute names + # for the rspec have_attributes matcher used below to pass + attributes[:min_value] = attributes.delete :batch_min_value if attributes.include?(:batch_min_value) + attributes[:max_value] = attributes.delete :batch_max_value if attributes.include?(:batch_max_value) + expect(batched_migrations).to all(have_attributes(attributes)) if attributes.present? end diff --git a/spec/support/matchers/event_store.rb b/spec/support/matchers/event_store.rb index eb5b37f39e5..14f6a42d7f4 100644 --- a/spec/support/matchers/event_store.rb +++ b/spec/support/matchers/event_store.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true RSpec::Matchers.define :publish_event do |expected_event_class| + include RSpec::Matchers::Composable + supports_block_expectations match do |proc| @@ -15,10 +17,17 @@ RSpec::Matchers.define :publish_event do |expected_event_class| proc.call @events.any? do |event| - event.instance_of?(expected_event_class) && event.data == @expected_data + event.instance_of?(expected_event_class) && match_data?(event.data, @expected_data) end end + def match_data?(actual, expected) + values_match?(actual.keys, expected.keys) && + actual.keys.each do |key| + values_match?(actual[key], expected[key]) + end + end + chain :with do |expected_data| @expected_data = expected_data end diff --git a/spec/support/matchers/match_file.rb b/spec/support/matchers/match_file.rb index 4e522b52912..0c1f95d45f5 100644 --- a/spec/support/matchers/match_file.rb +++ b/spec/support/matchers/match_file.rb @@ -2,6 +2,6 @@ RSpec::Matchers.define :match_file do |expected| match do |actual| - expect(Digest::MD5.hexdigest(actual)).to eq(Digest::MD5.hexdigest(File.read(expected))) + expect(Digest::SHA256.hexdigest(actual)).to eq(Digest::SHA256.hexdigest(File.read(expected))) end end diff --git a/spec/support/services/issuable_import_csv_service_shared_examples.rb b/spec/support/services/issuable_import_csv_service_shared_examples.rb index 07118198969..0dea6cfb729 100644 --- a/spec/support/services/issuable_import_csv_service_shared_examples.rb +++ b/spec/support/services/issuable_import_csv_service_shared_examples.rb @@ -37,6 +37,10 @@ RSpec.shared_examples 'issuable import csv service' do |issuable_type| end describe '#execute' do + before do + project.add_developer(user) + end + context 'invalid file extension' do let(:file) { fixture_file_upload('spec/fixtures/banana_sample.gif') } diff --git a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb index 8635c9a8ff9..b31fe9ee0d1 100644 --- a/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb +++ b/spec/support/shared_contexts/controllers/ldap_omniauth_callbacks_controller_shared_context.rb @@ -14,6 +14,8 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do { main: ldap_config_defaults(:main) } end + let(:multiple_ldap_servers_license_available) { true } + def ldap_config_defaults(key, hash = {}) { provider_name: "ldap#{key}", @@ -23,6 +25,7 @@ RSpec.shared_context 'Ldap::OmniauthCallbacksController' do end before do + stub_licensed_features(multiple_ldap_servers: multiple_ldap_servers_license_available) stub_ldap_setting(ldap_settings) described_class.define_providers! Rails.application.reload_routes! diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb index aa8bc6fa79f..a3c688bb69e 100644 --- a/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_logging/structured_logger_shared_context.rb @@ -16,9 +16,6 @@ RSpec.shared_context 'structured_logger' do "created_at" => created_at.to_f, "enqueued_at" => created_at.to_f, "correlation_id" => 'cid', - "error_message" => "wrong number of arguments (2 for 3)", - "error_class" => "ArgumentError", - "error_backtrace" => [], "exception.message" => "wrong number of arguments (2 for 3)", "exception.class" => "ArgumentError", "exception.backtrace" => [] @@ -32,7 +29,6 @@ RSpec.shared_context 'structured_logger' do let(:clock_thread_cputime_end) { 1.333333799 } let(:start_payload) do job.except( - 'error_message', 'error_class', 'error_backtrace', 'exception.backtrace', 'exception.class', 'exception.message' ).merge( 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: start', @@ -73,9 +69,6 @@ RSpec.shared_context 'structured_logger' do end_payload.merge( 'message' => 'TestWorker JID-da883554ee4fe414012f5f42: fail: 0.0 sec', 'job_status' => 'fail', - 'error_class' => 'ArgumentError', - 'error_message' => 'Something went wrong', - 'error_backtrace' => be_a(Array).and(be_present), 'exception.class' => 'ArgumentError', 'exception.message' => 'Something went wrong', 'exception.backtrace' => be_a(Array).and(be_present) diff --git a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb index 0d992f33c61..449db59e35d 100644 --- a/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb +++ b/spec/support/shared_contexts/lib/gitlab/sidekiq_middleware/server_metrics_shared_context.rb @@ -10,6 +10,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do let(:gitaly_seconds_metric) { double('gitaly seconds metric') } let(:failed_total_metric) { double('failed total metric') } let(:retried_total_metric) { double('retried total metric') } + let(:interrupted_total_metric) { double('interrupted total metric') } let(:redis_requests_total) { double('redis calls total metric') } let(:running_jobs_metric) { double('running jobs metric') } let(:redis_seconds_metric) { double('redis seconds metric') } @@ -30,6 +31,7 @@ RSpec.shared_context 'server metrics with mocked prometheus' do allow(Gitlab::Metrics).to receive(:histogram).with(:sidekiq_elasticsearch_requests_duration_seconds, anything, anything, anything).and_return(elasticsearch_seconds_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_failed_total, anything).and_return(failed_total_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_retried_total, anything).and_return(retried_total_metric) + allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_jobs_interrupted_total, anything).and_return(interrupted_total_metric) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_redis_requests_total, anything).and_return(redis_requests_total) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_elasticsearch_requests_total, anything).and_return(elasticsearch_requests_total) allow(Gitlab::Metrics).to receive(:counter).with(:sidekiq_load_balancing_count, anything).and_return(load_balancing_metric) diff --git a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb index de52b58982e..a90fe9e1723 100644 --- a/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb +++ b/spec/support/shared_contexts/markdown_snapshot_shared_examples.rb @@ -5,12 +5,12 @@ require 'spec_helper' # See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing # for documentation on this spec. # rubocop:disable Layout/LineLength -RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_specification_dir, glfm_example_snapshots_dir| +RSpec.shared_context 'with API::Markdown Snapshot shared context' do |glfm_specification_dir| # rubocop:enable Layout/LineLength include ApiHelpers markdown_examples, html_examples = %w[markdown.yml html.yml].map do |file_name| - yaml = File.read("#{glfm_example_snapshots_dir}/#{file_name}") + yaml = File.read("#{glfm_specification_dir}/example_snapshots/#{file_name}") YAML.safe_load(yaml, symbolize_names: true, aliases: true) end diff --git a/spec/support/shared_contexts/navbar_structure_context.rb b/spec/support/shared_contexts/navbar_structure_context.rb index d277a45584d..6c2ed79b343 100644 --- a/spec/support/shared_contexts/navbar_structure_context.rb +++ b/spec/support/shared_contexts/navbar_structure_context.rb @@ -83,8 +83,6 @@ RSpec.shared_context 'project navbar structure' do nav_item: _('Monitor'), nav_sub_items: [ _('Metrics'), - _('Logs'), - _('Tracing'), _('Error Tracking'), _('Alerts'), _('Incidents'), @@ -112,6 +110,7 @@ RSpec.shared_context 'project navbar structure' do _('Access Tokens'), _('Repository'), _('CI/CD'), + _('Packages & Registries'), _('Monitor'), s_('UsageQuota|Usage Quotas') ] diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb index 483bca07ba6..eec6e92c5fe 100644 --- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb @@ -31,6 +31,7 @@ RSpec.shared_context 'GroupPolicy context' do admin_milestone admin_issue_board read_container_image + read_harbor_registry read_metrics_dashboard_annotation read_prometheus read_crm_contact diff --git a/spec/support/shared_contexts/policies/project_policy_shared_context.rb b/spec/support/shared_contexts/policies/project_policy_shared_context.rb index 7396643823c..789b385c435 100644 --- a/spec/support/shared_contexts/policies/project_policy_shared_context.rb +++ b/spec/support/shared_contexts/policies/project_policy_shared_context.rb @@ -12,6 +12,7 @@ RSpec.shared_context 'ProjectPolicy context' do let_it_be_with_refind(:private_project) { create(:project, :private, namespace: owner.namespace) } let_it_be_with_refind(:internal_project) { create(:project, :internal, namespace: owner.namespace) } let_it_be_with_refind(:public_project) { create(:project, :public, namespace: owner.namespace) } + let_it_be_with_refind(:public_project_in_group) { create(:project, :public, namespace: create(:group, :public)) } let(:base_guest_permissions) do %i[ @@ -29,7 +30,7 @@ RSpec.shared_context 'ProjectPolicy context' do create_snippet create_incident daily_statistics create_merge_request_in download_code download_wiki_code fork_project metrics_dashboard read_build read_commit_status read_confidential_issues read_container_image - read_deployment read_environment read_merge_request + read_harbor_registry read_deployment read_environment read_merge_request read_metrics_dashboard_annotation read_pipeline read_prometheus read_sentry_issue update_issue create_merge_request_in ] @@ -93,7 +94,7 @@ RSpec.shared_context 'ProjectPolicy context' do let(:owner_permissions) { base_owner_permissions + additional_owner_permissions } before_all do - [private_project, internal_project, public_project].each do |project| + [private_project, internal_project, public_project, public_project_in_group].each do |project| project.add_guest(guest) project.add_reporter(reporter) project.add_developer(developer) diff --git a/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb new file mode 100644 index 00000000000..98fc52add51 --- /dev/null +++ b/spec/support/shared_examples/controllers/snowplow_event_tracking_examples.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true +# +# Requires a context containing: +# - subject +# - project +# - feature_flag_name +# - category +# - action +# - namespace +# - user + +shared_examples 'Snowplow event tracking' do + let(:label) { nil } + + it 'is not emitted if FF is disabled' do + stub_feature_flags(feature_flag_name => false) + + subject + + expect_no_snowplow_event + end + + it 'is emitted' do + params = { + category: category, + action: action, + namespace: namespace, + user: user, + project: project, + label: label + }.compact + + subject + + expect_snowplow_event(**params) + end +end diff --git a/spec/support/shared_examples/csp.rb b/spec/support/shared_examples/csp.rb index 9143d0f4720..91242ae9f37 100644 --- a/spec/support/shared_examples/csp.rb +++ b/spec/support/shared_examples/csp.rb @@ -15,64 +15,66 @@ RSpec.shared_examples 'setting CSP' do |rule_name| end end - context 'when no CSP config' do - include_context 'csp config', nil + context 'csp config and feature toggle', :do_not_stub_snowplow_by_default do + context 'when no CSP config' do + include_context 'csp config', nil - it 'does not add CSP directives' do - is_expected.to be_blank + it 'does not add CSP directives' do + is_expected.to be_blank + end end - end - describe "when a CSP config exists for #{rule_name}" do - include_context 'csp config', rule_name.parameterize.underscore.to_sym + describe "when a CSP config exists for #{rule_name}" do + include_context 'csp config', rule_name.parameterize.underscore.to_sym - context 'when feature is enabled' do - it "appends to #{rule_name}" do - is_expected.to eql("#{rule_name} #{default_csp_values} #{allowlisted_url}") + context 'when feature is enabled' do + it "appends to #{rule_name}" do + is_expected.to eql("#{rule_name} #{default_csp_values} #{allowlisted_url}") + end end - end - context 'when feature is disabled' do - include_context 'disable feature' + context 'when feature is disabled' do + include_context 'disable feature' - it "keeps original #{rule_name}" do - is_expected.to eql("#{rule_name} #{default_csp_values}") + it "keeps original #{rule_name}" do + is_expected.to eql("#{rule_name} #{default_csp_values}") + end end end - end - describe "when a CSP config exists for default-src but not #{rule_name}" do - include_context 'csp config', :default_src + describe "when a CSP config exists for default-src but not #{rule_name}" do + include_context 'csp config', :default_src - context 'when feature is enabled' do - it "uses default-src values in #{rule_name}" do - is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{allowlisted_url}") + context 'when feature is enabled' do + it "uses default-src values in #{rule_name}" do + is_expected.to eql("default-src #{default_csp_values}; #{rule_name} #{default_csp_values} #{allowlisted_url}") + end end - end - context 'when feature is disabled' do - include_context 'disable feature' + context 'when feature is disabled' do + include_context 'disable feature' - it "does not add #{rule_name}" do - is_expected.to eql("default-src #{default_csp_values}") + it "does not add #{rule_name}" do + is_expected.to eql("default-src #{default_csp_values}") + end end end - end - describe "when a CSP config exists for font-src but not #{rule_name}" do - include_context 'csp config', :font_src + describe "when a CSP config exists for font-src but not #{rule_name}" do + include_context 'csp config', :font_src - context 'when feature is enabled' do - it "uses default-src values in #{rule_name}" do - is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{allowlisted_url}") + context 'when feature is enabled' do + it "uses default-src values in #{rule_name}" do + is_expected.to eql("font-src #{default_csp_values}; #{rule_name} #{allowlisted_url}") + end end - end - context 'when feature is disabled' do - include_context 'disable feature' + context 'when feature is disabled' do + include_context 'disable feature' - it "does not add #{rule_name}" do - is_expected.to eql("font-src #{default_csp_values}") + it "does not add #{rule_name}" do + is_expected.to eql("font-src #{default_csp_values}") + end end end 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 591f7973454..0ea82f37db0 100644 --- a/spec/support/shared_examples/features/content_editor_shared_examples.rb +++ b/spec/support/shared_examples/features/content_editor_shared_examples.rb @@ -31,8 +31,6 @@ RSpec.shared_examples 'edits content using the content editor' do page.go_back refresh - - click_button 'Edit rich text' end it 'applies theme classes to code blocks' do diff --git a/spec/support/shared_examples/features/discussion_comments_shared_example.rb b/spec/support/shared_examples/features/discussion_comments_shared_example.rb index 6c06cbf9082..24dc4bcfc59 100644 --- a/spec/support/shared_examples/features/discussion_comments_shared_example.rb +++ b/spec/support/shared_examples/features/discussion_comments_shared_example.rb @@ -293,7 +293,7 @@ RSpec.shared_examples 'thread comments for issue, epic and merge request' do |re it 'can be collapsed' do submit_reply('another text') - find('.js-collapse-replies').click + click_button s_('Notes|Collapse replies'), match: :first expect(page).to have_css('.discussion-notes .note', count: 1) expect(page).to have_content '1 reply' 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 index 58357b262f5..bca0e02fcdd 100644 --- a/spec/support/shared_examples/features/inviting_members_shared_examples.rb +++ b/spec/support/shared_examples/features/inviting_members_shared_examples.rb @@ -23,6 +23,22 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| ) end + it 'displays the user\'s avatar in the member input token', :js do + visit members_page_path + + input_invites(user2.name) + + expect(page).to have_selector(member_token_avatar_selector) + end + + it 'does not display an avatar in the member input token for an email address', :js do + visit members_page_path + + input_invites('test@example.com') + + expect(page).not_to have_selector(member_token_avatar_selector) + end + it 'invites user by email', :js, :snowplow, :aggregate_failures do visit members_page_path @@ -78,22 +94,23 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| end context 'when member is already a member by email' do - it 'fails with an error', :js do + it 'updates the member for that email', :js do + email = 'test@example.com' + visit members_page_path - invite_member('test@example.com', role: 'Developer') + invite_member(email, role: 'Developer') - invite_member('test@example.com', role: 'Reporter', refresh: false) + invite_member(email, 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") + expect(page).not_to have_selector(invite_modal_selector) page.refresh click_link 'Invited' - page.within find_invited_member_row('test@example.com') do - expect(page).to have_button('Developer') + page.within find_invited_member_row(email) do + expect(page).to have_button('Reporter') end end end @@ -131,8 +148,8 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| 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}" + expect(page).to have_content "#{user2.name}: Access level should be greater than or equal to Developer " \ + "inherited membership from group #{group.name}" page.refresh @@ -149,13 +166,31 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| group.add_maintainer(user3) end - it 'only shows the first user error', :js do + it 'shows the user errors and then removes them from the form', :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) + expect(page).to have_selector(member_token_error_selector(user2.id)) + expect(page).to have_selector(member_token_error_selector(user3.id)) + expect(page).to have_text("The following 2 members couldn't be invited") + expect(page).to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(page).to have_text("#{user3.name}: Access level should be greater than or equal to") + + remove_token(user2.id) + + expect(page).not_to have_selector(member_token_error_selector(user2.id)) + expect(page).to have_selector(member_token_error_selector(user3.id)) + expect(page).to have_text("The following member couldn't be invited") + expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + + remove_token(user3.id) + + expect(page).not_to have_selector(member_token_error_selector(user3.id)) + expect(page).not_to have_text("The following member couldn't be invited") + expect(page).not_to have_text("Review the invite errors and try again") + expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") page.refresh @@ -169,6 +204,19 @@ RSpec.shared_examples 'inviting members' do |snowplow_invite_label| expect(page).not_to have_button('Maintainer') end end + + it 'only shows the error for an invalid formatted email and does not display other member errors', :js do + visit subentity_members_page_path + + invite_member([user2.name, user3.name, 'bad@email'], role: role, refresh: false) + + expect(page).to have_selector(invite_modal_selector) + expect(page).to have_text('email contains an invalid email address') + expect(page).not_to have_text("The following 2 members couldn't be invited") + expect(page).not_to have_text("Review the invite errors and try again") + expect(page).not_to have_text("#{user2.name}: Access level should be greater than or equal to") + expect(page).not_to have_text("#{user3.name}: Access level should be greater than or equal to") + end end end end diff --git a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb index 4565108b5e4..9d023d9514a 100644 --- a/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_mr_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees merge request' do |action, save_button it "#{action} a MR with multiple assignees", :js do find('.js-assignee-search').click page.within '.dropdown-menu-user' do - click_link user.name unless action == 'creates' + click_link user.name click_link user2.name end diff --git a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb index a44a699c878..bbde448a1a1 100644 --- a/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb +++ b/spec/support/shared_examples/features/multiple_assignees_widget_mr_shared_examples.rb @@ -4,7 +4,7 @@ RSpec.shared_examples 'multiple assignees widget merge request' do |action, save it "#{action} a MR with multiple assignees", :js do find('.js-assignee-search').click page.within '.dropdown-menu-user' do - click_link user.name unless action == 'creates' + click_link user.name click_link user2.name end diff --git a/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb b/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb new file mode 100644 index 00000000000..79de2aedf3b --- /dev/null +++ b/spec/support/shared_examples/features/wiki/autocomplete_shared_examples.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'autocompletes items' do + before do + if defined?(project) + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:label, project: project, title: 'My Cool Label') + create(:milestone, project: project, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'JohnDoe123')) + else # group wikis + project = create(:project, group: group) + + create(:issue, project: project, title: 'My Cool Linked Issue') + create(:merge_request, source_project: project, title: 'My Cool Merge Request') + create(:group_label, group: group, title: 'My Cool Label') + create(:milestone, group: group, title: 'My Cool Milestone') + + project.add_maintainer(create(:user, name: 'JohnDoe123')) + end + end + + it 'works well for issues, labels, MRs, members, etc' do + fill_in :wiki_content, with: "#" + expect(page).to have_text 'My Cool Linked Issue' + + fill_in :wiki_content, with: "~" + expect(page).to have_text 'My Cool Label' + + fill_in :wiki_content, with: "!" + expect(page).to have_text 'My Cool Merge Request' + + fill_in :wiki_content, with: "%" + expect(page).to have_text 'My Cool Milestone' + + fill_in :wiki_content, with: "@" + expect(page).to have_text 'JohnDoe123' + + fill_in :wiki_content, with: ':smil' + expect(page).to have_text 'smile_cat' + end +end 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 12a4c6d7583..79c7c1891ac 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 @@ -146,6 +146,8 @@ RSpec.shared_examples 'User updates wiki page' do it_behaves_like 'edits content using the content editor' end end + + it_behaves_like 'autocompletes items' end context 'when the page is in a subdir', :js do diff --git a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb index 622a88e8323..9d8f37a3e64 100644 --- a/spec/support/shared_examples/finders/issues_finder_shared_examples.rb +++ b/spec/support/shared_examples/finders/issues_finder_shared_examples.rb @@ -269,6 +269,17 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context it 'returns items not assigned to that milestone' do expect(items).to contain_exactly(item2, item3, item4, item5) end + + context 'with multiple milestones' do + let(:milestone2) { create(:milestone, project: project2) } + let(:params) { { not: { milestone_title: [milestone.title, milestone2.title] } } } + + it 'returns items not assigned to both milestones' do + item2.update!(milestone: milestone2) + + expect(items).to contain_exactly(item3, item4, item5) + end + end end context 'filtering by group milestone' do @@ -962,7 +973,7 @@ RSpec.shared_examples 'issues or work items finder' do |factory, execute_context group = create(:group) project = create(:project, group: group) item = create(factory, project: project) - group.add_user(user, :owner) + group.add_member(user, :owner) expect(items).to include(item) end diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb new file mode 100644 index 00000000000..56c2ca22e15 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/work_items/update_description_widget_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update work item description widget' do + it 'updates the description widget' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :description).from(nil).to(new_description) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'description' => new_description, + 'type' => 'DESCRIPTION' + } + ) + end + + context 'when the updated work item is not valid' do + it 'returns validation errors without the work item' do + errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:description, 'error message') } + + allow_next_found_instance_of(::WorkItem) do |instance| + allow(instance).to receive(:valid?).and_return(false) + allow(instance).to receive(:errors).and_return(errors) + end + + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array(['Description error message']) + end + end +end diff --git a/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb b/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb new file mode 100644 index 00000000000..3c32b7e0310 --- /dev/null +++ b/spec/support/shared_examples/graphql/mutations/work_items/update_weight_widget_shared_examples.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'update work item weight widget' do + it 'updates the weight widget' do + expect do + post_graphql_mutation(mutation, current_user: current_user) + work_item.reload + end.to change(work_item, :weight).from(nil).to(new_weight) + + expect(response).to have_gitlab_http_status(:success) + expect(mutation_response['workItem']['widgets']).to include( + { + 'weight' => new_weight, + 'type' => 'WEIGHT' + } + ) + end + + context 'when the updated work item is not valid' do + it 'returns validation errors without the work item' do + errors = ActiveModel::Errors.new(work_item).tap { |e| e.add(:weight, 'error message') } + + allow_next_found_instance_of(::WorkItem) do |instance| + allow(instance).to receive(:valid?).and_return(false) + allow(instance).to receive(:errors).and_return(errors) + end + + post_graphql_mutation(mutation, current_user: current_user) + + expect(mutation_response['workItem']).to be_nil + expect(mutation_response['errors']).to match_array(['Weight error message']) + end + end +end diff --git a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb index 6d6e7b761f6..59927fa1cc9 100644 --- a/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb +++ b/spec/support/shared_examples/graphql/sorted_paginated_query_shared_examples.rb @@ -44,19 +44,25 @@ # end # end # + +# Include this context if your field does not accept a sort argument +RSpec.shared_context 'no sort argument' do + let(:sort_argument) { graphql_args } +end + RSpec.shared_examples 'sorted paginated query' do |conditions = {}| # Provided as a convenience when constructing queries using string concatenation let(:page_info) { 'pageInfo { startCursor endCursor }' } # Convenience for using default implementation of pagination_results_data let(:node_path) { ['id'] } + let(:sort_argument) { graphql_args(sort: sort_param) } it_behaves_like 'requires variables' do - let(:required_variables) { [:sort_param, :first_param, :all_records, :data_path, :current_user] } + let(:required_variables) { [:first_param, :all_records, :data_path, :current_user] } end describe do - let(:sort_argument) { graphql_args(sort: sort_param) } - let(:params) { sort_argument } + let(:params) { sort_argument } # Convenience helper for the large number of queries defined as a projection # from some root value indexed by full_path to a collection of objects with IID diff --git a/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb new file mode 100644 index 00000000000..85fcd426e3d --- /dev/null +++ b/spec/support/shared_examples/harbor/artifacts_controller_shared_examples.rb @@ -0,0 +1,162 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a harbor artifacts controller' do |args| + include HarborHelper + let_it_be(:user) { create(:user) } + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:json_header) { { accept: 'application/json' } } + + let(:mock_artifacts) do + [ + { + "digest": "sha256:661e8e44e5d7290fbd42d0495ab4ff6fdf1ad251a9f358969b3264a22107c14d", + "icon": "sha256:0048162a053eef4d4ce3fe7518615bef084403614f8bca43b40ae2e762e11e06", + "id": 1, + "project_id": 1, + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.901Z", + "repository_id": 1, + "size": 126745886, + "tags": [ + { + "artifact_id": 1, + "id": 1, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.920Z", + "repository_id": 1, + "signed": false + } + ], + "type": "IMAGE" + } + ] + end + + let(:repository_id) { 'test' } + + shared_examples 'responds with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'responds with 200 status with json' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).not_to render_template(:index) + end + end + + shared_examples 'responds with 302 status' do + it 'returns 302' do + subject + + expect(response).to redirect_to(new_user_session_path) + end + end + + shared_examples 'responds with 422 status with json' do + it 'returns 422' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + before do + stub_request(:get, + "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts"\ + "?page=1&page_size=10&with_tag=true") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }).to_return(status: 200, body: mock_artifacts.to_json, headers: { "x-total-count": 2 }) + container.add_reporter(user) + sign_in(user) + end + + describe 'GET #index.json' do + subject do + get harbor_artifact_url(container, repository_id), headers: json_header + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status with json' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with anonymous user' do + before do + sign_out(user) + end + + it_behaves_like "responds with #{args[:anonymous_status_code]} status" + end + + context 'with unauthorized user' do + before do + sign_in(unauthorized_user) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with valid params' do + context 'with valid repository' do + subject do + get harbor_artifact_url(container, repository_id), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + + context 'with valid page' do + subject do + get harbor_artifact_url(container, repository_id, page: '1'), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + + context 'with valid limit' do + subject do + get harbor_artifact_url(container, repository_id, limit: '10'), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + end + + context 'with invalid params' do + context 'with invalid page' do + subject do + get harbor_artifact_url(container, repository_id, page: 'aaa'), headers: json_header + end + + it_behaves_like 'responds with 422 status with json' + end + + context 'with invalid limit' do + subject do + get harbor_artifact_url(container, repository_id, limit: 'aaa'), headers: json_header + end + + it_behaves_like 'responds with 422 status with json' + end + end + end +end diff --git a/spec/support/shared_examples/harbor/container_shared_examples.rb b/spec/support/shared_examples/harbor/container_shared_examples.rb new file mode 100644 index 00000000000..57274e0b457 --- /dev/null +++ b/spec/support/shared_examples/harbor/container_shared_examples.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'raises NotImplementedError when calling #container' do + describe '#container' do + it 'raises NotImplementedError' do + expect { controller.send(:container) }.to raise_error(NotImplementedError) + end + end +end diff --git a/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb new file mode 100644 index 00000000000..b35595a10b2 --- /dev/null +++ b/spec/support/shared_examples/harbor/repositories_controller_shared_examples.rb @@ -0,0 +1,172 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a harbor repositories controller' do |args| + include HarborHelper + let_it_be(:user) { create(:user) } + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:json_header) { { accept: 'application/json' } } + + let(:mock_repositories) do + [ + { + "artifact_count": 6, + "creation_time": "2022-04-24T10:59:02.719Z", + "id": 33, + "name": "test/photon", + "project_id": 3, + "pull_count": 12, + "update_time": "2022-04-24T11:06:27.678Z" + }, + { + "artifact_count": 1, + "creation_time": "2022-04-23T08:04:08.880Z", + "id": 1, + "name": "test/gemnasium", + "project_id": 3, + "pull_count": 0, + "update_time": "2022-04-23T08:04:08.880Z" + } + ] + end + + shared_examples 'responds with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'responds with 200 status with html' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to render_template(:index) + end + end + + shared_examples 'responds with 302 status' do + it 'returns 302' do + subject + + expect(response).to redirect_to(new_user_session_path) + end + end + + shared_examples 'responds with 200 status with json' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).not_to render_template(:index) + end + end + + shared_examples 'responds with 422 status with json' do + it 'returns 422' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + before do + stub_request(:get, "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories?page=1&page_size=10") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }).to_return(status: 200, body: mock_repositories.to_json, headers: { "x-total-count": 2 }) + container.add_reporter(user) + sign_in(user) + end + + describe 'GET #index.html' do + subject do + get harbor_repository_url(container) + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status with html' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with anonymous user' do + before do + sign_out(user) + end + + it_behaves_like "responds with #{args[:anonymous_status_code]} status" + end + + context 'with unauthorized user' do + before do + sign_in(unauthorized_user) + end + + it_behaves_like 'responds with 404 status' + end + end + + describe 'GET #index.json' do + subject do + get harbor_repository_url(container), headers: json_header + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status with json' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with valid params' do + context 'with valid page params' do + subject do + get harbor_repository_url(container, page: '1'), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + + context 'with valid limit params' do + subject do + get harbor_repository_url(container, limit: '10'), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + end + + context 'with invalid params' do + context 'with invalid page params' do + subject do + get harbor_repository_url(container, page: 'aaa'), headers: json_header + end + + it_behaves_like 'responds with 422 status with json' + end + + context 'with invalid limit params' do + subject do + get harbor_repository_url(container, limit: 'aaa'), headers: json_header + end + + it_behaves_like 'responds with 422 status with json' + end + end + end +end diff --git a/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb new file mode 100644 index 00000000000..46fea7fdff6 --- /dev/null +++ b/spec/support/shared_examples/harbor/tags_controller_shared_examples.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'a harbor tags controller' do |args| + include HarborHelper + let_it_be(:user) { create(:user) } + let_it_be(:unauthorized_user) { create(:user) } + let_it_be(:json_header) { { accept: 'application/json' } } + + let(:mock_artifacts) do + [ + { + "artifact_id": 1, + "id": 1, + "immutable": false, + "name": "2", + "pull_time": "0001-01-01T00:00:00.000Z", + "push_time": "2022-04-23T08:04:08.920Z", + "repository_id": 1, + "signed": false + } + ] + end + + let(:repository_id) { 'test' } + let(:artifact_id) { '1' } + + shared_examples 'responds with 404 status' do + it 'returns 404' do + subject + + expect(response).to have_gitlab_http_status(:not_found) + end + end + + shared_examples 'responds with 200 status with json' do + it 'renders the index template' do + subject + + expect(response).to have_gitlab_http_status(:ok) + expect(response).not_to render_template(:index) + end + end + + shared_examples 'responds with 302 status' do + it 'returns 302' do + subject + + expect(response).to redirect_to(new_user_session_path) + end + end + + shared_examples 'responds with 422 status with json' do + it 'returns 422' do + subject + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + before do + stub_request(:get, + "https://demo.goharbor.io/api/v2.0/projects/testproject/repositories/test/artifacts/1/tags"\ + "?page=1&page_size=10") + .with( + headers: { + 'Authorization': 'Basic aGFyYm9ydXNlcm5hbWU6aGFyYm9ycGFzc3dvcmQ=', + 'Content-Type': 'application/json' + }).to_return(status: 200, body: mock_artifacts.to_json, headers: { "x-total-count": 2 }) + container.add_reporter(user) + sign_in(user) + end + + describe 'GET #index.json' do + subject do + get(harbor_tag_url(container, repository_id, artifact_id), + headers: json_header) + end + + context 'with harbor registry feature flag enabled' do + it_behaves_like 'responds with 200 status with json' + end + + context 'with harbor registry feature flag disabled' do + before do + stub_feature_flags(harbor_registry_integration: false) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with anonymous user' do + before do + sign_out(user) + end + + it_behaves_like "responds with #{args[:anonymous_status_code]} status" + end + + context 'with unauthorized user' do + before do + sign_in(unauthorized_user) + end + + it_behaves_like 'responds with 404 status' + end + + context 'with valid params' do + context 'with valid repository' do + subject do + get harbor_tag_url(container, repository_id, artifact_id), headers: json_header + end + + it_behaves_like 'responds with 200 status with json' + end + + context 'with valid page' do + subject do + get(harbor_tag_url(container, repository_id, artifact_id, page: '1'), + headers: json_header) + end + + it_behaves_like 'responds with 200 status with json' + end + + context 'with valid limit' do + subject do + get(harbor_tag_url(container, repository_id, artifact_id, limit: '10'), + headers: json_header) + end + + it_behaves_like 'responds with 200 status with json' + end + end + + context 'with invalid params' do + context 'with invalid page' do + subject do + get(harbor_tag_url(container, repository_id, artifact_id, page: 'aaa'), + headers: json_header) + end + + it_behaves_like 'responds with 422 status with json' + end + + context 'with invalid limit' do + subject do + get(harbor_tag_url(container, repository_id, artifact_id, limit: 'aaa'), + headers: json_header) + end + + it_behaves_like 'responds with 422 status with json' + end + end + end +end diff --git a/spec/support/shared_examples/integrations/integration_settings_form.rb b/spec/support/shared_examples/integrations/integration_settings_form.rb index dfe5a071f91..5041ac4a660 100644 --- a/spec/support/shared_examples/integrations/integration_settings_form.rb +++ b/spec/support/shared_examples/integrations/integration_settings_form.rb @@ -20,6 +20,11 @@ RSpec.shared_examples 'integration settings form' do "#{integration.title} field #{field_name} not present" end + api_only_fields = integration.fields.select { _1[:api_only] } + api_only_fields.each do |field| + expect(page).not_to have_field("service[#{field.name}]", wait: 0) + end + sections = integration.sections events = parse_json(trigger_events_for_integration(integration)) diff --git a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb index 284c129221b..b786d7e5527 100644 --- a/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/ci/ci_trace_shared_examples.rb @@ -265,10 +265,9 @@ RSpec.shared_examples 'common trace features' do end context 'build token' do - let(:token) { 'my_secret_token' } + let(:token) { build.token } before do - build.update!(token: token) trace.append(token, 0) end diff --git a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb index 326800e6dc2..c9300aff3e6 100644 --- a/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb +++ b/spec/support/shared_examples/lib/gitlab/position_formatters_shared_examples.rb @@ -32,21 +32,7 @@ RSpec.shared_examples "position formatter" do subject { formatter.to_h } - context 'when file_identifier_hash is disabled' do - before do - stub_feature_flags(file_identifier_hash: false) - end - - it { is_expected.to eq(formatter_hash.except(:file_identifier_hash)) } - end - - context 'when file_identifier_hash is enabled' do - before do - stub_feature_flags(file_identifier_hash: true) - end - - it { is_expected.to eq(formatter_hash) } - end + it { is_expected.to eq(formatter_hash) } end describe '#==' do diff --git a/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb new file mode 100644 index 00000000000..a3e4379f4d3 --- /dev/null +++ b/spec/support/shared_examples/lib/gitlab/search_language_filter_shared_examples.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'search results filtered by language' do + let(:scope) { 'blobs' } + let(:filters) { { language: %w[Ruby Markdown] } } + let(:query) { 'def | popen | test' } + + before do + project.repository.index_commits_and_blobs + + ensure_elasticsearch_index! + end + + subject(:blob_results) { results.objects('blobs') } + + it 'filters by language', :sidekiq_inline, :aggregate_failures do + expected_paths = %w[ + files/ruby/popen.rb + files/markdown/ruby-style-guide.md + files/ruby/regex.rb + files/ruby/version_info.rb + CONTRIBUTING.md + ] + + paths = blob_results.map { |blob| blob.binary_path } + expect(blob_results.size).to eq(5) + expect(paths).to match_array(expected_paths) + end + + context 'when the search_blobs_language_aggregation feature flag is disabled' do + before do + stub_feature_flags(search_blobs_language_aggregation: false) + end + + it 'does not filter by language', :sidekiq_inline, :aggregate_failures do + expected_paths = %w[ + CHANGELOG + CONTRIBUTING.md + bar/branch-test.txt + custom-highlighting/test.gitlab-custom + files/ruby/popen.rb + files/ruby/regex.rb + files/ruby/version_info.rb + files/whitespace + encoding/test.txt + files/markdown/ruby-style-guide.md + ] + + paths = blob_results.map { |blob| blob.binary_path } + expect(blob_results.size).to eq(10) + expect(paths).to match_array(expected_paths) + end + end +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 b5d93aec1bf..9d280d9404a 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 @@ -32,3 +32,45 @@ RSpec.shared_examples 'does not track when feature flag is disabled' do |feature end end end + +RSpec.shared_examples 'a daily tracked issuable snowplow and service ping events' do + before do + stub_application_setting(usage_ping_enabled: true) + end + + 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 + + specify do + aggregate_failures do + expect(track_action(author: user1, project: project)).to be_truthy + expect(track_action(author: user1, project: project)).to be_truthy + expect(track_action(author: user2, project: project)).to be_truthy + expect(count_unique).to eq(2) + end + end + + it 'does not track edit actions if author is not present' do + expect(track_action(author: nil, project: project)).to be_nil + end + + it 'emits snowplow event' do + track_action(author: user1, project: project) + + expect_snowplow_event(category: 'issues_edit', action: action, user: user1, + namespace: project.namespace, project: project) + end + + context 'with route_hll_to_snowplow_phase2 disabled' do + before do + stub_feature_flags(route_hll_to_snowplow_phase2: false) + end + + it 'does not emit snowplow event' do + track_action(author: user1, project: project) + + expect_no_snowplow_event + end + end +end diff --git a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb b/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb deleted file mode 100644 index d4986975f03..00000000000 --- a/spec/support/shared_examples/merge_request_author_auto_assign_shared_examples.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -RSpec.shared_examples 'merge request author auto assign' do - it 'populates merge request author as assignee' do - expect(find('.js-assignee-search')).to have_content(user.name) - expect(page).not_to have_content 'Assign yourself' - end -end diff --git a/spec/support/shared_examples/models/chat_integration_shared_examples.rb b/spec/support/shared_examples/models/chat_integration_shared_examples.rb index fa10b03fa90..d189e91effd 100644 --- a/spec/support/shared_examples/models/chat_integration_shared_examples.rb +++ b/spec/support/shared_examples/models/chat_integration_shared_examples.rb @@ -357,7 +357,8 @@ RSpec.shared_examples "chat integration" do |integration_name| end context 'deployment events' do - let(:sample_data) { Gitlab::DataBuilder::Deployment.build(create(:deployment), Time.now) } + let(:deployment) { create(:deployment) } + let(:sample_data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) } it_behaves_like "untriggered #{integration_name} integration" end diff --git a/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb b/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb deleted file mode 100644 index 744262d79ea..00000000000 --- a/spec/support/shared_examples/models/clusters/elastic_stack_client_shared.rb +++ /dev/null @@ -1,82 +0,0 @@ -# frozen_string_literal: true - -# Input -# - factory: [:clusters_applications_elastic_stack, :clusters_integrations_elastic_stack] -RSpec.shared_examples 'cluster-based #elasticsearch_client' do |factory| - describe '#elasticsearch_client' do - context 'cluster is nil' do - subject { build(factory, cluster: nil) } - - it 'returns nil' do - expect(subject.cluster).to be_nil - expect(subject.elasticsearch_client).to be_nil - end - end - - context "cluster doesn't have kubeclient" do - let(:cluster) { create(:cluster) } - - subject { create(factory, cluster: cluster) } - - it 'returns nil' do - expect(subject.elasticsearch_client).to be_nil - end - end - - context 'cluster has kubeclient' do - let(:cluster) { create(:cluster, :project, :provided_by_gcp) } - let(:kubernetes_url) { subject.cluster.platform_kubernetes.api_url } - let(:kube_client) { subject.cluster.kubeclient.core_client } - - subject { create(factory, cluster: cluster) } - - before do - subject.cluster.platform_kubernetes.namespace = 'a-namespace' - stub_kubeclient_discover(cluster.platform_kubernetes.api_url) - - create(:cluster_kubernetes_namespace, - cluster: cluster, - cluster_project: cluster.cluster_project, - project: cluster.cluster_project.project) - end - - it 'creates proxy elasticsearch_client' do - expect(subject.elasticsearch_client).to be_instance_of(Elasticsearch::Transport::Client) - end - - it 'copies proxy_url, options and headers from kube client to elasticsearch_client' do - expect(Elasticsearch::Client) - .to(receive(:new)) - .with(url: a_valid_url, adapter: :net_http) - .and_call_original - - client = subject.elasticsearch_client - faraday_connection = client.transport.connections.first.connection - - expect(faraday_connection.headers["Authorization"]).to eq(kube_client.headers[:Authorization]) - expect(faraday_connection.ssl.cert_store).to be_instance_of(OpenSSL::X509::Store) - expect(faraday_connection.ssl.verify).to eq(1) - expect(faraday_connection.options.timeout).to be_nil - end - - context 'when cluster is not reachable' do - before do - allow(kube_client).to receive(:proxy_url).and_raise(Kubeclient::HttpError.new(401, 'Unauthorized', nil)) - end - - it 'returns nil' do - expect(subject.elasticsearch_client).to be_nil - end - end - - context 'when timeout is provided' do - it 'sets timeout in elasticsearch_client' do - client = subject.elasticsearch_client(timeout: 123) - faraday_connection = client.transport.connections.first.connection - - expect(faraday_connection.options.timeout).to eq(123) - end - end - end - end -end diff --git a/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb b/spec/support/shared_examples/models/concerns/integrations/slack_mattermost_notifier_shared_examples.rb index 2e062cda4e9..d80be5be3b3 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 @@ -230,7 +230,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name context 'deployment events' do let_it_be(:deployment) { create(:deployment) } - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.current) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, 'created', Time.current) } it_behaves_like 'calls the integration API with the event message', /Deploy to (.*?) created/ end @@ -677,7 +677,7 @@ RSpec.shared_examples Integrations::SlackMattermostNotifier do |integration_name create(:deployment, :success, project: project, sha: project.commit.sha, ref: project.default_branch) end - let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, Time.now) } + let(:data) { Gitlab::DataBuilder::Deployment.build(deployment, deployment.status, Time.now) } before do allow(chat_integration).to receive_messages( diff --git a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb index a2b4cdc33d0..d06e8391a9a 100644 --- a/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb +++ b/spec/support/shared_examples/models/concerns/timebox_shared_examples.rb @@ -82,7 +82,7 @@ RSpec.shared_examples 'a timebox' do |timebox_type| it { is_expected.to belong_to(:group) } it { is_expected.to have_many(:issues) } it { is_expected.to have_many(:merge_requests) } - it { is_expected.to have_many(:labels) } + it { is_expected.to have_many(:labels).through(:issues) } end describe '#timebox_name' do diff --git a/spec/support/shared_examples/models/issuable_participants_shared_examples.rb b/spec/support/shared_examples/models/issuable_participants_shared_examples.rb new file mode 100644 index 00000000000..c3eaae0ace2 --- /dev/null +++ b/spec/support/shared_examples/models/issuable_participants_shared_examples.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'issuable participants' do + context 'when resource parent is public' do + context 'and users are referenced on notes' do + let_it_be(:notes_author) { create(:user) } + + let(:note_params) { params.merge(author: notes_author) } + + before do + create(:note, note_params) + end + + it 'includes the issue author' do + expect(issuable.participants).to include(issuable.author) + end + + it 'includes the authors of the notes' do + expect(issuable.participants).to include(notes_author) + end + + context 'and note is confidential' do + context 'and mentions users' do + let_it_be(:guest_1) { create(:user) } + let_it_be(:guest_2) { create(:user) } + let_it_be(:reporter) { create(:user) } + + before do + issuable_parent.add_guest(guest_1) + issuable_parent.add_guest(guest_2) + issuable_parent.add_reporter(reporter) + + confidential_note_params = + note_params.merge( + confidential: true, + note: "mentions #{guest_1.to_reference} and #{guest_2.to_reference} and #{reporter.to_reference}" + ) + + regular_note_params = + note_params.merge(note: "Mentions #{guest_2.to_reference}") + + create(:note, confidential_note_params) + create(:note, regular_note_params) + end + + it 'only includes users that can read the note as participants' do + expect(issuable.participants).to contain_exactly(issuable.author, notes_author, reporter, guest_2) + end + end + end + end + end +end diff --git a/spec/support/shared_examples/models/member_shared_examples.rb b/spec/support/shared_examples/models/member_shared_examples.rb index 75fff11cecd..aa40a2c7135 100644 --- a/spec/support/shared_examples/models/member_shared_examples.rb +++ b/spec/support/shared_examples/models/member_shared_examples.rb @@ -80,7 +80,7 @@ RSpec.shared_examples_for "member creation" do let_it_be(:admin) { create(:admin) } it 'returns a Member object', :aggregate_failures do - member = described_class.add_user(source, user, :maintainer) + member = described_class.add_member(source, user, :maintainer) expect(member).to be_a member_type expect(member).to be_persisted @@ -99,7 +99,7 @@ RSpec.shared_examples_for "member creation" do end it 'does not update the member' do - member = described_class.add_user(source, project_bot, :maintainer, current_user: user) + member = described_class.add_member(source, project_bot, :maintainer, current_user: user) expect(source.users.reload).to include(project_bot) expect(member).to be_persisted @@ -110,7 +110,7 @@ RSpec.shared_examples_for "member creation" do context 'when project_bot is not already a member' do it 'adds the member' do - member = described_class.add_user(source, project_bot, :maintainer, current_user: user) + member = described_class.add_member(source, project_bot, :maintainer, current_user: user) expect(source.users.reload).to include(project_bot) expect(member).to be_persisted @@ -120,7 +120,7 @@ RSpec.shared_examples_for "member creation" do 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.add_user(source, user, :maintainer, current_user: admin) + member = described_class.add_member(source, user, :maintainer, current_user: admin) expect(member).to be_persisted expect(source.users.reload).to include(user) @@ -130,7 +130,7 @@ RSpec.shared_examples_for "member creation" do context 'when admin mode is disabled' do it 'rejects setting members.created_by to the given admin current_user', :aggregate_failures do - member = described_class.add_user(source, user, :maintainer, current_user: admin) + member = described_class.add_member(source, user, :maintainer, current_user: admin) expect(member).not_to be_persisted expect(source.users.reload).not_to include(user) @@ -139,7 +139,7 @@ RSpec.shared_examples_for "member creation" do end it 'sets members.expires_at to the given expires_at' do - member = described_class.add_user(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)) + member = described_class.add_member(source, user, :maintainer, expires_at: Date.new(2016, 9, 22)) expect(member.expires_at).to eq(Date.new(2016, 9, 22)) end @@ -148,7 +148,7 @@ RSpec.shared_examples_for "member creation" do it "accepts the :#{sym_key} symbol as access level", :aggregate_failures do expect(source.users).not_to include(user) - member = described_class.add_user(source, user.id, sym_key) + member = described_class.add_member(source, user.id, sym_key) expect(member.access_level).to eq(int_access_level) expect(source.users.reload).to include(user) @@ -157,7 +157,7 @@ RSpec.shared_examples_for "member creation" do it "accepts the #{int_access_level} integer as access level", :aggregate_failures do expect(source.users).not_to include(user) - member = described_class.add_user(source, user.id, int_access_level) + member = described_class.add_member(source, user.id, int_access_level) expect(member.access_level).to eq(int_access_level) expect(source.users.reload).to include(user) @@ -169,7 +169,7 @@ RSpec.shared_examples_for "member creation" do it 'adds the user as a member' do expect(source.users).not_to include(user) - described_class.add_user(source, user.id, :maintainer) + described_class.add_member(source, user.id, :maintainer) expect(source.users.reload).to include(user) end @@ -179,7 +179,7 @@ RSpec.shared_examples_for "member creation" do it 'does not add the user as a member' do expect(source.users).not_to include(user) - described_class.add_user(source, non_existing_record_id, :maintainer) + described_class.add_member(source, non_existing_record_id, :maintainer) expect(source.users.reload).not_to include(user) end @@ -189,7 +189,7 @@ RSpec.shared_examples_for "member creation" do it 'adds the user as a member' do expect(source.users).not_to include(user) - described_class.add_user(source, user, :maintainer) + described_class.add_member(source, user, :maintainer) expect(source.users.reload).to include(user) end @@ -205,7 +205,7 @@ RSpec.shared_examples_for "member creation" do expect(source.requesters.exists?(user_id: user)).to be_truthy expect do - described_class.add_user(source, user, :maintainer) + described_class.add_member(source, user, :maintainer) end.to raise_error(Gitlab::Access::AccessDeniedError) expect(source.users.reload).not_to include(user) @@ -217,7 +217,7 @@ RSpec.shared_examples_for "member creation" do it 'adds the user as a member' do expect(source.users).not_to include(user) - described_class.add_user(source, user.email, :maintainer) + described_class.add_member(source, user.email, :maintainer) expect(source.users.reload).to include(user) end @@ -227,7 +227,7 @@ RSpec.shared_examples_for "member creation" do it 'creates an invited member' do expect(source.users).not_to include(user) - described_class.add_user(source, 'user@example.com', :maintainer) + described_class.add_member(source, 'user@example.com', :maintainer) expect(source.members.invite.pluck(:invite_email)).to include('user@example.com') end @@ -237,7 +237,7 @@ RSpec.shared_examples_for "member creation" do it 'creates an invited member', :aggregate_failures do email_starting_with_number = "#{user.id}_email@example.com" - described_class.add_user(source, email_starting_with_number, :maintainer) + described_class.add_member(source, email_starting_with_number, :maintainer) expect(source.members.invite.pluck(:invite_email)).to include(email_starting_with_number) expect(source.users.reload).not_to include(user) @@ -249,7 +249,7 @@ RSpec.shared_examples_for "member creation" do it 'creates the member' do expect(source.users).not_to include(user) - described_class.add_user(source, user, :maintainer, current_user: admin) + described_class.add_member(source, user, :maintainer, current_user: admin) expect(source.users.reload).to include(user) end @@ -263,7 +263,7 @@ RSpec.shared_examples_for "member creation" do expect(source.users).not_to include(user) expect(source.requesters.exists?(user_id: user)).to be_truthy - described_class.add_user(source, user, :maintainer, current_user: admin) + described_class.add_member(source, user, :maintainer, current_user: admin) expect(source.users.reload).to include(user) expect(source.requesters.reload.exists?(user_id: user)).to be_falsy @@ -275,7 +275,7 @@ RSpec.shared_examples_for "member creation" do it 'does not create the member', :aggregate_failures do expect(source.users).not_to include(user) - member = described_class.add_user(source, user, :maintainer, current_user: user) + member = described_class.add_member(source, user, :maintainer, current_user: user) expect(source.users.reload).not_to include(user) expect(member).not_to be_persisted @@ -290,7 +290,7 @@ RSpec.shared_examples_for "member creation" do expect(source.users).not_to include(user) expect(source.requesters.exists?(user_id: user)).to be_truthy - described_class.add_user(source, user, :maintainer, current_user: user) + described_class.add_member(source, user, :maintainer, current_user: user) expect(source.users.reload).not_to include(user) expect(source.requesters.exists?(user_id: user)).to be_truthy @@ -299,37 +299,51 @@ RSpec.shared_examples_for "member creation" do end context 'when member already exists' do - before do - source.add_user(user, :developer) - end + context 'when member is a user' do + before do + source.add_member(user, :developer) + end - context 'with no current_user' do - it 'updates the member' do - expect(source.users).to include(user) + context 'with no current_user' do + it 'updates the member' do + expect(source.users).to include(user) - described_class.add_user(source, user, :maintainer) + described_class.add_member(source, user, :maintainer) - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end end - end - context 'when current_user can update member', :enable_admin_mode do - it 'updates the member' do - expect(source.users).to include(user) + context 'when current_user can update member', :enable_admin_mode do + it 'updates the member' do + expect(source.users).to include(user) - described_class.add_user(source, user, :maintainer, current_user: admin) + described_class.add_member(source, user, :maintainer, current_user: admin) - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MAINTAINER) + end end - end - context 'when current_user cannot update member' do - it 'does not update the member' do - expect(source.users).to include(user) + context 'when current_user cannot update member' do + it 'does not update the member' do + expect(source.users).to include(user) + + described_class.add_member(source, user, :maintainer, current_user: user) + + expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) + end + end + end - described_class.add_user(source, user, :maintainer, current_user: user) + context 'when member is an invite by email' do + let_it_be(:email) { 'user@email.com' } + let_it_be(:existing_member) { source.add_developer(email) } - expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER) + it 'updates the member for that email' do + expect do + described_class.add_member(source, email, :maintainer) + end.to change { existing_member.reset.access_level }.from(Member::DEVELOPER).to(Member::MAINTAINER) + .and not_change { source.members.invite.count } end end end @@ -345,12 +359,12 @@ RSpec.shared_examples_for "bulk member creation" do # maintainers cannot add owners source.add_maintainer(user) - expect(described_class.add_users(source, [user1, user2], :owner, current_user: user)).to be_empty + expect(described_class.add_members(source, [user1, user2], :owner, current_user: user)).to be_empty end end it 'returns Member objects' do - members = described_class.add_users(source, [user1, user2], :maintainer) + members = described_class.add_members(source, [user1, user2], :maintainer) expect(members.map(&:user)).to contain_exactly(user1, user2) expect(members).to all(be_a(member_type)) @@ -358,7 +372,7 @@ RSpec.shared_examples_for "bulk member creation" do end it 'returns an empty array' do - members = described_class.add_users(source, [], :maintainer) + members = described_class.add_members(source, [], :maintainer) expect(members).to be_a Array expect(members).to be_empty @@ -367,7 +381,7 @@ RSpec.shared_examples_for "bulk member creation" do it 'supports different formats' do list = ['joe@local.test', admin, user1.id, user2.id.to_s] - members = described_class.add_users(source, list, :maintainer) + members = described_class.add_members(source, list, :maintainer) expect(members.size).to eq(4) expect(members.first).to be_invite @@ -375,7 +389,7 @@ RSpec.shared_examples_for "bulk member creation" do context 'with de-duplication' do it 'has the same user by id and user' do - members = described_class.add_users(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer) + members = described_class.add_members(source, [user1.id, user1, user1.id, user2, user2.id, user2], :maintainer) expect(members.map(&:user)).to contain_exactly(user1, user2) expect(members).to all(be_a(member_type)) @@ -383,7 +397,7 @@ RSpec.shared_examples_for "bulk member creation" do end it 'has the same user sent more than once' do - members = described_class.add_users(source, [user1, user1], :maintainer) + members = described_class.add_members(source, [user1, user1], :maintainer) expect(members.map(&:user)).to contain_exactly(user1) expect(members).to all(be_a(member_type)) @@ -392,7 +406,7 @@ RSpec.shared_examples_for "bulk member creation" do 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) + members = described_class.add_members(source, [user1, user1.email], :maintainer) expect(members.map(&:user)).to contain_exactly(user1) expect(members).to all(be_a(member_type)) @@ -400,7 +414,7 @@ RSpec.shared_examples_for "bulk member creation" do 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) + members = described_class.add_members(source, [user1.id, user1.email], :maintainer) expect(members.map(&:user)).to contain_exactly(user1) expect(members).to all(be_a(member_type)) @@ -409,12 +423,12 @@ RSpec.shared_examples_for "bulk member creation" do context 'when a member already exists' do before do - source.add_user(user1, :developer) + source.add_member(user1, :developer) end it 'has the same user sent more than once with the member already existing' do expect do - members = described_class.add_users(source, [user1, user1, user2], :maintainer) + members = described_class.add_members(source, [user1, user1, user2], :maintainer) expect(members.map(&:user)).to contain_exactly(user1, user2) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) @@ -425,7 +439,7 @@ RSpec.shared_examples_for "bulk member creation" do user3 = create(:user) expect do - members = described_class.add_users(source, [user1.id, user2, user3.id], :maintainer) + members = described_class.add_members(source, [user1.id, user2, user3.id], :maintainer) expect(members.map(&:user)).to contain_exactly(user1, user2, user3) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) @@ -436,7 +450,7 @@ RSpec.shared_examples_for "bulk member creation" do user3 = create(:user) expect do - members = described_class.add_users(source, [user1, user2, user3], :maintainer) + members = described_class.add_members(source, [user1, user2, user3], :maintainer) expect(members.map(&:user)).to contain_exactly(user1, user2, user3) expect(members).to all(be_a(member_type)) expect(members).to all(be_persisted) @@ -448,7 +462,7 @@ RSpec.shared_examples_for "bulk member creation" do let(:task_project) { source.is_a?(Group) ? create(:project, group: source) : source } it 'creates a member_task with the correct attributes', :aggregate_failures do - members = described_class.add_users(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id) + members = described_class.add_members(source, [user1], :developer, tasks_to_be_done: %w(ci code), tasks_project_id: task_project.id) member = members.last expect(member.tasks_to_be_done).to match_array([:ci, :code]) @@ -457,7 +471,7 @@ RSpec.shared_examples_for "bulk member creation" do context 'with an already existing member' do before do - source.add_user(user1, :developer) + source.add_member(user1, :developer) end it 'does not update tasks to be done if tasks already exist', :aggregate_failures do @@ -465,7 +479,7 @@ RSpec.shared_examples_for "bulk member creation" do create(:member_task, member: member, project: task_project, tasks_to_be_done: %w(code ci)) expect do - described_class.add_users(source, + described_class.add_members(source, [user1.id], :developer, tasks_to_be_done: %w(issues), @@ -479,7 +493,7 @@ RSpec.shared_examples_for "bulk member creation" do it 'adds tasks to be done if they do not exist', :aggregate_failures do expect do - described_class.add_users(source, + described_class.add_members(source, [user1.id], :developer, tasks_to_be_done: %w(issues), diff --git a/spec/support/shared_examples/models/mentionable_shared_examples.rb b/spec/support/shared_examples/models/mentionable_shared_examples.rb index e23658d1774..f9612dd61be 100644 --- a/spec/support/shared_examples/models/mentionable_shared_examples.rb +++ b/spec/support/shared_examples/models/mentionable_shared_examples.rb @@ -260,6 +260,25 @@ RSpec.shared_examples 'mentions in notes' do |mentionable_type| expect(mentionable.referenced_projects(user)).to eq [mentionable.project].compact # epic.project is nil, and we want empty [] expect(mentionable.referenced_groups(user)).to eq [group] end + + if [:epic, :issue].include?(mentionable_type) + context 'and note is confidential' do + let_it_be(:guest) { create(:user) } + + let(:note_desc) { "#{guest.to_reference} and #{user2.to_reference} and #{user.to_reference}" } + + before do + note.resource_parent.add_reporter(user2) + note.resource_parent.add_guest(guest) + # Bypass :confidential update model validation for testing purposes + note.update_attribute(:confidential, true) + end + + it 'returns only mentioned users that has permissions' do + expect(note.mentioned_users).to contain_exactly(user, user2) + end + end + end end end @@ -294,6 +313,26 @@ RSpec.shared_examples 'load mentions from DB' do |mentionable_type| end end + if [:epic, :issue].include?(mentionable_type) + context 'and note is confidential' do + let_it_be(:guest) { create(:user) } + + let(:note_desc) { "#{guest.to_reference} and #{mentioned_user.to_reference}" } + + before do + note.resource_parent.add_reporter(mentioned_user) + note.resource_parent.add_guest(guest) + # Bypass :confidential update model validation for testing purposes + note.update_attribute(:confidential, true) + note.store_mentions! + end + + it 'stores only mentioned users that has permissions' do + expect(mentionable.referenced_users).to contain_exactly(mentioned_user) + end + end + end + context 'when private projects and groups are mentioned' do let(:mega_user) { create(:user) } let(:private_project) { create(:project, :private) } diff --git a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb index ab04692616a..d42e925ed22 100644 --- a/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb +++ b/spec/support/shared_examples/quick_actions/issue/clone_quick_action_shared_examples.rb @@ -89,10 +89,13 @@ RSpec.shared_examples 'clone quick action' do let(:bug) { create(:label, project: project, title: 'bug') } let(:wontfix) { create(:label, project: project, title: 'wontfix') } - let!(:target_milestone) { create(:milestone, title: '1.0', project: target_project) } - before do target_project.add_maintainer(user) + + # create equivalent labels and milestones in the target project + create(:label, project: target_project, title: 'bug') + create(:label, project: target_project, title: 'wontfix') + create(:milestone, title: '1.0', project: target_project) end shared_examples 'applies the commands to issues in both projects, target and source' do diff --git a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb index e6b0772aec1..bb2f8965294 100644 --- a/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/conan_packages_shared_examples.rb @@ -1,6 +1,10 @@ # frozen_string_literal: true RSpec.shared_examples 'conan ping endpoint' do + it_behaves_like 'conan FIPS mode' do + subject { get api(url) } + end + it 'responds with 200 OK when no token provided' do get api(url) @@ -68,7 +72,7 @@ RSpec.shared_examples 'conan search endpoint' do project.update!(visibility: 'private') project.team.truncate user.project_authorizations.delete_all - project.add_user(user, role) unless role == :anonymous + project.add_member(user, role) unless role == :anonymous get api(url), params: params, headers: headers end @@ -85,6 +89,8 @@ end RSpec.shared_examples 'conan authenticate endpoint' do subject { get api(url), headers: headers } + it_behaves_like 'conan FIPS mode' + context 'when using invalid token' do let(:auth_token) { 'invalid_token' } @@ -159,6 +165,10 @@ RSpec.shared_examples 'conan authenticate endpoint' do end RSpec.shared_examples 'conan check_credentials endpoint' do + it_behaves_like 'conan FIPS mode' do + subject { get api(url), headers: headers } + end + it 'responds with a 200 OK with PAT' do get api(url), headers: headers @@ -390,6 +400,7 @@ end RSpec.shared_examples 'recipe snapshot endpoint' do subject { get api(url), headers: headers } + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' @@ -415,6 +426,7 @@ end RSpec.shared_examples 'package snapshot endpoint' do subject { get api(url), headers: headers } + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'empty recipe for not found package' @@ -436,6 +448,10 @@ RSpec.shared_examples 'package snapshot endpoint' do end RSpec.shared_examples 'recipe download_urls endpoint' do + it_behaves_like 'conan FIPS mode' do + let(:recipe_path) { package.conan_recipe_path } + end + it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'recipe download_urls' @@ -443,6 +459,10 @@ RSpec.shared_examples 'recipe download_urls endpoint' do end RSpec.shared_examples 'package download_urls endpoint' do + it_behaves_like 'conan FIPS mode' do + let(:recipe_path) { package.conan_recipe_path } + end + it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects recipe for invalid project' it_behaves_like 'package download_urls' @@ -457,6 +477,7 @@ RSpec.shared_examples 'recipe upload_urls endpoint' do 'conanmanifest.txt': 123 } end + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' it_behaves_like 'handling empty values for username and channel' @@ -519,6 +540,7 @@ RSpec.shared_examples 'package upload_urls endpoint' do 'conan_package.tgz': 523 } end + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid upload_url params' it_behaves_like 'handling empty values for username and channel' @@ -556,6 +578,7 @@ end RSpec.shared_examples 'delete package endpoint' do let(:recipe_path) { package.conan_recipe_path } + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'handling empty values for username and channel' @@ -665,6 +688,7 @@ RSpec.shared_examples 'not found request' do end RSpec.shared_examples 'recipe file download endpoint' do + it_behaves_like 'conan FIPS mode' it_behaves_like 'a public project with packages' it_behaves_like 'an internal project with packages' it_behaves_like 'a private project with packages' @@ -672,6 +696,7 @@ RSpec.shared_examples 'recipe file download endpoint' do end RSpec.shared_examples 'package file download endpoint' do + it_behaves_like 'conan FIPS mode' it_behaves_like 'a public project with packages' it_behaves_like 'an internal project with packages' it_behaves_like 'a private project with packages' @@ -697,6 +722,7 @@ RSpec.shared_examples 'project not found by project id' do end RSpec.shared_examples 'workhorse authorize endpoint' do + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'workhorse authorization' @@ -718,6 +744,7 @@ RSpec.shared_examples 'workhorse recipe file upload endpoint' do ) end + it_behaves_like 'conan FIPS mode' it_behaves_like 'rejects invalid recipe' it_behaves_like 'rejects invalid file_name', 'conanfile.py.git%2fgit-upload-pack' it_behaves_like 'uploads a package file' @@ -979,3 +1006,9 @@ RSpec.shared_examples 'workhorse authorization' do end end end + +RSpec.shared_examples 'conan FIPS mode' do + context 'when FIPS mode is enabled', :fips_mode do + it_behaves_like 'returning response status', :not_found + end +end diff --git a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb index e0225070986..2ba42b8e8fa 100644 --- a/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_common_shared_examples.rb @@ -15,3 +15,9 @@ RSpec.shared_examples 'rejects Debian access with unknown container id' do |anon end end end + +RSpec.shared_examples 'Debian API FIPS mode' do + context 'when FIPS mode is enabled', :fips_mode do + it_behaves_like 'returning response status', :not_found + end +end diff --git a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb index 5cd63c33936..f13ac05591c 100644 --- a/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_distributions_shared_examples.rb @@ -3,6 +3,8 @@ RSpec.shared_examples 'Debian distributions GET request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' + it_behaves_like 'Debian API FIPS mode' + it "returns #{status}#{and_body}" do subject @@ -17,6 +19,8 @@ end RSpec.shared_examples 'Debian distributions PUT request' do |status, body| and_body = body.nil? ? '' : ' and expected body' + it_behaves_like 'Debian API FIPS mode' + if status == :success it 'updates distribution', :aggregate_failures do expect(::Packages::Debian::UpdateDistributionService).to receive(:new).with(distribution, api_params.except(:codename)).and_call_original @@ -49,6 +53,8 @@ end RSpec.shared_examples 'Debian distributions DELETE request' do |status, body| and_body = body.nil? ? '' : ' and expected body' + it_behaves_like 'Debian API FIPS mode' + if status == :success it 'updates distribution', :aggregate_failures do expect { subject } diff --git a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb index 9f96cb2a164..de7032450a5 100644 --- a/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/debian_packages_shared_examples.rb @@ -3,6 +3,8 @@ RSpec.shared_examples 'Debian packages GET request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' + it_behaves_like 'Debian API FIPS mode' + it "returns #{status}#{and_body}" do subject @@ -17,6 +19,8 @@ end RSpec.shared_examples 'Debian packages upload request' do |status, body = nil| and_body = body.nil? ? '' : ' and expected body' + it_behaves_like 'Debian API FIPS mode' + if status == :created it 'creates package files', :aggregate_failures do expect(::Packages::Debian::FindOrCreateIncomingService).to receive(:new).with(container, user).and_call_original 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 e534a02e562..8ab820e9d43 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 @@ -64,7 +64,8 @@ RSpec.shared_examples 'group and project boards query' do context 'when ascending' do it_behaves_like 'sorted paginated query' do - let(:sort_param) { } + include_context 'no sort argument' + let(:first_param) { 2 } def pagination_results_data(nodes) diff --git a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb index a42a1fda62e..b459e479c91 100644 --- a/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/graphql/mutations/snippets_shared_examples.rb @@ -22,7 +22,7 @@ RSpec.shared_examples 'snippet edit usage data counters' do context 'when user is not sessionless', :clean_gitlab_redis_sessions do before do - stub_session('warden.user.user.key' => [[current_user.id], current_user.encrypted_password[0, 29]]) + stub_session('warden.user.user.key' => [[current_user.id], current_user.authenticatable_salt]) end it 'tracks usage data actions', :clean_gitlab_redis_sessions do diff --git a/spec/support/shared_examples/requests/api/hooks_shared_examples.rb b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb new file mode 100644 index 00000000000..013945bd578 --- /dev/null +++ b/spec/support/shared_examples/requests/api/hooks_shared_examples.rb @@ -0,0 +1,415 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'web-hook API endpoints test hook' do |prefix| + describe "POST #{prefix}/:hook_id" do + it 'tests the hook' do + expect(WebHookService) + .to receive(:new).with(hook, anything, String, force: false) + .and_return(instance_double(WebHookService, execute: nil)) + + post api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:created) + end + end +end + +RSpec.shared_examples 'web-hook API endpoints with branch-filter' do |prefix| + describe "POST #{prefix}/hooks" do + it "returns a 422 error if branch filter is not valid" do + post api(collection_uri, user), + params: { url: "http://example.com", push_events_branch_filter: '~badbranchname/' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end +end + +RSpec.shared_examples 'web-hook API endpoints' do |prefix| + def hooks_count + scope.count + end + + def hook_param_overrides + if defined?(super) + super + else + { push_events_branch_filter: 'some-feature-branch' } + end + end + + let(:hook_params) do + event_names.to_h { [_1, true] }.merge(hook_param_overrides).merge( + url: "http://example.com", + url_variables: [ + { key: 'token', value: 'very-secret' }, + { key: 'abc', value: 'other value' } + ] + ) + end + + let(:update_params) do + { + push_events: false, + job_events: true, + push_events_branch_filter: 'updated-branch-filter' + } + end + + let(:default_values) { {} } + + describe "GET #{prefix}/hooks" do + context "authorized user" do + it "returns all hooks" do + get api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_collection_schema + end + end + + context "when user is forbidden" do + it "prevents access to hooks" do + get api(collection_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is unauthorized" do + it "prevents access to hooks" do + get api(collection_uri, nil) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + + context 'the hook has URL variables' do + before do + hook.update!(url_variables: { 'token' => 'supers3cret' }) + end + + it 'returns the names of the url variables' do + get api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to contain_exactly( + a_hash_including( + 'url_variables' => [{ 'key' => 'token' }] + ) + ) + end + end + end + + describe "GET #{prefix}/hooks/:hook_id" do + context "authorized user" do + it "returns a project hook" do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_hook_schema + + expect(json_response['url']).to eq(hook.url) + end + + it "returns a 404 error if hook id is not available" do + get api(hook_uri(non_existing_record_id), user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + context 'the hook is disabled' do + before do + hook.disable! + end + + it "has the correct alert status", :aggregate_failures do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include('alert_status' => 'disabled') + end + end + + context 'the hook is backed-off' do + before do + hook.backoff! + end + + it "has the correct alert status", :aggregate_failures do + get api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response).to include( + 'alert_status' => 'temporarily_disabled', + 'disabled_until' => hook.disabled_until.iso8601(3) + ) + end + end + end + + context "when user is forbidden" do + it "does not access an existing hook" do + get api(hook_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + end + end + + context "when user is unauthorized" do + it "does not access an existing hook" do + get api(hook_uri, nil) + + expect(response).to have_gitlab_http_status(:unauthorized) + end + end + end + + describe "POST #{prefix}/hooks" do + let(:hook_creation_params) { hook_params } + + it "adds hook", :aggregate_failures do + expect do + post api(collection_uri, user), + params: hook_creation_params + end.to change { hooks_count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_hook_schema + + expect(json_response['url']).to eq(hook_creation_params[:url]) + hook_param_overrides.each do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + event_names.each do |name| + expect(json_response[name.to_s]).to eq(true), name + end + expect(json_response['url_variables']).to match_array [ + { 'key' => 'token' }, + { 'key' => 'abc' } + ] + expect(json_response).not_to include('token') + end + + it "adds the token without including it in the response" do + token = "secret token" + + expect do + post api(collection_uri, user), + params: { url: "http://example.com", token: token } + end.to change { hooks_count }.by(1) + + expect(response).to have_gitlab_http_status(:created) + expect(json_response["url"]).to eq("http://example.com") + expect(json_response).not_to include("token") + + hook = scope.find(json_response["id"]) + + expect(hook.url).to eq("http://example.com") + expect(hook.token).to eq(token) + end + + it "returns a 400 error if url not given" do + post api(collection_uri, user), params: { event_names.first => true } + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it "returns a 400 error if no parameters are provided" do + post api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it 'sets default values for events', :aggregate_failures do + post api(collection_uri, user), params: { url: 'http://mep.mep' } + + expect(response).to have_gitlab_http_status(:created) + expect(response).to match_hook_schema + expect(json_response['enable_ssl_verification']).to be true + event_names.each do |name| + expect(json_response[name.to_s]).to eq(default_values.fetch(name, false)), name + end + end + + it "returns a 422 error if token not valid" do + post api(collection_uri, user), + params: { url: "http://example.com", token: "foo\nbar" } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error if url not valid" do + post api(collection_uri, user), params: { url: "ftp://example.com" } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "PUT #{prefix}/hooks/:hook_id" do + it "updates an existing hook" do + put api(hook_uri, user), params: update_params + + expect(response).to have_gitlab_http_status(:ok) + expect(response).to match_hook_schema + + update_params.each do |k, v| + expect(json_response[k.to_s]).to eq(v) + end + end + + it 'updates the URL variables' do + hook.update!(url_variables: { 'abc' => 'some value' }) + + put api(hook_uri, user), + params: { url_variables: [{ key: 'def', value: 'other value' }] } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['url_variables']).to match_array [ + { 'key' => 'abc' }, + { 'key' => 'def' } + ] + end + + it "adds the token without including it in the response" do + token = "secret token" + + put api(hook_uri, user), params: { url: "http://example.org", token: token } + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response["url"]).to eq("http://example.org") + expect(json_response).not_to include("token") + + expect(hook.reload.url).to eq("http://example.org") + expect(hook.reload.token).to eq(token) + end + + it "returns 404 error if hook id not found" do + put api(hook_uri(non_existing_record_id), user), params: { url: 'http://example.org' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns 400 error if no parameters are provided" do + put api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:bad_request) + end + + it "returns a 422 error if url is not valid" do + put api(hook_uri, user), params: { url: 'ftp://example.com' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error if token is not valid" do + put api(hook_uri, user), params: { token: %w[foo bar].join("\n") } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "DELETE /projects/:id/hooks/:hook_id" do + it "deletes hook from project" do + expect do + delete api(hook_uri, user) + + expect(response).to have_gitlab_http_status(:no_content) + end.to change { hooks_count }.by(-1) + end + + it "returns a 404 error when deleting non existent hook" do + delete api(hook_uri(non_existing_record_id), user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error if hook id not given" do + delete api(collection_uri, user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns forbidden if a user attempts to delete hooks they do not own" do + delete api(hook_uri, unauthorized_user) + + expect(response).to have_gitlab_http_status(:forbidden) + expect(WebHook.exists?(hook.id)).to be_truthy + end + + it_behaves_like '412 response' do + let(:request) { api(hook_uri, user) } + end + end + + describe "PUT #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do + it 'sets the variable' do + expect do + put api("#{hook_uri}/url_variables/abc", user), + params: { value: 'some secret value' } + end.to change { hook.reload.url_variables }.to(eq('abc' => 'some secret value')) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'overwrites existing values' do + hook.update!(url_variables: { 'abc' => 'xyz', 'def' => 'other value' }) + + put api("#{hook_uri}/url_variables/abc", user), + params: { value: 'some secret value' } + + expect(response).to have_gitlab_http_status(:no_content) + expect(hook.reload.url_variables).to eq('abc' => 'some secret value', 'def' => 'other value') + end + + it "returns a 404 error when editing non existent hook" do + put api("#{hook_uri(non_existing_record_id)}/url_variables/abc", user), + params: { value: 'xyz' } + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 422 error when the key is illegal" do + put api("#{hook_uri}/url_variables/abc%20def", user), + params: { value: 'xyz' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + + it "returns a 422 error when the value is illegal" do + put api("#{hook_uri}/url_variables/abc", user), + params: { value: '' } + + expect(response).to have_gitlab_http_status(:unprocessable_entity) + end + end + + describe "DELETE #{prefix}/hooks/:hook_id/url_variables/:key", :aggregate_failures do + before do + hook.update!(url_variables: { 'abc' => 'prior value', 'def' => 'other value' }) + end + + it 'unsets the variable' do + expect do + delete api("#{hook_uri}/url_variables/abc", user) + end.to change { hook.reload.url_variables }.to(eq({ 'def' => 'other value' })) + + expect(response).to have_gitlab_http_status(:no_content) + end + + it 'returns 404 for keys that do not exist' do + hook.update!(url_variables: { 'def' => 'other value' }) + + delete api("#{hook_uri}/url_variables/abc", user) + + expect(response).to have_gitlab_http_status(:not_found) + end + + it "returns a 404 error when deleting a variable from a non existent hook" do + delete api(hook_uri(non_existing_record_id) + "/url_variables/abc", user) + + expect(response).to have_gitlab_http_status(:not_found) + 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 e7e30665b08..a59235486ec 100644 --- a/spec/support/shared_examples/requests/api/notes_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/notes_shared_examples.rb @@ -275,7 +275,9 @@ RSpec.shared_examples 'noteable API' do |parent_type, noteable_type, id_name| context 'when request exceeds the rate limit', :freeze_time, :clean_gitlab_redis_rate_limiting do before do stub_application_setting(notes_create_limit: 1) - allow(::Gitlab::ApplicationRateLimiter).to receive(:increment).and_return(2) + allow_next_instance_of(Gitlab::ApplicationRateLimiter::BaseStrategy) do |strategy| + allow(strategy).to receive(:increment).and_return(2) + end end it 'prevents user from creating more notes' do diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb index 795545e4ad1..1a248bb04e7 100644 --- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member = true| +RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member = true, md5_digest = true| RSpec.shared_examples 'creating pypi package files' do it 'creates package files' do expect { subject } @@ -14,6 +14,17 @@ RSpec.shared_examples 'PyPI package creation' do |user_type, status, add_member expect(package.name).to eq params[:name] expect(package.version).to eq params[:version] expect(package.pypi_metadatum.required_python).to eq params[:requires_python] + + if md5_digest + expect(package.package_files.first.file_md5).not_to be_nil + else + expect(package.package_files.first.file_md5).to be_nil + end + end + + context 'with FIPS mode', :fips_mode do + it_behaves_like 'returning response status', :unprocessable_entity if md5_digest + it_behaves_like 'returning response status', status unless md5_digest end end diff --git a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb index 70cc9b1e6b5..544a0ed8fdd 100644 --- a/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb +++ b/spec/support/shared_examples/requests/api/terraform/modules/v1/packages_shared_examples.rb @@ -52,6 +52,24 @@ RSpec.shared_examples 'an unimplemented route' do it_behaves_like 'when package feature is disabled' end +RSpec.shared_examples 'redirects to version download' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returns a valid response' do + subject + + expect(request.url).to include 'module-1/system/download' + expect(response.headers).to include 'Location' + expect(response.headers['Location']).to include 'module-1/system/1.0.1/download' + end + end +end + RSpec.shared_examples 'grants terraform module download' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do @@ -84,6 +102,22 @@ RSpec.shared_examples 'returns terraform module packages' do |user_type, status, end end +RSpec.shared_examples 'returns terraform module version' do |user_type, status, add_member = true| + context "for user type #{user_type}" do + before do + group.send("add_#{user_type}", user) if add_member && user_type != :anonymous + end + + it_behaves_like 'returning response status', status + + it 'returning a valid response' do + subject + + expect(json_response).to match_schema('public_api/v4/packages/terraform/modules/v1/single_version') + end + end +end + RSpec.shared_examples 'returns no terraform module packages' do |user_type, status, add_member = true| context "for user type #{user_type}" do before do diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb index 86e7da5bcbe..f8e096297d3 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/alert_recovery_shared_examples.rb @@ -56,7 +56,7 @@ RSpec.shared_examples 'processes recovery alert' do context 'seen for the first time' do let(:alert) { AlertManagement::Alert.last } - include_examples 'processes never-before-seen recovery alert' + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end context 'for an existing alert with the same fingerprint' do @@ -107,7 +107,7 @@ RSpec.shared_examples 'processes recovery alert' do context 'which is resolved' do let_it_be(:alert) { create(:alert_management_alert, :resolved, project: project, fingerprint: gitlab_fingerprint, monitoring_tool: source) } - include_examples 'processes never-before-seen recovery alert' + it_behaves_like 'alerts service responds with an error and takes no actions', :bad_request end end end diff --git a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb index 132f1e0422e..3add5485fca 100644 --- a/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management/alert_processing/incident_resolution_shared_examples.rb @@ -6,18 +6,24 @@ # - `alert`, alert for which related incidents should be closed # - `project`, project of the alert RSpec.shared_examples 'closes related incident if enabled' do - context 'with issue' do + context 'with incident' do before do - alert.update!(issue: create(:issue, project: project)) + alert.update!(issue: create(:incident, project: project)) end - it { expect { subject }.to change { alert.issue.reload.closed? }.from(false).to(true) } - it { expect { subject }.to change(ResourceStateEvent, :count).by(1) } + specify do + expect { Sidekiq::Testing.inline! { subject } } + .to change { alert.issue.reload.closed? }.from(false).to(true) + .and change(ResourceStateEvent, :count).by(1) + end end - context 'without issue' do - it { expect { subject }.not_to change { alert.reload.issue } } - it { expect { subject }.not_to change(ResourceStateEvent, :count) } + context 'without incident' do + specify do + expect(::IncidentManagement::CloseIncidentWorker).not_to receive(:perform_async) + + subject + end end context 'with incident setting disabled' do @@ -28,17 +34,23 @@ RSpec.shared_examples 'closes related incident if enabled' do end RSpec.shared_examples 'does not close related incident' do - context 'with issue' do + context 'with incident' do before do - alert.update!(issue: create(:issue, project: project)) + alert.update!(issue: create(:incident, project: project)) end - it { expect { subject }.not_to change { alert.issue.reload.state } } - it { expect { subject }.not_to change(ResourceStateEvent, :count) } + specify do + expect { Sidekiq::Testing.inline! { subject } } + .to not_change { alert.issue.reload.state } + .and not_change(ResourceStateEvent, :count) + end end - context 'without issue' do - it { expect { subject }.not_to change { alert.reload.issue } } - it { expect { subject }.not_to change(ResourceStateEvent, :count) } + context 'without incident' do + specify do + expect(::IncidentManagement::CloseIncidentWorker).not_to receive(:perform_async) + + subject + end end end diff --git a/spec/support/shared_examples/services/alert_management_shared_examples.rb b/spec/support/shared_examples/services/alert_management_shared_examples.rb index f644f1a1687..571cb7dc03d 100644 --- a/spec/support/shared_examples/services/alert_management_shared_examples.rb +++ b/spec/support/shared_examples/services/alert_management_shared_examples.rb @@ -68,14 +68,14 @@ RSpec.shared_examples 'processes one firing and one resolved prometheus alerts' expect(Gitlab::AppLogger).not_to receive(:warn) expect { subject } - .to change(AlertManagement::Alert, :count).by(2) - .and change(Note, :count).by(4) + .to change(AlertManagement::Alert, :count).by(1) + .and change(Note, :count).by(1) expect(subject).to be_success expect(subject.payload[:alerts]).to all(be_a_kind_of(AlertManagement::Alert)) - expect(subject.payload[:alerts].size).to eq(2) + expect(subject.payload[:alerts].size).to eq(1) end it_behaves_like 'processes incident issues' - it_behaves_like 'sends alert notification emails', count: 2 + it_behaves_like 'sends alert notification emails' end 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 f18869fb380..3be59af6a37 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 @@ -1,10 +1,11 @@ # frozen_string_literal: true RSpec.shared_context 'container registry auth service context' do + let_it_be(:rsa_key) { OpenSSL::PKey::RSA.generate(3072) } + let(:current_project) { nil } let(:current_user) { nil } let(:current_params) { {} } - let(:rsa_key) { OpenSSL::PKey::RSA.generate(512) } let(:payload) { JWT.decode(subject[:token], rsa_key, true, { algorithm: 'RS256' }).first } let(:authentication_abilities) do diff --git a/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb new file mode 100644 index 00000000000..a62cffc0e1b --- /dev/null +++ b/spec/support/shared_examples/services/feature_flags/client_shared_examples.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +shared_examples_for 'update feature flag client' do + let!(:client) { create(:operations_feature_flags_client, project: project) } + + it 'updates last feature flag updated at' do + freeze_time do + expect { subject }.to change { client.reload.last_feature_flag_updated_at }.from(nil).to(Time.current) + end + end +end + +shared_examples_for 'does not update feature flag client' do + let!(:client) { create(:operations_feature_flags_client, project: project) } + + it 'does not update last feature flag updated at' do + expect { subject }.not_to change { client.reload.last_feature_flag_updated_at } + end +end diff --git a/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb new file mode 100644 index 00000000000..4655585a092 --- /dev/null +++ b/spec/support/shared_examples/usage_data_counters/work_item_activity_unique_counter_shared_examples.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'counter that does not track the event' do + it 'does not track the event' do + expect { 3.times { track_event } }.to not_change { + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: event_name, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + } + end +end + +RSpec.shared_examples 'work item unique counter' do + context 'when track_work_items_activity FF is enabled' do + it 'tracks a unique event only once' do + expect { 3.times { track_event } }.to change { + Gitlab::UsageDataCounters::HLLRedisCounter.unique_events( + event_names: event_name, + start_date: 2.weeks.ago, + end_date: 2.weeks.from_now + ) + }.by(1) + end + + context 'when author is nil' do + let(:user) { nil } + + it_behaves_like 'counter that does not track the event' + end + end + + context 'when track_work_items_activity FF is disabled' do + before do + stub_feature_flags(track_work_items_activity: false) + end + + it_behaves_like 'counter that does not track the event' + end +end diff --git a/spec/support/shared_examples/views/themed_layout_examples.rb b/spec/support/shared_examples/views/themed_layout_examples.rb new file mode 100644 index 00000000000..b6c53dce4cb --- /dev/null +++ b/spec/support/shared_examples/views/themed_layout_examples.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +RSpec.shared_examples "a layout which reflects the application theme setting", :themed_layout do + context 'as a themed layout' do + let(:default_theme_class) { ::Gitlab::Themes.default.css_class } + + context 'when no theme is explicitly selected' do + it 'renders with the default theme' do + render + + expect(rendered).to have_selector("body.#{default_theme_class}") + end + end + + context 'when user is authenticated & has selected a specific theme' do + before do + allow(view).to receive(:user_application_theme).and_return(chosen_theme.css_class) + end + + where(chosen_theme: ::Gitlab::Themes.available_themes) + + with_them do + it "renders with the #{params[:chosen_theme].name} theme" do + render + + if chosen_theme.css_class != default_theme_class + expect(rendered).not_to have_selector("body.#{default_theme_class}") + end + + expect(rendered).to have_selector("body.#{chosen_theme.css_class}") + end + end + end + end +end diff --git a/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb b/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb new file mode 100644 index 00000000000..491662d17d3 --- /dev/null +++ b/spec/support/shared_examples/work_items/widgetable_service_shared_examples.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +RSpec.shared_examples_for 'work item widgetable service' do + it 'executes callbacks for expected widgets' do + supported_widgets.each do |widget| + expect_next_instance_of(widget[:klass]) do |widget_instance| + expect(widget_instance).to receive(widget[:callback]).with(params: widget[:params]) + end + end + + service_execute + 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 54962eac100..1da21633504 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 @@ -229,6 +229,7 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d describe 'executing an entire migration', :freeze_time, if: Gitlab::Database.has_config?(tracking_database) do include Gitlab::Database::DynamicModelHelpers + include Database::DatabaseHelpers let(:migration_class) do Class.new(Gitlab::BackgroundMigration::BatchedMigrationJob) do @@ -347,5 +348,20 @@ RSpec.shared_examples 'it runs batched background migration jobs' do |tracking_d it 'does not update non-matching records in the range' do expect { full_migration_run }.not_to change { example_data.where('status <> 1 AND some_column <> 0').count } end + + context 'health status' do + subject(:migration_run) { described_class.new.perform } + + it 'puts migration on hold when there is autovaccum activity on related tables' do + swapout_view_for_table(:postgres_autovacuum_activity, connection: connection) + create( + :postgres_autovacuum_activity, + table: migration.table_name, + table_identifier: "public.#{migration.table_name}" + ) + + expect { migration_run }.to change { migration.reload.on_hold? }.from(false).to(true) + end + end end end diff --git a/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb b/spec/support/shared_examples/workers/concerns/git_garbage_collect_methods_shared_examples.rb index 77c4a3431e2..503e331ea2e 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 @@ -1,10 +1,6 @@ # frozen_string_literal: true -require 'fileutils' - RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| - include GitHelpers - let!(:lease_uuid) { SecureRandom.uuid } let!(:lease_key) { "resource_housekeeping:#{resource.id}" } let(:params) { [resource.id, task, lease_key, lease_uuid] } @@ -246,39 +242,6 @@ RSpec.shared_examples 'can collect git garbage' do |update_statistics: true| subject.perform(resource.id, 'prune', lease_key, lease_uuid) end - - # Create a new commit on a random new branch - def create_objects(resource) - rugged = rugged_repo(resource.repository) - old_commit = rugged.branches.first.target - new_commit_sha = Rugged::Commit.create( - rugged, - message: "hello world #{SecureRandom.hex(6)}", - author: { email: 'foo@bar', name: 'baz' }, - committer: { email: 'foo@bar', name: 'baz' }, - tree: old_commit.tree, - parents: [old_commit] - ) - rugged.references.create("refs/heads/#{SecureRandom.hex(6)}", new_commit_sha) - end - - def packs(resource) - Dir["#{path_to_repo}/objects/pack/*.pack"] - end - - def packed_refs(resource) - path = File.join(path_to_repo, 'packed-refs') - FileUtils.touch(path) - File.read(path) - end - - def path_to_repo - @path_to_repo ||= File.join(TestEnv.repos_path, resource.repository.relative_path) - end - - def bitmap_path(pack) - pack.sub(/\.pack\z/, '.bitmap') - end end context 'with bitmaps enabled' do diff --git a/spec/support/snowplow.rb b/spec/support/snowplow.rb deleted file mode 100644 index e58be667b37..00000000000 --- a/spec/support/snowplow.rb +++ /dev/null @@ -1,16 +0,0 @@ -# frozen_string_literal: true - -require_relative 'stub_snowplow' - -RSpec.configure do |config| - config.include SnowplowHelpers, :snowplow - config.include StubSnowplow, :snowplow - - config.before(:each, :snowplow) do - stub_snowplow - end - - config.after(:each, :snowplow) do - Gitlab::Tracking.send(:snowplow).send(:tracker).flush - end -end |