Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/build-images.gitlab-ci.yml12
-rw-r--r--.gitlab/ci/global.gitlab-ci.yml19
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml8
-rw-r--r--.gitlab/ci/rules.gitlab-ci.yml4
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.checksum2
-rw-r--r--Gemfile.lock5
-rw-r--r--app/assets/javascripts/branches/components/delete_merged_branches.vue171
-rw-r--r--app/assets/javascripts/branches/init_delete_merged_branches.js23
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue81
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue429
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue32
-rw-r--r--app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue209
-rw-r--r--app/assets/javascripts/ci_variable_list/index.js62
-rw-r--r--app/assets/javascripts/ci_variable_list/store/actions.js208
-rw-r--r--app/assets/javascripts/ci_variable_list/store/getters.js6
-rw-r--r--app/assets/javascripts/ci_variable_list/store/index.js19
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutation_types.js33
-rw-r--r--app/assets/javascripts/ci_variable_list/store/mutations.js128
-rw-r--r--app/assets/javascripts/ci_variable_list/store/state.js26
-rw-r--r--app/assets/javascripts/ci_variable_list/store/utils.js45
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue91
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue190
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js56
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql15
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql7
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js2
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_navigation.vue2
-rw-r--r--app/assets/javascripts/sentry/constants.js43
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js45
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/users_controller.rb35
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb3
-rw-r--r--app/controllers/groups/settings/packages_and_registries_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb3
-rw-r--r--app/helpers/form_helper.rb9
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/namespace.rb1
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/merge_requests/update_assignees_service.rb13
-rw-r--r--app/services/notes/create_service.rb2
-rw-r--r--app/services/users/migrate_to_ghost_user_service.rb113
-rw-r--r--app/views/admin/application_settings/_package_registry.html.haml2
-rw-r--r--app/views/admin/users/_head.html.haml6
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml14
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/_results.html.haml1
-rw-r--r--app/views/search/_results_status.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar_assignees.html.haml2
-rw-r--r--config/feature_flags/development/ci_variable_settings_graphql.yml8
-rw-r--r--config/feature_flags/development/enable_old_sentry_clientside_integration.yml8
-rw-r--r--config/feature_flags/development/index_user_callback.yml8
-rw-r--r--config/feature_flags/development/limit_assignees_per_issuable.yml8
-rw-r--r--config/open_api.yml2
-rw-r--r--db/migrate/20221108015813_add_telesign_to_application_settings.rb11
-rw-r--r--db/post_migrate/20221102090940_create_next_ci_partitions_record.rb29
-rw-r--r--db/post_migrate/20221102090943_create_second_partition_for_builds_metadata.rb62
-rw-r--r--db/schema_migrations/202211020909401
-rw-r--r--db/schema_migrations/202211020909431
-rw-r--r--db/schema_migrations/202211080158131
-rw-r--r--db/structure.sql4
-rw-r--r--doc/administration/reference_architectures/3k_users.md1
-rw-r--r--doc/development/migration_style_guide.md25
-rw-r--r--doc/development/shell_commands.md2
-rw-r--r--doc/development/testing_guide/best_practices.md12
-rw-r--r--doc/update/index.md17
-rw-r--r--doc/user/analytics/dora_metrics.md7
-rw-r--r--doc/user/application_security/policies/scan-result-policies.md3
-rw-r--r--doc/user/markdown.md16
-rw-r--r--doc/user/project/merge_requests/img/conflict_ui_v15_6.pngbin13672 -> 14089 bytes
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/branches.rb2
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/entities/basic_project_details.rb2
-rw-r--r--lib/api/entities/issuable_entity.rb14
-rw-r--r--lib/api/entities/issue_basic.rb16
-rw-r--r--lib/api/entities/project.rb2
-rw-r--r--lib/api/entities/release.rb10
-rw-r--r--lib/api/files.rb2
-rw-r--r--lib/api/issue_links.rb55
-rw-r--r--lib/api/lint.rb2
-rw-r--r--lib/api/releases.rb10
-rw-r--r--lib/api/repositories.rb4
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/gitlab/ci/parsers/sbom/cyclonedx.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gon_helper.rb13
-rw-r--r--lib/sbom/package_url.rb11
-rw-r--r--lib/sbom/package_url/argument_validator.rb90
-rw-r--r--lib/sbom/package_url/decoder.rb35
-rw-r--r--lib/sbom/package_url/encoder.rb8
-rw-r--r--lib/sbom/package_url/normalizer.rb47
-rw-r--r--lib/sbom/package_url/string_utils.rb6
-rw-r--r--locale/gitlab.pot81
-rw-r--r--qa/qa/page/project/branches/show.rb16
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb2
-rwxr-xr-xscripts/build_qa_image23
-rw-r--r--scripts/review_apps/base-config.yaml6
-rw-r--r--spec/controllers/admin/users_controller_spec.rb108
-rw-r--r--spec/factories/users.rb4
-rw-r--r--spec/features/admin/users/user_spec.rb67
-rw-r--r--spec/features/admin_variables_spec.rb18
-rw-r--r--spec/features/group_variables_spec.rb18
-rw-r--r--spec/features/project_variables_spec.rb63
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb323
-rw-r--r--spec/features/search/user_searches_for_comments_spec.rb61
-rw-r--r--spec/features/search/user_searches_for_commits_spec.rb74
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb177
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb84
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb67
-rw-r--r--spec/features/search/user_searches_for_projects_spec.rb5
-rw-r--r--spec/features/search/user_searches_for_users_spec.rb118
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb70
-rw-r--r--spec/features/search/user_uses_header_search_field_spec.rb6
-rw-r--r--spec/fixtures/lib/sbom/package-url-test-cases.json502
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap139
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js143
-rw-r--r--spec/frontend/branches/mock_data.js7
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js119
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js323
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js38
-rw-r--r--spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js86
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js319
-rw-r--r--spec/frontend/ci_variable_list/store/getters_spec.js21
-rw-r--r--spec/frontend/ci_variable_list/store/mutations_spec.js136
-rw-r--r--spec/frontend/ci_variable_list/store/utils_spec.js49
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js78
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js17
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js14
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js280
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js75
-rw-r--r--spec/helpers/form_helper_spec.rb42
-rw-r--r--spec/lib/api/entities/release_spec.rb8
-rw-r--r--spec/lib/gitlab/gon_helper_spec.rb62
-rw-r--r--spec/lib/sbom/package_url/argument_validator_spec.rb51
-rw-r--r--spec/lib/sbom/package_url/decoder_spec.rb22
-rw-r--r--spec/lib/sbom/package_url/encoder_spec.rb4
-rw-r--r--spec/lib/sbom/package_url/normalizer_spec.rb76
-rw-r--r--spec/lib/sbom/package_url_spec.rb66
-rw-r--r--spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb63
-rw-r--r--spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb61
-rw-r--r--spec/models/group_spec.rb9
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/requests/api/project_import_spec.rb4
-rw-r--r--spec/scripts/trigger-build_spec.rb1
-rw-r--r--spec/services/users/migrate_to_ghost_user_service_spec.rb97
-rw-r--r--spec/support/helpers/search_helpers.rb4
-rw-r--r--spec/support/rate_limiter.rb7
-rw-r--r--spec/support/rspec_order_todo.yml1
-rw-r--r--spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb114
-rw-r--r--spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb75
-rw-r--r--spec/tasks/gitlab/gitaly_rake_spec.rb2
159 files changed, 3613 insertions, 3696 deletions
diff --git a/.gitlab/ci/build-images.gitlab-ci.yml b/.gitlab/ci/build-images.gitlab-ci.yml
index 3c7056a92c1..a60a5f6040c 100644
--- a/.gitlab/ci/build-images.gitlab-ci.yml
+++ b/.gitlab/ci/build-images.gitlab-ci.yml
@@ -1,7 +1,13 @@
.base-image-build:
extends: .use-kaniko
variables:
- GIT_LFS_SKIP_SMUDGE: 1
+ GIT_LFS_SKIP_SMUDGE: 1 # disable pulling objects from lfs
+ retry: 2
+
+.base-image-build-buildx:
+ extends: .use-buildx
+ variables:
+ GIT_LFS_SKIP_SMUDGE: 1 # disable pulling objects from lfs
retry: 2
# This image is used by:
@@ -10,12 +16,12 @@
# See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/index.html#testing-code-in-merge-requests for more details.
build-qa-image:
extends:
- - .base-image-build
+ - .base-image-build-buildx
- .build-images:rules:build-qa-image
stage: build-images
needs: []
script:
- - ./scripts/build_qa_image
+ - run_timed_command "scripts/build_qa_image"
# This image is used by:
# - The `CNG` pipelines (via the `review-build-cng` job): https://gitlab.com/gitlab-org/build/CNG/-/blob/cfc67136d711e1c8c409bf8e57427a644393da2f/.gitlab-ci.yml#L335
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml
index d461183a2ca..add728a9983 100644
--- a/.gitlab/ci/global.gitlab-ci.yml
+++ b/.gitlab/ci/global.gitlab-ci.yml
@@ -46,7 +46,7 @@
files:
- GITALY_SERVER_VERSION
- lib/gitlab/setup_helper.rb
- prefix: "gitaly-binaries-${GITALY_SERVER_VERSION}-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}"
+ prefix: "gitaly-binaries-debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}"
paths:
- ${TMP_TEST_FOLDER}/gitaly/_build/bin/
- ${TMP_TEST_FOLDER}/gitaly/_build/deps/git/install/
@@ -361,3 +361,20 @@
tags:
# See https://gitlab.com/gitlab-com/www-gitlab-com/-/issues/7019 for tag descriptions
- gitlab-org-docker
+
+.use-buildx:
+ extends: .use-docker-in-docker
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-bullseye-slim:docker-${DOCKER_VERSION}-buildx-0.8
+ variables:
+ QEMU_IMAGE: tonistiigi/binfmt:qemu-v7.0.0
+ before_script:
+ - source scripts/utils.sh
+ - echo "$CI_REGISTRY_PASSWORD" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin
+ - |
+ if [[ "${ARCH}" =~ arm64 ]]; then
+ echo -e "\033[1;33mInstalling latest qemu emulators\033[0m"
+ docker pull -q ${QEMU_IMAGE};
+ docker run --rm --privileged ${QEMU_IMAGE} --uninstall qemu-*;
+ docker run --rm --privileged ${QEMU_IMAGE} --install all;
+ fi
+ - docker buildx create --use # creates and set's to active buildkit builder
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 7ecc8db3b1b..85c5c7d1b1d 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -32,14 +32,15 @@ review-build-cng-env:
extends:
- .default-retry
- .review:rules:review-build-cng
- image: ${GITLAB_DEPENDENCY_PROXY_ADDRESS}ruby:3.0-alpine3.13
+ image: ${REGISTRY_HOST}/${REGISTRY_GROUP}/gitlab-build-images/debian-${DEBIAN_VERSION}-ruby-${RUBY_VERSION}:bundler-2.3
stage: prepare
needs: []
before_script:
- source ./scripts/utils.sh
- install_gitlab_gem
script:
- - 'ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env'
+ - ruby -r./scripts/trigger-build.rb -e "puts Trigger.variables_for_env_file(Trigger::CNG.new.variables)" > build.env
+ - ruby -e 'puts "FULL_RUBY_VERSION=#{RUBY_VERSION}"' >> build.env
- cat build.env
artifacts:
reports:
@@ -77,6 +78,7 @@ review-build-cng:
GITLAB_SHELL_VERSION: "${GITLAB_SHELL_VERSION}"
GITLAB_WORKHORSE_VERSION: "${GITLAB_WORKHORSE_VERSION}"
GITALY_SERVER_VERSION: "${GITALY_SERVER_VERSION}"
+ RUBY_VERSION: "${FULL_RUBY_VERSION}"
trigger:
project: gitlab-org/build/CNG-mirror
branch: $TRIGGER_BRANCH
@@ -89,7 +91,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "138c146a5ba787942f66d4c7d795d224d6ba206a"
+ GITLAB_HELM_CHART_REF: "ed813953079c1d81aa69d4cb8171c69aa9741f01" # 6.5.4: https://gitlab.com/gitlab-org/charts/gitlab/-/commit/ed813953079c1d81aa69d4cb8171c69aa9741f01
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index db7b6473c06..c6cfb491e61 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -685,7 +685,11 @@
changes: *code-qa-patterns
- <<: *if-auto-deploy-branches
- <<: *if-default-branch-or-tag
+ variables:
+ ARCH: amd64,arm64
- <<: *if-dot-com-gitlab-org-schedule
+ variables:
+ ARCH: amd64,arm64
- <<: *if-force-ci
- <<: *if-ruby3-branch
diff --git a/Gemfile b/Gemfile
index f4ef72beeab..87869f6f1e4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -578,3 +578,6 @@ gem 'arr-pm', '~> 0.0.12'
# Apple plist parsing
gem 'CFPropertyList'
+
+# For phone verification
+gem 'telesignenterprise', '~> 2.2'
diff --git a/Gemfile.checksum b/Gemfile.checksum
index c9d8615cf5e..f8202c49abe 100644
--- a/Gemfile.checksum
+++ b/Gemfile.checksum
@@ -582,6 +582,8 @@
{"name":"sys-filesystem","version":"1.4.3","platform":"ruby","checksum":"390919de89822ad6d3ba3daf694d720be9d83ed95cdf7adf54d4573c98b17421"},
{"name":"sysexits","version":"1.2.0","platform":"ruby","checksum":"598241c4ae57baa403c125182dfdcc0d1ac4c0fb606dd47fbed57e4aaf795662"},
{"name":"tanuki_emoji","version":"0.6.0","platform":"ruby","checksum":"4ce91aefed2d076b73fba3eff50e89660c3d25691787a9fe4c0dfabb4218c12a"},
+{"name":"telesign","version":"2.2.4","platform":"ruby","checksum":"dcc6e96ea7bcb4da1e2ae786bfe7a4d670a4b5f94ae95dfcdde77d547c544c42"},
+{"name":"telesignenterprise","version":"2.2.2","platform":"ruby","checksum":"f147a03263a8c2fe0a0db1a7a9454a6ee37d9e8abd58eaca305bdd8081f9f1b3"},
{"name":"temple","version":"0.8.2","platform":"ruby","checksum":"c12071214346c606dbd219b4117276d04a9f2c20d65e66a66b2c4ec18efc1f18"},
{"name":"term-ansicolor","version":"1.7.1","platform":"ruby","checksum":"92339ffec77c4bddc786a29385c91601dd52fc68feda23609bba0491229b05f7"},
{"name":"terminal-table","version":"1.8.0","platform":"ruby","checksum":"13371f069af18e9baa4e44d404a4ada9301899ce0530c237ac1a96c19f652294"},
diff --git a/Gemfile.lock b/Gemfile.lock
index 33cf0d9dae9..c6345cb7f5d 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -1435,6 +1435,10 @@ GEM
ffi (~> 1.1)
sysexits (1.2.0)
tanuki_emoji (0.6.0)
+ telesign (2.2.4)
+ net-http-persistent (>= 3.0.0, < 5.0)
+ telesignenterprise (2.2.2)
+ telesign (~> 2.2.3)
temple (0.8.2)
term-ansicolor (1.7.1)
tins (~> 1.0)
@@ -1843,6 +1847,7 @@ DEPENDENCIES
state_machines-activerecord (~> 0.8.0)
sys-filesystem (~> 1.4.3)
tanuki_emoji (~> 0.6)
+ telesignenterprise (~> 2.2)
terser (= 1.0.2)
test-prof (~> 1.0.7)
test_file_finder (~> 0.1.3)
diff --git a/app/assets/javascripts/branches/components/delete_merged_branches.vue b/app/assets/javascripts/branches/components/delete_merged_branches.vue
new file mode 100644
index 00000000000..70974f2e725
--- /dev/null
+++ b/app/assets/javascripts/branches/components/delete_merged_branches.vue
@@ -0,0 +1,171 @@
+<script>
+import { GlButton, GlFormInput, GlModal, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { sprintf, s__, __ } from '~/locale';
+
+export const i18n = {
+ deleteButtonText: s__('Branches|Delete merged branches'),
+ buttonTooltipText: s__("Branches|Delete all branches that are merged into '%{defaultBranch}'"),
+ modalTitle: s__('Branches|Delete all merged branches?'),
+ modalMessage: s__(
+ 'Branches|You are about to %{strongStart}delete all branches%{strongEnd} that were merged into %{codeStart}%{defaultBranch}%{codeEnd}.',
+ ),
+ notVisibleBranchesWarning: s__(
+ 'Branches|This may include merged branches that are not visible on the current screen.',
+ ),
+ protectedBranchWarning: s__(
+ "Branches|A branch won't be deleted if it is protected or associated with an open merge request.",
+ ),
+ permanentEffectWarning: s__(
+ 'Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}.',
+ ),
+ confirmationMessage: s__(
+ 'Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}.',
+ ),
+ cancelButtonText: __('Cancel'),
+};
+
+export default {
+ csrf,
+ components: {
+ GlModal,
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ formPath: {
+ type: String,
+ required: true,
+ },
+ defaultBranch: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ areAllBranchesVisible: false,
+ enteredText: '',
+ };
+ },
+ computed: {
+ buttonTooltipText() {
+ return sprintf(this.$options.i18n.buttonTooltipText, { defaultBranch: this.defaultBranch });
+ },
+ modalMessage() {
+ return sprintf(this.$options.i18n.modalMessage, {
+ defaultBranch: this.defaultBranch,
+ });
+ },
+ isDeletingConfirmed() {
+ return this.enteredText.trim().toLowerCase() === 'delete';
+ },
+ isDeleteButtonDisabled() {
+ return !this.isDeletingConfirmed;
+ },
+ },
+ methods: {
+ openModal() {
+ this.$refs.modal.show();
+ },
+ submitForm() {
+ if (!this.isDeleteButtonDisabled) {
+ this.$refs.form.submit();
+ }
+ },
+ closeModal() {
+ this.$refs.modal.hide();
+ },
+ },
+ i18n,
+};
+</script>
+
+<template>
+ <div>
+ <gl-button
+ v-gl-tooltip="buttonTooltipText"
+ class="gl-mr-3"
+ data-qa-selector="delete_merged_branches_button"
+ category="secondary"
+ variant="danger"
+ @click="openModal"
+ >{{ $options.i18n.deleteButtonText }}
+ </gl-button>
+ <gl-modal
+ ref="modal"
+ size="sm"
+ modal-id="delete-merged-branches"
+ :title="$options.i18n.modalTitle"
+ >
+ <form ref="form" :action="formPath" method="post" @submit.prevent>
+ <p>
+ <gl-sprintf :message="modalMessage">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ {{ $options.i18n.notVisibleBranchesWarning }}
+ </p>
+ <p>
+ {{ $options.i18n.protectedBranchWarning }}
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.permanentEffectWarning">
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p>
+ <gl-sprintf :message="$options.i18n.confirmationMessage">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ <gl-form-input
+ v-model="enteredText"
+ data-qa-selector="delete_merged_branches_input"
+ type="text"
+ size="sm"
+ class="gl-mt-2"
+ aria-labelledby="input-label"
+ autocomplete="off"
+ @keyup.enter="submitForm"
+ />
+ </p>
+
+ <input ref="method" type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </form>
+
+ <template #modal-footer>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ >
+ <gl-button data-testid="delete-merged-branches-cancel-button" @click="closeModal">
+ {{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ <gl-button
+ ref="deleteMergedBrancesButton"
+ :disabled="isDeleteButtonDisabled"
+ variant="danger"
+ data-qa-selector="delete_merged_branches_confirmation_button"
+ data-testid="delete-merged-branches-confirmation-button"
+ @click="submitForm"
+ >{{ $options.i18n.deleteButtonText }}</gl-button
+ >
+ </div>
+ </template>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/branches/init_delete_merged_branches.js b/app/assets/javascripts/branches/init_delete_merged_branches.js
new file mode 100644
index 00000000000..998db07d8de
--- /dev/null
+++ b/app/assets/javascripts/branches/init_delete_merged_branches.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import DeleteMergedBranches from '~/branches/components/delete_merged_branches.vue';
+
+export default function initDeleteMergedBranchesModal() {
+ const el = document.querySelector('.js-delete-merged-branches');
+ if (!el) {
+ return false;
+ }
+
+ const { formPath, defaultBranch } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(DeleteMergedBranches, {
+ props: {
+ formPath,
+ defaultBranch,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
deleted file mode 100644
index ecb39f214ec..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_environments_dropdown.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlSearchBoxByType } from '@gitlab/ui';
-import { mapGetters } from 'vuex';
-import { __, sprintf } from '~/locale';
-
-export default {
- name: 'CiEnvironmentsDropdown',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlSearchBoxByType,
- },
- props: {
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- searchTerm: '',
- };
- },
- computed: {
- ...mapGetters(['joinedEnvironments']),
- composedCreateButtonLabel() {
- return sprintf(__('Create wildcard: %{searchTerm}'), { searchTerm: this.searchTerm });
- },
- shouldRenderCreateButton() {
- return this.searchTerm && !this.joinedEnvironments.includes(this.searchTerm);
- },
- filteredResults() {
- const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.joinedEnvironments.filter((resultString) =>
- resultString.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
- },
- methods: {
- selectEnvironment(selected) {
- this.$emit('selectEnvironment', selected);
- this.searchTerm = '';
- },
- createClicked() {
- this.$emit('createClicked', this.searchTerm);
- this.searchTerm = '';
- },
- isSelected(env) {
- return this.value === env;
- },
- clearSearch() {
- this.searchTerm = '';
- },
- },
-};
-</script>
-<template>
- <gl-dropdown :text="value" @show="clearSearch">
- <gl-search-box-by-type v-model.trim="searchTerm" data-testid="ci-environment-search" />
- <gl-dropdown-item
- v-for="environment in filteredResults"
- :key="environment"
- :is-checked="isSelected(environment)"
- is-check-item
- @click="selectEnvironment(environment)"
- >
- {{ environment }}
- </gl-dropdown-item>
- <gl-dropdown-item v-if="!filteredResults.length" ref="noMatchingResults">{{
- __('No matching results')
- }}</gl-dropdown-item>
- <template v-if="shouldRenderCreateButton">
- <gl-dropdown-divider />
- <gl-dropdown-item data-testid="create-wildcard-button" @click="createClicked">
- {{ composedCreateButtonLabel }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
deleted file mode 100644
index fa90e0e3e6c..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_modal.vue
+++ /dev/null
@@ -1,429 +0,0 @@
-<script>
-import {
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
-} from '@gitlab/ui';
-import { mapActions, mapState } from 'vuex';
-import { getCookie, setCookie } from '~/lib/utils/common_utils';
-import { __ } from '~/locale';
-import Tracking from '~/tracking';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { mapComputed } from '~/vuex_shared/bindings';
-import {
- AWS_TOKEN_CONSTANTS,
- ADD_CI_VARIABLE_MODAL_ID,
- AWS_TIP_DISMISSED_COOKIE_NAME,
- AWS_TIP_MESSAGE,
- CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- ENVIRONMENT_SCOPE_LINK_TITLE,
- EVENT_LABEL,
- EVENT_ACTION,
-} from '../constants';
-import LegacyCiEnvironmentsDropdown from './legacy_ci_environments_dropdown.vue';
-import { awsTokens, awsTokenList } from './ci_variable_autocomplete_tokens';
-
-const trackingMixin = Tracking.mixin({ label: EVENT_LABEL });
-
-export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- tokens: awsTokens,
- tokenList: awsTokenList,
- awsTipMessage: AWS_TIP_MESSAGE,
- containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
- environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
- components: {
- LegacyCiEnvironmentsDropdown,
- GlAlert,
- GlButton,
- GlCollapse,
- GlFormCheckbox,
- GlFormCombobox,
- GlFormGroup,
- GlFormSelect,
- GlFormInput,
- GlFormTextarea,
- GlIcon,
- GlLink,
- GlModal,
- GlSprintf,
- },
- mixins: [glFeatureFlagsMixin(), trackingMixin],
- data() {
- return {
- isTipDismissed: getCookie(AWS_TIP_DISMISSED_COOKIE_NAME) === 'true',
- validationErrorEventProperty: '',
- };
- },
- computed: {
- ...mapState([
- 'projectId',
- 'environments',
- 'typeOptions',
- 'variable',
- 'variableBeingEdited',
- 'isGroup',
- 'maskableRegex',
- 'selectedEnvironment',
- 'isProtectedByDefault',
- 'awsLogoSvgPath',
- 'awsTipDeployLink',
- 'awsTipCommandsLink',
- 'awsTipLearnLink',
- 'containsVariableReferenceLink',
- 'protectedEnvironmentVariablesLink',
- 'maskedEnvironmentVariablesLink',
- 'environmentScopeLink',
- ]),
- ...mapComputed(
- [
- { key: 'key', updateFn: 'updateVariableKey' },
- { key: 'secret_value', updateFn: 'updateVariableValue' },
- { key: 'variable_type', updateFn: 'updateVariableType' },
- { key: 'environment_scope', updateFn: 'setEnvironmentScope' },
- { key: 'protected_variable', updateFn: 'updateVariableProtected' },
- { key: 'masked', updateFn: 'updateVariableMasked' },
- ],
- false,
- 'variable',
- ),
- isTipVisible() {
- return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
- },
- canSubmit() {
- return (
- this.variableValidationState &&
- this.variable.key !== '' &&
- this.variable.secret_value !== ''
- );
- },
- canMask() {
- const regex = RegExp(this.maskableRegex);
- return regex.test(this.variable.secret_value);
- },
- containsVariableReference() {
- const regex = /\$/;
- return regex.test(this.variable.secret_value);
- },
- displayMaskedError() {
- return !this.canMask && this.variable.masked;
- },
- maskedState() {
- if (this.displayMaskedError) {
- return false;
- }
- return true;
- },
- modalActionText() {
- return this.variableBeingEdited ? __('Update variable') : __('Add variable');
- },
- maskedFeedback() {
- return this.displayMaskedError ? __('This variable can not be masked.') : '';
- },
- tokenValidationFeedback() {
- const tokenSpecificFeedback = this.$options.tokens?.[this.variable.key]?.invalidMessage;
- if (!this.tokenValidationState && tokenSpecificFeedback) {
- return tokenSpecificFeedback;
- }
- return '';
- },
- tokenValidationState() {
- const validator = this.$options.tokens?.[this.variable.key]?.validation;
-
- if (validator) {
- return validator(this.variable.secret_value);
- }
-
- return true;
- },
- scopedVariablesAvailable() {
- return !this.isGroup || this.glFeatures.groupScopedCiVariables;
- },
- variableValidationFeedback() {
- return `${this.tokenValidationFeedback} ${this.maskedFeedback}`;
- },
- variableValidationState() {
- return this.variable.secret_value === '' || (this.tokenValidationState && this.maskedState);
- },
- },
- watch: {
- variable: {
- handler() {
- this.trackVariableValidationErrors();
- },
- deep: true,
- },
- },
- methods: {
- ...mapActions([
- 'addVariable',
- 'updateVariable',
- 'resetEditing',
- 'displayInputValue',
- 'clearModal',
- 'deleteVariable',
- 'setEnvironmentScope',
- 'addWildCardScope',
- 'resetSelectedEnvironment',
- 'setSelectedEnvironment',
- 'setVariableProtected',
- ]),
- dismissTip() {
- setCookie(AWS_TIP_DISMISSED_COOKIE_NAME, 'true', { expires: 90 });
- this.isTipDismissed = true;
- },
- deleteVarAndClose() {
- this.deleteVariable();
- this.hideModal();
- },
- hideModal() {
- this.$refs.modal.hide();
- },
- resetModalHandler() {
- if (this.variableBeingEdited) {
- this.resetEditing();
- }
-
- this.clearModal();
- this.resetSelectedEnvironment();
- this.resetValidationErrorEvents();
- },
- updateOrAddVariable() {
- if (this.variableBeingEdited) {
- this.updateVariable();
- } else {
- this.addVariable();
- }
- this.hideModal();
- },
- setVariableProtectedByDefault() {
- if (this.isProtectedByDefault && !this.variableBeingEdited) {
- this.setVariableProtected();
- }
- },
- trackVariableValidationErrors() {
- const property = this.getTrackingErrorProperty();
- if (!this.validationErrorEventProperty && property) {
- this.track(EVENT_ACTION, { property });
- this.validationErrorEventProperty = property;
- }
- },
- getTrackingErrorProperty() {
- let property;
- if (this.variable.secret_value?.length && !property) {
- if (this.displayMaskedError && this.maskableRegex?.length) {
- const supportedChars = this.maskableRegex.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.secret_value.replace(regex, '');
- }
- if (this.containsVariableReference) {
- property = '$';
- }
- }
-
- return property;
- },
- resetValidationErrorEvents() {
- this.validationErrorEventProperty = '';
- },
- },
-};
-</script>
-
-<template>
- <gl-modal
- ref="modal"
- :modal-id="$options.modalId"
- :title="modalActionText"
- static
- lazy
- @hidden="resetModalHandler"
- @shown="setVariableProtectedByDefault"
- >
- <form>
- <gl-form-combobox
- v-model="key"
- :token-list="$options.tokenList"
- :label-text="__('Key')"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- />
-
- <gl-form-group
- :label="__('Value')"
- label-for="ci-variable-value"
- :state="variableValidationState"
- :invalid-feedback="variableValidationFeedback"
- >
- <gl-form-textarea
- id="ci-variable-value"
- ref="valueField"
- v-model="secret_value"
- :state="variableValidationState"
- rows="3"
- max-rows="10"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- class="gl-font-monospace!"
- spellcheck="false"
- />
- </gl-form-group>
-
- <div class="d-flex">
- <gl-form-group :label="__('Type')" label-for="ci-variable-type" class="w-50 gl-mr-5">
- <gl-form-select id="ci-variable-type" v-model="variable_type" :options="typeOptions" />
- </gl-form-group>
-
- <gl-form-group label-for="ci-variable-env" class="w-50" data-testid="environment-scope">
- <template #label>
- {{ __('Environment scope') }}
- <gl-link
- :title="$options.environmentScopeLinkTitle"
- :href="environmentScopeLink"
- target="_blank"
- data-testid="environment-scope-link"
- >
- <gl-icon name="question" :size="12" />
- </gl-link>
- </template>
- <legacy-ci-environments-dropdown
- v-if="scopedVariablesAvailable"
- class="w-100"
- :value="environment_scope"
- @selectEnvironment="setEnvironmentScope"
- @createClicked="addWildCardScope"
- />
-
- <gl-form-input v-else v-model="environment_scope" class="w-100" readonly />
- </gl-form-group>
- </div>
-
- <gl-form-group :label="__('Flags')" label-for="ci-variable-flags">
- <gl-form-checkbox
- v-model="protected_variable"
- class="mb-0"
- data-testid="ci-variable-protected-checkbox"
- >
- {{ __('Protect variable') }}
- <gl-link target="_blank" :href="protectedEnvironmentVariablesLink">
- <gl-icon name="question" :size="12" />
- </gl-link>
- <p class="gl-mt-2 text-secondary">
- {{ __('Export variable to pipelines running on protected branches and tags only.') }}
- </p>
- </gl-form-checkbox>
-
- <gl-form-checkbox
- ref="masked-ci-variable"
- v-model="masked"
- data-testid="ci-variable-masked-checkbox"
- >
- {{ __('Mask variable') }}
- <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
- <gl-icon name="question" :size="12" />
- </gl-link>
- <p class="gl-mt-2 gl-mb-0 text-secondary">
- {{ __('Variable will be masked in job logs.') }}
- <span
- :class="{
- 'bold text-plain': displayMaskedError,
- }"
- >
- {{ __('Requires values to meet regular expression requirements.') }}</span
- >
- <gl-link target="_blank" :href="maskedEnvironmentVariablesLink">{{
- __('More information')
- }}</gl-link>
- </p>
- </gl-form-checkbox>
- </gl-form-group>
- </form>
- <gl-collapse :visible="isTipVisible">
- <gl-alert
- :title="__('Deploying to AWS is easy with GitLab')"
- variant="tip"
- data-testid="aws-guidance-tip"
- @dismiss="dismissTip"
- >
- <div class="gl-display-flex gl-flex-direction-row">
- <div>
- <p>
- <gl-sprintf :message="$options.awsTipMessage">
- <template #deployLink="{ content }">
- <gl-link :href="awsTipDeployLink" target="_blank">{{ content }}</gl-link>
- </template>
- <template #commandsLink="{ content }">
- <gl-link :href="awsTipCommandsLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p>
- <gl-button
- :href="awsTipLearnLink"
- target="_blank"
- category="secondary"
- variant="info"
- class="gl-overflow-wrap-break"
- >{{ __('Learn more about deploying to AWS') }}</gl-button
- >
- </p>
- </div>
- <img
- class="gl-mt-3"
- :alt="__('Amazon Web Services Logo')"
- :src="awsLogoSvgPath"
- height="32"
- />
- </div>
- </gl-alert>
- </gl-collapse>
- <gl-alert
- v-if="containsVariableReference"
- :title="__('Value might contain a variable reference')"
- :dismissible="false"
- variant="warning"
- data-testid="contains-variable-reference"
- >
- <gl-sprintf :message="$options.containsVariableReferenceMessage">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #docsLink="{ content }">
- <gl-link :href="containsVariableReferenceLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-alert>
- <template #modal-footer>
- <gl-button @click="hideModal">{{ __('Cancel') }}</gl-button>
- <gl-button
- v-if="variableBeingEdited"
- ref="deleteCiVariable"
- variant="danger"
- category="secondary"
- data-qa-selector="ci_variable_delete_button"
- @click="deleteVarAndClose"
- >{{ __('Delete variable') }}</gl-button
- >
- <gl-button
- ref="updateOrAddVariable"
- :disabled="!canSubmit"
- variant="confirm"
- category="primary"
- data-testid="ciUpdateOrAddVariableBtn"
- data-qa-selector="ci_variable_save_button"
- @click="updateOrAddVariable"
- >{{ modalActionText }}
- </gl-button>
- </template>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
deleted file mode 100644
index f1fe188348d..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_settings.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { mapState, mapActions } from 'vuex';
-import LegacyCiVariableModal from './legacy_ci_variable_modal.vue';
-import LegacyCiVariableTable from './legacy_ci_variable_table.vue';
-
-export default {
- components: {
- LegacyCiVariableModal,
- LegacyCiVariableTable,
- },
- computed: {
- ...mapState(['isGroup', 'isProject']),
- },
- mounted() {
- if (this.isProject) {
- this.fetchEnvironments();
- }
- },
- methods: {
- ...mapActions(['fetchEnvironments']),
- },
-};
-</script>
-
-<template>
- <div class="row">
- <div class="col-lg-12">
- <legacy-ci-variable-table />
- <legacy-ci-variable-modal />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
deleted file mode 100644
index f3a84e22316..00000000000
--- a/app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue
+++ /dev/null
@@ -1,209 +0,0 @@
-<script>
-import { GlTable, GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-import { mapState, mapActions } from 'vuex';
-import { s__, __ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { ADD_CI_VARIABLE_MODAL_ID } from '../constants';
-
-export default {
- modalId: ADD_CI_VARIABLE_MODAL_ID,
- fields: [
- {
- key: 'variable_type',
- label: s__('CiVariables|Type'),
- thClass: 'gl-w-10p',
- },
- {
- key: 'key',
- label: s__('CiVariables|Key'),
- tdClass: 'text-plain',
- sortable: true,
- },
- {
- key: 'value',
- label: s__('CiVariables|Value'),
- thClass: 'gl-w-15p',
- },
- {
- key: 'options',
- label: s__('CiVariables|Options'),
- thClass: 'gl-w-10p',
- },
- {
- key: 'environment_scope',
- label: s__('CiVariables|Environments'),
- },
- {
- key: 'actions',
- label: '',
- tdClass: 'text-right',
- thClass: 'gl-w-5p',
- },
- ],
- components: {
- GlButton,
- GlTable,
- },
- directives: {
- GlModalDirective,
- GlTooltip: GlTooltipDirective,
- },
- mixins: [glFeatureFlagsMixin()],
- computed: {
- ...mapState(['variables', 'valuesHidden', 'isLoading', 'isDeleting']),
- valuesButtonText() {
- return this.valuesHidden ? __('Reveal values') : __('Hide values');
- },
- isTableEmpty() {
- return !this.variables || this.variables.length === 0;
- },
- fields() {
- return this.$options.fields;
- },
- variablesWithOptions() {
- return this.variables?.map((item, index) => ({
- ...item,
- options: this.getOptions(item),
- index,
- }));
- },
- },
- mounted() {
- this.fetchVariables();
- },
- methods: {
- ...mapActions(['fetchVariables', 'toggleValues', 'editVariable']),
- getOptions(item) {
- const options = [];
- if (item.protected) {
- options.push(s__('CiVariables|Protected'));
- }
- if (item.masked) {
- options.push(s__('CiVariables|Masked'));
- }
- return options.join(', ');
- },
- editVariableClicked(index = -1) {
- this.editVariable(this.variables[index] ?? null);
- },
- },
-};
-</script>
-
-<template>
- <div class="ci-variable-table" data-testid="ci-variable-table">
- <gl-table
- :fields="fields"
- :items="variablesWithOptions"
- tbody-tr-class="js-ci-variable-row"
- data-qa-selector="ci_variable_table_content"
- sort-by="key"
- sort-direction="asc"
- stacked="lg"
- table-class="text-secondary"
- fixed
- show-empty
- sort-icon-left
- no-sort-reset
- >
- <template #table-colgroup="scope">
- <col v-for="field in scope.fields" :key="field.key" :style="field.customStyle" />
- </template>
- <template #cell(key)="{ item }">
- <div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
- >
- <span
- :id="`ci-variable-key-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-word-break-word"
- >{{ item.key }}</span
- >
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
- :title="__('Copy key')"
- :data-clipboard-text="item.key"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </template>
- <template #cell(value)="{ item }">
- <div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
- >
- <span v-if="valuesHidden">*****</span>
- <span
- v-else
- :id="`ci-variable-value-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- >{{ item.value }}</span
- >
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
- :title="__('Copy value')"
- :data-clipboard-text="item.value"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </template>
- <template #cell(options)="{ item }">
- <span>{{ item.options }}</span>
- </template>
- <template #cell(environment_scope)="{ item }">
- <div
- class="gl-display-flex gl-align-items-flex-start gl-justify-content-end gl-lg-justify-content-start gl-mr-n3"
- >
- <span
- :id="`ci-variable-env-${item.id}`"
- class="gl-display-inline-block gl-max-w-full gl-word-break-word"
- >{{ item.environment_scope }}</span
- >
- <gl-button
- v-gl-tooltip
- category="tertiary"
- icon="copy-to-clipboard"
- class="gl-my-n3 gl-ml-2"
- :title="__('Copy environment')"
- :data-clipboard-text="item.environment_scope"
- :aria-label="__('Copy to clipboard')"
- />
- </div>
- </template>
- <template #cell(actions)="{ item }">
- <gl-button
- v-gl-modal-directive="$options.modalId"
- icon="pencil"
- :aria-label="__('Edit')"
- data-qa-selector="edit_ci_variable_button"
- @click="editVariableClicked(item.index)"
- />
- </template>
- <template #empty>
- <p class="gl-text-center gl-py-6 gl-text-black-normal gl-mb-0">
- {{ __('There are no variables yet.') }}
- </p>
- </template>
- </gl-table>
- <div class="ci-variable-actions gl-display-flex gl-mt-5">
- <gl-button
- v-gl-modal-directive="$options.modalId"
- class="gl-mr-3"
- data-qa-selector="add_ci_variable_button"
- variant="confirm"
- category="primary"
- >{{ __('Add variable') }}</gl-button
- >
- <gl-button
- v-if="!isTableEmpty"
- data-qa-selector="reveal_ci_variable_value_button"
- @click="toggleValues(!valuesHidden)"
- >{{ valuesButtonText }}</gl-button
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci_variable_list/index.js b/app/assets/javascripts/ci_variable_list/index.js
index 1b69da9e086..174a59aba42 100644
--- a/app/assets/javascripts/ci_variable_list/index.js
+++ b/app/assets/javascripts/ci_variable_list/index.js
@@ -5,9 +5,7 @@ import { parseBoolean } from '~/lib/utils/common_utils';
import CiAdminVariables from './components/ci_admin_variables.vue';
import CiGroupVariables from './components/ci_group_variables.vue';
import CiProjectVariables from './components/ci_project_variables.vue';
-import LegacyCiVariableSettings from './components/legacy_ci_variable_settings.vue';
import { cacheConfig, resolvers } from './graphql/settings';
-import createStore from './store';
const mountCiVariableListApp = (containerEl) => {
const {
@@ -76,62 +74,10 @@ const mountCiVariableListApp = (containerEl) => {
});
};
-const mountLegacyCiVariableListApp = (containerEl) => {
- const {
- endpoint,
- projectId,
- isGroup,
- isProject,
- maskableRegex,
- protectedByDefault,
- awsLogoSvgPath,
- awsTipDeployLink,
- awsTipCommandsLink,
- awsTipLearnLink,
- containsVariableReferenceLink,
- protectedEnvironmentVariablesLink,
- maskedEnvironmentVariablesLink,
- environmentScopeLink,
- } = containerEl.dataset;
-
- const parsedIsProject = parseBoolean(isProject);
- const parsedIsGroup = parseBoolean(isGroup);
- const isProtectedByDefault = parseBoolean(protectedByDefault);
-
- const store = createStore({
- endpoint,
- projectId,
- isGroup: parsedIsGroup,
- isProject: parsedIsProject,
- maskableRegex,
- isProtectedByDefault,
- awsLogoSvgPath,
- awsTipDeployLink,
- awsTipCommandsLink,
- awsTipLearnLink,
- containsVariableReferenceLink,
- protectedEnvironmentVariablesLink,
- maskedEnvironmentVariablesLink,
- environmentScopeLink,
- });
-
- return new Vue({
- el: containerEl,
- store,
- render(createElement) {
- return createElement(LegacyCiVariableSettings);
- },
- });
-};
-
-export default (containerId = 'js-ci-project-variables') => {
+export default (containerId = 'js-ci-variables') => {
const el = document.getElementById(containerId);
- if (el) {
- if (gon.features?.ciVariableSettingsGraphql) {
- mountCiVariableListApp(el);
- } else {
- mountLegacyCiVariableListApp(el);
- }
- }
+ if (!el) return;
+
+ mountCiVariableListApp(el);
};
diff --git a/app/assets/javascripts/ci_variable_list/store/actions.js b/app/assets/javascripts/ci_variable_list/store/actions.js
deleted file mode 100644
index ac31e845b0d..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/actions.js
+++ /dev/null
@@ -1,208 +0,0 @@
-import Api from '~/api';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-import * as types from './mutation_types';
-import { prepareDataForApi, prepareDataForDisplay, prepareEnvironments } from './utils';
-
-export const toggleValues = ({ commit }, valueState) => {
- commit(types.TOGGLE_VALUES, valueState);
-};
-
-export const clearModal = ({ commit }) => {
- commit(types.CLEAR_MODAL);
-};
-
-export const resetEditing = ({ commit, dispatch }) => {
- // fetch variables again if modal is being edited and then hidden
- // without saving changes, to cover use case of reactivity in the table
- dispatch('fetchVariables');
- commit(types.RESET_EDITING);
-};
-
-export const setVariableProtected = ({ commit }) => {
- commit(types.SET_VARIABLE_PROTECTED);
-};
-
-export const requestAddVariable = ({ commit }) => {
- commit(types.REQUEST_ADD_VARIABLE);
-};
-
-export const receiveAddVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_ADD_VARIABLE_SUCCESS);
-};
-
-export const receiveAddVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_ADD_VARIABLE_ERROR, error);
-};
-
-export const addVariable = ({ state, dispatch }) => {
- dispatch('requestAddVariable');
-
- return axios
- .patch(state.endpoint, {
- variables_attributes: [prepareDataForApi(state.variable)],
- })
- .then(() => {
- dispatch('receiveAddVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveAddVariableError', error);
- });
-};
-
-export const requestUpdateVariable = ({ commit }) => {
- commit(types.REQUEST_UPDATE_VARIABLE);
-};
-
-export const receiveUpdateVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_UPDATE_VARIABLE_SUCCESS);
-};
-
-export const receiveUpdateVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_UPDATE_VARIABLE_ERROR, error);
-};
-
-export const updateVariable = ({ state, dispatch }) => {
- dispatch('requestUpdateVariable');
-
- const updatedVariable = prepareDataForApi(state.variable);
- updatedVariable.secrect_value = updateVariable.value;
-
- return axios
- .patch(state.endpoint, { variables_attributes: [updatedVariable] })
- .then(() => {
- dispatch('receiveUpdateVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveUpdateVariableError', error);
- });
-};
-
-export const editVariable = ({ commit }, variable) => {
- const variableToEdit = variable;
- variableToEdit.secret_value = variableToEdit.value;
- commit(types.VARIABLE_BEING_EDITED, variableToEdit);
-};
-
-export const requestVariables = ({ commit }) => {
- commit(types.REQUEST_VARIABLES);
-};
-export const receiveVariablesSuccess = ({ commit }, variables) => {
- commit(types.RECEIVE_VARIABLES_SUCCESS, variables);
-};
-
-export const fetchVariables = ({ dispatch, state }) => {
- dispatch('requestVariables');
-
- return axios
- .get(state.endpoint)
- .then(({ data }) => {
- dispatch('receiveVariablesSuccess', prepareDataForDisplay(data.variables));
- })
- .catch(() => {
- createAlert({
- message: __('There was an error fetching the variables.'),
- });
- });
-};
-
-export const requestDeleteVariable = ({ commit }) => {
- commit(types.REQUEST_DELETE_VARIABLE);
-};
-
-export const receiveDeleteVariableSuccess = ({ commit }) => {
- commit(types.RECEIVE_DELETE_VARIABLE_SUCCESS);
-};
-
-export const receiveDeleteVariableError = ({ commit }, error) => {
- commit(types.RECEIVE_DELETE_VARIABLE_ERROR, error);
-};
-
-export const deleteVariable = ({ dispatch, state }) => {
- dispatch('requestDeleteVariable');
-
- const destroy = true;
-
- return axios
- .patch(state.endpoint, { variables_attributes: [prepareDataForApi(state.variable, destroy)] })
- .then(() => {
- dispatch('receiveDeleteVariableSuccess');
- dispatch('fetchVariables');
- })
- .catch((error) => {
- createAlert({
- message: error.response.data[0],
- });
- dispatch('receiveDeleteVariableError', error);
- });
-};
-
-export const requestEnvironments = ({ commit }) => {
- commit(types.REQUEST_ENVIRONMENTS);
-};
-
-export const receiveEnvironmentsSuccess = ({ commit }, environments) => {
- commit(types.RECEIVE_ENVIRONMENTS_SUCCESS, environments);
-};
-
-export const fetchEnvironments = ({ dispatch, state }) => {
- dispatch('requestEnvironments');
-
- return Api.environments(state.projectId)
- .then((res) => {
- dispatch('receiveEnvironmentsSuccess', prepareEnvironments(res.data));
- })
- .catch(() => {
- createAlert({
- message: __('There was an error fetching the environments information.'),
- });
- });
-};
-
-export const setEnvironmentScope = ({ commit, dispatch }, environment) => {
- commit(types.SET_ENVIRONMENT_SCOPE, environment);
- dispatch('setSelectedEnvironment', environment);
-};
-
-export const addWildCardScope = ({ commit, dispatch }, environment) => {
- commit(types.ADD_WILD_CARD_SCOPE, environment);
- commit(types.SET_ENVIRONMENT_SCOPE, environment);
- dispatch('setSelectedEnvironment', environment);
-};
-
-export const resetSelectedEnvironment = ({ commit }) => {
- commit(types.RESET_SELECTED_ENVIRONMENT);
-};
-
-export const setSelectedEnvironment = ({ commit }, environment) => {
- commit(types.SET_SELECTED_ENVIRONMENT, environment);
-};
-
-export const updateVariableKey = ({ commit }, { key }) => {
- commit(types.UPDATE_VARIABLE_KEY, key);
-};
-
-export const updateVariableValue = ({ commit }, { secret_value }) => {
- commit(types.UPDATE_VARIABLE_VALUE, secret_value);
-};
-
-export const updateVariableType = ({ commit }, { variable_type }) => {
- commit(types.UPDATE_VARIABLE_TYPE, variable_type);
-};
-
-export const updateVariableProtected = ({ commit }, { protected_variable }) => {
- commit(types.UPDATE_VARIABLE_PROTECTED, protected_variable);
-};
-
-export const updateVariableMasked = ({ commit }, { masked }) => {
- commit(types.UPDATE_VARIABLE_MASKED, masked);
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/getters.js b/app/assets/javascripts/ci_variable_list/store/getters.js
deleted file mode 100644
index 6570f455541..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/getters.js
+++ /dev/null
@@ -1,6 +0,0 @@
-import { uniq } from 'lodash';
-
-export const joinedEnvironments = (state) => {
- const scopesFromVariables = (state.variables || []).map((variable) => variable.environment_scope);
- return uniq(state.environments.concat(scopesFromVariables)).sort();
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/index.js b/app/assets/javascripts/ci_variable_list/store/index.js
deleted file mode 100644
index 83802f6a36f..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export default (initialState = {}) =>
- new Vuex.Store({
- actions,
- mutations,
- getters,
- state: {
- ...state(),
- ...initialState,
- },
- });
diff --git a/app/assets/javascripts/ci_variable_list/store/mutation_types.js b/app/assets/javascripts/ci_variable_list/store/mutation_types.js
deleted file mode 100644
index 5db8f610192..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/mutation_types.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const TOGGLE_VALUES = 'TOGGLE_VALUES';
-export const VARIABLE_BEING_EDITED = 'VARIABLE_BEING_EDITED';
-export const RESET_EDITING = 'RESET_EDITING';
-export const CLEAR_MODAL = 'CLEAR_MODAL';
-export const SET_VARIABLE_PROTECTED = 'SET_VARIABLE_PROTECTED';
-
-export const REQUEST_VARIABLES = 'REQUEST_VARIABLES';
-export const RECEIVE_VARIABLES_SUCCESS = 'RECEIVE_VARIABLES_SUCCESS';
-
-export const REQUEST_DELETE_VARIABLE = 'REQUEST_DELETE_VARIABLE';
-export const RECEIVE_DELETE_VARIABLE_SUCCESS = 'RECEIVE_DELETE_VARIABLE_SUCCESS';
-export const RECEIVE_DELETE_VARIABLE_ERROR = 'RECEIVE_DELETE_VARIABLE_ERROR';
-
-export const REQUEST_ADD_VARIABLE = 'REQUEST_ADD_VARIABLE';
-export const RECEIVE_ADD_VARIABLE_SUCCESS = 'RECEIVE_ADD_VARIABLE_SUCCESS';
-export const RECEIVE_ADD_VARIABLE_ERROR = 'RECEIVE_ADD_VARIABLE_ERROR';
-
-export const REQUEST_UPDATE_VARIABLE = 'REQUEST_UPDATE_VARIABLE';
-export const RECEIVE_UPDATE_VARIABLE_SUCCESS = 'RECEIVE_UPDATE_VARIABLE_SUCCESS';
-export const RECEIVE_UPDATE_VARIABLE_ERROR = 'RECEIVE_UPDATE_VARIABLE_ERROR';
-
-export const REQUEST_ENVIRONMENTS = 'REQUEST_ENVIRONMENTS';
-export const RECEIVE_ENVIRONMENTS_SUCCESS = 'RECEIVE_ENVIRONMENTS_SUCCESS';
-export const SET_ENVIRONMENT_SCOPE = 'SET_ENVIRONMENT_SCOPE';
-export const ADD_WILD_CARD_SCOPE = 'ADD_WILD_CARD_SCOPE';
-export const RESET_SELECTED_ENVIRONMENT = 'RESET_SELECTED_ENVIRONMENT';
-export const SET_SELECTED_ENVIRONMENT = 'SET_SELECTED_ENVIRONMENT';
-
-export const UPDATE_VARIABLE_KEY = 'UPDATE_VARIABLE_KEY';
-export const UPDATE_VARIABLE_VALUE = 'UPDATE_VARIABLE_VALUE';
-export const UPDATE_VARIABLE_TYPE = 'UPDATE_VARIABLE_TYPE';
-export const UPDATE_VARIABLE_PROTECTED = 'UPDATE_VARIABLE_PROTECTED';
-export const UPDATE_VARIABLE_MASKED = 'UPDATE_VARIABLE_MASKED';
diff --git a/app/assets/javascripts/ci_variable_list/store/mutations.js b/app/assets/javascripts/ci_variable_list/store/mutations.js
deleted file mode 100644
index 0e7c61cecb8..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/mutations.js
+++ /dev/null
@@ -1,128 +0,0 @@
-import { displayText } from '../constants';
-import * as types from './mutation_types';
-
-export default {
- [types.REQUEST_VARIABLES](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_VARIABLES_SUCCESS](state, variables) {
- state.isLoading = false;
- state.variables = variables;
- },
-
- [types.REQUEST_DELETE_VARIABLE](state) {
- state.isDeleting = true;
- },
-
- [types.RECEIVE_DELETE_VARIABLE_SUCCESS](state) {
- state.isDeleting = false;
- },
-
- [types.RECEIVE_DELETE_VARIABLE_ERROR](state, error) {
- state.isDeleting = false;
- state.error = error;
- },
-
- [types.REQUEST_ADD_VARIABLE](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_ADD_VARIABLE_SUCCESS](state) {
- state.isLoading = false;
- },
-
- [types.RECEIVE_ADD_VARIABLE_ERROR](state, error) {
- state.isLoading = false;
- state.error = error;
- },
-
- [types.REQUEST_UPDATE_VARIABLE](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_UPDATE_VARIABLE_SUCCESS](state) {
- state.isLoading = false;
- },
-
- [types.RECEIVE_UPDATE_VARIABLE_ERROR](state, error) {
- state.isLoading = false;
- state.error = error;
- },
-
- [types.TOGGLE_VALUES](state, valueState) {
- state.valuesHidden = valueState;
- },
-
- [types.REQUEST_ENVIRONMENTS](state) {
- state.isLoading = true;
- },
-
- [types.RECEIVE_ENVIRONMENTS_SUCCESS](state, environments) {
- state.isLoading = false;
- state.environments = environments;
- state.environments.unshift(displayText.allEnvironmentsText);
- },
-
- [types.VARIABLE_BEING_EDITED](state, variable) {
- state.variableBeingEdited = true;
- state.variable = variable;
- },
-
- [types.CLEAR_MODAL](state) {
- state.variable = {
- variable_type: displayText.variableText,
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: displayText.allEnvironmentsText,
- };
- },
-
- [types.RESET_EDITING](state) {
- state.variableBeingEdited = false;
- state.showInputValue = false;
- },
-
- [types.SET_ENVIRONMENT_SCOPE](state, environment) {
- state.variable.environment_scope = environment;
- },
-
- [types.ADD_WILD_CARD_SCOPE](state, environment) {
- state.environments.push(environment);
- state.environments.sort();
- },
-
- [types.RESET_SELECTED_ENVIRONMENT](state) {
- state.selectedEnvironment = '';
- },
-
- [types.SET_SELECTED_ENVIRONMENT](state, environment) {
- state.selectedEnvironment = environment;
- },
-
- [types.SET_VARIABLE_PROTECTED](state) {
- state.variable.protected_variable = true;
- },
-
- [types.UPDATE_VARIABLE_KEY](state, key) {
- state.variable.key = key;
- },
-
- [types.UPDATE_VARIABLE_VALUE](state, value) {
- state.variable.secret_value = value;
- },
-
- [types.UPDATE_VARIABLE_TYPE](state, type) {
- state.variable.variable_type = type;
- },
-
- [types.UPDATE_VARIABLE_PROTECTED](state, bool) {
- state.variable.protected_variable = bool;
- },
-
- [types.UPDATE_VARIABLE_MASKED](state, bool) {
- state.variable.masked = bool;
- },
-};
diff --git a/app/assets/javascripts/ci_variable_list/store/state.js b/app/assets/javascripts/ci_variable_list/store/state.js
deleted file mode 100644
index 96b27792664..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/state.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { displayText } from '../constants';
-
-export default () => ({
- endpoint: null,
- projectId: null,
- isGroup: null,
- maskableRegex: null,
- isProtectedByDefault: null,
- isLoading: false,
- isDeleting: false,
- variable: {
- variable_type: displayText.variableText,
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: displayText.allEnvironmentsText,
- },
- variables: null,
- valuesHidden: true,
- error: null,
- environments: [],
- typeOptions: [displayText.variableText, displayText.fileText],
- variableBeingEdited: false,
- selectedEnvironment: '',
-});
diff --git a/app/assets/javascripts/ci_variable_list/store/utils.js b/app/assets/javascripts/ci_variable_list/store/utils.js
deleted file mode 100644
index f46a671ae7b..00000000000
--- a/app/assets/javascripts/ci_variable_list/store/utils.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { cloneDeep } from 'lodash';
-import { displayText, types, allEnvironments } from '../constants';
-
-const variableTypeHandler = (type) =>
- type === displayText.variableText ? types.variableType : types.fileType;
-
-export const prepareDataForDisplay = (variables) => {
- const variablesToDisplay = [];
- variables.forEach((variable) => {
- const variableCopy = variable;
- if (variableCopy.variable_type === types.variableType) {
- variableCopy.variable_type = displayText.variableText;
- } else {
- variableCopy.variable_type = displayText.fileText;
- }
- variableCopy.secret_value = variableCopy.value;
-
- if (variableCopy.environment_scope === allEnvironments.type) {
- variableCopy.environment_scope = displayText.allEnvironmentsText;
- }
- variableCopy.protected_variable = variableCopy.protected;
- variablesToDisplay.push(variableCopy);
- });
- return variablesToDisplay;
-};
-
-export const prepareDataForApi = (variable, destroy = false) => {
- const variableCopy = cloneDeep(variable);
- variableCopy.protected = variableCopy.protected_variable.toString();
- delete variableCopy.protected_variable;
- variableCopy.masked = variableCopy.masked.toString();
- variableCopy.variable_type = variableTypeHandler(variableCopy.variable_type);
- if (variableCopy.environment_scope === displayText.allEnvironmentsText) {
- variableCopy.environment_scope = allEnvironments.type;
- }
-
- if (destroy) {
- // eslint-disable-next-line
- variableCopy._destroy = destroy;
- }
-
- return variableCopy;
-};
-
-export const prepareEnvironments = (environments) => environments.map((e) => e.name);
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue
new file mode 100644
index 00000000000..c7fddadab1b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/forwarding_settings.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlFormCheckbox, GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import {
+ PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ PACKAGE_FORWARDING_ENFORCE_LABEL,
+} from '~/packages_and_registries/settings/group/constants';
+
+export default {
+ name: 'ForwardingSettings',
+ i18n: {
+ PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ PACKAGE_FORWARDING_ENFORCE_LABEL,
+ },
+ components: {
+ GlFormCheckbox,
+ GlFormGroup,
+ GlSprintf,
+ },
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ forwarding: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ lockForwarding: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ modelNames: {
+ type: Object,
+ required: true,
+ validator(value) {
+ return isEqual(Object.keys(value), ['forwarding', 'lockForwarding', 'isLocked']);
+ },
+ },
+ },
+ computed: {
+ fields() {
+ return [
+ {
+ testid: 'forwarding-checkbox',
+ label: PACKAGE_FORWARDING_CHECKBOX_LABEL,
+ updateField: this.modelNames.forwarding,
+ checked: this.forwarding,
+ },
+ {
+ testid: 'lock-forwarding-checkbox',
+ label: PACKAGE_FORWARDING_ENFORCE_LABEL,
+ updateField: this.modelNames.lockForwarding,
+ checked: this.lockForwarding,
+ },
+ ];
+ },
+ },
+ methods: {
+ update(type, value) {
+ this.$emit('update', type, value);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group :label="label">
+ <gl-form-checkbox
+ v-for="field in fields"
+ :key="field.testid"
+ :checked="field.checked"
+ :disabled="disabled"
+ :data-testid="field.testid"
+ @change="update(field.updateField, $event)"
+ >
+ <gl-sprintf :message="field.label">
+ <template #packageType>
+ {{ label }}
+ </template>
+ </gl-sprintf>
+ </gl-form-checkbox>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index f285dfc0755..36eb65c623b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -2,6 +2,7 @@
import { GlAlert } from '@gitlab/ui';
import { n__ } from '~/locale';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
+import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
@@ -11,6 +12,7 @@ export default {
components: {
GlAlert,
PackagesSettings,
+ PackagesForwardingSettings,
DependencyProxySettings,
},
inject: ['groupPath'],
@@ -82,6 +84,12 @@ export default {
@error="handleError(2)"
/>
+ <packages-forwarding-settings
+ :forward-settings="packageSettings"
+ @success="handleSuccess(2)"
+ @error="handleError(2)"
+ />
+
<dependency-proxy-settings
:dependency-proxy-settings="dependencyProxySettings"
:dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy"
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
new file mode 100644
index 00000000000..b7d7f0aaca7
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue
@@ -0,0 +1,190 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { isEqual } from 'lodash';
+import {
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ PACKAGE_FORWARDING_FORM_BUTTON,
+ PACKAGE_FORWARDING_FIELDS,
+ MAVEN_FORWARDING_FIELDS,
+} from '~/packages_and_registries/settings/group/constants';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+
+import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import ForwardingSettings from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
+
+export default {
+ name: 'PackageForwardingSettings',
+ i18n: {
+ PACKAGE_FORWARDING_FORM_BUTTON,
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ },
+ components: {
+ ForwardingSettings,
+ GlButton,
+ SettingsBlock,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['groupPath'],
+ props: {
+ forwardSettings: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ mutationLoading: false,
+ workingCopy: { ...this.forwardSettings },
+ };
+ },
+ computed: {
+ packageForwardingFields() {
+ const fields = PACKAGE_FORWARDING_FIELDS;
+
+ if (this.glFeatures.mavenCentralRequestForwarding) {
+ return fields.concat(MAVEN_FORWARDING_FIELDS);
+ }
+
+ return fields;
+ },
+ isEdited() {
+ return !isEqual(this.forwardSettings, this.workingCopy);
+ },
+ isDisabled() {
+ return !this.isEdited || this.mutationLoading;
+ },
+ npmMutation() {
+ if (this.workingCopy.npmPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ npmPackageRequestsForwarding: this.workingCopy.npmPackageRequestsForwarding,
+ lockNpmPackageRequestsForwarding: this.workingCopy.lockNpmPackageRequestsForwarding,
+ };
+ },
+ pypiMutation() {
+ if (this.workingCopy.pypiPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ pypiPackageRequestsForwarding: this.workingCopy.pypiPackageRequestsForwarding,
+ lockPypiPackageRequestsForwarding: this.workingCopy.lockPypiPackageRequestsForwarding,
+ };
+ },
+ mavenMutation() {
+ if (this.workingCopy.mavenPackageRequestsForwardingLocked) {
+ return {};
+ }
+
+ return {
+ mavenPackageRequestsForwarding: this.workingCopy.mavenPackageRequestsForwarding,
+ lockMavenPackageRequestsForwarding: this.workingCopy.lockMavenPackageRequestsForwarding,
+ };
+ },
+ mutationVariables() {
+ return {
+ ...this.npmMutation,
+ ...this.pypiMutation,
+ ...this.mavenMutation,
+ };
+ },
+ },
+ watch: {
+ forwardSettings(newValue) {
+ this.workingCopy = { ...newValue };
+ },
+ },
+ methods: {
+ isForwardingFieldsDisabled(fields) {
+ const isLocked = fields?.modelNames?.isLocked;
+
+ return this.mutationLoading || this.workingCopy[isLocked];
+ },
+ forwardingFieldsForwarding(fields) {
+ const forwarding = fields?.modelNames?.forwarding;
+
+ return this.workingCopy[forwarding];
+ },
+ forwardingFieldsLockForwarding(fields) {
+ const lockForwarding = fields?.modelNames?.lockForwarding;
+
+ return this.workingCopy[lockForwarding];
+ },
+ async submit() {
+ this.mutationLoading = true;
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: updateNamespacePackageSettings,
+ variables: {
+ input: {
+ namespacePath: this.groupPath,
+ ...this.mutationVariables,
+ },
+ },
+ update: updateGroupPackageSettings(this.groupPath),
+ optimisticResponse: updateGroupPackagesSettingsOptimisticResponse({
+ ...this.forwardSettings,
+ ...this.mutationVariables,
+ }),
+ });
+
+ if (data.updateNamespacePackageSettings?.errors?.length > 0) {
+ throw new Error();
+ } else {
+ this.$emit('success');
+ }
+ } catch {
+ this.$emit('error');
+ } finally {
+ this.mutationLoading = false;
+ }
+ },
+ updateWorkingCopy(type, value) {
+ this.$set(this.workingCopy, type, value);
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block>
+ <template #title> {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_HEADER }}</template>
+ <template #description>
+ <span data-testid="description">
+ {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_DESCRIPTION }}
+ </span>
+ </template>
+ <template #default>
+ <form @submit.prevent="submit">
+ <forwarding-settings
+ v-for="forwardingFields in packageForwardingFields"
+ :key="forwardingFields.label"
+ :data-testid="forwardingFields.testid"
+ :disabled="isForwardingFieldsDisabled(forwardingFields)"
+ :forwarding="forwardingFieldsForwarding(forwardingFields)"
+ :label="forwardingFields.label"
+ :lock-forwarding="forwardingFieldsLockForwarding(forwardingFields)"
+ :model-names="forwardingFields.modelNames"
+ @update="updateWorkingCopy"
+ />
+ <gl-button
+ type="submit"
+ :disabled="isDisabled"
+ :loading="mutationLoading"
+ category="primary"
+ variant="confirm"
+ class="js-no-auto-disable gl-mr-4"
+ >
+ {{ $options.i18n.PACKAGE_FORWARDING_FORM_BUTTON }}
+ </gl-button>
+ </form>
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index 2dd6d3f76f6..c93cd7f7d78 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -7,6 +7,8 @@ export const PACKAGE_SETTINGS_DESCRIPTION = s__(
);
export const PACKAGE_FORMATS_TABLE_HEADER = s__('PackageRegistry|Package formats');
export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
+export const NPM_PACKAGE_FORMAT = s__('PackageRegistry|npm');
+export const PYPI_PACKAGE_FORMAT = s__('PackageRegistry|PyPI');
export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
@@ -15,11 +17,65 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
'PackageRegistry|Publish packages if their name or version matches this regex.',
);
+export const PACKAGE_FORWARDING_SETTINGS_HEADER = s__('PackageRegistry|Package forwarding');
+export const PACKAGE_FORWARDING_SETTINGS_DESCRIPTION = s__(
+ 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.',
+);
+export const PACKAGE_FORWARDING_CHECKBOX_LABEL = s__(
+ `PackageRegistry|Forward %{packageType} package requests`,
+);
+export const PACKAGE_FORWARDING_ENFORCE_LABEL = s__(
+ `PackageRegistry|Enforce %{packageType} setting for all subgroups`,
+);
+
+const MAVEN_PACKAGE_REQUESTS_FORWARDING = 'mavenPackageRequestsForwarding';
+const LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING = 'lockMavenPackageRequestsForwarding';
+const MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'mavenPackageRequestsForwardingLocked';
+const NPM_PACKAGE_REQUESTS_FORWARDING = 'npmPackageRequestsForwarding';
+const LOCK_NPM_PACKAGE_REQUESTS_FORWARDING = 'lockNpmPackageRequestsForwarding';
+const NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'npmPackageRequestsForwardingLocked';
+const PYPI_PACKAGE_REQUESTS_FORWARDING = 'pypiPackageRequestsForwarding';
+const LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING = 'lockPypiPackageRequestsForwarding';
+const PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED = 'pypiPackageRequestsForwardingLocked';
+
+export const PACKAGE_FORWARDING_FORM_BUTTON = __('Save changes');
+
export const DEPENDENCY_PROXY_HEADER = s__('DependencyProxy|Dependency Proxy');
export const DEPENDENCY_PROXY_DESCRIPTION = s__(
'DependencyProxy|Enable the Dependency Proxy and settings for clearing the cache.',
);
+export const PACKAGE_FORWARDING_FIELDS = [
+ {
+ label: NPM_PACKAGE_FORMAT,
+ testid: 'npm',
+ modelNames: {
+ forwarding: NPM_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_NPM_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: NPM_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+ },
+ {
+ label: PYPI_PACKAGE_FORMAT,
+ testid: 'pypi',
+ modelNames: {
+ forwarding: PYPI_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_PYPI_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: PYPI_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+ },
+];
+
+export const MAVEN_FORWARDING_FIELDS = {
+ label: MAVEN_PACKAGE_FORMAT,
+ testid: 'maven',
+ modelNames: {
+ forwarding: MAVEN_PACKAGE_REQUESTS_FORWARDING,
+ lockForwarding: LOCK_MAVEN_PACKAGE_REQUESTS_FORWARDING,
+ isLocked: MAVEN_PACKAGE_REQUESTS_FORWARDING_LOCKED,
+ },
+};
+
// Parameters
export const PACKAGES_DOCS_PATH = helpPagePath('user/packages/index');
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
new file mode 100644
index 00000000000..267e40263f2
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql
@@ -0,0 +1,15 @@
+fragment PackageSettingsFields on PackageSettings {
+ mavenDuplicatesAllowed
+ mavenDuplicateExceptionRegex
+ genericDuplicatesAllowed
+ genericDuplicateExceptionRegex
+ mavenPackageRequestsForwarding
+ lockMavenPackageRequestsForwarding
+ mavenPackageRequestsForwardingLocked
+ npmPackageRequestsForwarding
+ lockNpmPackageRequestsForwarding
+ npmPackageRequestsForwardingLocked
+ pypiPackageRequestsForwarding
+ lockPypiPackageRequestsForwarding
+ pypiPackageRequestsForwardingLocked
+}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
index 5c245ff9453..5558cb66f42 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql
@@ -1,10 +1,9 @@
+#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql"
+
mutation updateNamespacePackageSettings($input: UpdateNamespacePackageSettingsInput!) {
updateNamespacePackageSettings(input: $input) {
packageSettings {
- mavenDuplicatesAllowed
- mavenDuplicateExceptionRegex
- genericDuplicatesAllowed
- genericDuplicateExceptionRegex
+ ...PackageSettingsFields
}
errors
}
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
index 404d9d26d49..82a282d6d81 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql
@@ -1,3 +1,5 @@
+#import "~/packages_and_registries/settings/group/graphql/fragments/package_settings_fields.fragment.graphql"
+
query getGroupPackagesSettings($fullPath: ID!) {
group(fullPath: $fullPath) {
id
@@ -9,10 +11,7 @@ query getGroupPackagesSettings($fullPath: ID!) {
enabled
}
packageSettings {
- mavenDuplicatesAllowed
- mavenDuplicateExceptionRegex
- genericDuplicatesAllowed
- genericDuplicateExceptionRegex
+ ...PackageSettingsFields
}
}
}
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index f3530b46845..ac5e0b28dd1 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -3,6 +3,7 @@ import BranchSortDropdown from '~/branches/branch_sort_dropdown';
import initDiverganceGraph from '~/branches/divergence_graph';
import initDeleteBranchButton from '~/branches/init_delete_branch_button';
import initDeleteBranchModal from '~/branches/init_delete_branch_modal';
+import initDeleteMergedBranches from '~/branches/init_delete_merged_branches';
const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
'.js-branch-list',
@@ -11,6 +12,7 @@ const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
initDiverganceGraph(divergingCountsEndpoint, defaultBranch);
BranchSortDropdown();
initDeprecatedRemoveRowBehavior();
+initDeleteMergedBranches();
document
.querySelectorAll('.js-delete-branch-button')
diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
index f2782f96da1..f5e1525090e 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
@@ -45,7 +45,7 @@ export default {
</script>
<template>
- <nav class="search-filter">
+ <nav data-testid="search-filter">
<gl-nav vertical pills>
<gl-nav-item
v-for="(item, scope, index) in navigation"
diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js
new file mode 100644
index 00000000000..fd96da5faf6
--- /dev/null
+++ b/app/assets/javascripts/sentry/constants.js
@@ -0,0 +1,43 @@
+import { __ } from '~/locale';
+
+export const IGNORE_ERRORS = [
+ // Random plugins/extensions
+ 'top.GLOBALS',
+ // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
+ 'originalCreateNotification',
+ 'canvas.contentDocument',
+ 'MyApp_RemoveAllHighlights',
+ 'http://tt.epicplay.com',
+ __("Can't find variable: ZiteReader"),
+ __('jigsaw is not defined'),
+ __('ComboSearch is not defined'),
+ 'http://loading.retry.widdit.com/',
+ 'atomicFindClose',
+ // Facebook borked
+ 'fb_xd_fragment',
+ // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
+ // reduce this. (thanks @acdha)
+ 'bmi_SafeAddOnload',
+ 'EBCallBackMessageReceived',
+ // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
+ 'conduitPage',
+];
+
+export const DENY_URLS = [
+ // Facebook flakiness
+ /graph\.facebook\.com/i,
+ // Facebook blocked
+ /connect\.facebook\.net\/en_US\/all\.js/i,
+ // Woopra flakiness
+ /eatdifferent\.com\.woopra-ns\.com/i,
+ /static\.woopra\.com\/js\/woopra\.js/i,
+ // Chrome extensions
+ /extensions\//i,
+ /^chrome:\/\//i,
+ // Other plugins
+ /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
+ /webappstoolbarba\.texthelp\.com\//i,
+ /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
+];
+
+export const SAMPLE_RATE = 0.95;
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 8f3c4c644bf..4c5b8dbad5a 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,52 +1,11 @@
import * as Sentry from '@sentry/browser';
import $ from 'jquery';
import { __ } from '~/locale';
-
-const IGNORE_ERRORS = [
- // Random plugins/extensions
- 'top.GLOBALS',
- // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
- 'originalCreateNotification',
- 'canvas.contentDocument',
- 'MyApp_RemoveAllHighlights',
- 'http://tt.epicplay.com',
- __("Can't find variable: ZiteReader"),
- __('jigsaw is not defined'),
- __('ComboSearch is not defined'),
- 'http://loading.retry.widdit.com/',
- 'atomicFindClose',
- // Facebook borked
- 'fb_xd_fragment',
- // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
- // reduce this. (thanks @acdha)
- 'bmi_SafeAddOnload',
- 'EBCallBackMessageReceived',
- // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
- 'conduitPage',
-];
-
-const BLACKLIST_URLS = [
- // Facebook flakiness
- /graph\.facebook\.com/i,
- // Facebook blocked
- /connect\.facebook\.net\/en_US\/all\.js/i,
- // Woopra flakiness
- /eatdifferent\.com\.woopra-ns\.com/i,
- /static\.woopra\.com\/js\/woopra\.js/i,
- // Chrome extensions
- /extensions\//i,
- /^chrome:\/\//i,
- // Other plugins
- /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
- /webappstoolbarba\.texthelp\.com\//i,
- /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
-];
-
-const SAMPLE_RATE = 0.95;
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
const SentryConfig = {
IGNORE_ERRORS,
- BLACKLIST_URLS,
+ BLACKLIST_URLS: DENY_URLS,
SAMPLE_RATE,
init(options = {}) {
this.options = options;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index a1195b572f8..ec9441c2b9b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -13,10 +13,6 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
before_action :disable_query_limiting, only: [:usage_data]
- before_action do
- push_frontend_feature_flag(:ci_variable_settings_graphql)
- end
-
feature_category :not_owned, [ # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
:general, :reporting, :metrics_and_profiling, :network,
:preferences, :update, :reset_health_check_token
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index b47facf6c47..2c8b4888d5d 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -26,6 +26,8 @@ class Admin::UsersController < Admin::ApplicationController
end
def show
+ @can_impersonate = can_impersonate_user
+ @impersonation_error_text = @can_impersonate ? nil : impersonation_error_text
end
# rubocop: disable CodeReuse/ActiveRecord
@@ -47,7 +49,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def impersonate
- if can?(user, :log_in) && !user.password_expired? && !impersonation_in_progress?
+ if can_impersonate_user
session[:impersonator_id] = current_user.id
warden.set_user(user, scope: :user)
@@ -59,18 +61,7 @@ class Admin::UsersController < Admin::ApplicationController
redirect_to root_path
else
- flash[:alert] =
- if impersonation_in_progress?
- _("You are already impersonating another user")
- elsif user.blocked?
- _("You cannot impersonate a blocked user")
- elsif user.password_expired?
- _("You cannot impersonate a user with an expired password")
- elsif user.internal?
- _("You cannot impersonate an internal user")
- else
- _("You cannot impersonate a user who cannot log in")
- end
+ flash[:alert] = impersonation_error_text
redirect_to admin_user_path(user)
end
@@ -380,6 +371,24 @@ class Admin::UsersController < Admin::ApplicationController
def log_impersonation_event
Gitlab::AppLogger.info(_("User %{current_user_username} has started impersonating %{username}") % { current_user_username: current_user.username, username: user.username })
end
+
+ def can_impersonate_user
+ can?(user, :log_in) && !user.password_expired? && !impersonation_in_progress?
+ end
+
+ def impersonation_error_text
+ if impersonation_in_progress?
+ _("You are already impersonating another user")
+ elsif user.blocked?
+ _("You cannot impersonate a blocked user")
+ elsif user.password_expired?
+ _("You cannot impersonate a user with an expired password")
+ elsif user.internal?
+ _("You cannot impersonate an internal user")
+ else
+ _("You cannot impersonate a user who cannot log in")
+ end
+ end
end
Admin::UsersController.prepend_mod_with('Admin::UsersController')
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index e164a834519..b1afac1f1c7 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -10,9 +10,6 @@ module Groups
before_action :define_variables, only: [:show]
before_action :push_licensed_features, only: [:show]
before_action :assign_variables_to_gon, only: [:show]
- before_action do
- push_frontend_feature_flag(:ci_variable_settings_graphql, @group)
- end
feature_category :continuous_integration
urgency :low
diff --git a/app/controllers/groups/settings/packages_and_registries_controller.rb b/app/controllers/groups/settings/packages_and_registries_controller.rb
index 411b8577c3f..ec4a0b312ee 100644
--- a/app/controllers/groups/settings/packages_and_registries_controller.rb
+++ b/app/controllers/groups/settings/packages_and_registries_controller.rb
@@ -7,6 +7,10 @@ module Groups
before_action :authorize_admin_group!
before_action :verify_packages_enabled!
+ before_action do
+ push_frontend_feature_flag(:maven_central_request_forwarding, group)
+ end
+
feature_category :package_registry
urgency :low
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index bf231bf012d..8aef1c3d24d 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -11,9 +11,6 @@ module Projects
before_action :authorize_admin_pipeline!
before_action :check_builds_available!
before_action :define_variables
- before_action do
- push_frontend_feature_flag(:ci_variable_settings_graphql, @project)
- end
helper_method :highlight_badge
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index 9e42aeea9ce..963f0b7afc4 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -40,7 +40,7 @@ module FormHelper
end
def dropdown_max_select(data, feature_flag)
- return data[:'max-select'] unless Feature.enabled?(feature_flag)
+ return data[:'max-select'] unless feature_flag.nil? || Feature.enabled?(feature_flag)
if data[:'max-select'] && data[:'max-select'] < ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
data[:'max-select']
@@ -162,12 +162,7 @@ module FormHelper
new_options[:title] = _('Select assignee(s)')
new_options[:data][:'dropdown-header'] = 'Assignee(s)'
-
- if Feature.enabled?(:limit_assignees_per_issuable)
- new_options[:data][:'max-select'] = ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
- else
- new_options[:data].delete(:'max-select')
- end
+ new_options[:data][:'max-select'] = ::Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
new_options
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 7de0011e91b..b8ac2afa7d6 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -419,7 +419,7 @@ module SearchHelper
result = { label: label, scope: scope_name, data: data, link: search_path(search_params), active: active_scope }
if active_scope
- result[:count] = !@timeout ? @search_results.formatted_count(scope_name) : 0
+ result[:count] = !@timeout ? @search_results.formatted_count(scope_name) : "0"
end
result[:count_link] = search_count_path(search_params) unless active_scope
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index d75f81e2839..adbbddd635c 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -678,6 +678,8 @@ class ApplicationSetting < ApplicationRecord
attr_encrypted :arkose_labs_private_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
attr_encrypted :cube_api_key, encryption_options_base_32_aes_256_gcm
attr_encrypted :jitsu_administrator_password, encryption_options_base_32_aes_256_gcm
+ attr_encrypted :telesign_customer_xid, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
+ attr_encrypted :telesign_api_key, encryption_options_base_32_aes_256_gcm.merge(encode: false, encode_iv: false)
validates :disable_feed_token,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 14cb6659d03..31b2a8d7cc1 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -240,7 +240,6 @@ module Issuable
end
def validate_assignee_size_length
- return true unless Feature.enabled?(:limit_assignees_per_issuable)
return true unless assignees.size > MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS
errors.add :assignees,
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index bef68586c66..51c39ad4ec3 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -91,6 +91,7 @@ class Namespace < ApplicationRecord
validates :name,
presence: true,
length: { maximum: 255 }
+ validates :name, uniqueness: { scope: [:type, :parent_id] }, if: -> { parent_id.present? }
validates :description, length: { maximum: 255 }
diff --git a/app/models/user.rb b/app/models/user.rb
index 1858c134484..24f947183a2 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -425,10 +425,6 @@ class User < ApplicationRecord
end
# rubocop: disable CodeReuse/ServiceClass
- # Ideally we should not call a service object here but user.block
- # is also called by Users::MigrateToGhostUserService which references
- # this state transition object in order to do a rollback.
- # For this reason the tradeoff is to disable this cop.
after_transition any => :blocked do |user|
user.run_after_commit do
Ci::DropPipelineService.new.execute_async_for_all(user.pipelines, :user_blocked, user)
diff --git a/app/services/merge_requests/update_assignees_service.rb b/app/services/merge_requests/update_assignees_service.rb
index 79a3e9f3c22..d45d55cbebc 100644
--- a/app/services/merge_requests/update_assignees_service.rb
+++ b/app/services/merge_requests/update_assignees_service.rb
@@ -19,16 +19,9 @@ module MergeRequests
attrs = update_attrs.merge(assignee_ids: new_ids)
- # We now have assignees validation on merge request
- # If we use an update with bang, it will explode,
- # instead we need to check if its valid then return if its not valid.
- if Feature.enabled?(:limit_assignees_per_issuable)
- merge_request.update(**attrs)
-
- return merge_request unless merge_request.valid?
- else
- merge_request.update!(**attrs)
- end
+ merge_request.update(**attrs)
+
+ return merge_request unless merge_request.valid?
# Defer the more expensive operations (handle_assignee_changes) to the background
MergeRequests::HandleAssigneesChangeService
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 1aaf7fb769a..555d60dc291 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -137,8 +137,6 @@ module Notes
end
def invalid_assignees?(update_params)
- return false unless Feature.enabled?(:limit_assignees_per_issuable)
-
if update_params.key?(:assignee_ids)
possible_assignees = update_params[:assignee_ids]&.uniq&.size
diff --git a/app/services/users/migrate_to_ghost_user_service.rb b/app/services/users/migrate_to_ghost_user_service.rb
deleted file mode 100644
index 3eb220c0e40..00000000000
--- a/app/services/users/migrate_to_ghost_user_service.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-# frozen_string_literal: true
-
-# When a user is destroyed, some of their associated records are
-# moved to a "Ghost User", to prevent these associated records from
-# being destroyed.
-#
-# For example, all the issues/MRs a user has created are _not_ destroyed
-# when the user is destroyed.
-module Users
- class MigrateToGhostUserService
- extend ActiveSupport::Concern
-
- attr_reader :ghost_user, :user, :hard_delete
-
- def initialize(user)
- @user = user
- @ghost_user = User.ghost
- end
-
- # If an admin attempts to hard delete a user, in some cases associated
- # records may have a NOT NULL constraint on the user ID that prevent that record
- # from being destroyed. In such situations we must assign the record to the ghost user.
- # Passing in `hard_delete: true` will ensure these records get assigned to
- # the ghost user before the user is destroyed. Other associated records will be destroyed.
- # letting the other associated records be destroyed.
- def execute(hard_delete: false)
- @hard_delete = hard_delete
- transition = user.block_transition
-
- # Block the user before moving records to prevent a data race.
- # For example, if the user creates an issue after `migrate_issues`
- # runs and before the user is destroyed, the destroy will fail with
- # an exception.
- user.block
-
- begin
- user.transaction do
- migrate_records
- end
- rescue Exception # rubocop:disable Lint/RescueException
- # Reverse the user block if record migration fails
- if transition
- transition.rollback
- user.save!
- end
-
- raise
- end
-
- user.reset
- end
-
- private
-
- def migrate_records
- return if hard_delete
-
- migrate_issues
- migrate_merge_requests
- migrate_notes
- migrate_abuse_reports
- migrate_award_emoji
- migrate_snippets
- migrate_reviews
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def migrate_issues
- batched_migrate(Issue, :author_id)
- batched_migrate(Issue, :last_edited_by_id)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- # rubocop: disable CodeReuse/ActiveRecord
- def migrate_merge_requests
- batched_migrate(MergeRequest, :author_id)
- batched_migrate(MergeRequest, :merge_user_id)
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def migrate_notes
- batched_migrate(Note, :author_id)
- end
-
- def migrate_abuse_reports
- user.reported_abuse_reports.update_all(reporter_id: ghost_user.id)
- end
-
- def migrate_award_emoji
- user.award_emoji.update_all(user_id: ghost_user.id)
- end
-
- def migrate_snippets
- snippets = user.snippets.only_project_snippets
- snippets.update_all(author_id: ghost_user.id)
- end
-
- def migrate_reviews
- batched_migrate(Review, :author_id)
- end
-
- # rubocop:disable CodeReuse/ActiveRecord
- def batched_migrate(base_scope, column, batch_size: 50)
- loop do
- update_count = base_scope.where(column => user.id).limit(batch_size).update_all(column => ghost_user.id)
- break if update_count == 0
- end
- end
- # rubocop:enable CodeReuse/ActiveRecord
- end
-end
-
-Users::MigrateToGhostUserService.prepend_mod_with('Users::MigrateToGhostUserService')
diff --git a/app/views/admin/application_settings/_package_registry.html.haml b/app/views/admin/application_settings/_package_registry.html.haml
index 3506038ca68..66b04006beb 100644
--- a/app/views/admin/application_settings/_package_registry.html.haml
+++ b/app/views/admin/application_settings/_package_registry.html.haml
@@ -6,7 +6,7 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _("Control how the GitLab Package Registry functions.")
+ = s_('PackageRegistry|Configure package forwarding and package file size limits.')
= render_if_exists 'admin/application_settings/ee_package_registry'
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 0ceff211806..1fa7c9c8651 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -30,9 +30,11 @@
.gl-p-2
#js-admin-user-actions{ data: admin_user_actions_data_attributes(@user) }
- if @user != current_user
- - if impersonation_enabled? && @user.can?(:log_in)
+ - if impersonation_enabled?
.gl-p-2
- = link_to _('Impersonate'), impersonate_admin_user_path(@user), method: :post, class: "btn btn-default gl-button", data: { qa_selector: 'impersonate_user_link' }
+ %span.btn-group{ class: !@can_impersonate ? 'has-tooltip' : nil, title: @impersonation_error_text }
+ = render Pajamas::ButtonComponent.new(disabled: !@can_impersonate, method: :post, href: impersonate_admin_user_path(@user), button_options: { data: { qa_selector: 'impersonate_user_link', testid: 'impersonate_user_link' } }) do
+ = _('Impersonate')
- if can_force_email_confirmation?(@user)
.gl-p-2
= render Pajamas::ButtonComponent.new(variant: :default, button_options: { class: 'js-confirm-modal-button', data: confirm_user_data(@user) }) do
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 9ca11b35064..08865abbe86 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -10,7 +10,7 @@
- is_group = !@group.nil?
- is_project = !@project.nil?
-#js-ci-project-variables{ data: { endpoint: save_endpoint,
+#js-ci-variables{ data: { endpoint: save_endpoint,
is_project: is_project.to_s,
project_id: @project&.id || '',
project_full_path: @project&.full_path || '',
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 82276938d45..475bc9e1c20 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -13,16 +13,10 @@
#js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } }
- if can? current_user, :push_code, @project
- = link_to project_merged_branches_path(@project),
- class: 'gl-button btn btn-danger btn-danger-secondary has-tooltip',
- title: s_("Branches|Delete all branches that are merged into '%{default_branch}'") % { default_branch: @project.repository.root_ref },
- method: :delete,
- aria: { label: s_('Branches|Delete merged branches') },
- data: { confirm: s_('Branches|Deleting the merged branches cannot be undone. Are you sure?'),
- confirm_btn_variant: 'danger',
- container: 'body',
- qa_selector: 'delete_merged_branches_link' } do
- = s_('Branches|Delete merged branches')
+ .js-delete-merged-branches{ data: {
+ default_branch: @project.repository.root_ref,
+ form_path: project_merged_branches_path(@project) }
+ }
= link_to new_project_branch_path(@project), class: 'gl-button btn btn-confirm' do
= s_('Branches|New branch')
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index 54aa9aad8a5..c15afd7bd5b 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -5,7 +5,7 @@
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
- = gl_tabs_nav({ class: 'search-filter scrolling-tabs nav-links'}) do
+ = gl_tabs_nav({ class: 'scrolling-tabs nav-links', data: { testid: 'search-filter' } }) do
- if @project
- if project_search_tabs?(:blobs)
= search_filter_link 'blobs', _("Code"), data: { qa_selector: 'code_tab' }
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index ea2ea92dfce..027ae6bf77c 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -7,7 +7,6 @@
.gl-w-full.gl-flex-grow-1.gl-overflow-x-hidden
= render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
= render partial: 'search/results_list'
-
- else
= render partial: 'search/results_status', locals: { search_service: @search_service } unless @search_objects.to_a.empty?
diff --git a/app/views/search/_results_status.html.haml b/app/views/search/_results_status.html.haml
index e6bb0c18b90..adea6b598f7 100644
--- a/app/views/search/_results_status.html.haml
+++ b/app/views/search/_results_status.html.haml
@@ -3,7 +3,6 @@
- return unless search_service.show_results_status?
- if Feature.enabled?(:search_page_vertical_nav, current_user)
- = render partial: 'search/results_status_vert_nav', locals: { search_service: @search_service }
-
+ = render partial: 'search/results_status_vert_nav', locals: { search_service: search_service }
- else
- = render partial: 'search/results_status_horiz_nav', locals: { search_service: @search_service }
+ = render partial: 'search/results_status_horiz_nav', locals: { search_service: search_service }
diff --git a/app/views/shared/issuable/_sidebar_assignees.html.haml b/app/views/shared/issuable/_sidebar_assignees.html.haml
index b8a9a82d9b2..8ca30d7ca97 100644
--- a/app/views/shared/issuable/_sidebar_assignees.html.haml
+++ b/app/views/shared/issuable/_sidebar_assignees.html.haml
@@ -39,7 +39,7 @@
- data[:multi_select] = true
- data['dropdown-title'] = title
- data['dropdown-header'] = dropdown_options[:data][:'dropdown-header']
- - data['max-select'] = dropdown_max_select(dropdown_options[:data], :limit_assignees_per_issuable)
+ - data['max-select'] = dropdown_max_select(dropdown_options[:data], nil)
- options[:data].merge!(data)
= render 'shared/issuable/sidebar_user_dropdown',
diff --git a/config/feature_flags/development/ci_variable_settings_graphql.yml b/config/feature_flags/development/ci_variable_settings_graphql.yml
deleted file mode 100644
index 0af109968ab..00000000000
--- a/config/feature_flags/development/ci_variable_settings_graphql.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_variable_settings_graphql
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89332
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/364423
-milestone: '15.1'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/enable_old_sentry_clientside_integration.yml b/config/feature_flags/development/enable_old_sentry_clientside_integration.yml
deleted file mode 100644
index 0507d7cc559..00000000000
--- a/config/feature_flags/development/enable_old_sentry_clientside_integration.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: enable_old_sentry_clientside_integration
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/102650
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/344832
-milestone: '15.6'
-type: development
-group: group::runner
-default_enabled: true
diff --git a/config/feature_flags/development/index_user_callback.yml b/config/feature_flags/development/index_user_callback.yml
deleted file mode 100644
index c9a1f175547..00000000000
--- a/config/feature_flags/development/index_user_callback.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: index_user_callback
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/101326
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/378364
-milestone: '15.6'
-type: development
-group: group::global search
-default_enabled: false
diff --git a/config/feature_flags/development/limit_assignees_per_issuable.yml b/config/feature_flags/development/limit_assignees_per_issuable.yml
deleted file mode 100644
index d950b8c2f09..00000000000
--- a/config/feature_flags/development/limit_assignees_per_issuable.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: limit_assignees_per_issuable
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/95673
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/373237
-milestone: '15.5'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/open_api.yml b/config/open_api.yml
index ddf3d23dfc9..6e767f51ef8 100644
--- a/config/open_api.yml
+++ b/config/open_api.yml
@@ -69,6 +69,8 @@ metadata:
description: Operations related to group packages
- name: integrations
description: Operations related to integrations
+ - name: issue_links
+ description: Operations related to issue links
- name: merge_requests
description: Operations related to merge requests
- name: metadata
diff --git a/db/migrate/20221108015813_add_telesign_to_application_settings.rb b/db/migrate/20221108015813_add_telesign_to_application_settings.rb
new file mode 100644
index 00000000000..f8e4fb5340b
--- /dev/null
+++ b/db/migrate/20221108015813_add_telesign_to_application_settings.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class AddTelesignToApplicationSettings < Gitlab::Database::Migration[2.0]
+ def change
+ add_column :application_settings, :encrypted_telesign_customer_xid, :binary
+ add_column :application_settings, :encrypted_telesign_customer_xid_iv, :binary
+
+ add_column :application_settings, :encrypted_telesign_api_key, :binary
+ add_column :application_settings, :encrypted_telesign_api_key_iv, :binary
+ end
+end
diff --git a/db/post_migrate/20221102090940_create_next_ci_partitions_record.rb b/db/post_migrate/20221102090940_create_next_ci_partitions_record.rb
new file mode 100644
index 00000000000..4bd89a70daa
--- /dev/null
+++ b/db/post_migrate/20221102090940_create_next_ci_partitions_record.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+class CreateNextCiPartitionsRecord < Gitlab::Database::Migration[2.0]
+ NEXT_PARTITION_ID = 101
+
+ disable_ddl_transaction!
+ restrict_gitlab_migration gitlab_schema: :gitlab_ci
+
+ def up
+ return unless Gitlab.com?
+
+ execute(<<~SQL)
+ INSERT INTO "ci_partitions" ("id", "created_at", "updated_at")
+ VALUES (#{NEXT_PARTITION_ID}, now(), now())
+ ON CONFLICT DO NOTHING;
+ SQL
+
+ reset_pk_sequence!('ci_partitions')
+ end
+
+ def down
+ return unless Gitlab.com?
+
+ execute(<<~SQL)
+ DELETE FROM "ci_partitions"
+ WHERE "ci_partitions"."id" = #{NEXT_PARTITION_ID};
+ SQL
+ end
+end
diff --git a/db/post_migrate/20221102090943_create_second_partition_for_builds_metadata.rb b/db/post_migrate/20221102090943_create_second_partition_for_builds_metadata.rb
new file mode 100644
index 00000000000..6923e6f6cba
--- /dev/null
+++ b/db/post_migrate/20221102090943_create_second_partition_for_builds_metadata.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+class CreateSecondPartitionForBuildsMetadata < Gitlab::Database::Migration[2.0]
+ TABLE_NAME = 'p_ci_builds_metadata'
+ BUILDS_TABLE = 'ci_builds'
+ NEXT_PARTITION_ID = 101
+ PARTITION_NAME = 'gitlab_partitions_dynamic.ci_builds_metadata_101'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab.com?
+
+ with_lock_retries(**lock_args) do
+ connection.execute(<<~SQL)
+ LOCK TABLE #{BUILDS_TABLE} IN SHARE UPDATE EXCLUSIVE MODE;
+ LOCK TABLE ONLY #{TABLE_NAME} IN ACCESS EXCLUSIVE MODE;
+ SQL
+
+ connection.execute(<<~SQL)
+ CREATE TABLE IF NOT EXISTS #{PARTITION_NAME}
+ PARTITION OF #{TABLE_NAME}
+ FOR VALUES IN (#{NEXT_PARTITION_ID});
+ SQL
+ end
+ end
+
+ def down
+ return unless Gitlab.com?
+ return unless table_exists?(PARTITION_NAME)
+
+ with_lock_retries(**lock_args) do
+ connection.execute(<<~SQL)
+ LOCK TABLE #{BUILDS_TABLE}, #{TABLE_NAME}, #{PARTITION_NAME} IN ACCESS EXCLUSIVE MODE;
+ SQL
+
+ connection.execute(<<~SQL)
+ ALTER TABLE #{TABLE_NAME} DETACH PARTITION #{PARTITION_NAME};
+ SQL
+
+ connection.execute(<<~SQL)
+ DROP TABLE IF EXISTS #{PARTITION_NAME} CASCADE;
+ SQL
+ end
+ end
+
+ private
+
+ def lock_args
+ {
+ raise_on_exhaustion: true,
+ timing_configuration: lock_timing_configuration
+ }
+ end
+
+ def lock_timing_configuration
+ iterations = Gitlab::Database::WithLockRetries::DEFAULT_TIMING_CONFIGURATION
+ aggressive_iterations = Array.new(5) { [10.seconds, 1.minute] }
+
+ iterations + aggressive_iterations
+ end
+end
diff --git a/db/schema_migrations/20221102090940 b/db/schema_migrations/20221102090940
new file mode 100644
index 00000000000..c0ef7881688
--- /dev/null
+++ b/db/schema_migrations/20221102090940
@@ -0,0 +1 @@
+3be66e9f4239eb75f14118d1fd795f1a1bcd2d6bc4e34fe58a0c8422e33c893a \ No newline at end of file
diff --git a/db/schema_migrations/20221102090943 b/db/schema_migrations/20221102090943
new file mode 100644
index 00000000000..bc7ff679c6e
--- /dev/null
+++ b/db/schema_migrations/20221102090943
@@ -0,0 +1 @@
+8e907e086c4b23dd08163c4d946ec4a0202288f7da08eff565a159bccdd445f2 \ No newline at end of file
diff --git a/db/schema_migrations/20221108015813 b/db/schema_migrations/20221108015813
new file mode 100644
index 00000000000..39263419da6
--- /dev/null
+++ b/db/schema_migrations/20221108015813
@@ -0,0 +1 @@
+d6b24d6346bd9b32dd726d61048e7eea791d02016b9b4c3a8cb561b2430e1fdb \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index d1901a84f5a..350ac2ad454 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11525,6 +11525,10 @@ CREATE TABLE application_settings (
disable_admin_oauth_scopes boolean DEFAULT false NOT NULL,
default_preferred_language text DEFAULT 'en'::text NOT NULL,
disable_download_button boolean DEFAULT false NOT NULL,
+ encrypted_telesign_customer_xid bytea,
+ encrypted_telesign_customer_xid_iv bytea,
+ encrypted_telesign_api_key bytea,
+ encrypted_telesign_api_key_iv bytea,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_container_registry_pre_import_tags_rate_positive CHECK ((container_registry_pre_import_tags_rate >= (0)::numeric)),
CONSTRAINT app_settings_dep_proxy_ttl_policies_worker_capacity_positive CHECK ((dependency_proxy_ttl_group_policy_worker_capacity >= 0)),
diff --git a/doc/administration/reference_architectures/3k_users.md b/doc/administration/reference_architectures/3k_users.md
index 74074f0803b..4fc6af3f72e 100644
--- a/doc/administration/reference_architectures/3k_users.md
+++ b/doc/administration/reference_architectures/3k_users.md
@@ -2269,7 +2269,6 @@ but with smaller performance requirements, several modifications can be consider
- PostgreSQL and PgBouncer: PgBouncer nodes could be removed and instead be enabled on PostgreSQL nodes with the Internal Load Balancer pointing to them. However, to enable [Database Load Balancing](../postgresql/database_load_balancing.md), a separate PgBouncer array is still required.
- Reducing the node counts: Some node types do not need consensus and can run with fewer nodes (but more than one for redundancy). This will also lead to reduced performance.
- GitLab Rails and Sidekiq: Stateless services don't have a minimum node count. Two are enough for redundancy.
- - Gitaly and Praefect: A quorum is not strictly necessary. Two Gitaly nodes and two Praefect nodes are enough for redundancy.
- PostgreSQL and PgBouncer: A quorum is not strictly necessary. Two PostgreSQL nodes and two PgBouncer nodes are enough for redundancy.
- Running select components in reputable Cloud PaaS solutions: Select components of the GitLab setup can instead be run on Cloud Provider PaaS solutions. By doing this, additional dependent components can also be removed:
- PostgreSQL: Can be run on reputable Cloud PaaS solutions such as Google Cloud SQL or Amazon RDS. In this setup, the PgBouncer and Consul nodes are no longer required:
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 529b0802991..5764c876e4d 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -906,9 +906,14 @@ end
Table **has records** but **no foreign keys**:
-- First release: Remove the application code related to the table, such as models,
-controllers and services.
-- Second release: Use the `drop_table` method in your migration.
+- Remove the application code related to the table, such as models,
+ controllers and services.
+- In a post-deployment migration, use `drop_table`.
+
+This can all be in a single migration if you're sure the code is not used.
+If you want to reduce risk slightly, consider putting the migrations into a
+second merge request after the application changes are merged. This approach
+provides an opportunity to roll back.
```ruby
def up
@@ -922,12 +927,16 @@ end
Table **has foreign keys**:
-- First release: Remove the application code related to the table, such as models,
-controllers, and services.
-- Second release: Remove the foreign keys using the `with_lock_retries`
-helper method. Use `drop_table` in another migration file.
+- Remove the application code related to the table, such as models,
+ controllers, and services.
+- In a post-deployment migration, remove the foreign keys using the
+ `with_lock_retries` helper method. In another subsequent post-deployment
+ migration, use `drop_table`.
-**Migrations for the second release:**
+This can all be in a single migration if you're sure the code is not used.
+If you want to reduce risk slightly, consider putting the migrations into a
+second merge request after the application changes are merged. This approach
+provides an opportunity to roll back.
Removing the foreign key on the `projects` table:
diff --git a/doc/development/shell_commands.md b/doc/development/shell_commands.md
index 3935e98199a..d78a005d76b 100644
--- a/doc/development/shell_commands.md
+++ b/doc/development/shell_commands.md
@@ -71,6 +71,8 @@ FileUtils.touch myfile
This coding style could have prevented CVE-2013-4546.
+See also <https://gitlab.com/gitlab-org/gitlab/-/merge_requests/93030>, and <https://starlabs.sg/blog/2022/07-gitlab-project-import-rce-analysis-cve-2022-2185/> for another example.
+
## Separate options from arguments with --
Make the difference between options and arguments clear to the argument parsers of system commands with `--`. This is supported by many but not all Unix commands.
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index aa7344f8506..b6bf3c7805a 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -933,9 +933,7 @@ In most specs, the Rails cache is actually an in-memory store. This is replaced
between specs, so calls to `Rails.cache.read` and `Rails.cache.write` are safe.
However, if a spec makes direct Redis calls, it should mark itself with the
`:clean_gitlab_redis_cache`, `:clean_gitlab_redis_shared_state` or
-`:clean_gitlab_redis_queues` traits as appropriate. To avoid triggering rate
-limiting in specs, mark the spec with the `:clean_gitlab_redis_rate_limiting`
-trait.
+`:clean_gitlab_redis_queues` traits as appropriate.
#### Background jobs / Sidekiq
@@ -969,6 +967,14 @@ it "really connects to Prometheus", :permit_dns do
And if you need more specific control, the DNS blocking is implemented in
`spec/support/helpers/dns_helpers.rb` and these methods can be called elsewhere.
+#### Rate Limiting
+
+[Rate limiting](../../security/rate_limits.md) is enabled in the test suite. Rate limits
+may be triggered in feature specs that use the `:js` trait. In most cases, triggering rate
+limiting can be avoided by marking the spec with the `:clean_gitlab_redis_rate_limiting`
+trait. This trait clears the rate limiting data stored in Redis cache between specs. If
+a single test triggers the rate limit, the `:disable_rate_limit` can be used instead.
+
#### Stubbing File methods
In the situations where you need to
diff --git a/doc/update/index.md b/doc/update/index.md
index a49ad5bc4ce..dbac4304897 100644
--- a/doc/update/index.md
+++ b/doc/update/index.md
@@ -475,11 +475,28 @@ and [Helm Chart deployments](https://docs.gitlab.com/charts/). They come with ap
- Git 2.37.0 and later is required by Gitaly. For installations from source, we recommend you use the [Git version provided by Gitaly](../install/installation.md#git).
+### 15.5.0
+
+- GitLab 15.4.0 introduced a default [Sidekiq routing rule](../administration/sidekiq/extra_sidekiq_routing.md) that routes all jobs to the `default` queue. For instances using [queue selectors](../administration/sidekiq/extra_sidekiq_processes.md#queue-selector), this will cause [performance problems](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1991) as some Sidekiq processes will be idle.
+ - The default routing rule has been reverted in 15.5.4, so upgrading to that version or later will return to the previous behavior.
+ - If a GitLab instance now listens only to the `default` queue (which is not currently recommended), it will be required to add this routing rule back in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ sidekiq['routing_rules'] = [['*', 'default']]
+ ```
+
### 15.4.0
- GitLab 15.4.0 includes a [batched background migration](#batched-background-migrations) to [remove incorrect values from `expire_at` in `ci_job_artifacts` table](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/89318).
This migration might take hours or days to complete on larger GitLab instances.
- By default, Gitaly and Praefect nodes use the time server at `pool.ntp.org`. If your instance can not connect to `pool.ntp.org`, [configure the `NTP_HOST` variable](../administration/gitaly/praefect.md#customize-time-server-setting).
+- GitLab 15.4.0 introduced a default [Sidekiq routing rule](../administration/sidekiq/extra_sidekiq_routing.md) that routes all jobs to the `default` queue. For instances using [queue selectors](../administration/sidekiq/extra_sidekiq_processes.md#queue-selector), this will cause [performance problems](https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1991) as some Sidekiq processes will be idle.
+ - The default routing rule has been reverted in 15.4.5, so upgrading to that version or later will return to the previous behavior.
+ - If a GitLab instance now listens only to the `default` queue (which is not currently recommended), it will be required to add this routing rule back in `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ sidekiq['routing_rules'] = [['*', 'default']]
+ ```
### 15.3.3
diff --git a/doc/user/analytics/dora_metrics.md b/doc/user/analytics/dora_metrics.md
index a85cd25f712..b5f37203817 100644
--- a/doc/user/analytics/dora_metrics.md
+++ b/doc/user/analytics/dora_metrics.md
@@ -108,6 +108,13 @@ Custom charts to visualize DORA data with Insights YAML-based reports.
With this new visualization, software leaders can track metrics improvements, understand patterns in their metrics trends, and compare performance between groups and projects.
+### Measure DORA metrics without using GitLab CI/CD pipelines
+
+Deployment frequency is calculated based on the deployments record, which is created for typical push-based deployments.
+These deployment records are not created for pull-based deployments, for example when Container Images are connected to GitLab with an agent.
+
+To track DORA metrics in these cases, you can [create a deployment record](../../api/deployments.md#create-a-deployment) using the Deployments API.
+
### Supported DORA metrics in GitLab
| Metric | Level | API | UI chart | Comments |
diff --git a/doc/user/application_security/policies/scan-result-policies.md b/doc/user/application_security/policies/scan-result-policies.md
index 6d6c8a03d55..7482df18cc3 100644
--- a/doc/user/application_security/policies/scan-result-policies.md
+++ b/doc/user/application_security/policies/scan-result-policies.md
@@ -22,7 +22,8 @@ job is fully executed. The following video gives you an overview of GitLab scan
## Scan result policy editor
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77814) in GitLab 14.8.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77814) in GitLab 14.8.
+> - [Enabled by default](https://gitlab.com/gitlab-org/gitlab/-/issues/369473) in GitLab 15.6.
NOTE:
Only project Owners have the [permissions](../../permissions.md#project-members-permissions)
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c44047d74b3..b6f3ba1cfdd 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -1160,17 +1160,15 @@ These details <em>remain</em> <strong>hidden</strong> until expanded.
Markdown inside these tags is also supported.
-NOTE:
-If your Markdown isn't rendering correctly, try adding
-`{::options parse_block_html="true" /}` to the top of the page, and add
-`markdown="span"` to the opening summary tag like this: `<summary markdown="span">`.
-
-Remember to leave a blank line after the `</summary>` tag and before the `</details>` tag,
-as shown in the example:
+Remember to leave a blank line before and after any Markdown sections, as shown in the example:
````html
<details>
-<summary>Click this to collapse/fold.</summary>
+<summary>
+
+Click this to _collapse/fold._
+
+</summary>
These details _remain_ **hidden** until expanded.
@@ -1187,7 +1185,7 @@ works correctly in GitLab.
-->
<details>
-<summary>Click this to collapse/fold.</summary>
+<summary>Click this to <em>collapse/fold.</em></summary>
These details <em>remain</em> <b>hidden</b> until expanded.
diff --git a/doc/user/project/merge_requests/img/conflict_ui_v15_6.png b/doc/user/project/merge_requests/img/conflict_ui_v15_6.png
index baa1cda3104..d5d5ad14edb 100644
--- a/doc/user/project/merge_requests/img/conflict_ui_v15_6.png
+++ b/doc/user/project/merge_requests/img/conflict_ui_v15_6.png
Binary files differ
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 94dfb7f598c..ffb0cdf8991 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -218,6 +218,7 @@ module API
mount ::API::ImportGithub
mount ::API::Integrations
mount ::API::Invitations
+ mount ::API::IssueLinks
mount ::API::Keys
mount ::API::Lint
mount ::API::Markdown
@@ -291,7 +292,6 @@ module API
mount ::API::Groups
mount ::API::HelmPackages
mount ::API::Integrations::JiraConnect::Subscriptions
- mount ::API::IssueLinks
mount ::API::Issues
mount ::API::Labels
mount ::API::MavenPackages
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index a7418deb88a..845e42c2ed8 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -14,7 +14,7 @@ module API
before do
require_repository_enabled!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
end
rescue_from Gitlab::Git::Repository::NoRepository do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 3b122fb23a2..63a13b83a9b 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -9,7 +9,7 @@ module API
before do
require_repository_enabled!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
verify_pagination_params!
end
diff --git a/lib/api/entities/basic_project_details.rb b/lib/api/entities/basic_project_details.rb
index f8f5b59cdc1..2585b2d0b6d 100644
--- a/lib/api/entities/basic_project_details.rb
+++ b/lib/api/entities/basic_project_details.rb
@@ -6,7 +6,7 @@ module API
include ::API::ProjectsRelationBuilder
include Gitlab::Utils::StrongMemoize
- expose :default_branch_or_main, documentation: { type: 'string', example: 'main' }, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ expose :default_branch_or_main, documentation: { type: 'string', example: 'main' }, as: :default_branch, if: -> (project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
# Avoids an N+1 query: https://github.com/mbleigh/acts-as-taggable-on/issues/91#issuecomment-168273770
expose :topic_names, as: :tag_list, documentation: { type: 'string', is_array: true, example: 'tag' }
diff --git a/lib/api/entities/issuable_entity.rb b/lib/api/entities/issuable_entity.rb
index e2c674c0b8b..4e70f945a48 100644
--- a/lib/api/entities/issuable_entity.rb
+++ b/lib/api/entities/issuable_entity.rb
@@ -3,10 +3,16 @@
module API
module Entities
class IssuableEntity < Grape::Entity
- expose :id, :iid
- expose(:project_id) { |entity| entity&.project.try(:id) }
- expose :title, :description
- expose :state, :created_at, :updated_at
+ expose :id, documentation: { type: 'integer', example: 84 }
+ expose :iid, documentation: { type: 'integer', example: 14 }
+ expose :project_id, documentation: { type: 'integer', example: 4 } do |entity|
+ entity&.project.try(:id)
+ end
+ expose :title, documentation: { type: 'string', example: 'Impedit et ut et dolores vero provident ullam est' }
+ expose :description, documentation: { type: 'string', example: 'Repellendus impedit et vel velit dignissimos.' }
+ expose :state, documentation: { type: 'string', example: 'closed' }
+ expose :created_at, documentation: { type: 'dateTime', example: '2022-08-17T12:46:35.053Z' }
+ expose :updated_at, documentation: { type: 'dateTime', example: '2022-11-14T17:22:01.470Z' }
def presented
lazy_issuable_metadata
diff --git a/lib/api/entities/issue_basic.rb b/lib/api/entities/issue_basic.rb
index 20f66c026e6..89fb8bbe1c0 100644
--- a/lib/api/entities/issue_basic.rb
+++ b/lib/api/entities/issue_basic.rb
@@ -7,10 +7,10 @@ module API
item.upcase if item.respond_to?(:upcase)
end
- expose :closed_at
+ expose :closed_at, documentation: { type: 'dateTime', example: '2022-11-15T08:30:55.232Z' }
expose :closed_by, using: Entities::UserBasic
- expose :labels do |issue, options|
+ expose :labels, documentation: { type: 'string', is_array: true, example: 'bug' } do |issue, options|
if options[:with_labels_details]
::API::Entities::LabelBasic.represent(issue.labels.sort_by(&:title))
else
@@ -23,7 +23,7 @@ module API
expose :issue_type,
as: :type,
format_with: :upcase,
- documentation: { type: "String", desc: "One of #{::WorkItems::Type.allowed_types_for_issues.map(&:upcase)}" }
+ documentation: { type: 'String', example: 'ISSUE', desc: "One of #{::WorkItems::Type.allowed_types_for_issues.map(&:upcase)}" }
expose :assignee, using: ::API::Entities::UserBasic do |issue|
issue.assignees.first
@@ -33,12 +33,12 @@ module API
expose(:merge_requests_count) { |issue, options| issuable_metadata.merge_requests_count }
expose(:upvotes) { |issue, options| issuable_metadata.upvotes }
expose(:downvotes) { |issue, options| issuable_metadata.downvotes }
- expose :due_date
- expose :confidential
- expose :discussion_locked
- expose :issue_type
+ expose :due_date, documentation: { type: 'date', example: '2022-11-20' }
+ expose :confidential, documentation: { type: 'boolean' }
+ expose :discussion_locked, documentation: { type: 'boolean' }
+ expose :issue_type, documentation: { type: 'string', example: 'issue' }
- expose :web_url do |issue|
+ expose :web_url, documentation: { type: 'string', example: 'http://example.com/example/example/issues/14' } do |issue|
Gitlab::UrlBuilder.build(issue)
end
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index 947b0a3c0c1..1c1bafbf161 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -114,7 +114,7 @@ module API
end
expose :build_timeout, documentation: { type: 'integer', example: 3600 }
expose :auto_cancel_pending_pipelines, documentation: { type: 'string', example: 'enabled' }
- expose :ci_config_path, documentation: { type: 'string', example: '' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :download_code, project) }
+ expose :ci_config_path, documentation: { type: 'string', example: '' }, if: -> (project, options) { Ability.allowed?(options[:current_user], :read_code, project) }
expose :shared_with_groups, documentation: { is_array: true } do |project, options|
user = options[:current_user]
diff --git a/lib/api/entities/release.rb b/lib/api/entities/release.rb
index 5feb1edbff6..c1a48a46d64 100644
--- a/lib/api/entities/release.rb
+++ b/lib/api/entities/release.rb
@@ -9,7 +9,7 @@ module API
MarkupHelper.markdown_field(entity, :description, current_user: options[:current_user])
end
expose :author, using: Entities::UserBasic, if: -> (release, _) { release.author.present? }
- expose :commit, using: Entities::Commit, if: ->(_, _) { can_download_code? }
+ expose :commit, using: Entities::Commit, if: ->(_, _) { can_read_code? }
expose :milestones,
using: Entities::MilestoneWithStats,
if: -> (release, _) { release.milestones.present? && can_read_milestone? } do |release, _|
@@ -23,10 +23,10 @@ module API
expose :assets do
expose :assets_count, documentation: { type: 'integer', example: 2 }, as: :count
- expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_download_code? }
+ expose :sources, using: Entities::Releases::Source, if: ->(_, _) { can_read_code? }
expose :sorted_links, as: :links, using: Entities::Releases::Link
end
- expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_download_code? }
+ expose :evidences, using: Entities::Releases::Evidence, expose_nil: false, if: ->(_, _) { can_read_code? }
expose :_links do
expose :self_url, as: :self, expose_nil: false
expose :edit_url, expose_nil: false
@@ -34,8 +34,8 @@ module API
private
- def can_download_code?
- Ability.allowed?(options[:current_user], :download_code, object.project)
+ def can_read_code?
+ Ability.allowed?(options[:current_user], :read_code, object.project)
end
def can_read_milestone?
diff --git a/lib/api/files.rb b/lib/api/files.rb
index 68dd2647703..fa749299b9a 100644
--- a/lib/api/files.rb
+++ b/lib/api/files.rb
@@ -30,7 +30,7 @@ module API
end
def assign_file_vars!
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
@commit = user_project.commit(params[:ref])
not_found!('Commit') unless @commit
diff --git a/lib/api/issue_links.rb b/lib/api/issue_links.rb
index 0f92f7aeb91..020b02248a0 100644
--- a/lib/api/issue_links.rb
+++ b/lib/api/issue_links.rb
@@ -6,16 +6,27 @@ module API
before { authenticate! }
+ ISSUE_LINKS_TAGS = %w[issue_links].freeze
+
feature_category :team_planning
urgency :low
params do
- requires :id, types: [String, Integer], desc: 'The ID or URL-encoded path of the project'
- requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
+ requires :id, types: [String, Integer],
+ desc: 'The ID or URL-encoded path of the project owned by the authenticated user'
+ requires :issue_iid, type: Integer, desc: 'The internal ID of a project’s issue'
end
resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get related issues' do
+ desc 'List issue relations' do
+ detail 'Get a list of a given issue’s linked issues, sorted by the relationship creation datetime (ascending).'\
+ 'Issues are filtered according to the user authorizations.'
success Entities::RelatedIssue
+ is_array true
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
get ':id/issues/:issue_iid/links' do
source_issue = find_project_issue(params[:issue_iid])
@@ -30,14 +41,23 @@ module API
include_subscribed: false
end
- desc 'Relate issues' do
+ desc 'Create an issue link' do
+ detail 'Creates a two-way relation between two issues.'\
+ 'The user must be allowed to update both issues to succeed.'
success Entities::IssueLink
+ failure [
+ { code: 400, message: 'Bad Request' },
+ { code: 401, message: 'Unauthorized' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :target_project_id, type: String, desc: 'The ID of the target project'
- requires :target_issue_iid, type: Integer, desc: 'The IID of the target issue'
+ requires :target_project_id, types: [String, Integer],
+ desc: 'The ID or URL-encoded path of a target project'
+ requires :target_issue_iid, types: [String, Integer], desc: 'The internal ID of a target project’s issue'
optional :link_type, type: String, values: IssueLink.link_types.keys,
- desc: 'The type of the relation'
+ desc: 'The type of the relation (“relates_to”, “blocks”, “is_blocked_by”),'\
+ 'defaults to “relates_to”)'
end
# rubocop: disable CodeReuse/ActiveRecord
post ':id/issues/:issue_iid/links' do
@@ -61,12 +81,17 @@ module API
end
# rubocop: enable CodeReuse/ActiveRecord
- desc 'Get issues relation' do
- detail 'This feature was introduced in GitLab 15.1.'
+ desc 'Get an issue link' do
+ detail 'Gets details about an issue link. This feature was introduced in GitLab 15.1.'
success Entities::IssueLink
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ requires :issue_link_id, types: [String, Integer], desc: 'ID of an issue relationship'
end
get ':id/issues/:issue_iid/links/:issue_link_id' do
issue = find_project_issue(params[:issue_iid])
@@ -77,11 +102,17 @@ module API
present issue_link, with: Entities::IssueLink
end
- desc 'Remove issues relation' do
+ desc 'Delete an issue link' do
+ detail 'Deletes an issue link, thus removes the two-way relationship.'
success Entities::IssueLink
+ failure [
+ { code: 401, message: 'Unauthorized' },
+ { code: 404, message: 'Not found' }
+ ]
+ tags ISSUE_LINKS_TAGS
end
params do
- requires :issue_link_id, type: Integer, desc: 'The ID of an issue link'
+ requires :issue_link_id, types: [String, Integer], desc: 'The ID of an issue relationship'
end
delete ':id/issues/:issue_iid/links/:issue_link_id' do
issue = find_project_issue(params[:issue_iid])
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
index 1d19d653d8b..89787ba00c2 100644
--- a/lib/api/lint.rb
+++ b/lib/api/lint.rb
@@ -56,7 +56,7 @@ module API
end
get ':id/ci/lint', urgency: :low do
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
if user_project.commit.present?
content = user_project.repository.gitlab_ci_yml_for(user_project.commit.id, user_project.ci_config_path_or_default)
diff --git a/lib/api/releases.rb b/lib/api/releases.rb
index ec9907b18f9..e6884e66200 100644
--- a/lib/api/releases.rb
+++ b/lib/api/releases.rb
@@ -131,7 +131,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/:tag_name', requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
not_found! unless release
@@ -157,7 +157,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/:tag_name/downloads/*file_path', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
not_found! unless release
@@ -185,7 +185,7 @@ module API
end
route_setting :authentication, job_token_allowed: true
get ':id/releases/permalink/latest(/)(*suffix_path)', format: false, requirements: RELEASE_ENDPOINT_REQUIREMENTS do
- authorize_download_code!
+ authorize_read_code!
# Try to find the latest release
latest_release = find_latest_release
@@ -373,6 +373,10 @@ module API
authorize! :download_code, user_project
end
+ def authorize_read_code!
+ authorize! :read_code, user_project
+ end
+
def authorize_create_evidence!
# extended in EE
end
diff --git a/lib/api/repositories.rb b/lib/api/repositories.rb
index beba2842316..70535496b12 100644
--- a/lib/api/repositories.rb
+++ b/lib/api/repositories.rb
@@ -41,7 +41,7 @@ module API
end
end
- before { authorize! :download_code, user_project }
+ before { authorize! :read_code, user_project }
feature_category :source_code_management
@@ -63,7 +63,7 @@ module API
end
def assign_blob_vars!(limit:)
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
@repo = user_project.repository
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 0022b51bd92..b412a17bc6f 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -7,7 +7,7 @@ module API
TAG_ENDPOINT_REQUIREMENTS = API::NAMESPACE_OR_PROJECT_REQUIREMENTS.merge(tag_name: API::NO_SLASH_URL_PART_REGEX)
before do
- authorize! :download_code, user_project
+ authorize! :read_code, user_project
not_found! unless user_project.repo_exists?
end
diff --git a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
index f1a07af1bf9..bc62fbe55ec 100644
--- a/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
+++ b/lib/gitlab/ci/parsers/sbom/cyclonedx.rb
@@ -70,7 +70,7 @@ module Gitlab
)
report.add_component(component) if component.ingestible?
- rescue ::Sbom::PackageUrl::InvalidPackageURL
+ rescue ::Sbom::PackageUrl::InvalidPackageUrl
report.add_error("/components/#{index}/purl is invalid")
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c68c096bea1..735c7fcf80c 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -371,8 +371,6 @@ module Gitlab
end
def self.expected_server_version
- return ENV[SERVER_VERSION_FILE] if ENV[SERVER_VERSION_FILE]
-
path = Rails.root.join(SERVER_VERSION_FILE)
path.read.chomp
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index a0daa03bbed..ecb57bfc1a2 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -17,14 +17,15 @@ module Gitlab
gon.markdown_surround_selection = current_user&.markdown_surround_selection
gon.markdown_automatic_lists = current_user&.markdown_automatic_lists
- # Support for Sentry setup via configuration will be removed in 16.0
- # in favor of Gitlab::CurrentSettings.
- if Feature.enabled?(:enable_old_sentry_clientside_integration) && Gitlab.config.sentry.enabled
- gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
- gon.sentry_environment = Gitlab.config.sentry.environment
+ if Gitlab.config.sentry.enabled
+ gon.sentry_dsn = Gitlab.config.sentry.clientside_dsn
+ gon.sentry_environment = Gitlab.config.sentry.environment
end
- if Feature.enabled?(:enable_new_sentry_clientside_integration) && Gitlab::CurrentSettings.sentry_enabled
+ # Support for Sentry setup via configuration files will be removed in 16.0
+ # in favor of Gitlab::CurrentSettings.
+ if Feature.enabled?(:enable_new_sentry_clientside_integration,
+ current_user) && Gitlab::CurrentSettings.sentry_enabled
gon.sentry_dsn = Gitlab::CurrentSettings.sentry_clientside_dsn
gon.sentry_environment = Gitlab::CurrentSettings.sentry_environment
end
diff --git a/lib/sbom/package_url.rb b/lib/sbom/package_url.rb
index 3b545ebebf2..d8f4e876b82 100644
--- a/lib/sbom/package_url.rb
+++ b/lib/sbom/package_url.rb
@@ -44,7 +44,7 @@ module Sbom
class PackageUrl
# Raised when attempting to parse an invalid package URL string.
# @see #parse
- InvalidPackageURL = Class.new(ArgumentError)
+ InvalidPackageUrl = Class.new(ArgumentError)
# The URL scheme, which has a constant value of `"pkg"`.
def scheme
@@ -79,20 +79,19 @@ module Sbom
# @param qualifiers [Hash] Extra qualifying data for a package, specific to the type of package.
# @param subpath [String] An extra subpath within a package, relative to the package root.
def initialize(type:, name:, namespace: nil, version: nil, qualifiers: nil, subpath: nil)
- raise ArgumentError, 'type is required' unless type.present?
- raise ArgumentError, 'name is required' unless name.present?
-
- @type = type.downcase
+ @type = type&.downcase
@namespace = namespace
@name = name
@version = version
@qualifiers = qualifiers
@subpath = subpath
+
+ ArgumentValidator.new(self).validate!
end
# Creates a new PackageUrl from a string.
# @param [String] string The package URL string.
- # @raise [InvalidPackageURL] If the string is not a valid package URL.
+ # @raise [InvalidPackageUrl] If the string is not a valid package URL.
# @return [PackageUrl]
def self.parse(string)
Decoder.new(string).decode!
diff --git a/lib/sbom/package_url/argument_validator.rb b/lib/sbom/package_url/argument_validator.rb
new file mode 100644
index 00000000000..639ee9f89b6
--- /dev/null
+++ b/lib/sbom/package_url/argument_validator.rb
@@ -0,0 +1,90 @@
+# frozen_string_literal: true
+
+module Sbom
+ class PackageUrl
+ class ArgumentValidator
+ QUALIFIER_KEY_REGEXP = /^[A-Za-z\d._-]+$/.freeze
+ START_WITH_NUMBER_REGEXP = /^\d/.freeze
+
+ def initialize(package)
+ @type = package.type
+ @namespace = package.namespace
+ @name = package.name
+ @version = package.version
+ @qualifiers = package.qualifiers
+ @errors = []
+ end
+
+ def validate!
+ validate_type
+ validate_name
+ validate_qualifiers
+ validate_by_type
+
+ raise ArgumentError, formatted_errors if invalid?
+ end
+
+ private
+
+ def invalid?
+ errors.present?
+ end
+
+ attr_reader :type, :namespace, :name, :version, :qualifiers, :errors
+
+ def formatted_errors
+ errors.join(', ')
+ end
+
+ def validate_type
+ errors.push('Type is required') if type.blank?
+ end
+
+ def validate_name
+ errors.push('Name is required') if name.blank?
+ end
+
+ def validate_qualifiers
+ return if qualifiers.nil?
+
+ keys = qualifiers.keys
+ errors.push('Qualifier keys must be unique') unless keys.uniq.size == keys.size
+
+ keys.each do |key|
+ errors.push(key_error(key, 'contains illegal characters')) unless key.match?(QUALIFIER_KEY_REGEXP)
+ errors.push(key_error(key, 'may not start with a number')) if key.match?(START_WITH_NUMBER_REGEXP)
+ end
+ end
+
+ def key_error(key, text)
+ "Qualifier key `#{key}` #{text}"
+ end
+
+ def validate_by_type
+ case type
+ when 'conan'
+ validate_conan
+ when 'cran'
+ validate_cran
+ when 'swift'
+ validate_swift
+ end
+ end
+
+ def validate_conan
+ return unless namespace.blank? ^ (qualifiers.nil? || qualifiers.exclude?('channel'))
+
+ errors.push('Conan packages require the channel be present if published in a namespace and vice-versa')
+ end
+
+ def validate_cran
+ errors.push('Cran packages require a version') if version.blank?
+ end
+
+ def validate_swift
+ errors.push('Swift packages require a namespace') if namespace.blank?
+ errors.push('Swift packages require a version') if version.blank?
+ end
+ end
+ end
+end
diff --git a/lib/sbom/package_url/decoder.rb b/lib/sbom/package_url/decoder.rb
index 5a31343995d..ceadc36660c 100644
--- a/lib/sbom/package_url/decoder.rb
+++ b/lib/sbom/package_url/decoder.rb
@@ -43,14 +43,18 @@ module Sbom
decode_name!
decode_namespace!
- PackageUrl.new(
- type: @type,
- name: @name,
- namespace: @namespace,
- version: @version,
- qualifiers: @qualifiers,
- subpath: @subpath
- )
+ begin
+ PackageUrl.new(
+ type: @type,
+ name: @name,
+ namespace: @namespace,
+ version: @version,
+ qualifiers: @qualifiers,
+ subpath: @subpath
+ )
+ rescue ArgumentError => e
+ raise InvalidPackageUrl, e.message
+ end
end
private
@@ -84,7 +88,7 @@ module Sbom
# - The left side lowercased is the scheme: `scheme`
# - The right side is the remainder: `type/namespace/name@version`
@scheme, @string = partition(@string, ':', from: :left)
- raise InvalidPackageURL, 'invalid or missing "pkg:" URL scheme' unless @scheme == 'pkg'
+ raise InvalidPackageUrl, 'invalid or missing "pkg:" URL scheme' unless @scheme == 'pkg'
end
def decode_type!
@@ -94,8 +98,7 @@ module Sbom
# Given the string: `type/namespace/name@version`
# - The left side lowercased is the type: `type`
# - The right side is the remainder: `namespace/name@version`
- @type, @string = partition(@string, '/', from: :left)
- raise InvalidPackageURL, 'invalid or missing package type' if @type.blank?
+ @type, @string = partition(@string, '/', from: :left, &:downcase)
end
def decode_version!
@@ -116,20 +119,24 @@ module Sbom
# - The right size is the name: `name`
# - The name must be URI decoded
@name, @string = partition(@string, '/', from: :right, require_separator: false) do |name|
- URI.decode_www_form_component(name)
+ decoded_name = URI.decode_www_form_component(name)
+ Normalizer.new(type: @type, text: decoded_name).normalize_name
end
end
def decode_namespace!
# If there is anything remaining, this is the namespace.
# The namespace may contain multiple segments delimited by `/`.
- @namespace = decode_segments(@string, &:empty?) if @string.present?
+ return if @string.blank?
+
+ @namespace = decode_segments(@string, &:empty?)
+ @namespace = Normalizer.new(type: @type, text: @namespace).normalize_namespace
end
def decode_segment(segment)
decoded = URI.decode_www_form_component(segment)
- raise InvalidPackageURL, 'slash-separated segments may not contain `/`' if decoded.include?('/')
+ raise InvalidPackageUrl, 'slash-separated segments may not contain `/`' if decoded.include?('/')
decoded
end
diff --git a/lib/sbom/package_url/encoder.rb b/lib/sbom/package_url/encoder.rb
index 1412824b76f..9cf05095571 100644
--- a/lib/sbom/package_url/encoder.rb
+++ b/lib/sbom/package_url/encoder.rb
@@ -84,11 +84,11 @@ module Sbom
# - UTF-8-encode the name if needed in your programming language
# - Append the percent-encoded name to the purl
if @namespace.nil?
- io.write(URI.encode_www_form_component(@name))
+ io.write(URI.encode_www_form_component(@name, Encoding::UTF_8))
else
io.write(encode_segments(@namespace, &:empty?))
io.write('/')
- io.write(URI.encode_www_form_component(strip(@name, '/')))
+ io.write(URI.encode_www_form_component(strip(@name, '/'), Encoding::UTF_8))
end
end
@@ -99,7 +99,7 @@ module Sbom
# - UTF-8-encode the version if needed in your programming language
# - Append the percent-encoded version to the purl
io.write('@')
- io.write(URI.encode_www_form_component(@version))
+ io.write(URI.encode_www_form_component(@version, Encoding::UTF_8))
end
def encode_qualifiers!
@@ -115,7 +115,7 @@ module Sbom
next "#{key.downcase}=#{value.join(',')}" if key == 'checksums' && value.is_a?(::Array)
- "#{key.downcase}=#{URI.encode_www_form_component(value)}"
+ "#{key.downcase}=#{URI.encode_www_form_component(value, Encoding::UTF_8)}"
end.sort.join('&')
end
diff --git a/lib/sbom/package_url/normalizer.rb b/lib/sbom/package_url/normalizer.rb
new file mode 100644
index 00000000000..663df6f72a5
--- /dev/null
+++ b/lib/sbom/package_url/normalizer.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+module Sbom
+ class PackageUrl
+ class Normalizer
+ def initialize(type:, text:)
+ @type = type
+ @text = text
+ end
+
+ def normalize_namespace
+ return if text.nil?
+
+ normalize
+ end
+
+ def normalize_name
+ raise ArgumentError, 'Name is required' if text.nil?
+
+ normalize
+ end
+
+ private
+
+ def normalize
+ case type
+ when 'bitbucket', 'github'
+ downcase
+ when 'pypi'
+ normalize_pypi
+ else
+ text
+ end
+ end
+
+ attr_reader :type, :text
+
+ def downcase
+ text.downcase
+ end
+
+ def normalize_pypi
+ downcase.tr('_', '-')
+ end
+ end
+ end
+end
diff --git a/lib/sbom/package_url/string_utils.rb b/lib/sbom/package_url/string_utils.rb
index 7b476292c72..c1ea8de95b2 100644
--- a/lib/sbom/package_url/string_utils.rb
+++ b/lib/sbom/package_url/string_utils.rb
@@ -29,7 +29,9 @@ module Sbom
private
def strip(string, char)
- string.delete_prefix(char).delete_suffix(char)
+ string = string.delete_prefix(char) while string.start_with?(char)
+ string = string.delete_suffix(char) while string.end_with?(char)
+ string
end
def split_segments(string)
@@ -66,7 +68,7 @@ module Sbom
return [nil, value] if separator.empty? && require_separator
- value = yield(value, remainder) if block_given?
+ value = yield(value) if block_given?
[value, remainder]
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 3fa779062d7..64f787b32f3 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4018,6 +4018,9 @@ msgstr ""
msgid "All users with matching cards"
msgstr ""
+msgid "Allow %{strongOpen}%{group_name}%{strongClose} to sign you in?"
+msgstr ""
+
msgid "Allow access to members of the following group"
msgstr ""
@@ -7041,6 +7044,9 @@ msgstr ""
msgid "Branches: %{source_branch} → %{target_branch}"
msgstr ""
+msgid "Branches|A branch won't be deleted if it is protected or associated with an open merge request."
+msgstr ""
+
msgid "Branches|Active"
msgstr ""
@@ -7062,7 +7068,10 @@ msgstr ""
msgid "Branches|Compare"
msgstr ""
-msgid "Branches|Delete all branches that are merged into '%{default_branch}'"
+msgid "Branches|Delete all branches that are merged into '%{defaultBranch}'"
+msgstr ""
+
+msgid "Branches|Delete all merged branches?"
msgstr ""
msgid "Branches|Delete branch"
@@ -7083,9 +7092,6 @@ msgstr ""
msgid "Branches|Deleting the %{strongStart}%{branchName}%{strongEnd} branch cannot be undone. Are you sure?"
msgstr ""
-msgid "Branches|Deleting the merged branches cannot be undone. Are you sure?"
-msgstr ""
-
msgid "Branches|Filter by branch name"
msgstr ""
@@ -7107,6 +7113,9 @@ msgstr ""
msgid "Branches|Please type the following to confirm:"
msgstr ""
+msgid "Branches|Plese type the following to confirm: %{codeStart}delete%{codeEnd}."
+msgstr ""
+
msgid "Branches|Show active branches"
msgstr ""
@@ -7140,6 +7149,12 @@ msgstr ""
msgid "Branches|This branch hasn't been merged into %{defaultBranchName}. To avoid data loss, consider merging this branch before deleting it."
msgstr ""
+msgid "Branches|This bulk action is %{strongStart}permanent and cannot be undone or recovered%{strongEnd}."
+msgstr ""
+
+msgid "Branches|This may include merged branches that are not visible on the current screen."
+msgstr ""
+
msgid "Branches|To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above."
msgstr ""
@@ -7152,6 +7167,9 @@ msgstr ""
msgid "Branches|Yes, delete protected branch"
msgstr ""
+msgid "Branches|You are about to %{strongStart}delete all branches%{strongEnd} that were merged into %{codeStart}%{defaultBranch}%{codeEnd}."
+msgstr ""
+
msgid "Branches|You're about to permanently delete the branch %{branchName}."
msgstr ""
@@ -10857,9 +10875,6 @@ msgstr ""
msgid "Control how the CI_JOB_TOKEN CI/CD variable is used for API access between projects."
msgstr ""
-msgid "Control how the GitLab Package Registry functions."
-msgstr ""
-
msgid "Control whether to display customer experience improvement content and third-party offers in GitLab."
msgstr ""
@@ -17382,9 +17397,6 @@ msgstr ""
msgid "Format: %{dateFormat}"
msgstr ""
-msgid "Forward %{package_type} package requests to the %{registry_type} Registry if the packages are not found in the GitLab Package Registry"
-msgstr ""
-
msgid "Found errors in your %{gitlab_ci_yml}:"
msgstr ""
@@ -28871,6 +28883,9 @@ msgstr ""
msgid "PackageRegistry|Conan Command"
msgstr ""
+msgid "PackageRegistry|Configure package forwarding and package file size limits."
+msgstr ""
+
msgid "PackageRegistry|Copy .pypirc content"
msgstr ""
@@ -28978,6 +28993,12 @@ msgstr ""
msgid "PackageRegistry|Duplicate packages"
msgstr ""
+msgid "PackageRegistry|Enforce %{packageType} setting for all subgroups"
+msgstr ""
+
+msgid "PackageRegistry|Enforce %{package_type} setting for all subgroups"
+msgstr ""
+
msgid "PackageRegistry|Error publishing"
msgstr ""
@@ -29002,6 +29023,18 @@ msgstr ""
msgid "PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
msgstr ""
+msgid "PackageRegistry|Forward %{packageType} package requests"
+msgstr ""
+
+msgid "PackageRegistry|Forward %{package_type} package requests"
+msgstr ""
+
+msgid "PackageRegistry|Forward package requests"
+msgstr ""
+
+msgid "PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry."
+msgstr ""
+
msgid "PackageRegistry|Generic"
msgstr ""
@@ -29089,6 +29122,9 @@ msgstr ""
msgid "PackageRegistry|Package formats"
msgstr ""
+msgid "PackageRegistry|Package forwarding"
+msgstr ""
+
msgid "PackageRegistry|Package has %{updatesCount} archived update"
msgid_plural "PackageRegistry|Package has %{updatesCount} archived updates"
msgstr[0] ""
@@ -29623,6 +29659,24 @@ msgstr ""
msgid "Phone"
msgstr ""
+msgid "PhoneVerification|Enter a valid code."
+msgstr ""
+
+msgid "PhoneVerification|Something went wrong. Please try again."
+msgstr ""
+
+msgid "PhoneVerification|The code has expired. Request a new code and try again."
+msgstr ""
+
+msgid "PhoneVerification|There was a problem with the phone number you entered. Enter a different phone number and try again."
+msgstr ""
+
+msgid "PhoneVerification|There was a problem with the phone number you entered. Enter a valid phone number."
+msgstr ""
+
+msgid "PhoneVerification|You've reached the maximum number of tries. Request a new code and try again."
+msgstr ""
+
msgid "Pick a name"
msgstr ""
@@ -35590,19 +35644,16 @@ msgstr ""
msgid "SAML single sign-on for %{group_name}"
msgstr ""
-msgid "SAML|Allow %{groupName} to sign you in?"
-msgstr ""
-
msgid "SAML|Sign in to GitLab to connect your organization's account"
msgstr ""
-msgid "SAML|The %{groupName} group allows you to sign in using single sign-on."
+msgid "SAML|The %{strongOpen}%{group_path}%{strongClose} group allows you to sign in using single sign-on."
msgstr ""
msgid "SAML|To access %{strongOpen}%{group_name}%{strongClose}, you must sign in using single sign-on through an external sign-in page."
msgstr ""
-msgid "SAML|To allow %{groupName} to manage your GitLab account %{username} after you sign in successfully using single sign-on, select %{strongStart}Authorize%{strongEnd}."
+msgid "SAML|To allow %{strongOpen}%{group_name}%{strongClose} to manage your GitLab account %{strongOpen}%{username}%{strongClose} (%{email}) after you sign in successfully using single sign-on, select %{strongOpen}Authorize%{strongClose}."
msgstr ""
msgid "SAML|Your organization's SSO has been connected to your GitLab account"
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 22b960b47ce..7163bc7464d 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -5,8 +5,6 @@ module QA
module Project
module Branches
class Show < Page::Base
- include Page::Component::ConfirmModal
-
view 'app/assets/javascripts/branches/components/delete_branch_button.vue' do
element :delete_branch_button
end
@@ -25,8 +23,10 @@ module QA
element :all_branches_container
end
- view 'app/views/projects/branches/index.html.haml' do
- element :delete_merged_branches_link
+ view 'app/assets/javascripts/branches/components/delete_merged_branches.vue' do
+ element :delete_merged_branches_button
+ element :delete_merged_branches_input
+ element :delete_merged_branches_confirmation_button
end
def delete_branch(branch_name)
@@ -53,9 +53,11 @@ module QA
end
end
- def delete_merged_branches
- click_element(:delete_merged_branches_link)
- click_confirmation_ok_button
+ def delete_merged_branches(branches_length)
+ click_element(:delete_merged_branches_button)
+ fill_element(:delete_merged_branches_input, branches_length)
+ click_element(:delete_merged_branches_confirmation_button)
+ finished_loading?
end
end
end
diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb
index 7ee015ceb98..316920ffa90 100644
--- a/qa/qa/page/project/settings/ci_variables.rb
+++ b/qa/qa/page/project/settings/ci_variables.rb
@@ -14,13 +14,6 @@ module QA
element :ci_variable_delete_button
end
- view 'app/assets/javascripts/ci_variable_list/components/legacy_ci_variable_table.vue' do
- element :ci_variable_table_content
- element :add_ci_variable_button
- element :edit_ci_variable_button
- element :reveal_ci_variable_value_button
- end
-
def fill_variable(key, value, masked = false)
within_element(:ci_variable_key_field) { find('input').set key }
fill_element :ci_variable_value_field, value
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index 849022f5a93..866c6a146de 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -76,7 +76,7 @@ module QA
expect(branches_page).to have_no_branch(third_branch)
- branches_page.delete_merged_branches
+ branches_page.delete_merged_branches('delete')
expect(branches_page).to have_content(
'Merged branches are being deleted. This can take some time depending on the number of branches. Please refresh the page to see changes.'
diff --git a/scripts/build_qa_image b/scripts/build_qa_image
index 4b7eb73e784..477bec29ba7 100755
--- a/scripts/build_qa_image
+++ b/scripts/build_qa_image
@@ -1,8 +1,8 @@
-#!/bin/sh
+#!/bin/bash
QA_IMAGE_NAME="gitlab-ee-qa"
-if [ "${CI_PROJECT_NAME}" == "gitlabhq" ] || [ "${CI_PROJECT_NAME}" == "gitlab-foss" ]; then
+if [[ "${CI_PROJECT_NAME}" == "gitlabhq" || "${CI_PROJECT_NAME}" == "gitlab-foss" ]]; then
QA_IMAGE_NAME="gitlab-ce-qa"
fi
@@ -15,24 +15,29 @@ IMAGE_TAG=${CI_COMMIT_TAG#v}
IMAGE_TAG=${IMAGE_TAG:-$CI_COMMIT_REF_SLUG}
QA_IMAGE_BRANCH="${CI_REGISTRY}/${CI_PROJECT_PATH}/${QA_IMAGE_NAME}:${IMAGE_TAG}"
+QA_IMAGE_MASTER="${CI_REGISTRY}/${CI_PROJECT_PATH}/${QA_IMAGE_NAME}:master"
-DESTINATIONS="--destination=${QA_IMAGE} --destination=${QA_IMAGE_BRANCH}"
+DESTINATIONS="--tag ${QA_IMAGE} --tag ${QA_IMAGE_BRANCH}"
# Auto-deploy tag format uses first 12 letters of commit SHA. Tag with that
# reference also for EE images.
if [ "${QA_IMAGE_NAME}" == "gitlab-ee-qa" ]; then
QA_IMAGE_FOR_AUTO_DEPLOY="${CI_REGISTRY}/${CI_PROJECT_PATH}/${QA_IMAGE_NAME}:${CI_COMMIT_SHA:0:11}"
- DESTINATIONS="${DESTINATIONS} --destination=$QA_IMAGE_FOR_AUTO_DEPLOY"
+ DESTINATIONS="${DESTINATIONS} --tag $QA_IMAGE_FOR_AUTO_DEPLOY"
fi
echo "Building QA image for destinations: ${DESTINATIONS}"
-/kaniko/executor \
- --context="${CI_PROJECT_DIR}" \
- --dockerfile="${CI_PROJECT_DIR}/qa/Dockerfile" \
+docker buildx build \
+ --cache-to=type=inline \
+ --cache-from="$QA_IMAGE_BRANCH" \
+ --cache-from="$QA_IMAGE_MASTER" \
+ --platform=${ARCH:-amd64} \
--build-arg=CHROME_VERSION="${CHROME_VERSION}" \
--build-arg=DOCKER_VERSION="${DOCKER_VERSION}" \
--build-arg=RUBY_VERSION="${RUBY_VERSION}" \
--build-arg=QA_BUILD_TARGET="${QA_BUILD_TARGET:-qa}" \
- --cache=true \
- ${DESTINATIONS}
+ --file="${CI_PROJECT_DIR}/qa/Dockerfile" \
+ --push \
+ ${DESTINATIONS} \
+ ${CI_PROJECT_DIR}
diff --git a/scripts/review_apps/base-config.yaml b/scripts/review_apps/base-config.yaml
index 43dc562c58a..f845dd04e8f 100644
--- a/scripts/review_apps/base-config.yaml
+++ b/scripts/review_apps/base-config.yaml
@@ -50,7 +50,8 @@ gitlab:
minReplicas: 1
maxReplicas: 1
hpa:
- targetAverageValue: 500m
+ cpu:
+ targetAverageValue: 500m
deployment:
livenessProbe:
timeoutSeconds: 5
@@ -80,7 +81,8 @@ gitlab:
cpu: 1282m
memory: 2890Mi
hpa:
- targetAverageValue: 650m
+ cpu:
+ targetAverageValue: 650m
toolbox:
resources:
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 18e0cf539f7..eecb803fb1a 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -63,6 +63,114 @@ RSpec.describe Admin::UsersController do
expect(response).to be_redirect
expect(response.location).to end_with(user.username)
end
+
+ describe 'impersonation_error_text' do
+ context 'when user can be impersonated' do
+ it 'sets impersonation_error_text to nil' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(nil)
+ end
+ end
+
+ context 'when impersonation is already in progress' do
+ let(:admin2) { create(:admin) }
+
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You are already impersonating another user"))
+ end
+ end
+
+ context 'when user is blocked' do
+ before do
+ user.block
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a blocked user"))
+ end
+ end
+
+ context "when the user's password is expired" do
+ before do
+ user.update!(password_expires_at: 1.day.ago)
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a user with an expired password"))
+ end
+ end
+
+ context "when the user is internal" do
+ before do
+ user.update!(user_type: :migration_bot)
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate an internal user"))
+ end
+ end
+
+ context "when the user is a project bot" do
+ before do
+ user.update!(user_type: :project_bot)
+ end
+
+ it 'sets impersonation_error_text' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:impersonation_error_text)).to eq(_("You cannot impersonate a user who cannot log in"))
+ end
+ end
+ end
+
+ describe 'can_impersonate' do
+ context 'when user can be impersonated' do
+ it 'sets can_impersonate to true' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:can_impersonate)).to eq(true)
+ end
+ end
+
+ context 'when impersonation is already in progress' do
+ let(:admin2) { create(:admin) }
+
+ before do
+ post :impersonate, params: { id: admin2.username }
+ end
+
+ it 'sets can_impersonate to false' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:can_impersonate)).to eq(false)
+ end
+ end
+
+ context 'when user cannot log in' do
+ before do
+ user.update!(user_type: :project_bot)
+ end
+
+ it 'sets can_impersonate to false' do
+ get :show, params: { id: user.username.downcase }
+
+ expect(assigns(:can_impersonate)).to eq(false)
+ end
+ end
+ end
end
describe 'DELETE #destroy', :sidekiq_might_not_need_inline do
diff --git a/spec/factories/users.rb b/spec/factories/users.rb
index 2e7c6116fe6..2b53a469841 100644
--- a/spec/factories/users.rb
+++ b/spec/factories/users.rb
@@ -27,6 +27,10 @@ FactoryBot.define do
after(:build) { |user, _| user.block! }
end
+ trait :locked do
+ after(:build) { |user, _| user.lock_access! }
+ end
+
trait :disallowed_password do
password { User::DISALLOWED_PASSWORDS.first }
end
diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb
index 6b8517e2ce2..35b5c755b66 100644
--- a/spec/features/admin/users/user_spec.rb
+++ b/spec/features/admin/users/user_spec.rb
@@ -150,13 +150,32 @@ RSpec.describe 'Admin::Users::User' do
context 'before impersonating' do
subject { visit admin_user_path(user_to_visit) }
- let(:user_to_visit) { another_user }
+ let_it_be(:user_to_visit) { another_user }
+
+ shared_examples "user that cannot be impersonated" do
+ it 'disables impersonate button' do
+ subject
+
+ impersonate_btn = find('[data-testid="impersonate_user_link"]')
+
+ expect(impersonate_btn).not_to be_nil
+ expect(impersonate_btn['disabled']).not_to be_nil
+ end
+
+ it "shows tooltip with correct error message" do
+ subject
+
+ expect(find("span[title='#{impersonation_error_msg}']")).not_to be_nil
+ end
+ end
context 'for other users' do
it 'shows impersonate button for other users' do
subject
expect(page).to have_content('Impersonate')
+ impersonate_btn = find('[data-testid="impersonate_user_link"]')
+ expect(impersonate_btn['disabled']).to be_nil
end
end
@@ -171,15 +190,51 @@ RSpec.describe 'Admin::Users::User' do
end
context 'for blocked user' do
- before do
- another_user.block
+ let_it_be(:blocked_user) { create(:user, :blocked) }
+ let(:user_to_visit) { blocked_user }
+ let(:impersonation_error_msg) { _('You cannot impersonate a blocked user') }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for user with expired password' do
+ let(:user_to_visit) do
+ another_user.update!(password_expires_at: Time.zone.now - 5.minutes)
+ another_user
end
- it 'does not show impersonate button for blocked user' do
- subject
+ let(:impersonation_error_msg) { _("You cannot impersonate a user with an expired password") }
- expect(page).not_to have_content('Impersonate')
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for internal user' do
+ let_it_be(:internal_user) { create(:user, :bot) }
+ let(:user_to_visit) { internal_user }
+ let(:impersonation_error_msg) { _("You cannot impersonate an internal user") }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'for locked user' do
+ let_it_be(:locked_user) { create(:user, :locked) }
+ let(:user_to_visit) { locked_user }
+ let(:impersonation_error_msg) { _("You cannot impersonate a user who cannot log in") }
+
+ it_behaves_like "user that cannot be impersonated"
+ end
+
+ context 'when already impersonating another user' do
+ let_it_be(:admin_user) { create(:user, :admin) }
+ let(:impersonation_error_msg) { _("You are already impersonating another user") }
+
+ subject do
+ visit admin_user_path(admin_user)
+ click_link 'Impersonate'
+ visit admin_user_path(another_user)
end
+
+ it_behaves_like "user that cannot be impersonated"
end
context 'when impersonation is disabled' do
diff --git a/spec/features/admin_variables_spec.rb b/spec/features/admin_variables_spec.rb
index 174d4567520..9ec22bbe948 100644
--- a/spec/features/admin_variables_spec.rb
+++ b/spec/features/admin_variables_spec.rb
@@ -12,23 +12,9 @@ RSpec.describe 'Instance variables', :js do
stub_env('IN_MEMORY_APPLICATION_SETTINGS', 'false')
sign_in(admin)
gitlab_enable_admin_mode_sign_in(admin)
+ visit page_path
wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list', isAdmin: true
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
-
- it_behaves_like 'variable list', isAdmin: true
- end
+ it_behaves_like 'variable list', isAdmin: true
end
diff --git a/spec/features/group_variables_spec.rb b/spec/features/group_variables_spec.rb
index ab24162ad5a..e2c659d7dfe 100644
--- a/spec/features/group_variables_spec.rb
+++ b/spec/features/group_variables_spec.rb
@@ -11,23 +11,9 @@ RSpec.describe 'Group variables', :js do
before do
group.add_owner(user)
gitlab_sign_in(user)
+ visit page_path
wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list'
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
-
- it_behaves_like 'variable list'
- end
+ it_behaves_like 'variable list'
end
diff --git a/spec/features/project_variables_spec.rb b/spec/features/project_variables_spec.rb
index d3bedbf3a75..33b4af3b5aa 100644
--- a/spec/features/project_variables_spec.rb
+++ b/spec/features/project_variables_spec.rb
@@ -12,62 +12,29 @@ RSpec.describe 'Project variables', :js do
sign_in(user)
project.add_maintainer(user)
project.variables << variable
+ visit page_path
+ wait_for_requests
end
- context 'with disabled ff `ci_variable_settings_graphql' do
- before do
- stub_feature_flags(ci_variable_settings_graphql: false)
- visit page_path
- end
-
- it_behaves_like 'variable list'
-
- it 'adds a new variable with an environment scope' do
- click_button('Add variable')
-
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
- find('[data-testid="environment-scope"]').click
- find('[data-testid="ci-environment-search"]').set('review/*')
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
+ it_behaves_like 'variable list'
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
- end
- end
-
- context 'with enabled ff `ci_variable_settings_graphql' do
- before do
- visit page_path
- end
+ it 'adds a new variable with an environment scope' do
+ click_button('Add variable')
- it_behaves_like 'variable list'
+ page.within('#add-ci-variable') do
+ fill_in 'Key', with: 'akey'
+ find('#ci-variable-value').set('akey_value')
+ find('[data-testid="environment-scope"]').click
+ find('[data-testid="ci-environment-search"]').set('review/*')
+ find('[data-testid="create-wildcard-button"]').click
- it 'adds a new variable with an environment scope' do
click_button('Add variable')
+ end
- page.within('#add-ci-variable') do
- fill_in 'Key', with: 'akey'
- find('#ci-variable-value').set('akey_value')
- find('[data-testid="environment-scope"]').click
- find('[data-testid="ci-environment-search"]').set('review/*')
- find('[data-testid="create-wildcard-button"]').click
-
- click_button('Add variable')
- end
-
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="ci-variable-table"]') do
- expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
- end
+ page.within('[data-testid="ci-variable-table"]') do
+ expect(find('.js-ci-variable-row:first-child [data-label="Environments"]').text).to eq('review/*')
end
end
end
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 50e6eb66466..ee74ac84a73 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -2,228 +2,237 @@
require 'spec_helper'
-RSpec.describe 'User searches for code' do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository, namespace: user.namespace) }
-
- context 'when signed in' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'finds a file' do
- visit(project_path(project))
+RSpec.describe 'User searches for code', :js, :disable_rate_limiter do
+ using RSpec::Parameterized::TableSyntax
- submit_search('application.js')
- select_search_scope('Code')
+ let_it_be(:user) { create(:user) }
+ let_it_be_with_reload(:project) { create(:project, :repository, namespace: user.namespace) }
- expect(page).to have_selector('.results', text: 'application.js')
- expect(page).to have_selector('.file-content .code')
- expect(page).to have_selector("span.line[lang='javascript']")
- expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
- expect(page).to have_button('Copy file path')
- end
-
- context 'when on a project page', :js do
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ context 'when signed in' do
before do
- visit(search_path)
- find('[data-testid="project-filter"]').click
-
- wait_for_requests
-
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_maintainer(user)
+ sign_in(user)
end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'blobs'
+ it 'finds a file' do
+ visit(project_path(project))
- it 'finds code and links to blob' do
- fill_in('dashboard_search', with: 'rspec')
- find('.gl-search-box-by-click-search-button').click
+ submit_search('application.js')
+ select_search_scope('Code')
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
-
- find("#blob-L3").click
- expect(current_url).to match(%r{blob/master/.gitignore#L3})
+ expect(page).to have_selector('.results', text: 'application.js')
+ expect(page).to have_selector('.file-content .code')
+ expect(page).to have_selector("span.line[lang='javascript']")
+ expect(page).to have_link('application.js', href: %r{master/files/js/application.js})
+ expect(page).to have_button('Copy file path')
end
- it 'finds code and links to blame' do
- fill_in('dashboard_search', with: 'rspec')
- find('.gl-search-box-by-click-search-button').click
+ context 'when on a project page' do
+ before do
+ visit(search_path)
+ find('[data-testid="project-filter"]').click
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+ wait_for_requests
- find("#blame-L3").click
- expect(current_url).to match(%r{blame/master/.gitignore#L3})
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
+ end
- it 'search mutiple words with refs switching' do
- expected_result = 'Use `snake_case` for naming files'
- search = 'for naming files'
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'blobs' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- fill_in('dashboard_search', with: search)
- find('.gl-search-box-by-click-search-button').click
+ it 'finds code and links to blob' do
+ expected_result = 'Update capybara, rspec-rails, poltergeist to recent versions'
- expect(page).to have_selector('.results', text: expected_result)
+ fill_in('dashboard_search', with: 'rspec')
+ find('.gl-search-box-by-click-search-button').click
- find('.ref-selector').click
- wait_for_requests
+ expect(page).to have_selector('.results', text: expected_result)
- page.within('.ref-selector') do
- find('li', text: 'v1.0.0').click
+ find("#blob-L3").click
+ expect(current_url).to match(%r{blob/master/.gitignore#L3})
end
- expect(page).to have_selector('.results', text: expected_result)
+ it 'finds code and links to blame' do
+ expected_result = 'Update capybara, rspec-rails, poltergeist to recent versions'
- expect(find_field('dashboard_search').value).to eq(search)
- expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
- end
- end
+ fill_in('dashboard_search', with: 'rspec')
+ find('.gl-search-box-by-click-search-button').click
- context 'when :new_header_search is true' do
- context 'search code within refs', :js do
- let(:ref_name) { 'v1.0.0' }
+ expect(page).to have_selector('.results', text: expected_result)
- before do
- # This feature is diabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: true)
- visit(project_tree_path(project, ref_name))
-
- submit_search('gitlab-grack')
- select_search_scope('Code')
+ find("#blame-L3").click
+ expect(current_url).to match(%r{blame/master/.gitignore#L3})
end
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ it 'search multiple words with refs switching' do
+ expected_result = 'Use `snake_case` for naming files'
+ search = 'for naming files'
- it 'persists branch name across search' do
+ fill_in('dashboard_search', with: search)
find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
- end
- # this example is use to test the desgine that the refs is not
- # only repersent the branch as well as the tags.
- it 'ref swither list all the branchs and tags' do
+ expect(page).to have_selector('.results', text: expected_result)
+
find('.ref-selector').click
wait_for_requests
page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
+ find('li', text: 'v1.0.0').click
end
+
+ expect(page).to have_selector('.results', text: expected_result)
+
+ expect(find_field('dashboard_search').value).to eq(search)
+ expect(find("#blob-L1502")[:href]).to match(%r{blob/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
+ expect(find("#blame-L1502")[:href]).to match(%r{blame/v1.0.0/files/markdown/ruby-style-guide.md#L1502})
end
+ end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
+ context 'when :new_header_search is true' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- find('.ref-selector').click
- wait_for_requests
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: true)
+ visit(project_tree_path(project, ref_name))
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
+ end
+
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
+
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
+
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
wait_for_requests
- find('li', text: ref).click
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
- end
- end
- end
+ it 'search result changes when refs switched' do
+ ref = 'master'
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
- context 'when :new_header_search is false' do
- context 'search code within refs', :js do
- let(:ref_name) { 'v1.0.0' }
+ find('.ref-selector').click
+ wait_for_requests
- before do
- # This feature is diabled by default in spec_helper.rb.
- # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
- # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
- stub_feature_flags(new_header_search: false)
- visit(project_tree_path(project, ref_name))
-
- submit_search('gitlab-grack')
- select_search_scope('Code')
- end
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
- it 'shows ref switcher in code result summary' do
- expect(find('.ref-selector')).to have_text(ref_name)
- end
+ find('li', text: ref).click
+ end
- it 'persists branch name across search' do
- find('.gl-search-box-by-click-search-button').click
- expect(find('.ref-selector')).to have_text(ref_name)
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ end
end
+ end
- # this example is use to test the desgine that the refs is not
- # only repersent the branch as well as the tags.
- it 'ref swither list all the branchs and tags' do
- find('.ref-selector').click
- wait_for_requests
+ context 'when :new_header_search is false' do
+ context 'search code within refs' do
+ let(:ref_name) { 'v1.0.0' }
- page.within('.ref-selector') do
- expect(page).to have_selector('li', text: 'add-ipython-files')
- expect(page).to have_selector('li', text: 'v1.0.0')
+ before do
+ # This feature is disabled by default in spec_helper.rb.
+ # We missed a feature breaking bug, so to prevent this regression, testing both scenarios for this spec.
+ # This can be removed as part of closing https://gitlab.com/gitlab-org/gitlab/-/issues/339348.
+ stub_feature_flags(new_header_search: false)
+ visit(project_tree_path(project, ref_name))
+
+ submit_search('gitlab-grack')
+ select_search_scope('Code')
end
- end
- it 'search result changes when refs switched' do
- ref = 'master'
- expect(find('.results')).not_to have_content('path = gitlab-grack')
+ it 'shows ref switcher in code result summary' do
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- find('.ref-selector').click
- wait_for_requests
+ it 'persists branch name across search' do
+ find('.gl-search-box-by-click-search-button').click
+ expect(find('.ref-selector')).to have_text(ref_name)
+ end
- page.within('.ref-selector') do
- fill_in _('Search by Git revision'), with: ref
+ # this example is use to test the design that the refs is not
+ # only represent the branch as well as the tags.
+ it 'ref switcher list all the branches and tags' do
+ find('.ref-selector').click
wait_for_requests
- find('li', text: ref).click
+ page.within('.ref-selector') do
+ expect(page).to have_selector('li', text: 'add-ipython-files')
+ expect(page).to have_selector('li', text: 'v1.0.0')
+ end
end
- expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ it 'search result changes when refs switched' do
+ ref = 'master'
+ expect(find('.results')).not_to have_content('path = gitlab-grack')
+
+ find('.ref-selector').click
+ wait_for_requests
+
+ page.within('.ref-selector') do
+ fill_in _('Search by Git revision'), with: ref
+ wait_for_requests
+
+ find('li', text: ref).click
+ end
+
+ expect(page).to have_selector('.results', text: 'path = gitlab-grack')
+ end
end
end
- end
- it 'no ref switcher shown in issue result summary', :js do
- issue = create(:issue, title: 'test', project: project)
- visit(project_tree_path(project))
+ it 'no ref switcher shown in issue result summary' do
+ issue = create(:issue, title: 'test', project: project)
+ visit(project_tree_path(project))
- submit_search('test')
- select_search_scope('Code')
+ submit_search('test')
+ select_search_scope('Code')
- expect(page).to have_selector('.ref-selector')
+ expect(page).to have_selector('.ref-selector')
- select_search_scope('Issues')
+ select_search_scope('Issues')
- expect(find(:css, '.results')).to have_link(issue.title)
- expect(page).not_to have_selector('.ref-selector')
+ expect(find(:css, '.results')).to have_link(issue.title)
+ expect(page).not_to have_selector('.ref-selector')
+ end
end
- end
- context 'when signed out' do
- let(:project) { create(:project, :public, :repository) }
-
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- visit(project_path(project))
- end
+ context 'when signed out' do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ end
- it 'finds code' do
- submit_search('rspec')
- select_search_scope('Code')
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
- expect(page).to have_selector('.results', text: 'Update capybara, rspec-rails, poltergeist to recent versions')
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
+ end
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_comments_spec.rb b/spec/features/search/user_searches_for_comments_spec.rb
index a6793bc3aa7..3c39e9f41d4 100644
--- a/spec/features/search/user_searches_for_comments_spec.rb
+++ b/spec/features/search/user_searches_for_comments_spec.rb
@@ -2,45 +2,52 @@
require 'spec_helper'
-RSpec.describe 'User searches for comments' do
- let(:project) { create(:project, :repository) }
- let(:user) { create(:user) }
+RSpec.describe 'User searches for comments', :js, :disable_rate_limiter do
+ using RSpec::Parameterized::TableSyntax
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_reporter(user)
- sign_in(user)
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:user) { create(:user) }
- visit(project_path(project))
- end
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_reporter(user)
+ sign_in(user)
- include_examples 'search timeouts', 'notes'
+ visit(project_path(project))
+ end
- context 'when a comment is in commits' do
- context 'when comment belongs to an invalid commit' do
- let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
+ include_examples 'search timeouts', 'notes' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- it 'finds a commit' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ context 'when a comment is in commits' do
+ context 'when comment belongs to an invalid commit' do
+ let(:comment) { create(:note_on_commit, author: user, project: project, commit_id: 12345678, note: 'Bug here') }
- page.within('.results') do
- expect(page).to have_content('Commit deleted')
- expect(page).to have_content('12345678')
+ it 'finds a commit' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
+
+ page.within('.results') do
+ expect(page).to have_content('Commit deleted')
+ expect(page).to have_content('12345678')
+ end
end
end
end
- end
- context 'when a comment is in a snippet' do
- let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
- let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
+ context 'when a comment is in a snippet' do
+ let(:snippet) { create(:project_snippet, :private, project: project, author: user, title: 'Some title') }
+ let(:comment) { create(:note, noteable: snippet, author: user, note: 'Supercalifragilisticexpialidocious', project: project) }
- it 'finds a snippet' do
- submit_search(comment.note)
- select_search_scope('Comments')
+ it 'finds a snippet' do
+ submit_search(comment.note)
+ select_search_scope('Comments')
- expect(page).to have_selector('.results', text: snippet.title)
+ expect(page).to have_selector('.results', text: snippet.title)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_commits_spec.rb b/spec/features/search/user_searches_for_commits_spec.rb
index 4ec2a9e6cff..e5d86c27942 100644
--- a/spec/features/search/user_searches_for_commits_spec.rb
+++ b/spec/features/search/user_searches_for_commits_spec.rb
@@ -2,54 +2,62 @@
require 'spec_helper'
-RSpec.describe 'User searches for commits', :js do
+RSpec.describe 'User searches for commits', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, :repository) }
let(:sha) { '6d394385cf567f80a8fd85055db1ab4c5295806f' }
- let(:user) { create(:user) }
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_reporter(user)
- sign_in(user)
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_reporter(user)
+ sign_in(user)
- visit(search_path(project_id: project.id))
- end
+ visit(search_path(project_id: project.id))
+ end
- include_examples 'search timeouts', 'commits'
+ include_examples 'search timeouts', 'commits' do
+ let(:additional_params) { { project_id: project.id } }
+ end
- context 'when searching by SHA' do
- it 'finds a commit and redirects to its page' do
- submit_search(sha)
+ context 'when searching by SHA' do
+ it 'finds a commit and redirects to its page' do
+ submit_search(sha)
- expect(page).to have_current_path(project_commit_path(project, sha))
- end
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
- it 'finds a commit in uppercase and redirects to its page' do
- submit_search(sha.upcase)
+ it 'finds a commit in uppercase and redirects to its page' do
+ submit_search(sha.upcase)
- expect(page).to have_current_path(project_commit_path(project, sha))
+ expect(page).to have_current_path(project_commit_path(project, sha))
+ end
end
- end
- context 'when searching by message' do
- it 'finds a commit and holds on /search page' do
- project.repository.commit_files(
- user,
- message: 'Message referencing another sha: "deadbeef"',
- branch_name: 'master',
- actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
- )
+ context 'when searching by message' do
+ it 'finds a commit and holds on /search page' do
+ project.repository.commit_files(
+ user,
+ message: 'Message referencing another sha: "deadbeef"',
+ branch_name: 'master',
+ actions: [{ action: :create, file_path: 'a/new.file', contents: 'new file' }]
+ )
- submit_search('deadbeef')
+ submit_search('deadbeef')
- expect(page).to have_current_path('/search', ignore_query: true)
- end
+ expect(page).to have_current_path('/search', ignore_query: true)
+ end
- it 'finds multiple commits' do
- submit_search('See merge request')
- select_search_scope('Commits')
+ it 'finds multiple commits' do
+ submit_search('See merge request')
+ select_search_scope('Commits')
- expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
+ expect(page).to have_selector('.commit-row-description', visible: false, count: 9)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index 51d2f355848..22d48bd38f2 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -2,9 +2,12 @@
require 'spec_helper'
-RSpec.describe 'User searches for issues', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
+RSpec.describe 'User searches for issues', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
+
let!(:issue1) { create(:issue, title: 'issue Foo', project: project, created_at: 1.hour.ago) }
let!(:issue2) { create(:issue, :closed, :confidential, title: 'issue Bar', project: project) }
@@ -14,127 +17,133 @@ RSpec.describe 'User searches for issues', :js do
select_search_scope('Issues')
end
- context 'when signed in' do
- before do
- project.add_maintainer(user)
- sign_in(user)
- stub_feature_flags(search_page_vertical_nav: false)
-
- visit(search_path)
- end
+ where(search_page_vertical_nav_enabled: [true, false])
- include_examples 'top right search form'
- include_examples 'search timeouts', 'issues'
+ with_them do
+ context 'when signed in' do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- it 'finds an issue' do
- search_for_issue(issue1.title)
+ project.add_maintainer(user)
+ sign_in(user)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ visit(search_path)
end
- end
-
- it 'hides confidential icon for non-confidential issues' do
- search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
- end
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'issues'
- it 'shows confidential icon for confidential issues' do
- search_for_issue(issue2.title)
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_css('[data-testid="eye-slash-icon"]')
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
end
- end
- it 'shows correct badge for open issues' do
- search_for_issue(issue1.title)
+ it 'hides confidential icon for non-confidential issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_css('.badge-success')
- expect(page).not_to have_css('.badge-info')
+ page.within('.results') do
+ expect(page).not_to have_css('[data-testid="eye-slash-icon"]')
+ end
end
- end
- it 'shows correct badge for closed issues' do
- search_for_issue(issue2.title)
+ it 'shows confidential icon for confidential issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page).not_to have_css('.badge-success')
- expect(page).to have_css('.badge-info')
+ page.within('.results') do
+ expect(page).to have_css('[data-testid="eye-slash-icon"]')
+ end
end
- end
- it 'sorts by created date' do
- search_for_issue('issue')
+ it 'shows correct badge for open issues' do
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue2.title)
- expect(page.all('.search-result-row').last).to have_link(issue1.title)
+ page.within('.results') do
+ expect(page).to have_css('.badge-success')
+ expect(page).not_to have_css('.badge-info')
+ end
end
- find('[data-testid="sort-highest-icon"]').click
+ it 'shows correct badge for closed issues' do
+ search_for_issue(issue2.title)
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(issue1.title)
- expect(page.all('.search-result-row').last).to have_link(issue2.title)
+ page.within('.results') do
+ expect(page).not_to have_css('.badge-success')
+ expect(page).to have_css('.badge-info')
+ end
end
- end
-
- context 'when on a project page' do
- it 'finds an issue' do
- find('[data-testid="project-filter"]').click
- wait_for_requests
+ it 'sorts by created date' do
+ search_for_issue('issue')
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(issue2.title)
+ expect(page.all('.search-result-row').last).to have_link(issue1.title)
end
- search_for_issue(issue1.title)
+ find('[data-testid="sort-highest-icon"]').click
page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ expect(page.all('.search-result-row').first).to have_link(issue1.title)
+ expect(page.all('.search-result-row').last).to have_link(issue2.title)
end
end
- end
- end
- context 'when signed out' do
- context 'when block_anonymous_global_searches is disabled' do
- let(:project) { create(:project, :public) }
+ context 'when on a project page' do
+ it 'finds an issue' do
+ find('[data-testid="project-filter"]').click
- before do
- stub_feature_flags(block_anonymous_global_searches: false)
- stub_feature_flags(search_page_vertical_nav: false)
- visit(search_path)
- end
+ wait_for_requests
- include_examples 'top right search form'
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- it 'finds an issue' do
- search_for_issue(issue1.title)
+ search_for_issue(issue1.title)
- page.within('.results') do
- expect(page).to have_link(issue1.title)
- expect(page).not_to have_link(issue2.title)
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
end
end
end
- context 'when block_anonymous_global_searches is enabled' do
+ context 'when signed out' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
- visit(search_path)
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
end
- it 'is redirected to login page' do
- expect(page).to have_content('You must be logged in to search across all of GitLab')
+ context 'when block_anonymous_global_searches is disabled' do
+ let_it_be(:project) { create(:project, :public) }
+
+ before do
+ stub_feature_flags(block_anonymous_global_searches: false)
+
+ visit(search_path)
+ end
+
+ include_examples 'top right search form'
+
+ it 'finds an issue' do
+ search_for_issue(issue1.title)
+
+ page.within('.results') do
+ expect(page).to have_link(issue1.title)
+ expect(page).not_to have_link(issue2.title)
+ end
+ end
+ end
+
+ context 'when block_anonymous_global_searches is enabled' do
+ it 'is redirected to login page' do
+ visit(search_path)
+
+ expect(page).to have_content('You must be logged in to search across all of GitLab')
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index a4fbe3a6e59..9bbf2cf16d8 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -2,7 +2,9 @@
require 'spec_helper'
-RSpec.describe 'User searches for merge requests', :js do
+RSpec.describe 'User searches for merge requests', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
let!(:merge_request1) { create(:merge_request, title: 'Merge Request Foo', source_project: project, target_project: project, created_at: 1.hour.ago) }
@@ -14,62 +16,64 @@ RSpec.describe 'User searches for merge requests', :js do
select_search_scope('Merge requests')
end
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ sign_in(user)
- visit(search_path)
- end
+ visit(search_path)
+ end
- include_examples 'top right search form'
- include_examples 'search timeouts', 'merge_requests'
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'merge_requests'
- it 'finds a merge request' do
- search_for_mr(merge_request1.title)
+ it 'finds a merge request' do
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
- # Each result should have MR refs like `gitlab-org/gitlab!1`
- page.all('.search-result-row').each do |e|
- expect(e.text).to match(/!\d+/)
+ # Each result should have MR refs like `gitlab-org/gitlab!1`
+ page.all('.search-result-row').each do |e|
+ expect(e.text).to match(/!\d+/)
+ end
end
end
- end
- it 'sorts by created date' do
- search_for_mr('Merge Request')
+ it 'sorts by created date' do
+ search_for_mr('Merge Request')
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
- end
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request2.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request1.title)
+ end
- find('[data-testid="sort-highest-icon"]').click
+ find('[data-testid="sort-highest-icon"]').click
- page.within('.results') do
- expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
- expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page.all('.search-result-row').first).to have_link(merge_request1.title)
+ expect(page.all('.search-result-row').last).to have_link(merge_request2.title)
+ end
end
- end
- context 'when on a project page' do
- it 'finds a merge request' do
- find('[data-testid="project-filter"]').click
+ context 'when on a project page' do
+ it 'finds a merge request' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- search_for_mr(merge_request1.title)
+ search_for_mr(merge_request1.title)
- page.within('.results') do
- expect(page).to have_link(merge_request1.title)
- expect(page).not_to have_link(merge_request2.title)
+ page.within('.results') do
+ expect(page).to have_link(merge_request1.title)
+ expect(page).not_to have_link(merge_request2.title)
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 6773059830c..702d4e60022 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -2,44 +2,30 @@
require 'spec_helper'
-RSpec.describe 'User searches for milestones', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
- let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
- let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
+RSpec.describe 'User searches for milestones', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
- before do
- project.add_maintainer(user)
- sign_in(user)
- stub_feature_flags(search_page_vertical_nav: false)
+ let_it_be(:user) { create(:user) }
+ let_it_be(:project) { create(:project, namespace: user.namespace) }
- visit(search_path)
- end
+ let!(:milestone1) { create(:milestone, title: 'Foo', project: project) }
+ let!(:milestone2) { create(:milestone, title: 'Bar', project: project) }
- include_examples 'top right search form'
- include_examples 'search timeouts', 'milestones'
+ where(search_page_vertical_nav_enabled: [true, false])
- it 'finds a milestone' do
- fill_in('dashboard_search', with: milestone1.title)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Milestones')
+ with_them do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- page.within('.results') do
- expect(page).to have_link(milestone1.title)
- expect(page).not_to have_link(milestone2.title)
+ visit(search_path)
end
- end
-
- context 'when on a project page' do
- it 'finds a milestone' do
- find('[data-testid="project-filter"]').click
-
- wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'milestones'
+ it 'finds a milestone' do
fill_in('dashboard_search', with: milestone1.title)
find('.gl-search-box-by-click-search-button').click
select_search_scope('Milestones')
@@ -49,5 +35,26 @@ RSpec.describe 'User searches for milestones', :js do
expect(page).not_to have_link(milestone2.title)
end
end
+
+ context 'when on a project page' do
+ it 'finds a milestone' do
+ find('[data-testid="project-filter"]').click
+
+ wait_for_requests
+
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
+
+ fill_in('dashboard_search', with: milestone1.title)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Milestones')
+
+ page.within('.results') do
+ expect(page).to have_link(milestone1.title)
+ expect(page).not_to have_link(milestone2.title)
+ end
+ end
+ end
end
end
diff --git a/spec/features/search/user_searches_for_projects_spec.rb b/spec/features/search/user_searches_for_projects_spec.rb
index 5902859d1f5..15c6224b61b 100644
--- a/spec/features/search/user_searches_for_projects_spec.rb
+++ b/spec/features/search/user_searches_for_projects_spec.rb
@@ -2,15 +2,12 @@
require 'spec_helper'
-RSpec.describe 'User searches for projects', :js do
+RSpec.describe 'User searches for projects', :js, :disable_rate_limiter do
let!(:project) { create(:project, :public, name: 'Shop') }
context 'when signed out' do
context 'when block_anonymous_global_searches is disabled' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
stub_feature_flags(block_anonymous_global_searches: false)
end
diff --git a/spec/features/search/user_searches_for_users_spec.rb b/spec/features/search/user_searches_for_users_spec.rb
index e21a66fed92..1d649b42c8c 100644
--- a/spec/features/search/user_searches_for_users_spec.rb
+++ b/spec/features/search/user_searches_for_users_spec.rb
@@ -2,84 +2,90 @@
require 'spec_helper'
-RSpec.describe 'User searches for users' do
- let(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
- let(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
- let(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
-
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- sign_in(user1)
- end
-
- include_examples 'search timeouts', 'users'
+RSpec.describe 'User searches for users', :js, :clean_gitlab_redis_rate_limiting do
+ let_it_be(:user1) { create(:user, username: 'gob_bluth', name: 'Gob Bluth') }
+ let_it_be(:user2) { create(:user, username: 'michael_bluth', name: 'Michael Bluth') }
+ let_it_be(:user3) { create(:user, username: 'gob_2018', name: 'George Oscar Bluth') }
- context 'when on the dashboard' do
- it 'finds the user', :js do
- visit dashboard_projects_path
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
- submit_search('gob')
- select_search_scope('Users')
+ sign_in(user1)
+ end
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ include_examples 'search timeouts', 'users' do
+ before do
+ visit(search_path)
end
end
- end
- context 'when on the project page' do
- let(:project) { create(:project) }
+ context 'when on the dashboard' do
+ it 'finds the user' do
+ visit dashboard_projects_path
- before do
- create(:project_member, :developer, user: user1, project: project)
- create(:project_member, :developer, user: user2, project: project)
- user3
+ submit_search('gob')
+ select_search_scope('Users')
+
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
+ end
+ end
end
- it 'finds the user belonging to the project' do
- visit project_path(project)
+ context 'when on the project page' do
+ let_it_be_with_reload(:project) { create(:project) }
- submit_search('gob')
- select_search_scope('Users')
+ before do
+ project.add_developer(user1)
+ project.add_developer(user2)
+ end
+
+ it 'finds the user belonging to the project' do
+ visit project_path(project)
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ submit_search('gob')
+ select_search_scope('Users')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
+
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
end
end
- end
- context 'when on the group page' do
- let(:group) { create(:group) }
+ context 'when on the group page' do
+ let(:group) { create(:group) }
- before do
- create(:group_member, :developer, user: user1, group: group)
- create(:group_member, :developer, user: user2, group: group)
- user3
- end
+ before do
+ group.add_developer(user1)
+ group.add_developer(user2)
+ end
- it 'finds the user belonging to the group' do
- visit group_path(group)
+ it 'finds the user belonging to the group' do
+ visit group_path(group)
- submit_search('gob')
- select_search_scope('Users')
+ submit_search('gob')
+ select_search_scope('Users')
- page.within('.results') do
- expect(page).to have_content('Gob Bluth')
- expect(page).to have_content('@gob_bluth')
+ page.within('.results') do
+ expect(page).to have_content('Gob Bluth')
+ expect(page).to have_content('@gob_bluth')
- expect(page).not_to have_content('Michael Bluth')
- expect(page).not_to have_content('@michael_bluth')
+ expect(page).not_to have_content('Michael Bluth')
+ expect(page).not_to have_content('@michael_bluth')
- expect(page).not_to have_content('George Oscar Bluth')
- expect(page).not_to have_content('@gob_2018')
+ expect(page).not_to have_content('George Oscar Bluth')
+ expect(page).not_to have_content('@gob_2018')
+ end
end
end
end
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 2e390309022..0f20ad0aa07 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -2,55 +2,59 @@
require 'spec_helper'
-RSpec.describe 'User searches for wiki pages', :js do
- let(:user) { create(:user) }
+RSpec.describe 'User searches for wiki pages', :js, :clean_gitlab_redis_rate_limiting do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:user) { create(:user) }
+
let(:project) { create(:project, :repository, :wiki_repo, namespace: user.namespace) }
let!(:wiki_page) { create(:wiki_page, wiki: project.wiki, title: 'directory/title', content: 'Some Wiki content') }
- before do
- stub_feature_flags(search_page_vertical_nav: false)
- project.add_maintainer(user)
- sign_in(user)
-
- visit(search_path)
- end
+ where(search_page_vertical_nav_enabled: [true, false])
+ with_them do
+ before do
+ stub_feature_flags(search_page_vertical_nav: search_page_vertical_nav_enabled)
+ project.add_maintainer(user)
+ sign_in(user)
- include_examples 'top right search form'
- include_examples 'search timeouts', 'wiki_blobs'
+ visit(search_path)
+ end
- shared_examples 'search wiki blobs' do
- before do
- stub_feature_flags(search_page_vertical_nav: false)
+ include_examples 'top right search form'
+ include_examples 'search timeouts', 'wiki_blobs' do
+ let(:additional_params) { { project_id: project.id } }
end
- it 'finds a page' do
- find('[data-testid="project-filter"]').click
+ shared_examples 'search wiki blobs' do
+ it 'finds a page' do
+ find('[data-testid="project-filter"]').click
- wait_for_requests
+ wait_for_requests
- page.within('[data-testid="project-filter"]') do
- click_on(project.name)
- end
+ page.within('[data-testid="project-filter"]') do
+ click_on(project.name)
+ end
- fill_in('dashboard_search', with: search_term)
- find('.gl-search-box-by-click-search-button').click
- select_search_scope('Wiki')
+ fill_in('dashboard_search', with: search_term)
+ find('.gl-search-box-by-click-search-button').click
+ select_search_scope('Wiki')
- page.within('.results') do
- expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ page.within('.results') do
+ expect(page).to have_link(wiki_page.title, href: project_wiki_path(project, wiki_page.slug))
+ end
end
end
- end
- context 'when searching by content' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'content' }
+ context 'when searching by content' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'content' }
+ end
end
- end
- context 'when searching by title' do
- it_behaves_like 'search wiki blobs' do
- let(:search_term) { 'title' }
+ context 'when searching by title' do
+ it_behaves_like 'search wiki blobs' do
+ let(:search_term) { 'title' }
+ end
end
end
end
diff --git a/spec/features/search/user_uses_header_search_field_spec.rb b/spec/features/search/user_uses_header_search_field_spec.rb
index 827e3984896..04f22cd2a31 100644
--- a/spec/features/search/user_uses_header_search_field_spec.rb
+++ b/spec/features/search/user_uses_header_search_field_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe 'User uses header search field', :js do
+RSpec.describe 'User uses header search field', :js, :disable_rate_limiter do
include FilteredSearchHelpers
let_it_be(:project) { create(:project, :repository) }
@@ -17,10 +17,6 @@ RSpec.describe 'User uses header search field', :js do
end
before do
- stub_feature_flags(search_page_vertical_nav: false)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).and_return(0)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit).and_return(1000)
- allow(Gitlab::ApplicationRateLimiter).to receive(:threshold).with(:search_rate_limit_unauthenticated).and_return(1000)
sign_in(user)
end
diff --git a/spec/fixtures/lib/sbom/package-url-test-cases.json b/spec/fixtures/lib/sbom/package-url-test-cases.json
new file mode 100644
index 00000000000..448387397f6
--- /dev/null
+++ b/spec/fixtures/lib/sbom/package-url-test-cases.json
@@ -0,0 +1,502 @@
+[
+ {
+ "description": "valid maven purl",
+ "purl": "pkg:maven/org.apache.commons/io@1.3.4",
+ "canonical_purl": "pkg:maven/org.apache.commons/io@1.3.4",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": "1.3.4",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "basic valid maven purl without version",
+ "purl": "pkg:maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid go purl without version and with subpath",
+ "purl": "pkg:GOLANG/google.golang.org/genproto#/googleapis/api/annotations/",
+ "canonical_purl": "pkg:golang/google.golang.org/genproto#googleapis/api/annotations",
+ "type": "golang",
+ "namespace": "google.golang.org",
+ "name": "genproto",
+ "version": null,
+ "qualifiers": null,
+ "subpath": "googleapis/api/annotations",
+ "is_invalid": false
+ },
+ {
+ "description": "valid go purl with version and subpath",
+ "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/api/annotations/",
+ "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations",
+ "type": "golang",
+ "namespace": "google.golang.org",
+ "name": "genproto",
+ "version": "abcdedf",
+ "qualifiers": null,
+ "subpath": "googleapis/api/annotations",
+ "is_invalid": false
+ },
+ {
+ "description": "bitbucket namespace and name should be lowercased",
+ "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c",
+ "canonical_purl": "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c",
+ "type": "bitbucket",
+ "namespace": "birkenfeld",
+ "name": "pygments-main",
+ "version": "244fd47e07d1014f0aed9c",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "github namespace and name should be lowercased",
+ "purl": "pkg:github/Package-url/purl-Spec@244fd47e07d1004f0aed9c",
+ "canonical_purl": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
+ "type": "github",
+ "namespace": "package-url",
+ "name": "purl-spec",
+ "version": "244fd47e07d1004f0aed9c",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "debian can use qualifiers",
+ "purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
+ "canonical_purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
+ "type": "deb",
+ "namespace": "debian",
+ "name": "curl",
+ "version": "7.50.3-1",
+ "qualifiers": {
+ "arch": "i386",
+ "distro": "jessie"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "docker uses qualifiers and hash image id as versions",
+ "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io",
+ "canonical_purl": "pkg:docker/customer/dockerimage@sha256%3A244fd47e07d1004f0aed9c?repository_url=gcr.io",
+ "type": "docker",
+ "namespace": "customer",
+ "name": "dockerimage",
+ "version": "sha256:244fd47e07d1004f0aed9c",
+ "qualifiers": {
+ "repository_url": "gcr.io"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "Java gem can use a qualifier",
+ "purl": "pkg:gem/jruby-launcher@1.1.2?Platform=java",
+ "canonical_purl": "pkg:gem/jruby-launcher@1.1.2?platform=java",
+ "type": "gem",
+ "namespace": null,
+ "name": "jruby-launcher",
+ "version": "1.1.2",
+ "qualifiers": {
+ "platform": "java"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven often uses qualifiers",
+ "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repositorY_url=repo.spring.io/release",
+ "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io%2Frelease",
+ "type": "maven",
+ "namespace": "org.apache.xmlgraphics",
+ "name": "batik-anim",
+ "version": "1.9.1",
+ "qualifiers": {
+ "classifier": "sources",
+ "repository_url": "repo.spring.io/release"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven pom reference",
+ "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repositorY_url=repo.spring.io/release",
+ "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io%2Frelease",
+ "type": "maven",
+ "namespace": "org.apache.xmlgraphics",
+ "name": "batik-anim",
+ "version": "1.9.1",
+ "qualifiers": {
+ "extension": "pom",
+ "repository_url": "repo.spring.io/release"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "maven can come with a type qualifier",
+ "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll",
+ "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll",
+ "type": "maven",
+ "namespace": "net.sf.jacob-project",
+ "name": "jacob",
+ "version": "1.14.3",
+ "qualifiers": {
+ "classifier": "x86",
+ "type": "dll"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "npm can be scoped",
+ "purl": "pkg:npm/%40angular/animation@12.3.1",
+ "canonical_purl": "pkg:npm/%40angular/animation@12.3.1",
+ "type": "npm",
+ "namespace": "@angular",
+ "name": "animation",
+ "version": "12.3.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "nuget names are case sensitive",
+ "purl": "pkg:Nuget/EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "pkg:nuget/EnterpriseLibrary.Common@6.0.1304",
+ "type": "nuget",
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": "6.0.1304",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "pypi names have special rules and not case sensitive",
+ "purl": "pkg:PYPI/Django_package@1.11.1.dev1",
+ "canonical_purl": "pkg:pypi/django-package@1.11.1.dev1",
+ "type": "pypi",
+ "namespace": null,
+ "name": "django-package",
+ "version": "1.11.1.dev1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "rpm often use qualifiers",
+ "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25",
+ "canonical_purl": "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25",
+ "type": "rpm",
+ "namespace": "fedora",
+ "name": "curl",
+ "version": "7.50.3-1.fc25",
+ "qualifiers": {
+ "arch": "i386",
+ "distro": "fedora-25"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "a scheme is always required",
+ "purl": "EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "EnterpriseLibrary.Common@6.0.1304",
+ "type": null,
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "a type is always required",
+ "purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
+ "canonical_purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
+ "type": null,
+ "namespace": null,
+ "name": "EnterpriseLibrary.Common",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "a name is required",
+ "purl": "pkg:maven/@1.3.4",
+ "canonical_purl": "pkg:maven/@1.3.4",
+ "type": "maven",
+ "namespace": null,
+ "name": null,
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "slash / after scheme is not significant",
+ "purl": "pkg:/maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "double slash // after scheme is not significant",
+ "purl": "pkg://maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "slash /// after type is not significant",
+ "purl": "pkg:///maven/org.apache.commons/io",
+ "canonical_purl": "pkg:maven/org.apache.commons/io",
+ "type": "maven",
+ "namespace": "org.apache.commons",
+ "name": "io",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid maven purl with case sensitive namespace and name",
+ "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
+ "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
+ "type": "maven",
+ "namespace": "HTTPClient",
+ "name": "HTTPClient",
+ "version": "0.3-3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid maven purl containing a space in the version and qualifier",
+ "purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value",
+ "canonical_purl": "pkg:maven/mygroup/myartifact@1.0.0+Final?mykey=my+value",
+ "type": "maven",
+ "namespace": "mygroup",
+ "name": "myartifact",
+ "version": "1.0.0 Final",
+ "qualifiers": {
+ "mykey": "my value"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "checks for invalid qualifier keys",
+ "purl": "pkg:npm/myartifact@1.0.0?in%20production=true",
+ "canonical_purl": null,
+ "type": "npm",
+ "namespace": null,
+ "name": "myartifact",
+ "version": "1.0.0",
+ "qualifiers": {
+ "in production": "true"
+ },
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid conan purl",
+ "purl": "pkg:conan/cctz@2.3",
+ "canonical_purl": "pkg:conan/cctz@2.3",
+ "type": "conan",
+ "namespace": null,
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid conan purl with namespace and qualifier channel",
+ "purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable",
+ "canonical_purl": "pkg:conan/bincrafters/cctz@2.3?channel=stable",
+ "type": "conan",
+ "namespace": "bincrafters",
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": {
+ "channel": "stable"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid conan purl only namespace",
+ "purl": "pkg:conan/bincrafters/cctz@2.3",
+ "canonical_purl": "pkg:conan/bincrafters/cctz@2.3",
+ "type": "conan",
+ "namespace": "bincrafters",
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid conan purl only channel qualifier",
+ "purl": "pkg:conan/cctz@2.3?channel=stable",
+ "canonical_purl": "pkg:conan/cctz@2.3?channel=stable",
+ "type": "conan",
+ "namespace": null,
+ "name": "cctz",
+ "version": "2.3",
+ "qualifiers": {
+ "channel": "stable"
+ },
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid conda purl with qualifiers",
+ "purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2",
+ "canonical_purl": "pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2",
+ "type": "conda",
+ "namespace": null,
+ "name": "absl-py",
+ "version": "0.4.1",
+ "qualifiers": {
+ "build": "py36h06a4308_0",
+ "channel": "main",
+ "subdir": "linux-64",
+ "type": "tar.bz2"
+ },
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "valid cran purl",
+ "purl": "pkg:cran/A3@0.9.1",
+ "canonical_purl": "pkg:cran/A3@0.9.1",
+ "type": "cran",
+ "namespace": null,
+ "name": "A3",
+ "version": "0.9.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid cran purl without name",
+ "purl": "pkg:cran/@0.9.1",
+ "canonical_purl": "pkg:cran/@0.9.1",
+ "type": "cran",
+ "namespace": null,
+ "name": null,
+ "version": "0.9.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid cran purl without version",
+ "purl": "pkg:cran/A3",
+ "canonical_purl": "pkg:cran/A3",
+ "type": "cran",
+ "namespace": null,
+ "name": "A3",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid swift purl",
+ "purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire@5.4.3",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": "Alamofire",
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "invalid swift purl without namespace",
+ "purl": "pkg:swift/Alamofire@5.4.3",
+ "canonical_purl": "pkg:swift/Alamofire@5.4.3",
+ "type": "swift",
+ "namespace": null,
+ "name": "Alamofire",
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid swift purl without name",
+ "purl": "pkg:swift/github.com/Alamofire/@5.4.3",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/@5.4.3",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": null,
+ "version": "5.4.3",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "invalid swift purl without version",
+ "purl": "pkg:swift/github.com/Alamofire/Alamofire",
+ "canonical_purl": "pkg:swift/github.com/Alamofire/Alamofire",
+ "type": "swift",
+ "namespace": "github.com/Alamofire",
+ "name": "Alamofire",
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ },
+ {
+ "description": "valid hackage purl",
+ "purl": "pkg:hackage/AC-HalfInteger@1.2.1",
+ "canonical_purl": "pkg:hackage/AC-HalfInteger@1.2.1",
+ "type": "hackage",
+ "namespace": null,
+ "name": "AC-HalfInteger",
+ "version": "1.2.1",
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": false
+ },
+ {
+ "description": "name and version are always required",
+ "purl": "pkg:hackage",
+ "canonical_purl": "pkg:hackage",
+ "type": "hackage",
+ "namespace": null,
+ "name": null,
+ "version": null,
+ "qualifiers": null,
+ "subpath": null,
+ "is_invalid": true
+ }
+] \ No newline at end of file
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
new file mode 100644
index 00000000000..6aab3b51806
--- /dev/null
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -0,0 +1,139 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Delete merged branches component Delete merged branches confirmation modal matches snapshot 1`] = `
+<div>
+ <b-button-stub
+ class="gl-mr-3 gl-button btn-danger-secondary"
+ data-qa-selector="delete_merged_branches_button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+
+ </span>
+ </b-button-stub>
+
+ <div>
+ <form
+ action="/namespace/project/-/merged_branches"
+ method="post"
+ >
+ <p>
+ You are about to
+ <strong>
+ delete all branches
+ </strong>
+ that were merged into
+ <code>
+ master
+ </code>
+ .
+ </p>
+
+ <p>
+
+ This may include merged branches that are not visible on the current screen.
+
+ </p>
+
+ <p>
+
+ A branch won't be deleted if it is protected or associated with an open merge request.
+
+ </p>
+
+ <p>
+ This bulk action is
+ <strong>
+ permanent and cannot be undone or recovered
+ </strong>
+ .
+ </p>
+
+ <p>
+ Plese type the following to confirm:
+ <code>
+ delete
+ </code>
+ .
+ <b-form-input-stub
+ aria-labelledby="input-label"
+ autocomplete="off"
+ class="gl-form-input gl-mt-2 gl-form-input-sm"
+ data-qa-selector="delete_merged_branches_input"
+ debounce="0"
+ formatter="[Function]"
+ type="text"
+ value=""
+ />
+ </p>
+
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="mock-csrf-token"
+ />
+ </form>
+ <div
+ class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0 gl-mr-3"
+ >
+ <b-button-stub
+ class="gl-button"
+ data-testid="delete-merged-branches-cancel-button"
+ size="md"
+ tag="button"
+ type="button"
+ variant="default"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Cancel
+
+ </span>
+ </b-button-stub>
+
+ <b-button-stub
+ class="gl-button"
+ data-qa-selector="delete_merged_branches_confirmation_button"
+ data-testid="delete-merged-branches-confirmation-button"
+ disabled="true"
+ size="md"
+ tag="button"
+ type="button"
+ variant="danger"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Delete merged branches
+ </span>
+ </b-button-stub>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
new file mode 100644
index 00000000000..4f1e772f4a4
--- /dev/null
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -0,0 +1,143 @@
+import { GlButton, GlModal, GlFormInput, GlSprintf } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import DeleteMergedBranches, { i18n } from '~/branches/components/delete_merged_branches.vue';
+import { formPath, propsDataMock } from '../mock_data';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+let wrapper;
+
+const stubsData = {
+ GlModal: stubComponent(GlModal, {
+ template:
+ '<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ }),
+ GlButton,
+ GlFormInput,
+ GlSprintf,
+};
+
+const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ wrapper = mountFn(DeleteMergedBranches, {
+ propsData: {
+ ...propsDataMock,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs,
+ });
+};
+
+const findDeleteButton = () => wrapper.findComponent(GlButton);
+const findModal = () => wrapper.findComponent(GlModal);
+const findConfirmationButton = () =>
+ wrapper.findByTestId('delete-merged-branches-confirmation-button');
+const findCancelButton = () => wrapper.findByTestId('delete-merged-branches-cancel-button');
+const findFormInput = () => wrapper.findComponent(GlFormInput);
+const findForm = () => wrapper.find('form');
+const submitFormSpy = () => jest.spyOn(wrapper.vm.$refs.form, 'submit');
+
+describe('Delete merged branches component', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Delete merged branches button', () => {
+ it('has correct attributes, text and tooltip', () => {
+ expect(findDeleteButton().attributes()).toMatchObject({
+ category: 'secondary',
+ variant: 'danger',
+ });
+
+ expect(findDeleteButton().text()).toBe(i18n.deleteButtonText);
+ });
+
+ it('displays a tooltip', () => {
+ const tooltip = getBinding(findDeleteButton().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(wrapper.vm.buttonTooltipText);
+ });
+
+ it('opens modal when clicked', () => {
+ createComponent(mount);
+ jest.spyOn(wrapper.vm.$refs.modal, 'show');
+ findDeleteButton().trigger('click');
+
+ expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ });
+ });
+
+ describe('Delete merged branches confirmation modal', () => {
+ beforeEach(() => {
+ createComponent(shallowMountExtended, stubsData);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders correct modal title and text', () => {
+ const modalText = findModal().text();
+ expect(findModal().props('title')).toBe(i18n.modalTitle);
+ expect(modalText).toContain(i18n.notVisibleBranchesWarning);
+ expect(modalText).toContain(i18n.protectedBranchWarning);
+ });
+
+ it('renders confirm and cancel buttons with correct text', () => {
+ expect(findConfirmationButton().text()).toContain(i18n.deleteButtonText);
+ expect(findCancelButton().text()).toContain(i18n.cancelButtonText);
+ });
+
+ it('renders form with correct attributes and hiden inputs', () => {
+ const form = findForm();
+ expect(form.attributes()).toEqual({
+ action: formPath,
+ method: 'post',
+ });
+ expect(form.find('input[name="_method"]').attributes('value')).toBe('delete');
+ expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'mock-csrf-token',
+ );
+ });
+
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a disabled confirm button by default', () => {
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ });
+
+ it('keeps disabled state when wrong input is provided', async () => {
+ findFormInput().vm.$emit('input', 'hello');
+ await waitForPromises();
+ expect(findConfirmationButton().props('disabled')).toBe(true);
+ findConfirmationButton().trigger('click');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ findFormInput().trigger('keyup.enter');
+
+ expect(submitFormSpy()).not.toHaveBeenCalled();
+ });
+
+ it('submits form when correct amount is provided and the confirm button is clicked', async () => {
+ findFormInput().vm.$emit('input', 'delete');
+ await waitForPromises();
+ expect(findDeleteButton().props('disabled')).not.toBe(true);
+ findConfirmationButton().trigger('click');
+ expect(submitFormSpy()).toHaveBeenCalled();
+ });
+
+ it('calls hide on the modal when cancel button is clicked', () => {
+ const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
+ findCancelButton().trigger('click');
+ expect(closeModalSpy).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/branches/mock_data.js b/spec/frontend/branches/mock_data.js
new file mode 100644
index 00000000000..9e8839d8ce9
--- /dev/null
+++ b/spec/frontend/branches/mock_data.js
@@ -0,0 +1,7 @@
+export const formPath = '/namespace/project/-/merged_branches';
+const defaultBranch = 'master';
+
+export const propsDataMock = {
+ formPath,
+ defaultBranch,
+};
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
deleted file mode 100644
index b3e23ba4201..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_environments_dropdown_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import LegacyCiEnvironmentsDropdown from '~/ci_variable_list/components/legacy_ci_environments_dropdown.vue';
-
-Vue.use(Vuex);
-
-describe('Ci environments dropdown', () => {
- let wrapper;
- let store;
-
- const enterSearchTerm = (value) =>
- wrapper.find('[data-testid="ci-environment-search"]').setValue(value);
-
- const createComponent = (term) => {
- store = new Vuex.Store({
- getters: {
- joinedEnvironments: () => ['dev', 'prod', 'staging'],
- },
- });
-
- wrapper = mount(LegacyCiEnvironmentsDropdown, {
- store,
- propsData: {
- value: term,
- },
- });
- enterSearchTerm(term);
- };
-
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('No environments found', () => {
- beforeEach(() => {
- createComponent('stable');
- });
-
- it('renders create button with search term if environments do not contain search term', () => {
- expect(findAllDropdownItems()).toHaveLength(2);
- expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
- });
-
- it('renders empty results message', () => {
- expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
- });
-
- it('renders all environments when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('dev');
- expect(findDropdownItemByIndex(1).text()).toBe('prod');
- expect(findDropdownItemByIndex(2).text()).toBe('staging');
- });
-
- it('should not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
- });
-
- describe('Environments found', () => {
- beforeEach(async () => {
- createComponent('prod');
- await nextTick();
- });
-
- it('renders only the environment searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('prod');
- });
-
- it('should not display create button', () => {
- const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
- expect(environments).toHaveLength(0);
- expect(findAllDropdownItems()).toHaveLength(1);
- });
-
- it('should not display empty results message', () => {
- expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
- });
-
- it('should display active checkmark if active', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(false);
- });
-
- it('should clear the search term when showing the dropdown', () => {
- wrapper.findComponent(GlDropdown).trigger('click');
-
- expect(wrapper.find('[data-testid="ci-environment-search"]').text()).toBe('');
- });
-
- describe('Custom events', () => {
- it('should emit selectEnvironment if an environment is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
- expect(wrapper.emitted('selectEnvironment')).toEqual([['prod']]);
- });
-
- it('should emit createClicked if an environment is clicked', async () => {
- createComponent('newscope');
-
- await nextTick();
- findDropdownItemByIndex(1).vm.$emit('click');
- expect(wrapper.emitted('createClicked')).toEqual([['newscope']]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
deleted file mode 100644
index b607232907b..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_modal_spec.js
+++ /dev/null
@@ -1,323 +0,0 @@
-import { GlButton, GlFormInput } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mockTracking } from 'helpers/tracking_helper';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-import LegacyCiVariableModal from '~/ci_variable_list/components/legacy_ci_variable_modal.vue';
-import {
- AWS_ACCESS_KEY_ID,
- EVENT_LABEL,
- EVENT_ACTION,
- ENVIRONMENT_SCOPE_LINK_TITLE,
-} from '~/ci_variable_list/constants';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-import ModalStub from '../stubs';
-
-Vue.use(Vuex);
-
-describe('Ci variable modal', () => {
- let wrapper;
- let store;
- let trackingSpy;
-
- const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
-
- const createComponent = (method, options = {}) => {
- store = createStore({
- maskableRegex,
- isGroup: options.isGroup,
- environmentScopeLink: '/help/environments',
- });
- wrapper = method(LegacyCiVariableModal, {
- attachTo: document.body,
- stubs: {
- GlModal: ModalStub,
- },
- store,
- ...options,
- });
- };
-
- const findCiEnvironmentsDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
- const findModal = () => wrapper.findComponent(ModalStub);
- const findAddorUpdateButton = () => findModal().find('[data-testid="ciUpdateOrAddVariableBtn"]');
- const deleteVariableButton = () =>
- findModal()
- .findAllComponents(GlButton)
- .wrappers.find((button) => button.props('variant') === 'danger');
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('Basic interactions', () => {
- beforeEach(() => {
- createComponent(shallowMount);
- });
-
- it('button is disabled when no key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('true');
- });
- });
-
- describe('Adding a new variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variable = variable;
- });
-
- it('button is enabled when key/value pair are present', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
-
- it('Add variable button dispatches addVariable action', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('addVariable');
- });
-
- it('Clears the modal state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('clearModal');
- });
-
- it('should dispatch setVariableProtected when admin settings are configured to protect variables', () => {
- store.state.isProtectedByDefault = true;
- findModal().vm.$emit('shown');
-
- expect(store.dispatch).toHaveBeenCalledWith('setVariableProtected');
- });
- });
-
- describe('Adding a new non-AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value: 'value',
- secret_value: 'secret_value',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('does not show AWS guidance tip', () => {
- const tip = wrapper.find(`div[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(false);
- });
- });
-
- describe('Adding a new AWS variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy',
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- });
-
- it('shows AWS guidance tip', () => {
- const tip = wrapper.find(`[data-testid='aws-guidance-tip']`);
- expect(tip.exists()).toBe(true);
- expect(tip.isVisible()).toBe(true);
- });
- });
-
- describe.each`
- value | secret | rendered
- ${'value'} | ${'secret_value'} | ${false}
- ${'dollar$ign'} | ${'dollar$ign'} | ${true}
- `('Adding a new variable', ({ value, secret, rendered }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${rendered ? 'renders' : 'does not render'} the variable reference warning`, () => {
- const warning = wrapper.find(`[data-testid='contains-variable-reference']`);
- expect(warning.exists()).toBe(rendered);
- });
- });
-
- describe('Editing a variable', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- createComponent(shallowMount);
- jest.spyOn(store, 'dispatch').mockImplementation();
- store.state.variableBeingEdited = variable;
- });
-
- it('button text is Update variable when updating', () => {
- expect(findAddorUpdateButton().text()).toBe('Update variable');
- });
-
- it('Update variable button dispatches updateVariable with correct variable', () => {
- findAddorUpdateButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
- });
-
- it('Resets the editing state once modal is hidden', () => {
- findModal().vm.$emit('hidden');
- expect(store.dispatch).toHaveBeenCalledWith('resetEditing');
- });
-
- it('dispatches deleteVariable with correct variable to delete', () => {
- deleteVariableButton().vm.$emit('click');
- expect(store.dispatch).toHaveBeenCalledWith('deleteVariable');
- });
- });
-
- describe('Environment scope', () => {
- describe('group level variables', () => {
- it('renders the environment dropdown', () => {
- createComponent(shallowMount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: true,
- },
- },
- });
-
- expect(findCiEnvironmentsDropdown().exists()).toBe(true);
- expect(findCiEnvironmentsDropdown().isVisible()).toBe(true);
- });
-
- describe('licensed feature is not available', () => {
- it('disables the dropdown', () => {
- createComponent(mount, {
- isGroup: true,
- provide: {
- glFeatures: {
- groupScopedCiVariables: false,
- },
- },
- });
-
- const environmentScopeInput = wrapper
- .find('[data-testid="environment-scope"]')
- .findComponent(GlFormInput);
- expect(findCiEnvironmentsDropdown().exists()).toBe(false);
- expect(environmentScopeInput.attributes('readonly')).toBe('readonly');
- });
- });
- });
-
- it('renders a link to documentation on scopes', () => {
- createComponent(mount);
-
- const link = wrapper.find('[data-testid="environment-scope-link"]');
-
- expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
- expect(link.attributes('href')).toBe('/help/environments');
- });
- });
-
- describe('Validations', () => {
- const maskError = 'This variable can not be masked.';
-
- describe('when the mask state is invalid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidMaskVariable = {
- ...variable,
- key: 'qs',
- value: 'd:;',
- secret_value: 'd:;',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = invalidMaskVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it('disables the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBe('disabled');
- });
-
- it('shows the correct error text', () => {
- expect(findModal().text()).toContain(maskError);
- });
-
- it('sends the correct tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: ';',
- });
- });
- });
-
- describe.each`
- value | secret | masked | eventSent | trackingErrorProperty
- ${'value'} | ${'secretValue'} | ${false} | ${0} | ${null}
- ${'shortMasked'} | ${'short'} | ${true} | ${0} | ${null}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${false} | ${1} | ${'$'}
- ${'withDollar$Sign'} | ${'dollar$ign'} | ${true} | ${1} | ${'$'}
- ${'unsupported'} | ${'unsupported|char'} | ${true} | ${1} | ${'|'}
- ${'unsupportedMasked'} | ${'unsupported|char'} | ${false} | ${0} | ${null}
- `('Adding a new variable', ({ value, secret, masked, eventSent, trackingErrorProperty }) => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const invalidKeyVariable = {
- ...variable,
- key: 'key',
- value,
- secret_value: secret,
- masked,
- };
- createComponent(mount);
- store.state.variable = invalidKeyVariable;
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- });
-
- it(`${
- eventSent > 0 ? 'sends the correct' : 'does not send the'
- } variable validation tracking event`, () => {
- expect(trackingSpy).toHaveBeenCalledTimes(eventSent);
-
- if (eventSent > 0) {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, EVENT_ACTION, {
- label: EVENT_LABEL,
- property: trackingErrorProperty,
- });
- }
- });
- });
-
- describe('when both states are valid', () => {
- beforeEach(() => {
- const [variable] = mockData.mockVariables;
- const validMaskandKeyVariable = {
- ...variable,
- key: AWS_ACCESS_KEY_ID,
- value: '12345678',
- secret_value: '87654321',
- masked: true,
- };
- createComponent(mount);
- store.state.variable = validMaskandKeyVariable;
- });
-
- it('does not disable the submit button', () => {
- expect(findAddorUpdateButton().attributes('disabled')).toBeUndefined();
- });
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
deleted file mode 100644
index 7def4dd4f29..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_settings_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import LegacyCiVariableSettings from '~/ci_variable_list/components/legacy_ci_variable_settings.vue';
-import createStore from '~/ci_variable_list/store';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
- let isProject;
-
- const createComponent = (projectState) => {
- store = createStore();
- store.state.isProject = projectState;
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = shallowMount(LegacyCiVariableSettings, {
- store,
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchEnvironments when mounted', () => {
- isProject = true;
- createComponent(isProject);
- expect(store.dispatch).toHaveBeenCalledWith('fetchEnvironments');
- });
-
- it('does not dispatch fetchenvironments when in group context', () => {
- isProject = false;
- createComponent(isProject);
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js b/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
deleted file mode 100644
index 310afc8003a..00000000000
--- a/spec/frontend/ci_variable_list/components/legacy_ci_variable_table_spec.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import LegacyCiVariableTable from '~/ci_variable_list/components/legacy_ci_variable_table.vue';
-import createStore from '~/ci_variable_list/store';
-import mockData from '../services/mock_data';
-
-Vue.use(Vuex);
-
-describe('Ci variable table', () => {
- let wrapper;
- let store;
-
- const createComponent = () => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockImplementation();
- wrapper = mountExtended(LegacyCiVariableTable, {
- attachTo: document.body,
- store,
- });
- };
-
- const findRevealButton = () => wrapper.findByText('Reveal values');
- const findEditButton = () => wrapper.findByLabelText('Edit');
- const findEmptyVariablesPlaceholder = () => wrapper.findByText('There are no variables yet.');
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches fetchVariables when mounted', () => {
- expect(store.dispatch).toHaveBeenCalledWith('fetchVariables');
- });
-
- describe('When table is empty', () => {
- beforeEach(() => {
- store.state.variables = [];
- });
-
- it('displays empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(true);
- });
-
- it('hides the reveal button', () => {
- expect(findRevealButton().exists()).toBe(false);
- });
- });
-
- describe('When table has variables', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('does not display the empty message', () => {
- expect(findEmptyVariablesPlaceholder().exists()).toBe(false);
- });
-
- it('displays the reveal button', () => {
- expect(findRevealButton().exists()).toBe(true);
- });
-
- it('displays the correct amount of variables', async () => {
- expect(wrapper.findAll('.js-ci-variable-row')).toHaveLength(1);
- });
- });
-
- describe('Table click actions', () => {
- beforeEach(() => {
- store.state.variables = mockData.mockVariables;
- });
-
- it('reveals secret values when button is clicked', () => {
- findRevealButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('toggleValues', false);
- });
-
- it('dispatches editVariable with correct variable to edit', () => {
- findEditButton().trigger('click');
- expect(store.dispatch).toHaveBeenCalledWith('editVariable', mockData.mockVariables[0]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
deleted file mode 100644
index e8c81a53a55..00000000000
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ /dev/null
@@ -1,319 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-import Api from '~/api';
-import * as actions from '~/ci_variable_list/store/actions';
-import * as types from '~/ci_variable_list/store/mutation_types';
-import getInitialState from '~/ci_variable_list/store/state';
-import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import mockData from '../services/mock_data';
-
-jest.mock('~/api.js');
-jest.mock('~/flash.js');
-
-describe('CI variable list store actions', () => {
- let mock;
- let state;
- const mockVariable = {
- environment_scope: '*',
- id: 63,
- key: 'test_var',
- masked: false,
- protected: false,
- value: 'test_val',
- variable_type: 'env_var',
- _destory: true,
- };
- const payloadError = new Error('Request failed with status code 500');
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state = getInitialState();
- state.endpoint = '/variables';
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('toggleValues', () => {
- const valuesHidden = false;
- it('commits TOGGLE_VALUES mutation', () => {
- testAction(actions.toggleValues, valuesHidden, {}, [
- {
- type: types.TOGGLE_VALUES,
- payload: valuesHidden,
- },
- ]);
- });
- });
-
- describe('clearModal', () => {
- it('commits CLEAR_MODAL mutation', () => {
- testAction(actions.clearModal, {}, {}, [
- {
- type: types.CLEAR_MODAL,
- },
- ]);
- });
- });
-
- describe('resetEditing', () => {
- it('commits RESET_EDITING mutation', () => {
- testAction(
- actions.resetEditing,
- {},
- {},
- [
- {
- type: types.RESET_EDITING,
- },
- ],
- [{ type: 'fetchVariables' }],
- );
- });
- });
-
- describe('setVariableProtected', () => {
- it('commits SET_VARIABLE_PROTECTED mutation', () => {
- testAction(actions.setVariableProtected, {}, {}, [
- {
- type: types.SET_VARIABLE_PROTECTED,
- },
- ]);
- });
- });
-
- describe('deleteVariable', () => {
- it('dispatch correct actions on successful deleted variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- { type: 'receiveDeleteVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on delete failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.deleteVariable,
- {},
- state,
- [],
- [
- { type: 'requestDeleteVariable' },
- {
- type: 'receiveDeleteVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('updateVariable', () => {
- it('dispatch correct actions on successful updated variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.updateVariable,
- {},
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- { type: 'receiveUpdateVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on update failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.updateVariable,
- mockVariable,
- state,
- [],
- [
- { type: 'requestUpdateVariable' },
- {
- type: 'receiveUpdateVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('addVariable', () => {
- it('dispatch correct actions on successful added variable', () => {
- mock.onPatch(state.endpoint).reply(200);
-
- return testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- { type: 'receiveAddVariableSuccess' },
- { type: 'fetchVariables' },
- ],
- );
- });
-
- it('should show flash error and set error in state on add failure', async () => {
- mock.onPatch(state.endpoint).reply(500, '');
-
- await testAction(
- actions.addVariable,
- {},
- state,
- [],
- [
- { type: 'requestAddVariable' },
- {
- type: 'receiveAddVariableError',
- payload: payloadError,
- },
- ],
- );
- expect(createAlert).toHaveBeenCalled();
- });
- });
-
- describe('fetchVariables', () => {
- it('dispatch correct actions on fetchVariables', () => {
- mock.onGet(state.endpoint).reply(200, { variables: mockData.mockVariables });
-
- return testAction(
- actions.fetchVariables,
- {},
- state,
- [],
- [
- { type: 'requestVariables' },
- {
- type: 'receiveVariablesSuccess',
- payload: prepareDataForDisplay(mockData.mockVariables),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch variables failure', async () => {
- mock.onGet(state.endpoint).reply(500);
-
- await testAction(actions.fetchVariables, {}, state, [], [{ type: 'requestVariables' }]);
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the variables.',
- });
- });
- });
-
- describe('fetchEnvironments', () => {
- it('dispatch correct actions on fetchEnvironments', () => {
- Api.environments = jest.fn().mockResolvedValue({ data: mockData.mockEnvironments });
-
- return testAction(
- actions.fetchEnvironments,
- {},
- state,
- [],
- [
- { type: 'requestEnvironments' },
- {
- type: 'receiveEnvironmentsSuccess',
- payload: prepareEnvironments(mockData.mockEnvironments),
- },
- ],
- );
- });
-
- it('should show flash error and set error in state on fetch environments failure', async () => {
- Api.environments = jest.fn().mockRejectedValue();
-
- await testAction(actions.fetchEnvironments, {}, state, [], [{ type: 'requestEnvironments' }]);
-
- expect(createAlert).toHaveBeenCalledWith({
- message: 'There was an error fetching the environments information.',
- });
- });
- });
-
- describe('Update variable values', () => {
- it('updateVariableKey', () => {
- testAction(
- actions.updateVariableKey,
- { key: mockVariable.key },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_KEY,
- payload: mockVariable.key,
- },
- ],
- [],
- );
- });
-
- it('updateVariableValue', () => {
- testAction(
- actions.updateVariableValue,
- { secret_value: mockVariable.value },
- {},
- [
- {
- type: types.UPDATE_VARIABLE_VALUE,
- payload: mockVariable.value,
- },
- ],
- [],
- );
- });
-
- it('updateVariableType', () => {
- testAction(
- actions.updateVariableType,
- { variable_type: mockVariable.variable_type },
- {},
- [{ type: types.UPDATE_VARIABLE_TYPE, payload: mockVariable.variable_type }],
- [],
- );
- });
-
- it('updateVariableProtected', () => {
- testAction(
- actions.updateVariableProtected,
- { protected_variable: mockVariable.protected },
- {},
- [{ type: types.UPDATE_VARIABLE_PROTECTED, payload: mockVariable.protected }],
- [],
- );
- });
-
- it('updateVariableMasked', () => {
- testAction(
- actions.updateVariableMasked,
- { masked: mockVariable.masked },
- {},
- [{ type: types.UPDATE_VARIABLE_MASKED, payload: mockVariable.masked }],
- [],
- );
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/getters_spec.js b/spec/frontend/ci_variable_list/store/getters_spec.js
deleted file mode 100644
index 92f22b18763..00000000000
--- a/spec/frontend/ci_variable_list/store/getters_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import * as getters from '~/ci_variable_list/store/getters';
-import mockData from '../services/mock_data';
-
-describe('Ci variable getters', () => {
- describe('joinedEnvironments', () => {
- it('should join fetched environments with variable environment scopes', () => {
- const state = {
- environments: ['All (default)', 'staging', 'deployment', 'prod'],
- variables: mockData.mockVariableScopes,
- };
-
- expect(getters.joinedEnvironments(state)).toEqual([
- 'All (default)',
- 'deployment',
- 'prod',
- 'production',
- 'staging',
- ]);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/mutations_spec.js b/spec/frontend/ci_variable_list/store/mutations_spec.js
deleted file mode 100644
index c7d07ead09b..00000000000
--- a/spec/frontend/ci_variable_list/store/mutations_spec.js
+++ /dev/null
@@ -1,136 +0,0 @@
-import * as types from '~/ci_variable_list/store/mutation_types';
-import mutations from '~/ci_variable_list/store/mutations';
-import state from '~/ci_variable_list/store/state';
-
-describe('CI variable list mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('TOGGLE_VALUES', () => {
- it('should toggle state', () => {
- const valuesHidden = false;
-
- mutations[types.TOGGLE_VALUES](stateCopy, valuesHidden);
-
- expect(stateCopy.valuesHidden).toEqual(valuesHidden);
- });
- });
-
- describe('VARIABLE_BEING_EDITED', () => {
- it('should set the variable that is being edited', () => {
- mutations[types.VARIABLE_BEING_EDITED](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(true);
- });
- });
-
- describe('RESET_EDITING', () => {
- it('should reset variableBeingEdited to false', () => {
- mutations[types.RESET_EDITING](stateCopy);
-
- expect(stateCopy.variableBeingEdited).toBe(false);
- });
- });
-
- describe('CLEAR_MODAL', () => {
- it('should clear modal state', () => {
- const modalState = {
- variable_type: 'Variable',
- key: '',
- secret_value: '',
- protected_variable: false,
- masked: false,
- environment_scope: 'All (default)',
- };
-
- mutations[types.CLEAR_MODAL](stateCopy);
-
- expect(stateCopy.variable).toEqual(modalState);
- });
- });
-
- describe('RECEIVE_ENVIRONMENTS_SUCCESS', () => {
- it('should set environments', () => {
- const environments = ['env1', 'env2'];
-
- mutations[types.RECEIVE_ENVIRONMENTS_SUCCESS](stateCopy, environments);
-
- expect(stateCopy.environments).toEqual(['All (default)', 'env1', 'env2']);
- });
- });
-
- describe('SET_ENVIRONMENT_SCOPE', () => {
- const environment = 'production';
-
- it('should set environment scope on variable', () => {
- mutations[types.SET_ENVIRONMENT_SCOPE](stateCopy, environment);
-
- expect(stateCopy.variable.environment_scope).toBe('production');
- });
- });
-
- describe('ADD_WILD_CARD_SCOPE', () => {
- it('should add wild card scope to environments array and sort', () => {
- stateCopy.environments = ['dev', 'staging'];
- mutations[types.ADD_WILD_CARD_SCOPE](stateCopy, 'production');
-
- expect(stateCopy.environments).toEqual(['dev', 'production', 'staging']);
- });
- });
-
- describe('SET_VARIABLE_PROTECTED', () => {
- it('should set protected value to true', () => {
- mutations[types.SET_VARIABLE_PROTECTED](stateCopy);
-
- expect(stateCopy.variable.protected_variable).toBe(true);
- });
- });
-
- describe('UPDATE_VARIABLE_KEY', () => {
- it('should update variable key value', () => {
- const key = 'new_var';
- mutations[types.UPDATE_VARIABLE_KEY](stateCopy, key);
-
- expect(stateCopy.variable.key).toBe(key);
- });
- });
-
- describe('UPDATE_VARIABLE_VALUE', () => {
- it('should update variable value', () => {
- const value = 'variable_value';
- mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, value);
-
- expect(stateCopy.variable.secret_value).toBe(value);
- });
- });
-
- describe('UPDATE_VARIABLE_TYPE', () => {
- it('should update variable type value', () => {
- const type = 'File';
- mutations[types.UPDATE_VARIABLE_TYPE](stateCopy, type);
-
- expect(stateCopy.variable.variable_type).toBe(type);
- });
- });
-
- describe('UPDATE_VARIABLE_PROTECTED', () => {
- it('should update variable protected value', () => {
- const protectedValue = true;
- mutations[types.UPDATE_VARIABLE_PROTECTED](stateCopy, protectedValue);
-
- expect(stateCopy.variable.protected_variable).toBe(protectedValue);
- });
- });
-
- describe('UPDATE_VARIABLE_MASKED', () => {
- it('should update variable masked value', () => {
- const masked = true;
- mutations[types.UPDATE_VARIABLE_MASKED](stateCopy, masked);
-
- expect(stateCopy.variable.masked).toBe(masked);
- });
- });
-});
diff --git a/spec/frontend/ci_variable_list/store/utils_spec.js b/spec/frontend/ci_variable_list/store/utils_spec.js
deleted file mode 100644
index 5b10370324a..00000000000
--- a/spec/frontend/ci_variable_list/store/utils_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import {
- prepareDataForDisplay,
- prepareEnvironments,
- prepareDataForApi,
-} from '~/ci_variable_list/store/utils';
-import mockData from '../services/mock_data';
-
-describe('CI variables store utils', () => {
- it('prepares ci variables for display', () => {
- expect(prepareDataForDisplay(mockData.mockVariablesApi)).toStrictEqual(
- mockData.mockVariablesDisplay,
- );
- });
-
- it('prepares single ci variable for api', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0])).toStrictEqual({
- environment_scope: '*',
- id: 113,
- key: 'test_var',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val',
- value: 'test_val',
- variable_type: 'env_var',
- });
-
- expect(prepareDataForApi(mockData.mockVariablesDisplay[1])).toStrictEqual({
- environment_scope: '*',
- id: 114,
- key: 'test_var_2',
- masked: 'false',
- protected: 'false',
- secret_value: 'test_val_2',
- value: 'test_val_2',
- variable_type: 'file',
- });
- });
-
- it('prepares single ci variable for delete', () => {
- expect(prepareDataForApi(mockData.mockVariablesDisplay[0], true)).toHaveProperty(
- '_destroy',
- true,
- );
- });
-
- it('prepares environments for display', () => {
- expect(prepareEnvironments(mockData.mockEnvironments)).toStrictEqual(['staging', 'production']);
- });
-});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
new file mode 100644
index 00000000000..8f229182fe5
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/forwarding_settings_spec.js
@@ -0,0 +1,78 @@
+import { GlFormGroup, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import component from '~/packages_and_registries/settings/group/components/forwarding_settings.vue';
+
+describe('Forwarding Settings', () => {
+ let wrapper;
+
+ const defaultProps = {
+ disabled: false,
+ forwarding: false,
+ label: 'label',
+ lockForwarding: false,
+ modelNames: {
+ forwarding: 'forwardField',
+ lockForwarding: 'lockForwardingField',
+ isLocked: 'lockedField',
+ },
+ };
+
+ const mountComponent = (propsData = defaultProps) => {
+ wrapper = shallowMountExtended(component, {
+ propsData,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findForwardingCheckbox = () => wrapper.findByTestId('forwarding-checkbox');
+ const findLockForwardingCheckbox = () => wrapper.findByTestId('lock-forwarding-checkbox');
+
+ it('has a form group', () => {
+ mountComponent();
+
+ expect(findFormGroup().exists()).toBe(true);
+ expect(findFormGroup().attributes()).toMatchObject({
+ label: defaultProps.label,
+ });
+ });
+
+ describe.each`
+ name | finder | label | extraProps | field
+ ${'forwarding'} | ${findForwardingCheckbox} | ${'Forward label package requests'} | ${{ forwarding: true }} | ${defaultProps.modelNames.forwarding}
+ ${'lock forwarding'} | ${findLockForwardingCheckbox} | ${'Enforce label setting for all subgroups'} | ${{ lockForwarding: true }} | ${defaultProps.modelNames.lockForwarding}
+ `('$name checkbox', ({ name, finder, label, extraProps, field }) => {
+ it('is rendered', () => {
+ mountComponent();
+ expect(finder().exists()).toBe(true);
+ expect(finder().text()).toMatchInterpolatedText(label);
+ expect(finder().attributes('disabled')).toBeUndefined();
+ expect(finder().attributes('checked')).toBeUndefined();
+ });
+
+ it(`is checked when ${name} set`, () => {
+ mountComponent({ ...defaultProps, ...extraProps });
+
+ expect(finder().attributes('checked')).toBe('true');
+ });
+
+ it(`emits an update event with field ${field} set`, () => {
+ mountComponent();
+
+ finder().vm.$emit('change', true);
+
+ expect(wrapper.emitted('update')).toStrictEqual([[field, true]]);
+ });
+ });
+
+ describe('disabled', () => {
+ it('disables both checkboxes', () => {
+ mountComponent({ ...defaultProps, disabled: true });
+
+ expect(findForwardingCheckbox().attributes('disabled')).toEqual('true');
+ expect(findLockForwardingCheckbox().attributes('disabled')).toEqual('true');
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
index 31fc3ad419c..7edc321867c 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -7,6 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
+import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
@@ -60,6 +61,7 @@ describe('Group Settings App', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findPackageSettings = () => wrapper.findComponent(PackagesSettings);
+ const findPackageForwardingSettings = () => wrapper.findComponent(PackagesForwardingSettings);
const findDependencyProxySettings = () => wrapper.findComponent(DependencyProxySettings);
const waitForApolloQueryAndRender = async () => {
@@ -67,16 +69,18 @@ describe('Group Settings App', () => {
await nextTick();
};
- const packageSettingsProps = { packageSettings: packageSettings() };
+ const packageSettingsProps = { packageSettings };
+ const packageForwardingSettingsProps = { forwardSettings: { ...packageSettings } };
const dependencyProxyProps = {
dependencyProxySettings: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
};
describe.each`
- finder | entitySpecificProps | successMessage | errorMessage
- ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
- ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
+ finder | entitySpecificProps | successMessage | errorMessage
+ ${findPackageSettings} | ${packageSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findPackageForwardingSettings} | ${packageForwardingSettingsProps} | ${'Settings saved successfully'} | ${'An error occurred while saving the settings'}
+ ${findDependencyProxySettings} | ${dependencyProxyProps} | ${'Setting saved successfully'} | ${'An error occurred while saving the setting'}
`('settings blocks', ({ finder, entitySpecificProps, successMessage, errorMessage }) => {
beforeEach(() => {
mountComponent();
@@ -88,10 +92,7 @@ describe('Group Settings App', () => {
});
it('binds the correctProps', () => {
- expect(finder().props()).toMatchObject({
- isLoading: false,
- ...entitySpecificProps,
- });
+ expect(finder().props()).toMatchObject(entitySpecificProps);
});
describe('success event', () => {
diff --git a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
index 13eba39ec8c..807f332f4d3 100644
--- a/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
+++ b/spec/frontend/packages_and_registries/settings/group/components/package_settings_spec.js
@@ -48,7 +48,7 @@ describe('Packages Settings', () => {
apolloProvider,
provide: defaultProvide,
propsData: {
- packageSettings: packageSettings(),
+ packageSettings,
},
stubs: {
SettingsBlock,
@@ -83,7 +83,7 @@ describe('Packages Settings', () => {
};
const emitMavenSettingsUpdate = (override) => {
- findGenericDuplicatedSettingsExceptionsInput().vm.$emit('update', {
+ findMavenDuplicatedSettingsExceptionsInput().vm.$emit('update', {
mavenDuplicateExceptionRegex: ')',
...override,
});
@@ -117,7 +117,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed } = packageSettings();
+ const { mavenDuplicatesAllowed } = packageSettings;
expect(findMavenDuplicatedSettingsToggle().exists()).toBe(true);
@@ -132,7 +132,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', () => {
mountComponent({ mountFn: mountExtended });
- const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings();
+ const { mavenDuplicatesAllowed, mavenDuplicateExceptionRegex } = packageSettings;
expect(findMavenDuplicatedSettingsExceptionsInput().exists()).toBe(true);
@@ -170,7 +170,7 @@ describe('Packages Settings', () => {
it('renders toggle', () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed } = packageSettings();
+ const { genericDuplicatesAllowed } = packageSettings;
expect(findGenericDuplicatedSettingsToggle().exists()).toBe(true);
expect(findGenericDuplicatedSettingsToggle().props()).toMatchObject({
@@ -184,7 +184,7 @@ describe('Packages Settings', () => {
it('renders ExceptionsInput and assigns duplication allowness and exception props', async () => {
mountComponent({ mountFn: mountExtended });
- const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings();
+ const { genericDuplicatesAllowed, genericDuplicateExceptionRegex } = packageSettings;
expect(findGenericDuplicatedSettingsExceptionsInput().props()).toMatchObject({
duplicatesAllowed: genericDuplicatesAllowed,
@@ -239,7 +239,7 @@ describe('Packages Settings', () => {
emitMavenSettingsUpdate({ mavenDuplicateExceptionRegex });
expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
- ...packageSettings(),
+ ...packageSettings,
mavenDuplicateExceptionRegex,
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
new file mode 100644
index 00000000000..a0b257a9496
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/packages_forwarding_settings_spec.js
@@ -0,0 +1,280 @@
+import Vue from 'vue';
+import { GlButton } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
+import {
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ PACKAGE_FORWARDING_SETTINGS_HEADER,
+} from '~/packages_and_registries/settings/group/constants';
+
+import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
+import {
+ packageSettings,
+ packageForwardingSettings,
+ groupPackageSettingsMock,
+ groupPackageForwardSettingsMutationMock,
+ mutationErrorMock,
+ npmProps,
+ pypiProps,
+ mavenProps,
+} from '../mock_data';
+
+jest.mock('~/flash');
+jest.mock('~/packages_and_registries/settings/group/graphql/utils/optimistic_responses');
+
+describe('Packages Forwarding Settings', () => {
+ let wrapper;
+ let apolloProvider;
+ const mutationResolverFn = jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock());
+
+ const defaultProvide = {
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ forwardSettings = { ...packageSettings },
+ features = {},
+ mutationResolver = mutationResolverFn,
+ } = {}) => {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[updateNamespacePackageSettings, mutationResolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMountExtended(component, {
+ apolloProvider,
+ provide: {
+ ...defaultProvide,
+ glFeatures: {
+ ...features,
+ },
+ },
+ propsData: {
+ forwardSettings,
+ },
+ stubs: {
+ SettingsBlock,
+ },
+ });
+ };
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+ const findForm = () => wrapper.find('form');
+ const findSubmitButton = () => findForm().findComponent(GlButton);
+ const findDescription = () => wrapper.findByTestId('description');
+ const findMavenForwardingSettings = () => wrapper.findByTestId('maven');
+ const findNpmForwardingSettings = () => wrapper.findByTestId('npm');
+ const findPyPiForwardingSettings = () => wrapper.findByTestId('pypi');
+
+ const fillApolloCache = () => {
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getGroupPackagesSettingsQuery,
+ variables: {
+ fullPath: defaultProvide.groupPath,
+ },
+ ...groupPackageSettingsMock,
+ });
+ };
+
+ const updateNpmSettings = () => {
+ findNpmForwardingSettings().vm.$emit('update', 'npmPackageRequestsForwarding', false);
+ };
+
+ const submitForm = () => {
+ findForm().trigger('submit');
+ return waitForPromises();
+ };
+
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_FORWARDING_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(
+ PACKAGE_FORWARDING_SETTINGS_DESCRIPTION,
+ );
+ });
+
+ it('watches changes to props', async () => {
+ mountComponent();
+
+ expect(findNpmForwardingSettings().props()).toMatchObject(npmProps);
+
+ await wrapper.setProps({
+ forwardSettings: {
+ ...packageSettings,
+ npmPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ expect(findNpmForwardingSettings().props()).toMatchObject({ ...npmProps, disabled: true });
+ });
+
+ it('submit button is disabled', () => {
+ mountComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
+ describe.each`
+ type | finder | props | field
+ ${'npm'} | ${findNpmForwardingSettings} | ${npmProps} | ${'npmPackageRequestsForwarding'}
+ ${'pypi'} | ${findPyPiForwardingSettings} | ${pypiProps} | ${'pypiPackageRequestsForwarding'}
+ ${'maven'} | ${findMavenForwardingSettings} | ${mavenProps} | ${'mavenPackageRequestsForwarding'}
+ `('$type settings', ({ finder, props, field }) => {
+ beforeEach(() => {
+ mountComponent({ features: { mavenCentralRequestForwarding: true } });
+ });
+
+ it('assigns forwarding settings props', () => {
+ expect(finder().props()).toMatchObject(props);
+ });
+
+ it('on update event enables submit button', async () => {
+ finder().vm.$emit('update', field, false);
+
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+
+ describe('maven settings', () => {
+ describe('with feature turned off', () => {
+ it('does not exist', () => {
+ mountComponent();
+
+ expect(findMavenForwardingSettings().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('settings update', () => {
+ describe('success state', () => {
+ it('calls the mutation with the right variables', async () => {
+ const {
+ mavenPackageRequestsForwardingLocked,
+ npmPackageRequestsForwardingLocked,
+ pypiPackageRequestsForwardingLocked,
+ ...packageSettingsInput
+ } = packageForwardingSettings;
+
+ mountComponent();
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ ...packageSettingsInput,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('when field are locked calls the mutation with the right variables', async () => {
+ mountComponent({
+ forwardSettings: {
+ ...packageSettings,
+ mavenPackageRequestsForwardingLocked: true,
+ pypiPackageRequestsForwardingLocked: true,
+ },
+ });
+
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(mutationResolverFn).toHaveBeenCalledWith({
+ input: {
+ namespacePath: defaultProvide.groupPath,
+ lockNpmPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: false,
+ },
+ });
+ });
+
+ it('emits a success event', async () => {
+ mountComponent();
+ fillApolloCache();
+ updateNpmSettings();
+
+ await submitForm();
+
+ expect(wrapper.emitted('success')).toHaveLength(1);
+ });
+
+ it('has an optimistic response', async () => {
+ const npmPackageRequestsForwarding = false;
+ mountComponent();
+
+ fillApolloCache();
+
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(true);
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(updateGroupPackagesSettingsOptimisticResponse).toHaveBeenCalledWith({
+ ...packageSettings,
+ npmPackageRequestsForwarding,
+ });
+ expect(findNpmForwardingSettings().props('forwarding')).toBe(npmPackageRequestsForwarding);
+ });
+ });
+
+ describe('errors', () => {
+ it('mutation payload with root level errors', async () => {
+ const mutationResolver = jest.fn().mockResolvedValue(mutationErrorMock);
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+
+ it.each`
+ type | mutationResolver
+ ${'local'} | ${jest.fn().mockResolvedValue(groupPackageForwardSettingsMutationMock({ errors: ['foo'] }))}
+ ${'network'} | ${jest.fn().mockRejectedValue()}
+ `('mutation payload with $type error', async ({ mutationResolver }) => {
+ mountComponent({ mutationResolver });
+
+ fillApolloCache();
+
+ updateNpmSettings();
+ await submitForm();
+
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
index d53446de910..1ca9dc6daeb 100644
--- a/spec/frontend/packages_and_registries/settings/group/mock_data.js
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -1,9 +1,26 @@
-export const packageSettings = () => ({
+const packageDuplicateSettings = {
mavenDuplicatesAllowed: true,
mavenDuplicateExceptionRegex: '',
genericDuplicatesAllowed: true,
genericDuplicateExceptionRegex: '',
-});
+};
+
+export const packageForwardingSettings = {
+ mavenPackageRequestsForwarding: true,
+ lockMavenPackageRequestsForwarding: false,
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ pypiPackageRequestsForwarding: true,
+ lockPypiPackageRequestsForwarding: false,
+ mavenPackageRequestsForwardingLocked: false,
+ npmPackageRequestsForwardingLocked: false,
+ pypiPackageRequestsForwardingLocked: false,
+};
+
+export const packageSettings = {
+ ...packageDuplicateSettings,
+ ...packageForwardingSettings,
+};
export const dependencyProxySettings = (extend) => ({
enabled: true,
@@ -21,13 +38,52 @@ export const groupPackageSettingsMock = {
group: {
id: '1',
fullPath: 'foo_group_path',
- packageSettings: packageSettings(),
+ packageSettings: {
+ ...packageSettings,
+ __typename: 'PackageSettings',
+ },
dependencyProxySetting: dependencyProxySettings(),
dependencyProxyImageTtlPolicy: dependencyProxyImageTtlPolicy(),
},
},
};
+export const npmProps = {
+ forwarding: packageForwardingSettings.npmPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockNpmPackageRequestsForwarding,
+ label: 'npm',
+ disabled: false,
+ modelNames: {
+ forwarding: 'npmPackageRequestsForwarding',
+ lockForwarding: 'lockNpmPackageRequestsForwarding',
+ isLocked: 'npmPackageRequestsForwardingLocked',
+ },
+};
+
+export const pypiProps = {
+ forwarding: packageForwardingSettings.pypiPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockPypiPackageRequestsForwarding,
+ label: 'PyPI',
+ disabled: false,
+ modelNames: {
+ forwarding: 'pypiPackageRequestsForwarding',
+ lockForwarding: 'lockPypiPackageRequestsForwarding',
+ isLocked: 'pypiPackageRequestsForwardingLocked',
+ },
+};
+
+export const mavenProps = {
+ forwarding: packageForwardingSettings.mavenPackageRequestsForwarding,
+ lockForwarding: packageForwardingSettings.lockMavenPackageRequestsForwarding,
+ label: 'Maven',
+ disabled: false,
+ modelNames: {
+ forwarding: 'mavenPackageRequestsForwarding',
+ lockForwarding: 'lockMavenPackageRequestsForwarding',
+ isLocked: 'mavenPackageRequestsForwardingLocked',
+ },
+};
+
export const groupPackageSettingsMutationMock = (override) => ({
data: {
updateNamespacePackageSettings: {
@@ -43,6 +99,19 @@ export const groupPackageSettingsMutationMock = (override) => ({
},
});
+export const groupPackageForwardSettingsMutationMock = (override) => ({
+ data: {
+ updateNamespacePackageSettings: {
+ packageSettings: {
+ npmPackageRequestsForwarding: true,
+ lockNpmPackageRequestsForwarding: false,
+ },
+ errors: [],
+ ...override,
+ },
+ },
+});
+
export const dependencyProxySettingMutationMock = (override) => ({
data: {
updateDependencyProxySettings: {
diff --git a/spec/helpers/form_helper_spec.rb b/spec/helpers/form_helper_spec.rb
index 14ff5d97057..1797b0e32cd 100644
--- a/spec/helpers/form_helper_spec.rb
+++ b/spec/helpers/form_helper_spec.rb
@@ -44,43 +44,19 @@ RSpec.describe FormHelper do
describe '#assignees_dropdown_options' do
let(:merge_request) { build(:merge_request) }
- context "with the :limit_assignees_per_issuable feature flag on" do
- context "with multiple assignees" do
- it 'correctly returns the max amount of assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
+ context "with multiple assignees" do
+ it 'correctly returns the max amount of assignees to allow' do
+ allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
- end
- end
-
- context "with only 1 assignee" do
- it 'correctly returns the max amount of assignees to allow' do
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
+ .to eq(Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS)
end
end
- context "with the :limit_assignees_per_issuable feature flag off" do
- before do
- stub_feature_flags(limit_assignees_per_issuable: false)
- end
-
- context "with multiple assignees" do
- it 'correctly returns the max amount of assignees to allow' do
- allow(helper).to receive(:merge_request_supports_multiple_assignees?).and_return(true)
-
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(nil)
- end
- end
-
- context "with only 1 assignee" do
- it 'correctly returns the max amount of assignees to allow' do
- expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
- .to eq(1)
- end
+ context "with only 1 assignee" do
+ it 'correctly returns the max amount of assignees to allow' do
+ expect(helper.assignees_dropdown_options(:merge_request)[:data][:'max-select'])
+ .to eq(1)
end
end
end
diff --git a/spec/lib/api/entities/release_spec.rb b/spec/lib/api/entities/release_spec.rb
index aa2c5126bb9..d1e5f191614 100644
--- a/spec/lib/api/entities/release_spec.rb
+++ b/spec/lib/api/entities/release_spec.rb
@@ -16,13 +16,13 @@ RSpec.describe API::Entities::Release do
end
describe 'evidences' do
- context 'when the current user can download code' do
+ context 'when the current user can read code' do
let(:entity_evidence) { entity[:evidences].first }
it 'exposes the evidence sha and the json path' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
- .with(user, :download_code, project).and_return(true)
+ .with(user, :read_code, project).and_return(true)
expect(entity_evidence[:sha]).to eq(evidence.summary_sha)
expect(entity_evidence[:collected_at]).to eq(evidence.collected_at)
@@ -36,11 +36,11 @@ RSpec.describe API::Entities::Release do
end
end
- context 'when the current user cannot download code' do
+ context 'when the current user cannot read code' do
it 'does not expose any evidence data' do
allow(Ability).to receive(:allowed?).and_call_original
allow(Ability).to receive(:allowed?)
- .with(user, :download_code, project).and_return(false)
+ .with(user, :read_code, project).and_return(false)
expect(entity.keys).not_to include(:evidences)
end
diff --git a/spec/lib/gitlab/gon_helper_spec.rb b/spec/lib/gitlab/gon_helper_spec.rb
index 94192a9257c..5a1fcc5e2dc 100644
--- a/spec/lib/gitlab/gon_helper_spec.rb
+++ b/spec/lib/gitlab/gon_helper_spec.rb
@@ -41,67 +41,53 @@ RSpec.describe Gitlab::GonHelper do
end
describe 'sentry configuration' do
- let(:legacy_clientside_dsn) { 'https://xxx@sentry-legacy.example.com/1' }
let(:clientside_dsn) { 'https://xxx@sentry.example.com/1' }
- let(:environment) { 'production' }
+ let(:environment) { 'staging' }
- context 'with enable_old_sentry_clientside_integration enabled' do
+ describe 'sentry integration' do
before do
- stub_feature_flags(
- enable_old_sentry_clientside_integration: true,
- enable_new_sentry_clientside_integration: false
- )
-
- stub_config(sentry: { enabled: true, clientside_dsn: legacy_clientside_dsn, environment: environment })
+ stub_config(sentry: { enabled: true, clientside_dsn: clientside_dsn, environment: environment })
end
it 'sets sentry dsn and environment from config' do
- expect(gon).to receive(:sentry_dsn=).with(legacy_clientside_dsn)
+ expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
expect(gon).to receive(:sentry_environment=).with(environment)
helper.add_gon_variables
end
end
- context 'with enable_new_sentry_clientside_integration enabled' do
+ describe 'new sentry integration' do
before do
- stub_feature_flags(
- enable_old_sentry_clientside_integration: false,
- enable_new_sentry_clientside_integration: true
- )
-
stub_application_setting(sentry_enabled: true)
stub_application_setting(sentry_clientside_dsn: clientside_dsn)
stub_application_setting(sentry_environment: environment)
end
- it 'sets sentry dsn and environment from application settings' do
- expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
- expect(gon).to receive(:sentry_environment=).with(environment)
-
- helper.add_gon_variables
- end
- end
-
- context 'with enable_old_sentry_clientside_integration and enable_new_sentry_clientside_integration enabled' do
- before do
- stub_feature_flags(
- enable_old_sentry_clientside_integration: true,
- enable_new_sentry_clientside_integration: true
- )
+ context 'when enable_new_sentry_clientside_integration is disabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: false)
+ end
- stub_config(sentry: { enabled: true, clientside_dsn: legacy_clientside_dsn, environment: environment })
+ it 'does not set sentry dsn and environment from config' do
+ expect(gon).not_to receive(:sentry_dsn=).with(clientside_dsn)
+ expect(gon).not_to receive(:sentry_environment=).with(environment)
- stub_application_setting(sentry_enabled: true)
- stub_application_setting(sentry_clientside_dsn: clientside_dsn)
- stub_application_setting(sentry_environment: environment)
+ helper.add_gon_variables
+ end
end
- it 'sets sentry dsn and environment from application settings' do
- expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
- expect(gon).to receive(:sentry_environment=).with(environment)
+ context 'when enable_new_sentry_clientside_integration is enabled' do
+ before do
+ stub_feature_flags(enable_new_sentry_clientside_integration: true)
+ end
- helper.add_gon_variables
+ it 'sets sentry dsn and environment from config' do
+ expect(gon).to receive(:sentry_dsn=).with(clientside_dsn)
+ expect(gon).to receive(:sentry_environment=).with(environment)
+
+ helper.add_gon_variables
+ end
end
end
end
diff --git a/spec/lib/sbom/package_url/argument_validator_spec.rb b/spec/lib/sbom/package_url/argument_validator_spec.rb
new file mode 100644
index 00000000000..246da1c0bda
--- /dev/null
+++ b/spec/lib/sbom/package_url/argument_validator_spec.rb
@@ -0,0 +1,51 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::ArgumentValidator do
+ let(:mock_package_url) { Struct.new(:type, :namespace, :name, :version, :qualifiers, keyword_init: true) }
+ let(:package) do
+ mock_package_url.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers
+ )
+ end
+
+ subject(:validate) { described_class.new(package).validate! }
+
+ context 'with valid arguments' do
+ include_context 'with valid purl examples'
+
+ with_them do
+ it 'does not raise error' do
+ expect { validate }.not_to raise_error
+ end
+ end
+ end
+
+ context 'with invalid arguments' do
+ include_context 'with invalid purl examples'
+
+ with_them do
+ it 'raises an ArgumentError' do
+ expect { validate }.to raise_error(ArgumentError)
+ end
+ end
+ end
+
+ context 'with multiple errors' do
+ let(:type) { nil }
+ let(:name) { nil }
+ let(:package) { mock_package_url.new(type: type, name: name) }
+
+ it 'reports all errors' do
+ expect { validate }.to raise_error(ArgumentError, 'Type is required, Name is required')
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url/decoder_spec.rb b/spec/lib/sbom/package_url/decoder_spec.rb
index 1da3c35f403..5b480475b7c 100644
--- a/spec/lib/sbom/package_url/decoder_spec.rb
+++ b/spec/lib/sbom/package_url/decoder_spec.rb
@@ -7,9 +7,9 @@ require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_c
RSpec.describe Sbom::PackageUrl::Decoder do
describe '#decode' do
- subject(:decode) { described_class.new(url).decode! }
+ subject(:decode) { described_class.new(purl).decode! }
- include_context 'with purl matrix'
+ include_context 'with valid purl examples'
with_them do
it do
@@ -25,7 +25,7 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
context 'when no argument is passed' do
- let(:url) { nil }
+ let(:purl) { nil }
it 'raises an error' do
expect { decode }.to raise_error(ArgumentError)
@@ -33,17 +33,17 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
context 'when an invalid package URL string is passed' do
- where(:url) { ['invalid', 'pkg:nil'] }
+ include_context 'with invalid purl examples'
with_them do
it 'raises an error' do
- expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageURL)
+ expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageUrl)
end
end
end
context 'when namespace or subpath contains an encoded slash' do
- where(:url) do
+ where(:purl) do
[
'pkg:golang/google.org/golang/genproto#googleapis%2fapi%2fannotations',
'pkg:golang/google.org%2fgolang/genproto#googleapis/api/annotations'
@@ -51,12 +51,12 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
with_them do
- it { expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageURL) }
+ it { expect { decode }.to raise_error(Sbom::PackageUrl::InvalidPackageUrl) }
end
end
context 'when name contains an encoded slash' do
- let(:url) { 'pkg:golang/google.org/golang%2fgenproto#googleapis/api/annotations' }
+ let(:purl) { 'pkg:golang/google.org/golang%2fgenproto#googleapis/api/annotations' }
it do
is_expected.to have_attributes(
@@ -71,7 +71,7 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
context 'with URL encoded segments' do
- let(:url) do
+ let(:purl) do
'pkg:golang/namespace%21/google.golang.org%20genproto@version%21?k=v%21#googleapis%20api%20annotations'
end
@@ -88,7 +88,7 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
context 'when segments contain empty values' do
- let(:url) { 'pkg:golang/google.golang.org//.././genproto#googleapis/..//./api/annotations' }
+ let(:purl) { 'pkg:golang/google.golang.org//.././genproto#googleapis/..//./api/annotations' }
it 'removes them from the segments' do
is_expected.to have_attributes(
@@ -103,7 +103,7 @@ RSpec.describe Sbom::PackageUrl::Decoder do
end
context 'when qualifiers have no value' do
- let(:url) { 'pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25&foo=&bar=' }
+ let(:purl) { 'pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25&foo=&bar=' }
it 'they are ignored' do
is_expected.to have_attributes(
diff --git a/spec/lib/sbom/package_url/encoder_spec.rb b/spec/lib/sbom/package_url/encoder_spec.rb
index ff672170050..bdbd61636b5 100644
--- a/spec/lib/sbom/package_url/encoder_spec.rb
+++ b/spec/lib/sbom/package_url/encoder_spec.rb
@@ -20,10 +20,10 @@ RSpec.describe Sbom::PackageUrl::Encoder do
subject(:encode) { described_class.new(package).encode }
- include_context 'with purl matrix'
+ include_context 'with valid purl examples'
with_them do
- it { is_expected.to eq(url) }
+ it { is_expected.to eq(canonical_purl) }
end
end
end
diff --git a/spec/lib/sbom/package_url/normalizer_spec.rb b/spec/lib/sbom/package_url/normalizer_spec.rb
new file mode 100644
index 00000000000..bbc2bd3ca13
--- /dev/null
+++ b/spec/lib/sbom/package_url/normalizer_spec.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'rspec-parameterized'
+
+require_relative '../../../support/shared_contexts/lib/sbom/package_url_shared_contexts'
+
+RSpec.describe Sbom::PackageUrl::Normalizer do
+ shared_examples 'name normalization' do
+ context 'with bitbucket url' do
+ let(:type) { 'bitbucket' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text' do
+ is_expected.to eq('purl_spec')
+ end
+ end
+
+ context 'with github url' do
+ let(:type) { 'github' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text' do
+ is_expected.to eq('purl_spec')
+ end
+ end
+
+ context 'with pypi url' do
+ let(:type) { 'pypi' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'downcases text and replaces underscores' do
+ is_expected.to eq('purl-spec')
+ end
+ end
+
+ context 'with other urls' do
+ let(:type) { 'npm' }
+ let(:text) { 'Purl_Spec' }
+
+ it 'does not change the text' do
+ is_expected.to eq(text)
+ end
+ end
+ end
+
+ describe '#normalize_name' do
+ subject(:normalize_name) { described_class.new(type: type, text: text).normalize_name }
+
+ it_behaves_like 'name normalization'
+
+ context 'when text is nil' do
+ let(:type) { 'npm' }
+ let(:text) { nil }
+
+ it 'raises an error' do
+ expect { normalize_name }.to raise_error(ArgumentError, 'Name is required')
+ end
+ end
+ end
+
+ describe '#normalize_namespace' do
+ subject(:normalize_namespace) { described_class.new(type: type, text: text).normalize_namespace }
+
+ it_behaves_like 'name normalization'
+
+ context 'when text is nil' do
+ let(:type) { 'npm' }
+ let(:text) { nil }
+
+ it 'allows nil values' do
+ expect(normalize_namespace).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/lib/sbom/package_url_spec.rb b/spec/lib/sbom/package_url_spec.rb
index 72090c5bd29..6760b0a68e5 100644
--- a/spec/lib/sbom/package_url_spec.rb
+++ b/spec/lib/sbom/package_url_spec.rb
@@ -32,37 +32,46 @@ require_relative '../../support/shared_contexts/lib/sbom/package_url_shared_cont
RSpec.describe Sbom::PackageUrl do
include NextInstanceOf
- let(:args) do
- {
- type: 'example',
- namespace: 'test',
- name: 'test',
- version: '1.0.0',
- qualifiers: { 'arch' => 'x86_64' },
- subpath: 'path/to/package'
- }
- end
-
describe '#initialize' do
- subject { described_class.new(**args) }
+ subject do
+ described_class.new(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
context 'with well-formed arguments' do
- it { is_expected.to have_attributes(**args) }
+ include_context 'with valid purl examples'
+
+ with_them do
+ it do
+ is_expected.to have_attributes(
+ type: type,
+ namespace: namespace,
+ name: name,
+ version: version,
+ qualifiers: qualifiers,
+ subpath: subpath
+ )
+ end
+ end
end
context 'when no arguments are given' do
it { expect { described_class.new }.to raise_error(ArgumentError) }
end
- context 'when required parameters are missing' do
- where(:param) { %i[type name] }
-
- before do
- args[param] = nil
- end
+ context 'when parameters are invalid' do
+ include_context 'with invalid purl examples'
with_them do
- it { expect { subject }.to raise_error(ArgumentError) }
+ it 'raises an ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
+ end
end
end
@@ -98,7 +107,7 @@ RSpec.describe Sbom::PackageUrl do
end
describe '#to_h' do
- let(:purl) do
+ let(:package) do
described_class.new(
type: type,
namespace: namespace,
@@ -109,9 +118,9 @@ RSpec.describe Sbom::PackageUrl do
)
end
- subject(:to_h) { purl.to_h }
+ subject(:to_h) { package.to_h }
- include_context 'with purl matrix'
+ include_context 'with valid purl examples'
with_them do
it do
@@ -131,7 +140,16 @@ RSpec.describe Sbom::PackageUrl do
end
describe '#to_s' do
- let(:package) { described_class.new(**args) }
+ let(:package) do
+ described_class.new(
+ type: 'npm',
+ namespace: nil,
+ name: 'lodash',
+ version: nil,
+ qualifiers: nil,
+ subpath: nil
+ )
+ end
it 'delegates to_s to the encoder' do
expect_next_instance_of(described_class::Encoder, package) do |encoder|
diff --git a/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
new file mode 100644
index 00000000000..c55e4bcfba7
--- /dev/null
+++ b/spec/migrations/20221102090940_create_next_ci_partitions_record_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateNextCiPartitionsRecord, migration: :gitlab_ci do
+ let(:migration) { described_class.new }
+ let(:partitions) { table(:ci_partitions) }
+
+ describe '#up' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'creates next partitions record and resets the sequence' do
+ expect { migrate! }
+ .to change { partitions.where(id: 101).any? }
+ .from(false).to(true)
+
+ expect { partitions.create! }.not_to raise_error
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not create records' do
+ expect { migrate! }.not_to change(partitions, :count)
+ end
+ end
+ end
+
+ describe '#down' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'removes the record' do
+ migrate!
+
+ expect { migration.down }
+ .to change { partitions.where(id: 101).any? }
+ .from(true).to(false)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true, false)
+ end
+
+ it 'does not remove the record' do
+ expect { migrate! }.to change(partitions, :count).by(1)
+
+ expect { migration.down }.not_to change(partitions, :count)
+ end
+ end
+ end
+end
diff --git a/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
new file mode 100644
index 00000000000..99754d609ed
--- /dev/null
+++ b/spec/migrations/20221102090943_create_second_partition_for_builds_metadata_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe CreateSecondPartitionForBuildsMetadata, :migration do
+ let(:migration) { described_class.new }
+ let(:partitions) { table(:ci_partitions) }
+
+ describe '#up' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'creates a new partition' do
+ expect { migrate! }.to change { partitions_count }.by(1)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not create the partition' do
+ expect { migrate! }.not_to change { partitions_count }
+ end
+ end
+ end
+
+ describe '#down' do
+ context 'when on sass' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(true)
+ end
+
+ it 'removes the partition' do
+ migrate!
+
+ expect { migration.down }.to change { partitions_count }.by(-1)
+ end
+ end
+
+ context 'when self-managed' do
+ before do
+ allow(Gitlab).to receive(:com?).and_return(false)
+ end
+
+ it 'does not change the partitions count' do
+ migrate!
+
+ expect { migration.down }.not_to change { partitions_count }
+ end
+ end
+ end
+
+ def partitions_count
+ Gitlab::Database::PostgresPartition.for_parent_table(:p_ci_builds_metadata).size
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index c03fcdb6f9c..6ba450b6d57 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -99,6 +99,15 @@ RSpec.describe Group do
expect(group).to be_valid
end
+
+ it 'does not allow a subgroup to have the same name as an existing subgroup' do
+ sub_group1 = create(:group, parent: group, name: "SG", path: 'api')
+ sub_group2 = described_class.new(parent: group, name: "SG", path: 'api2')
+
+ expect(sub_group1).to be_valid
+ expect(sub_group2).not_to be_valid
+ expect(sub_group2.errors.full_messages.to_sentence).to eq('Name has already been taken')
+ end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 184500f3209..e76cd22d342 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -598,7 +598,7 @@ RSpec.describe Project, factory_default: :keep do
end
it 'contains errors related to the project being deleted' do
- expect(new_project.errors.full_messages.first).to eq(_('The project is still being deleted. Please try again later.'))
+ expect(new_project.errors.full_messages).to include(_('The project is still being deleted. Please try again later.'))
end
end
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index 401db766589..05fe55b06a1 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
it 'executes a limited number of queries' do
control_count = ActiveRecord::QueryRecorder.new { subject }.count
- expect(control_count).to be <= 110
+ expect(control_count).to be <= 111
end
it 'schedules an import using a namespace' do
@@ -215,7 +215,7 @@ RSpec.describe API::ProjectImport, :aggregate_failures do
subject
expect(response).to have_gitlab_http_status(:bad_request)
- expect(json_response['message']).to eq('Name has already been taken')
+ expect(json_response['message']).to eq('Project namespace name has already been taken')
end
context 'when param overwrite is true' do
diff --git a/spec/scripts/trigger-build_spec.rb b/spec/scripts/trigger-build_spec.rb
index 9032ba85b9f..ac8e3c7797c 100644
--- a/spec/scripts/trigger-build_spec.rb
+++ b/spec/scripts/trigger-build_spec.rb
@@ -229,7 +229,6 @@ RSpec.describe Trigger do
context "when set in a file" do
before do
- stub_env(version_file)
allow(File).to receive(:read).and_call_original
end
diff --git a/spec/services/users/migrate_to_ghost_user_service_spec.rb b/spec/services/users/migrate_to_ghost_user_service_spec.rb
deleted file mode 100644
index 073ebaae5b0..00000000000
--- a/spec/services/users/migrate_to_ghost_user_service_spec.rb
+++ /dev/null
@@ -1,97 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Users::MigrateToGhostUserService do
- let!(:user) { create(:user) }
- let!(:project) { create(:project, :repository) }
- let(:service) { described_class.new(user) }
- let(:always_ghost) { false }
-
- context "migrating a user's associated records to the ghost user" do
- context 'issues' do
- context 'deleted user is present as both author and edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:author, :last_edited_by] do
- let(:created_record) do
- create(:issue, project: project, author: user, last_edited_by: user)
- end
- end
- end
-
- context 'deleted user is present only as edited_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", Issue, [:last_edited_by] do
- let(:created_record) { create(:issue, project: project, author: create(:user), last_edited_by: user) }
- end
- end
- end
-
- context 'merge requests' do
- context 'deleted user is present as both author and merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:author, :merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, author: user, merge_user: user, target_branch: "first") }
- end
- end
-
- context 'deleted user is present only as both merge_user' do
- include_examples "migrating a deleted user's associated records to the ghost user", MergeRequest, [:merge_user] do
- let(:created_record) { create(:merge_request, source_project: project, merge_user: user, target_branch: "first") }
- end
- end
- end
-
- context 'notes' do
- include_examples "migrating a deleted user's associated records to the ghost user", Note do
- let(:created_record) { create(:note, project: project, author: user) }
- end
- end
-
- context 'abuse reports' do
- include_examples "migrating a deleted user's associated records to the ghost user", AbuseReport do
- let(:created_record) { create(:abuse_report, reporter: user, user: create(:user)) }
- end
- end
-
- context 'award emoji' do
- include_examples "migrating a deleted user's associated records to the ghost user", AwardEmoji, [:user] do
- let(:created_record) { create(:award_emoji, user: user) }
-
- context "when the awardable already has an award emoji of the same name assigned to the ghost user" do
- let(:awardable) { create(:issue) }
- let!(:existing_award_emoji) { create(:award_emoji, user: User.ghost, name: "thumbsup", awardable: awardable) }
- let!(:award_emoji) { create(:award_emoji, user: user, name: "thumbsup", awardable: awardable) }
-
- it "migrates the award emoji regardless" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record.user).to eq(User.ghost)
- end
-
- it "does not leave the migrated award emoji in an invalid state" do
- service.execute
-
- migrated_record = AwardEmoji.find_by_id(award_emoji.id)
-
- expect(migrated_record).to be_valid
- end
- end
- end
- end
-
- context 'snippets' do
- include_examples "migrating a deleted user's associated records to the ghost user", Snippet do
- let(:created_record) { create(:snippet, project: project, author: user) }
- end
- end
-
- context 'reviews' do
- let!(:user) { create(:user) }
- let(:service) { described_class.new(user) }
-
- include_examples "migrating a deleted user's associated records to the ghost user", Review, [:author] do
- let(:created_record) { create(:review, author: user) }
- end
- end
- end
-end
diff --git a/spec/support/helpers/search_helpers.rb b/spec/support/helpers/search_helpers.rb
index 581ef07752e..7d0f8c09933 100644
--- a/spec/support/helpers/search_helpers.rb
+++ b/spec/support/helpers/search_helpers.rb
@@ -33,13 +33,13 @@ module SearchHelpers
end
def select_search_scope(scope)
- page.within '.search-filter' do
+ page.within '[data-testid="search-filter"]' do
click_link scope
end
end
def has_search_scope?(scope)
- page.within '.search-filter' do
+ page.within '[data-testid="search-filter"]' do
has_link?(scope)
end
end
diff --git a/spec/support/rate_limiter.rb b/spec/support/rate_limiter.rb
new file mode 100644
index 00000000000..525d593c293
--- /dev/null
+++ b/spec/support/rate_limiter.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ config.before(:each, :disable_rate_limiter) do
+ allow(Gitlab::ApplicationRateLimiter).to receive(:throttled?).and_return(false)
+ end
+end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index f9d61117ef6..67b7023f1ff 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -997,7 +997,6 @@
- './ee/spec/graphql/types/vulnerability_severity_enum_spec.rb'
- './ee/spec/graphql/types/vulnerability_sort_enum_spec.rb'
- './ee/spec/graphql/types/vulnerability_state_enum_spec.rb'
-- './ee/spec/graphql/types/vulnerability_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_dependency_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_kubernetes_resource_type_spec.rb'
- './ee/spec/graphql/types/vulnerable_package_type_spec.rb'
diff --git a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
index b5c9e9cc7b0..263cf9f5e19 100644
--- a/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
+++ b/spec/support/shared_contexts/lib/sbom/package_url_shared_contexts.rb
@@ -1,98 +1,26 @@
# frozen_string_literal: true
-RSpec.shared_context 'with purl matrix' do
+require 'oj'
+
+def parameterized_test_matrix(invalid: false)
+ test_cases_path = File.join(
+ File.expand_path(__dir__), '..', '..', '..', '..', 'fixtures', 'lib', 'sbom', 'package-url-test-cases.json')
+ test_cases = Gitlab::Json.parse(File.read(test_cases_path))
+
+ test_cases.filter { _1.delete('is_invalid') == invalid }.each_with_object({}) do |test_case, memo|
+ description = test_case.delete('description')
+ memo[description] = test_case.symbolize_keys
+ end
+end
+
+RSpec.shared_context 'with valid purl examples' do
+ where do
+ parameterized_test_matrix(invalid: false)
+ end
+end
+
+RSpec.shared_context 'with invalid purl examples' do
where do
- {
- 'valid RubyGems package URL' => {
- url: 'pkg:gem/ruby-advisory-db-check@0.12.4',
- type: 'gem',
- namespace: nil,
- name: 'ruby-advisory-db-check',
- version: '0.12.4',
- qualifiers: nil,
- subpath: nil
- },
- 'valid BitBucket package URL' => {
- url: 'pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c',
- type: 'bitbucket',
- namespace: 'birkenfeld',
- name: 'pygments-main',
- version: '244fd47e07d1014f0aed9c',
- qualifiers: nil,
- subpath: nil
- },
- 'valid GitHub package URL' => {
- url: 'pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c',
- type: 'github',
- namespace: 'package-url',
- name: 'purl-spec',
- version: '244fd47e07d1004f0aed9c',
- qualifiers: nil,
- subpath: nil
- },
- 'valid Go module URL' => {
- url: 'pkg:golang/google.golang.org/genproto#googleapis/api/annotations',
- type: 'golang',
- namespace: 'google.golang.org',
- name: 'genproto',
- version: nil,
- qualifiers: nil,
- subpath: 'googleapis/api/annotations'
- },
- 'valid Maven package URL' => {
- url: 'pkg:maven/org.apache.commons/io@1.3.4',
- type: 'maven',
- namespace: 'org.apache.commons',
- name: 'io',
- version: '1.3.4',
- qualifiers: nil,
- subpath: nil
- },
- 'valid NPM package URL' => {
- url: 'pkg:npm/foobar@12.3.1',
- type: 'npm',
- namespace: nil,
- name: 'foobar',
- version: '12.3.1',
- qualifiers: nil,
- subpath: nil
- },
- 'valid NuGet package URL' => {
- url: 'pkg:nuget/EnterpriseLibrary.Common@6.0.1304',
- type: 'nuget',
- namespace: nil,
- name: 'EnterpriseLibrary.Common',
- version: '6.0.1304',
- qualifiers: nil,
- subpath: nil
- },
- 'valid PyPI package URL' => {
- url: 'pkg:pypi/django@1.11.1',
- type: 'pypi',
- namespace: nil,
- name: 'django',
- version: '1.11.1',
- qualifiers: nil,
- subpath: nil
- },
- 'valid RPM package URL' => {
- url: 'pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25',
- type: 'rpm',
- namespace: 'fedora',
- name: 'curl',
- version: '7.50.3-1.fc25',
- qualifiers: { 'arch' => 'i386', 'distro' => 'fedora-25' },
- subpath: nil
- },
- 'package URL with checksums' => {
- url: 'pkg:rpm/name?checksums=a,b,c',
- type: 'rpm',
- namespace: nil,
- name: 'name',
- version: nil,
- qualifiers: { 'checksums' => %w[a b c] },
- subpath: nil
- }
- }
+ parameterized_test_matrix(invalid: true)
end
end
diff --git a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
index 84dc2b20ddc..cc74c977064 100644
--- a/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
+++ b/spec/support/shared_examples/features/search/search_timeouts_shared_examples.rb
@@ -1,23 +1,23 @@
# frozen_string_literal: true
RSpec.shared_examples 'search timeouts' do |scope|
+ let(:additional_params) { {} }
+
context 'when search times out' do
before do
- stub_feature_flags(search_page_vertical_nav: false)
allow_next_instance_of(SearchService) do |service|
allow(service).to receive(:search_objects).and_raise(ActiveRecord::QueryCanceled)
end
- visit(search_path(search: 'test', scope: scope))
+ visit(search_path(search: 'test', scope: scope, **additional_params))
end
it 'renders timeout information' do
- # expect(page).to have_content('This endpoint has been requested too many times.')
expect(page).to have_content('Your search timed out')
end
it 'sets tab count to 0' do
- expect(page.find('.search-filter .active')).to have_text('0')
+ expect(page.find('[data-testid="search-filter"] .active')).to have_text('0')
end
end
end
diff --git a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
index e725de8ad31..f5431b29ee2 100644
--- a/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/max_issuable_examples.rb
@@ -12,49 +12,60 @@ RSpec.shared_examples 'does not exceed the issuable size limit' do
project.add_maintainer(user3)
end
- context 'when feature flag is turned on' do
- context "when the number of users of issuable does exceed the limit" do
- before do
- stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
+ context "when the number of users of issuable does exceed the limit" do
+ before do
+ stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 2)
+ end
+
+ it 'will not add more than the allowed number of users' do
+ allow_next_instance_of(update_service) do |service|
+ expect(service).not_to receive(:execute)
end
- it 'will not add more than the allowed number of users' do
- allow_next_instance_of(update_service) do |service|
- expect(service).not_to receive(:execute)
- end
+ note = described_class.new(project, user, opts.merge(
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
- note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ expect(note.errors[:validation]).to match_array([validation_message])
+ end
+ end
- expect(note.errors[:validation]).to match_array([validation_message])
- end
+ context "when the number of users does not exceed the limit" do
+ before do
+ stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6)
end
- context "when the number of users does not exceed the limit" do
- before do
- stub_const("Issuable::MAX_NUMBER_OF_ASSIGNEES_OR_REVIEWERS", 6)
+ it 'calls execute and does not return an error' do
+ allow_next_instance_of(update_service) do |service|
+ expect(service).to receive(:execute).and_call_original
end
- it 'calls execute and does not return an error' do
- allow_next_instance_of(update_service) do |service|
- expect(service).to receive(:execute).and_call_original
- end
-
- note = described_class.new(project, user, opts.merge(
- note: note_text,
- noteable_type: noteable_type,
- noteable_id: issuable.id,
- confidential: false
- )).execute
+ note = described_class.new(project, user, opts.merge(
+ note: note_text,
+ noteable_type: noteable_type,
+ noteable_id: issuable.id,
+ confidential: false
+ )).execute
- expect(note.errors[:validation]).to be_empty
- end
+ expect(note.errors[:validation]).to be_empty
end
end
+end
+
+RSpec.shared_examples 'does not exceed the issuable size limit with ff off' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:user3) { create(:user) }
+
+ before do
+ project.add_maintainer(user)
+ project.add_maintainer(user1)
+ project.add_maintainer(user2)
+ project.add_maintainer(user3)
+ end
context 'when feature flag is off' do
before do
diff --git a/spec/tasks/gitlab/gitaly_rake_spec.rb b/spec/tasks/gitlab/gitaly_rake_spec.rb
index 37df90fff22..d2f4fa0b8ef 100644
--- a/spec/tasks/gitlab/gitaly_rake_spec.rb
+++ b/spec/tasks/gitlab/gitaly_rake_spec.rb
@@ -10,7 +10,7 @@ RSpec.describe 'gitlab:gitaly namespace rake task', :silence_stdout do
let(:repo) { 'https://gitlab.com/gitlab-org/gitaly.git' }
let(:clone_path) { Rails.root.join('tmp/tests/gitaly').to_s }
let(:storage_path) { Rails.root.join('tmp/tests/repositories').to_s }
- let(:version) { Gitlab::GitalyClient.expected_server_version }
+ let(:version) { File.read(Rails.root.join(Gitlab::GitalyClient::SERVER_VERSION_FILE)).chomp }
describe 'clone' do
subject { run_rake_task('gitlab:gitaly:clone', clone_path, storage_path) }