-
- {{ error }}
-
-
+
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
new file mode 100644
index 00000000000..630aff9858f
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -0,0 +1,24 @@
+
diff --git a/app/assets/javascripts/content_editor/services/content_editor.js b/app/assets/javascripts/content_editor/services/content_editor.js
index 209160ad80c..a387322bff7 100644
--- a/app/assets/javascripts/content_editor/services/content_editor.js
+++ b/app/assets/javascripts/content_editor/services/content_editor.js
@@ -19,6 +19,10 @@ export class ContentEditor {
return doc.childCount === 0 || (doc.childCount === 1 && doc.child(0).childCount === 0);
}
+ dispose() {
+ this.tiptapEditor.destroy();
+ }
+
once(type, handler) {
this._eventHub.$once(type, handler);
}
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index 7abd3211a72..8ac3f719309 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -72,7 +72,9 @@ const uploadImage = async ({ editor, file, uploadsPath, renderMarkdown }) => {
);
} catch (e) {
editor.commands.deleteRange({ from: position, to: position + 1 });
- editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
+ editor.emit('error', {
+ error: __('An error occurred while uploading the image. Please try again.'),
+ });
}
};
@@ -100,7 +102,9 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown }) =
);
} catch (e) {
editor.commands.deleteRange({ from, to: from + 1 });
- editor.emit('error', __('An error occurred while uploading the file. Please try again.'));
+ editor.emit('error', {
+ error: __('An error occurred while uploading the file. Please try again.'),
+ });
}
};
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 1f211b18af1..68865281b27 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -6,7 +6,6 @@ import {
GlButton,
GlSprintf,
GlAlert,
- GlLoadingIcon,
GlModal,
GlModalDirective,
} from '@gitlab/ui';
@@ -114,7 +113,6 @@ export default {
GlButton,
GlModal,
MarkdownField,
- GlLoadingIcon,
ContentEditor: () =>
import(
/* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue'
@@ -136,11 +134,12 @@ export default {
commitMessage: '',
isDirty: false,
contentEditorRenderFailed: false,
+ contentEditorEmpty: false,
};
},
computed: {
noContent() {
- if (this.isContentEditorActive) return this.contentEditor?.empty;
+ if (this.isContentEditorActive) return this.contentEditorEmpty;
return !this.content.trim();
},
csrfToken() {
@@ -205,7 +204,7 @@ export default {
window.removeEventListener('beforeunload', this.onPageUnload);
},
methods: {
- getContentHTML(content) {
+ renderMarkdown(content) {
return axios
.post(this.pageInfo.markdownPreviewPath, { text: content })
.then(({ data }) => data.body);
@@ -232,6 +231,32 @@ export default {
this.isDirty = true;
},
+ async loadInitialContent(contentEditor) {
+ this.contentEditor = contentEditor;
+
+ try {
+ await this.contentEditor.setSerializedContent(this.content);
+ this.trackContentEditorLoaded();
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ async retryInitContentEditor() {
+ try {
+ this.contentEditorRenderFailed = false;
+ await this.contentEditor.setSerializedContent(this.content);
+ } catch (e) {
+ this.contentEditorRenderFailed = true;
+ }
+ },
+
+ handleContentEditorChange({ empty }) {
+ this.contentEditorEmpty = empty;
+ // TODO: Implement a precise mechanism to detect changes in the Content
+ this.isDirty = true;
+ },
+
onPageUnload(event) {
if (!this.isDirty) return undefined;
@@ -252,36 +277,8 @@ export default {
this.commitMessage = newCommitMessage;
},
- async initContentEditor() {
- this.isContentEditorLoading = true;
+ initContentEditor() {
this.useContentEditor = true;
-
- const { createContentEditor } = await import(
- /* webpackChunkName: 'content_editor' */ '~/content_editor/services/create_content_editor'
- );
- this.contentEditor =
- this.contentEditor ||
- createContentEditor({
- renderMarkdown: (markdown) => this.getContentHTML(markdown),
- uploadsPath: this.pageInfo.uploadsPath,
- tiptapOptions: {
- onUpdate: () => this.handleContentChange(),
- },
- });
-
- try {
- await this.contentEditor.setSerializedContent(this.content);
- this.isContentEditorLoading = false;
-
- this.trackContentEditorLoaded();
- } catch (e) {
- this.contentEditorRenderFailed = true;
- }
- },
-
- retryInitContentEditor() {
- this.contentEditorRenderFailed = false;
- this.initContentEditor();
},
switchToOldEditor() {
@@ -475,12 +472,12 @@ export default {
>
-
-
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 99b1e44f23b..7315bce1ed9 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -83,6 +83,7 @@
li.md-header-toolbar {
margin-left: auto;
display: none;
+ padding-bottom: $gl-padding-8;
&.active {
display: block;
@@ -92,8 +93,8 @@
display: flex;
justify-content: center;
width: 100%;
- padding-top: $gl-padding-top;
- padding-bottom: $gl-padding-top;
+ flex-wrap: wrap;
+ margin-top: $gl-padding-8;
}
}
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f904ef11f5b..06eebb95438 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -14,7 +14,6 @@
a,
button {
padding: $gl-padding-8;
- padding-bottom: $gl-padding-8 + 1;
font-size: 14px;
line-height: 28px;
color: $gl-text-color-secondary;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c025d8569a7..e2b5af73715 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -54,7 +54,7 @@
.common-note-form {
.md-area {
- padding: $gl-padding-top $gl-padding;
+ padding: $gl-padding-8 $gl-padding;
border: 1px solid $border-color;
border-radius: $border-radius-base;
transition: border-color ease-in-out 0.15s,
diff --git a/app/finders/packages/pypi/packages_finder.rb b/app/finders/packages/pypi/packages_finder.rb
index 642ca2cf2e6..47cfb59944b 100644
--- a/app/finders/packages/pypi/packages_finder.rb
+++ b/app/finders/packages/pypi/packages_finder.rb
@@ -3,11 +3,8 @@
module Packages
module Pypi
class PackagesFinder < ::Packages::GroupOrProjectPackageFinder
- def execute!
- results = packages.with_normalized_pypi_name(@params[:package_name])
- raise ActiveRecord::RecordNotFound if results.empty?
-
- results
+ def execute
+ packages.with_normalized_pypi_name(@params[:package_name])
end
private
diff --git a/db/migrate/20210806152104_add_pypi_package_requests_forwarding_to_application_settings.rb b/db/migrate/20210806152104_add_pypi_package_requests_forwarding_to_application_settings.rb
new file mode 100644
index 00000000000..34f8ec43a8f
--- /dev/null
+++ b/db/migrate/20210806152104_add_pypi_package_requests_forwarding_to_application_settings.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddPypiPackageRequestsForwardingToApplicationSettings < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ with_lock_retries do
+ add_column(:application_settings, :pypi_package_requests_forwarding, :boolean, default: true, null: false)
+ end
+ end
+
+ def down
+ with_lock_retries do
+ remove_column(:application_settings, :pypi_package_requests_forwarding)
+ end
+ end
+end
diff --git a/db/post_migrate/20210706212710_finalize_ci_job_artifacts_bigint_conversion.rb b/db/post_migrate/20210706212710_finalize_ci_job_artifacts_bigint_conversion.rb
new file mode 100644
index 00000000000..40977277bd1
--- /dev/null
+++ b/db/post_migrate/20210706212710_finalize_ci_job_artifacts_bigint_conversion.rb
@@ -0,0 +1,84 @@
+# frozen_string_literal: true
+
+# See https://docs.gitlab.com/ee/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class FinalizeCiJobArtifactsBigintConversion < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ TABLE_NAME = 'ci_job_artifacts'
+
+ def up
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'CopyColumnUsingBackgroundMigrationJob',
+ table_name: TABLE_NAME,
+ column_name: 'id',
+ job_arguments: [%w[id job_id], %w[id_convert_to_bigint job_id_convert_to_bigint]]
+ )
+
+ swap
+ end
+
+ def down
+ swap
+ end
+
+ private
+
+ def swap
+ add_concurrent_index TABLE_NAME, :id_convert_to_bigint, unique: true, name: 'index_ci_job_artifact_on_id_convert_to_bigint'
+ # This is to replace the existing "index_ci_job_artifacts_for_terraform_reports" btree (project_id, id) where (file_type = 18)
+ add_concurrent_index TABLE_NAME, [:project_id, :id_convert_to_bigint], name: 'index_ci_job_artifacts_for_terraform_reports_bigint', where: "file_type = 18"
+ # This is to replace the existing "index_ci_job_artifacts_id_for_terraform_reports" btree (id) where (file_type = 18)
+ add_concurrent_index TABLE_NAME, [:id_convert_to_bigint], name: 'index_ci_job_artifacts_id_for_terraform_reports_bigint', where: "file_type = 18"
+
+ # Add a FK on `project_pages_metadata(artifacts_archive_id)` to `id_convert_to_bigint`, the old FK (fk_69366a119e)
+ # will be removed when ci_job_artifacts_pkey constraint is droppped.
+ fk_artifacts_archive_id = concurrent_foreign_key_name(:project_pages_metadata, :artifacts_archive_id)
+ fk_artifacts_archive_id_tmp = "#{fk_artifacts_archive_id}_tmp"
+ add_concurrent_foreign_key :project_pages_metadata, TABLE_NAME,
+ column: :artifacts_archive_id, target_column: :id_convert_to_bigint,
+ name: fk_artifacts_archive_id_tmp,
+ on_delete: :nullify,
+ reverse_lock_order: true
+
+ with_lock_retries(raise_on_exhaustion: true) do
+ # We'll need ACCESS EXCLUSIVE lock on the related tables,
+ # lets make sure it can be acquired from the start
+ execute "LOCK TABLE #{TABLE_NAME}, project_pages_metadata IN ACCESS EXCLUSIVE MODE"
+
+ # Swap column names
+ temp_name = 'id_tmp'
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:id)} TO #{quote_column_name(temp_name)}"
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(:id_convert_to_bigint)} TO #{quote_column_name(:id)}"
+ execute "ALTER TABLE #{quote_table_name(TABLE_NAME)} RENAME COLUMN #{quote_column_name(temp_name)} TO #{quote_column_name(:id_convert_to_bigint)}"
+
+ # We need to update the trigger function in order to make PostgreSQL to
+ # regenerate the execution plan for it. This is to avoid type mismatch errors like
+ # "type of parameter 15 (bigint) does not match that when preparing the plan (integer)"
+ function_name = Gitlab::Database::UnidirectionalCopyTrigger.on_table(TABLE_NAME).name([:id, :job_id], [:id_convert_to_bigint, :job_id_convert_to_bigint])
+ execute "ALTER FUNCTION #{quote_table_name(function_name)} RESET ALL"
+
+ # Swap defaults
+ execute "ALTER SEQUENCE ci_job_artifacts_id_seq OWNED BY #{TABLE_NAME}.id"
+ change_column_default TABLE_NAME, :id, -> { "nextval('ci_job_artifacts_id_seq'::regclass)" }
+ change_column_default TABLE_NAME, :id_convert_to_bigint, 0
+
+ # Swap PK constraint
+ execute "ALTER TABLE #{TABLE_NAME} DROP CONSTRAINT ci_job_artifacts_pkey CASCADE" # this will drop ci_job_artifacts_pkey primary key
+ rename_index TABLE_NAME, 'index_ci_job_artifact_on_id_convert_to_bigint', 'ci_job_artifacts_pkey'
+ execute "ALTER TABLE #{TABLE_NAME} ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY USING INDEX ci_job_artifacts_pkey"
+
+ # Rename the rest of the indexes (we already hold an exclusive lock, so no need to use DROP INDEX CONCURRENTLY here
+ execute 'DROP INDEX index_ci_job_artifacts_for_terraform_reports'
+ rename_index TABLE_NAME, 'index_ci_job_artifacts_for_terraform_reports_bigint', 'index_ci_job_artifacts_for_terraform_reports'
+ execute 'DROP INDEX index_ci_job_artifacts_id_for_terraform_reports'
+ rename_index TABLE_NAME, 'index_ci_job_artifacts_id_for_terraform_reports_bigint', 'index_ci_job_artifacts_id_for_terraform_reports'
+
+ # Change the name of the temporary FK for project_pages_metadata(artifacts_archive_id) -> id
+ rename_constraint(:project_pages_metadata, fk_artifacts_archive_id_tmp, fk_artifacts_archive_id)
+ end
+ end
+end
diff --git a/db/schema_migrations/20210706212710 b/db/schema_migrations/20210706212710
new file mode 100644
index 00000000000..7a4e6df37a4
--- /dev/null
+++ b/db/schema_migrations/20210706212710
@@ -0,0 +1 @@
+33162af4ef99c32d3c5b38479e407d4911a8d3dce53407dbee6e5745c8e945ae
\ No newline at end of file
diff --git a/db/schema_migrations/20210806152104 b/db/schema_migrations/20210806152104
new file mode 100644
index 00000000000..a8bdc0615d5
--- /dev/null
+++ b/db/schema_migrations/20210806152104
@@ -0,0 +1 @@
+1bdbcc6ef5ccf7a2bfb1f9571885e218e230a81b632a2d993302bd87432963f3
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 8955a08a6ba..f107a569313 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9624,6 +9624,7 @@ CREATE TABLE application_settings (
usage_ping_features_enabled boolean DEFAULT false NOT NULL,
encrypted_customers_dot_jwt_signing_key bytea,
encrypted_customers_dot_jwt_signing_key_iv bytea,
+ pypi_package_requests_forwarding boolean DEFAULT true NOT NULL,
CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)),
CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)),
CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)),
@@ -10785,7 +10786,7 @@ CREATE SEQUENCE ci_instance_variables_id_seq
ALTER SEQUENCE ci_instance_variables_id_seq OWNED BY ci_instance_variables.id;
CREATE TABLE ci_job_artifacts (
- id integer NOT NULL,
+ id_convert_to_bigint integer DEFAULT 0 NOT NULL,
project_id integer NOT NULL,
job_id_convert_to_bigint integer DEFAULT 0 NOT NULL,
file_type integer NOT NULL,
@@ -10798,7 +10799,7 @@ CREATE TABLE ci_job_artifacts (
file_sha256 bytea,
file_format smallint,
file_location smallint,
- id_convert_to_bigint bigint DEFAULT 0 NOT NULL,
+ id bigint NOT NULL,
job_id bigint NOT NULL,
CONSTRAINT check_27f0f6dbab CHECK ((file_store IS NOT NULL))
);
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 325dce45047..664b4d7ca1f 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -79,6 +79,7 @@ Example response:
"asset_proxy_whitelist": ["example.com", "*.example.com", "your-instance.com"],
"asset_proxy_allowlist": ["example.com", "*.example.com", "your-instance.com"],
"npm_package_requests_forwarding": true,
+ "pypi_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300,
"raw_blob_request_limit": 300,
@@ -180,6 +181,7 @@ Example response:
"allow_local_requests_from_web_hooks_and_services": true,
"allow_local_requests_from_system_hooks": false,
"npm_package_requests_forwarding": true,
+ "pypi_package_requests_forwarding": true,
"snippet_size_limit": 52428800,
"issues_create_limit": 300,
"raw_blob_request_limit": 300,
@@ -347,6 +349,7 @@ listed in the descriptions of the relevant settings.
| `mirror_max_capacity` | integer | no | **(PREMIUM)** Maximum number of mirrors that can be synchronizing at the same time. |
| `mirror_max_delay` | integer | no | **(PREMIUM)** Maximum time (in minutes) between updates that a mirror can have when scheduled to synchronize. |
| `npm_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use npmjs.org as a default remote repository when the package is not found in the GitLab Package Registry for npm. |
+| `pypi_package_requests_forwarding` | boolean | no | **(PREMIUM)** Use pypi.org as a default remote repository when the package is not found in the GitLab Package Registry for PyPI. |
| `outbound_local_requests_whitelist` | array of strings | no | Define a list of trusted domains or IP addresses to which local requests are allowed when local requests for hooks and services are disabled.
| `pages_domain_verification_enabled` | boolean | no | Require users to prove ownership of custom domains. Domain verification is an essential security measure for public GitLab sites. Users are required to demonstrate they control a domain before it is enabled. |
| `password_authentication_enabled_for_git` | boolean | no | Enable authentication for Git over HTTP(S) via a GitLab account password. Default is `true`. |
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index 99e003957aa..3b56318e711 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -262,10 +262,20 @@ To disable it:
1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand the **Package Registry** section.
-1. Uncheck **Enable forwarding of npm package requests to npmjs.org**.
-1. Click **Save changes**.
+1. Clear the checkbox **Forward npm package requests to the npm Registry if the packages are not found in the GitLab Package Registry**.
+1. Select **Save changes**.
-![npm package requests forwarding](img/admin_package_registry_npm_package_requests_forward.png)
+### PyPI Forwarding **(PREMIUM SELF)**
+
+GitLab administrators can disable the forwarding of PyPI requests to [pypi.org](https://pypi.org/).
+
+To disable it:
+
+1. On the top bar, select **Menu >** **{admin}** **Admin**.
+1. On the left sidebar, select **Settings > CI/CD**.
+1. Expand the **Package Registry** section.
+1. Clear the checkbox **Forward PyPI package requests to the PyPI Registry if the packages are not found in the GitLab Package Registry**.
+1. Select **Save changes**.
### Package file size limits
diff --git a/doc/user/admin_area/settings/img/admin_package_registry_npm_package_requests_forward.png b/doc/user/admin_area/settings/img/admin_package_registry_npm_package_requests_forward.png
deleted file mode 100644
index b6068f5d19b..00000000000
Binary files a/doc/user/admin_area/settings/img/admin_package_registry_npm_package_requests_forward.png and /dev/null differ
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 61993d37bac..2d54cfc5f7d 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -328,6 +328,11 @@ more than once, a `400 Bad Request` error occurs.
## Install a PyPI package
+In [GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/233413),
+when a PyPI package is not found in the Package Registry, the request is forwarded to [pypi.org](https://pypi.org/).
+
+Administrators can disable this behavior in the [Continuous Integration settings](../../admin_area/settings/continuous_integration.md).
+
### Install from the project level
To install the latest version of a package, use the following command:
diff --git a/lib/api/helpers/packages/dependency_proxy_helpers.rb b/lib/api/helpers/packages/dependency_proxy_helpers.rb
index 989c4e1761b..b8ae1dddd7e 100644
--- a/lib/api/helpers/packages/dependency_proxy_helpers.rb
+++ b/lib/api/helpers/packages/dependency_proxy_helpers.rb
@@ -5,11 +5,17 @@ module API
module Packages
module DependencyProxyHelpers
REGISTRY_BASE_URLS = {
- npm: 'https://registry.npmjs.org/'
+ npm: 'https://registry.npmjs.org/',
+ pypi: 'https://pypi.org/simple/'
+ }.freeze
+
+ APPLICATION_SETTING_NAMES = {
+ npm: 'npm_package_requests_forwarding',
+ pypi: 'pypi_package_requests_forwarding'
}.freeze
def redirect_registry_request(forward_to_registry, package_type, options)
- if forward_to_registry && redirect_registry_request_available?
+ if forward_to_registry && redirect_registry_request_available?(package_type)
::Gitlab::Tracking.event(self.options[:for].name, "#{package_type}_request_forward")
redirect(registry_url(package_type, options))
else
@@ -25,11 +31,20 @@ module API
case package_type
when :npm
"#{base_url}#{options[:package_name]}"
+ when :pypi
+ "#{base_url}#{options[:package_name]}/"
end
end
- def redirect_registry_request_available?
- ::Gitlab::CurrentSettings.current_application_settings.npm_package_requests_forwarding
+ def redirect_registry_request_available?(package_type)
+ application_setting_name = APPLICATION_SETTING_NAMES[package_type]
+
+ raise ArgumentError, "Can't find application setting for package_type #{package_type}" unless application_setting_name
+
+ ::Gitlab::CurrentSettings
+ .current_application_settings
+ .attributes
+ .fetch(application_setting_name, false)
end
end
end
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index 8dd1631ebf8..706c0702fce 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -10,6 +10,7 @@ module API
helpers ::API::Helpers::PackagesManagerClientsHelpers
helpers ::API::Helpers::RelatedResourcesHelpers
helpers ::API::Helpers::Packages::BasicAuthHelpers
+ helpers ::API::Helpers::Packages::DependencyProxyHelpers
include ::API::Helpers::Packages::BasicAuthHelpers::Constants
feature_category :package_registry
@@ -82,15 +83,20 @@ module API
track_package_event('list_package', :pypi)
- packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute!
- presenter = ::Packages::Pypi::PackagePresenter.new(packages, group)
+ packages = Packages::Pypi::PackagesFinder.new(current_user, group, { package_name: params[:package_name] }).execute
+ empty_packages = packages.empty?
- # Adjusts grape output format
- # to be HTML
- content_type "text/html; charset=utf-8"
- env['api.format'] = :binary
+ redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
+ not_found!('Package') if empty_packages
+ presenter = ::Packages::Pypi::PackagePresenter.new(packages, group)
- body presenter.body
+ # Adjusts grape output format
+ # to be HTML
+ content_type "text/html; charset=utf-8"
+ env['api.format'] = :binary
+
+ body presenter.body
+ end
end
end
end
@@ -142,15 +148,20 @@ module API
track_package_event('list_package', :pypi, project: authorized_user_project, namespace: authorized_user_project.namespace)
- packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute!
- presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
+ packages = Packages::Pypi::PackagesFinder.new(current_user, authorized_user_project, { package_name: params[:package_name] }).execute
+ empty_packages = packages.empty?
- # Adjusts grape output format
- # to be HTML
- content_type "text/html; charset=utf-8"
- env['api.format'] = :binary
+ redirect_registry_request(empty_packages, :pypi, package_name: params[:package_name]) do
+ not_found!('Package') if empty_packages
+ presenter = ::Packages::Pypi::PackagePresenter.new(packages, authorized_user_project)
- body presenter.body
+ # Adjusts grape output format
+ # to be HTML
+ content_type "text/html; charset=utf-8"
+ env['api.format'] = :binary
+
+ body presenter.body
+ end
end
desc 'The PyPi Package upload endpoint' do
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 59a55462340..cea207d4b8c 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -4166,7 +4166,7 @@ msgid_plural "ApprovalRuleSummary|%{count} approvals required from %{membersCoun
msgstr[0] ""
msgstr[1] ""
-msgid "ApprovalRule|%{scanner} +%{additionalScanners} more"
+msgid "ApprovalRule|%{firstLabel} +%{numberOfAdditionalLabels} more"
msgstr ""
msgid "ApprovalRule|Add approvers"
@@ -4175,9 +4175,15 @@ msgstr ""
msgid "ApprovalRule|All scanners"
msgstr ""
+msgid "ApprovalRule|All severity levels"
+msgstr ""
+
msgid "ApprovalRule|Apply this approval rule to consider only the selected security scanners."
msgstr ""
+msgid "ApprovalRule|Apply this approval rule to consider only the selected severity levels."
+msgstr ""
+
msgid "ApprovalRule|Approval rules"
msgstr ""
@@ -4205,6 +4211,9 @@ msgstr ""
msgid "ApprovalRule|Please select at least one security scanner"
msgstr ""
+msgid "ApprovalRule|Please select at least one severity level"
+msgstr ""
+
msgid "ApprovalRule|Rule name"
msgstr ""
@@ -4217,6 +4226,12 @@ msgstr ""
msgid "ApprovalRule|Select scanners"
msgstr ""
+msgid "ApprovalRule|Select severity levels"
+msgstr ""
+
+msgid "ApprovalRule|Severity levels"
+msgstr ""
+
msgid "ApprovalRule|Target branch"
msgstr ""
@@ -4922,6 +4937,9 @@ msgstr ""
msgid "Automatic deployment rollbacks"
msgstr ""
+msgid "Automatic event tracking provides a traceable history for audits."
+msgstr ""
+
msgid "Automatically close associated incident when a recovery alert notification resolves an alert"
msgstr ""
@@ -14578,6 +14596,9 @@ 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 ""
@@ -26774,9 +26795,6 @@ msgstr ""
msgid "Promotions|Add Group Webhooks and GitLab Enterprise Edition."
msgstr ""
-msgid "Promotions|Audit Events is a way to keep track of important events that happened in GitLab."
-msgstr ""
-
msgid "Promotions|Better Protected Branches"
msgstr ""
@@ -26828,6 +26846,9 @@ msgstr ""
msgid "Promotions|Improve search with Advanced Search and GitLab Enterprise Edition."
msgstr ""
+msgid "Promotions|Keep track of events in your project"
+msgstr ""
+
msgid "Promotions|Learn more"
msgstr ""
@@ -26867,9 +26888,6 @@ msgstr ""
msgid "Promotions|Track activity with Contribution Analytics."
msgstr ""
-msgid "Promotions|Track your project with Audit Events."
-msgstr ""
-
msgid "Promotions|Try it for free"
msgstr ""
diff --git a/spec/finders/packages/pypi/packages_finder_spec.rb b/spec/finders/packages/pypi/packages_finder_spec.rb
index a69c2317261..1a44fb99009 100644
--- a/spec/finders/packages/pypi/packages_finder_spec.rb
+++ b/spec/finders/packages/pypi/packages_finder_spec.rb
@@ -14,14 +14,14 @@ RSpec.describe Packages::Pypi::PackagesFinder do
let(:package_name) { package2.name }
- describe 'execute!' do
- subject { described_class.new(user, scope, package_name: package_name).execute! }
+ describe 'execute' do
+ subject { described_class.new(user, scope, package_name: package_name).execute }
shared_examples 'when no package is found' do
context 'non-existing package' do
let(:package_name) { 'none' }
- it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ it { expect(subject).to be_empty }
end
end
@@ -29,7 +29,7 @@ RSpec.describe Packages::Pypi::PackagesFinder do
context 'non-existing package' do
let(:package_name) { package2.name.upcase.tr('-', '.') }
- it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ it { expect(subject).to be_empty }
end
end
@@ -45,7 +45,7 @@ RSpec.describe Packages::Pypi::PackagesFinder do
context 'within a group' do
let(:scope) { group }
- it { expect { subject }.to raise_error(ActiveRecord::RecordNotFound) }
+ it { expect(subject).to be_empty }
context 'user with access to only one project' do
before do
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 5b6794cbc66..d516baf6f0f 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -1,93 +1,175 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent } from '@tiptap/vue-2';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContentEditor from '~/content_editor/components/content_editor.vue';
+import ContentEditorError from '~/content_editor/components/content_editor_error.vue';
+import ContentEditorProvider from '~/content_editor/components/content_editor_provider.vue';
+import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import TopToolbar from '~/content_editor/components/top_toolbar.vue';
-import { createContentEditor } from '~/content_editor/services/create_content_editor';
+import {
+ LOADING_CONTENT_EVENT,
+ LOADING_SUCCESS_EVENT,
+ LOADING_ERROR_EVENT,
+} from '~/content_editor/constants';
+import { emitEditorEvent } from '../test_utils';
jest.mock('~/emoji');
describe('ContentEditor', () => {
let wrapper;
- let editor;
+ let contentEditor;
+ let renderMarkdown;
+ const uploadsPath = '/uploads';
const findEditorElement = () => wrapper.findByTestId('content-editor');
- const findErrorAlert = () => wrapper.findComponent(GlAlert);
+ const findEditorContent = () => wrapper.findComponent(EditorContent);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ const createWrapper = (propsData = {}) => {
+ renderMarkdown = jest.fn();
- const createWrapper = async (contentEditor) => {
wrapper = shallowMountExtended(ContentEditor, {
propsData: {
- contentEditor,
+ renderMarkdown,
+ uploadsPath,
+ ...propsData,
+ },
+ stubs: {
+ EditorStateObserver,
+ ContentEditorProvider,
+ },
+ listeners: {
+ initialized(editor) {
+ contentEditor = editor;
+ },
},
});
};
- beforeEach(() => {
- editor = createContentEditor({ renderMarkdown: () => true });
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders editor content component and attaches editor instance', () => {
- createWrapper(editor);
+ it('triggers initialized event and provides contentEditor instance as event data', () => {
+ createWrapper();
- const editorContent = wrapper.findComponent(EditorContent);
+ expect(contentEditor).not.toBeFalsy();
+ });
- expect(editorContent.props().editor).toBe(editor.tiptapEditor);
+ it('renders EditorContent component and provides tiptapEditor instance', () => {
+ createWrapper();
+
+ const editorContent = findEditorContent();
+
+ expect(editorContent.props().editor).toBe(contentEditor.tiptapEditor);
expect(editorContent.classes()).toContain('md');
});
+ it('renders ContentEditorProvider component', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(ContentEditorProvider).exists()).toBe(true);
+ });
+
it('renders top toolbar component', () => {
- createWrapper(editor);
+ createWrapper();
expect(wrapper.findComponent(TopToolbar).exists()).toBe(true);
});
- it.each`
- isFocused | classes
- ${true} | ${['md-area', 'is-focused']}
- ${false} | ${['md-area']}
- `(
- 'has $classes class selectors when tiptapEditor.isFocused = $isFocused',
- ({ isFocused, classes }) => {
- editor.tiptapEditor.isFocused = isFocused;
- createWrapper(editor);
+ it('adds is-focused class when focus event is emitted', async () => {
+ createWrapper();
- expect(findEditorElement().classes()).toStrictEqual(classes);
- },
- );
-
- it('adds isFocused class when tiptapEditor is focused', () => {
- editor.tiptapEditor.isFocused = true;
- createWrapper(editor);
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
expect(findEditorElement().classes()).toContain('is-focused');
});
- describe('displaying error', () => {
- const error = 'Content Editor error';
+ it('removes is-focused class when blur event is emitted', async () => {
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'focus' });
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'blur' });
+
+ expect(findEditorElement().classes()).not.toContain('is-focused');
+ });
+
+ it('emits change event when document is updated', async () => {
+ createWrapper();
+
+ await emitEditorEvent({ tiptapEditor: contentEditor.tiptapEditor, event: 'update' });
+
+ expect(wrapper.emitted('change')).toEqual([
+ [
+ {
+ empty: contentEditor.empty,
+ },
+ ],
+ ]);
+ });
+
+ it('renders content_editor_error component', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(ContentEditorError).exists()).toBe(true);
+ });
+
+ describe('when loading content', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ contentEditor.emit(LOADING_CONTENT_EVENT);
+
+ await nextTick();
+ });
+
+ it('displays loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('hides EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(false);
+ });
+ });
+
+ describe('when loading content succeeds', () => {
+ beforeEach(async () => {
+ createWrapper();
+
+ contentEditor.emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ contentEditor.emit(LOADING_SUCCESS_EVENT);
+ await nextTick();
+ });
+
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('displays EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(true);
+ });
+ });
+
+ describe('when loading content fails', () => {
+ const error = 'error';
beforeEach(async () => {
- createWrapper(editor);
-
- editor.tiptapEditor.emit('error', error);
+ createWrapper();
+ contentEditor.emit(LOADING_CONTENT_EVENT);
+ await nextTick();
+ contentEditor.emit(LOADING_ERROR_EVENT, error);
await nextTick();
});
- it('displays error notifications from the tiptap editor', () => {
- expect(findErrorAlert().text()).toBe(error);
+ it('hides loading indicator', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
});
- it('allows dismissing an error alert', async () => {
- findErrorAlert().vm.$emit('dismiss');
-
- await nextTick();
-
- expect(findErrorAlert().exists()).toBe(false);
+ it('displays EditorContent component', () => {
+ expect(findEditorContent().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d87a1459b50..1334b1ddaad 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -10,7 +10,7 @@ import httpStatus from '~/lib/utils/http_status';
import { loadMarkdownApiResult } from '../markdown_processing_examples';
import { createTestEditor, createDocBuilder } from '../test_utils';
-describe('content_editor/extensions/image', () => {
+describe('content_editor/extensions/attachment', () => {
let tiptapEditor;
let eq;
let doc;
@@ -144,8 +144,8 @@ describe('content_editor/extensions/image', () => {
it('emits an error event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: imageFile });
- tiptapEditor.on('error', (message) => {
- expect(message).toBe('An error occurred while uploading the image. Please try again.');
+ tiptapEditor.on('error', ({ error }) => {
+ expect(error).toBe('An error occurred while uploading the image. Please try again.');
done();
});
});
@@ -224,8 +224,8 @@ describe('content_editor/extensions/image', () => {
it('emits an error event that includes an error message', (done) => {
tiptapEditor.commands.uploadAttachment({ file: attachmentFile });
- tiptapEditor.on('error', (message) => {
- expect(message).toBe('An error occurred while uploading the file. Please try again.');
+ tiptapEditor.on('error', ({ error }) => {
+ expect(error).toBe('An error occurred while uploading the file. Please try again.');
done();
});
});
diff --git a/spec/frontend/content_editor/services/content_editor_spec.js b/spec/frontend/content_editor/services/content_editor_spec.js
index 8580d3249b9..e48687f1548 100644
--- a/spec/frontend/content_editor/services/content_editor_spec.js
+++ b/spec/frontend/content_editor/services/content_editor_spec.js
@@ -13,10 +13,22 @@ describe('content_editor/services/content_editor', () => {
beforeEach(() => {
const tiptapEditor = createTestEditor();
+ jest.spyOn(tiptapEditor, 'destroy');
+
serializer = { deserialize: jest.fn() };
contentEditor = new ContentEditor({ tiptapEditor, serializer });
});
+ describe('.dispose', () => {
+ it('destroys the tiptapEditor', () => {
+ expect(contentEditor.tiptapEditor.destroy).not.toHaveBeenCalled();
+
+ contentEditor.dispose();
+
+ expect(contentEditor.tiptapEditor.destroy).toHaveBeenCalled();
+ });
+ });
+
describe('when setSerializedContent succeeds', () => {
beforeEach(() => {
serializer.deserialize.mockResolvedValueOnce('');
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 97cb70dbd62..082a8977710 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -352,11 +352,6 @@ describe('WikiForm', () => {
await waitForPromises();
});
- it('editor is shown in a perpetual loading state', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findComponent(ContentEditor).exists()).toBe(false);
- });
-
it('disables the submit button', () => {
expect(findSubmitButton().props('disabled')).toBe(true);
});
diff --git a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
index 99b52236771..ae0c0f53acd 100644
--- a/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
+++ b/spec/lib/api/helpers/packages/dependency_proxy_helpers_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
let_it_be(:helper) { Class.new.include(described_class).new }
- describe 'redirect_registry_request' do
+ describe '#redirect_registry_request' do
using RSpec::Parameterized::TableSyntax
let(:options) { {} }
@@ -13,7 +13,7 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject { helper.redirect_registry_request(forward_to_registry, package_type, options) { helper.fallback } }
before do
- allow(helper).to receive(:options).and_return(for: API::NpmInstancePackages)
+ allow(helper).to receive(:options).and_return(for: described_class)
end
shared_examples 'executing fallback' do
@@ -34,38 +34,66 @@ RSpec.describe API::Helpers::Packages::DependencyProxyHelpers do
subject
- expect_snowplow_event(category: 'API::NpmInstancePackages', action: 'npm_request_forward')
+ expect_snowplow_event(category: described_class.to_s, action: "#{package_type}_request_forward")
end
end
- context 'with npm packages' do
- let(:package_type) { :npm }
+ %i[npm pypi].each do |forwardable_package_type|
+ context "with #{forwardable_package_type} packages" do
+ include_context 'dependency proxy helpers context'
- where(:application_setting, :forward_to_registry, :example_name) do
- true | true | 'executing redirect'
- true | false | 'executing fallback'
- false | true | 'executing fallback'
- false | false | 'executing fallback'
- end
+ let(:package_type) { forwardable_package_type }
- with_them do
- before do
- stub_application_setting(npm_package_requests_forwarding: application_setting)
+ where(:application_setting, :forward_to_registry, :example_name) do
+ true | true | 'executing redirect'
+ true | false | 'executing fallback'
+ false | true | 'executing fallback'
+ false | false | 'executing fallback'
end
- it_behaves_like params[:example_name]
+ with_them do
+ before do
+ allow_fetch_application_setting(attribute: "#{forwardable_package_type}_package_requests_forwarding", return_value: application_setting)
+ end
+
+ it_behaves_like params[:example_name]
+ end
end
end
- context 'with non-forwardable packages' do
+ context 'with non-forwardable package type' do
let(:forward_to_registry) { true }
before do
stub_application_setting(npm_package_requests_forwarding: true)
+ stub_application_setting(pypi_package_requests_forwarding: true)
end
- Packages::Package.package_types.keys.without('npm').each do |pkg_type|
+ Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
context "#{pkg_type}" do
+ let(:package_type) { pkg_type.to_sym }
+
+ it 'raises an error' do
+ expect { subject }.to raise_error(ArgumentError, "Can't find application setting for package_type #{package_type}")
+ end
+ end
+ end
+ end
+
+ describe '#registry_url' do
+ subject { helper.registry_url(package_type, package_name: 'test') }
+
+ where(:package_type, :expected_result) do
+ :npm | 'https://registry.npmjs.org/test'
+ :pypi | 'https://pypi.org/simple/test/'
+ end
+
+ with_them do
+ it { is_expected.to eq(expected_result) }
+ end
+
+ Packages::Package.package_types.keys.without('npm', 'pypi').each do |pkg_type|
+ context "with non-forwardable package type #{pkg_type}" do
let(:package_type) { pkg_type }
it 'raises an error' do
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index a3200ef6a5b..8df2460a2b6 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -23,7 +23,8 @@ RSpec.describe API::PypiPackages do
subject { get api(url), headers: headers }
describe 'GET /api/v4/groups/:id/-/packages/pypi/simple/:package_name' do
- let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package.name}" }
+ let(:package_name) { package.name }
+ let(:url) { "/groups/#{group.id}/-/packages/pypi/simple/#{package_name}" }
let(:snowplow_gitlab_standard_context) { {} }
it_behaves_like 'pypi simple API endpoint'
@@ -40,7 +41,7 @@ RSpec.describe API::PypiPackages do
it_behaves_like 'deploy token for package GET requests'
context 'with group path as id' do
- let(:url) { "/groups/#{CGI.escape(group.full_path)}/-/packages/pypi/simple/#{package.name}" }
+ let(:url) { "/groups/#{CGI.escape(group.full_path)}/-/packages/pypi/simple/#{package_name}"}
it_behaves_like 'deploy token for package GET requests'
end
@@ -60,7 +61,8 @@ RSpec.describe API::PypiPackages do
end
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
- let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package.name}" }
+ let(:package_name) { package.name }
+ let(:url) { "/projects/#{project.id}/packages/pypi/simple/#{package_name}" }
let(:snowplow_gitlab_standard_context) { { project: project, namespace: project.namespace } }
it_behaves_like 'pypi simple API endpoint'
diff --git a/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb b/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb
new file mode 100644
index 00000000000..7c8b6250d24
--- /dev/null
+++ b/spec/support/shared_contexts/lib/api/helpers/packages/dependency_proxy_helpers_shared_context.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_context 'dependency proxy helpers context' do
+ def allow_fetch_application_setting(attribute:, return_value:)
+ attributes = double
+ allow(::Gitlab::CurrentSettings.current_application_settings).to receive(:attributes).and_return(attributes)
+ allow(attributes).to receive(:fetch).with(attribute, false).and_return(return_value)
+ end
+end
diff --git a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
index c15c59e1a1d..0390e60747f 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_shared_examples.rb
@@ -46,6 +46,8 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
end
shared_examples 'handling all conditions' do
+ include_context 'dependency proxy helpers context'
+
where(:auth, :package_name_type, :request_forward, :visibility, :user_role, :expected_result, :expected_status) do
nil | :scoped_naming_convention | true | :public | nil | :accept | :ok
nil | :scoped_naming_convention | false | :public | nil | :accept | :ok
@@ -243,7 +245,7 @@ RSpec.shared_examples 'handling get metadata requests' do |scope: :project|
project.send("add_#{user_role}", user) if user_role
project.update!(visibility: visibility.to_s)
package.update!(name: package_name) unless package_name == 'non-existing-package'
- stub_application_setting(npm_package_requests_forwarding: request_forward)
+ allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: request_forward)
end
example_name = "#{params[:expected_result]} metadata request"
diff --git a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
index e6b3dc74b74..86b6975bf9f 100644
--- a/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/npm_packages_tags_shared_examples.rb
@@ -10,9 +10,10 @@ end
RSpec.shared_examples 'accept package tags request' do |status:|
using RSpec::Parameterized::TableSyntax
+ include_context 'dependency proxy helpers context'
before do
- stub_application_setting(npm_package_requests_forwarding: false)
+ allow_fetch_application_setting(attribute: "npm_package_requests_forwarding", return_value: false)
end
context 'with valid package name' do
diff --git a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
index 8a351226123..ed6d9ed43c8 100644
--- a/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/pypi_packages_shared_examples.rb
@@ -228,6 +228,35 @@ RSpec.shared_examples 'pypi simple API endpoint' do
it_behaves_like 'PyPI package versions', :developer, :success
end
+
+ context 'package request forward' do
+ include_context 'dependency proxy helpers context'
+
+ where(:forward, :package_in_project, :shared_examples_name, :expected_status) do
+ true | true | 'PyPI package versions' | :success
+ true | false | 'process PyPI api request' | :redirect
+ false | true | 'PyPI package versions' | :success
+ false | false | 'process PyPI api request' | :not_found
+ end
+
+ with_them do
+ let_it_be(:package) { create(:pypi_package, project: project, name: 'foobar') }
+
+ let(:package_name) do
+ if package_in_project
+ 'foobar'
+ else
+ 'barfoo'
+ end
+ end
+
+ before do
+ allow_fetch_application_setting(attribute: "pypi_package_requests_forwarding", return_value: forward)
+ end
+
+ it_behaves_like params[:shared_examples_name], :reporter, params[:expected_status]
+ end
+ end
end
RSpec.shared_examples 'pypi file download endpoint' do