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--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue25
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue44
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json17
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql30
-rw-r--r--app/views/groups/settings/_permissions.html.haml28
-rw-r--r--db/migrate/20210729161242_remove_foreign_keys_from_ci_test_case_failures.rb24
-rw-r--r--db/migrate/20210729192148_remove_foreign_keys_from_ci_test_cases.rb19
-rw-r--r--db/post_migrate/20210729192959_drop_ci_test_case_failures_table.rb24
-rw-r--r--db/post_migrate/20210729193056_drop_ci_test_cases_table.rb23
-rw-r--r--db/schema_migrations/202107291612421
-rw-r--r--db/schema_migrations/202107291921481
-rw-r--r--db/schema_migrations/202107291929591
-rw-r--r--db/schema_migrations/202107291930561
-rw-r--r--db/structure.sql57
-rw-r--r--doc/administration/monitoring/ip_whitelist.md2
-rw-r--r--doc/api/packages/debian.md101
-rw-r--r--doc/api/projects.md48
-rw-r--r--doc/development/documentation/structure.md12
-rw-r--r--doc/development/testing_guide/frontend_testing.md12
-rw-r--r--doc/user/compliance/license_compliance/img/policies_maintainer_edit_v14_2.pngbin30802 -> 9843 bytes
-rw-r--r--doc/user/gitlab_com/index.md2
-rw-r--r--doc/user/packages/debian_repository/index.md37
-rw-r--r--lib/api/entities/project.rb1
-rw-r--r--lib/api/helpers/projects_helpers.rb8
-rw-r--r--lib/gitlab/pagination/keyset/column_condition_builder.rb206
-rw-r--r--lib/gitlab/pagination/keyset/order.rb48
-rw-r--r--spec/frontend/authentication/two_factor_auth/index_spec.js4
-rw-r--r--spec/frontend/authentication/webauthn/error_spec.js16
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js8
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js7
-rw-r--r--spec/frontend/boards/components/board_form_spec.js9
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js17
-rw-r--r--spec/frontend/diffs/components/app_spec.js9
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js4
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js8
-rw-r--r--spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js42
-rw-r--r--spec/frontend/members/components/filter_sort/sort_dropdown_spec.js30
-rw-r--r--spec/frontend/members/components/members_tabs_spec.js6
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js11
-rw-r--r--spec/frontend/members/utils_spec.js17
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js3
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js40
-rw-r--r--spec/frontend/packages/details/components/app_spec.js10
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js15
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js88
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js13
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js39
-rw-r--r--spec/frontend/persistent_user_callout_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js8
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js17
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js16
-rw-r--r--spec/lib/gitlab/pagination/keyset/order_spec.rb85
-rw-r--r--spec/requests/api/project_attributes.yml1
-rw-r--r--spec/requests/api/projects_spec.rb103
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb2
-rw-r--r--spec/views/groups/show.html.haml_spec.rb2
59 files changed, 1000 insertions, 427 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 570b9c98af1..f8b417bf2d6 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-f69cea16bcc88ddf29fb6c4c67a5d788fbc00f9a
+780588a55b9219f3157cc984f7e1b7aa9f9124f2
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index 33d86dec767..e9329fb1d88 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -1,7 +1,12 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
+import {
+ getParameterByName,
+ setUrlParams,
+ queryToObject,
+ redirectTo,
+} from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
SEARCH_TOKEN_TYPE,
@@ -122,14 +127,16 @@ export default {
const sortParamValue = getParameterByName(SORT_QUERY_PARAM_NAME);
const activeTabParamValue = getParameterByName(ACTIVE_TAB_QUERY_PARAM_NAME);
- window.location.href = setUrlParams(
- {
- ...params,
- ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
- ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
- },
- window.location.href,
- true,
+ redirectTo(
+ setUrlParams(
+ {
+ ...params,
+ ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
+ ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
+ },
+ window.location.href,
+ true,
+ ),
);
},
},
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
index 89add0a0d31..1dc40f57efb 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/additional_metadata.vue
@@ -1,7 +1,11 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
-import { PackageType } from '~/packages/shared/constants';
+import {
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+} from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
export default {
@@ -25,12 +29,18 @@ export default {
},
computed: {
showMetadata() {
- const visibilityConditions = {
- [PackageType.NUGET]: this.packageEntity.nuget_metadatum,
- [PackageType.CONAN]: this.packageEntity.conan_metadatum,
- [PackageType.MAVEN]: this.packageEntity.maven_metadatum,
- };
- return visibilityConditions[this.packageEntity.package_type];
+ return [PACKAGE_TYPE_NUGET, PACKAGE_TYPE_CONAN, PACKAGE_TYPE_MAVEN].includes(
+ this.packageEntity.packageType,
+ );
+ },
+ showNugetMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
+ },
+ showConanMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_CONAN;
+ },
+ showMavenMetadata() {
+ return this.packageEntity.packageType === PACKAGE_TYPE_MAVEN;
},
},
};
@@ -41,12 +51,12 @@ export default {
<h3 class="gl-font-lg" data-testid="title">{{ __('Additional Metadata') }}</h3>
<div class="gl-bg-gray-50 gl-inset-border-1-gray-100 gl-rounded-base" data-testid="main">
- <template v-if="packageEntity.nuget_metadatum">
+ <template v-if="showNugetMetadata">
<details-row icon="project" padding="gl-p-4" dashed data-testid="nuget-source">
<gl-sprintf :message="$options.i18n.sourceText">
<template #link>
- <gl-link :href="packageEntity.nuget_metadatum.project_url" target="_blank">{{
- packageEntity.nuget_metadatum.project_url
+ <gl-link :href="packageEntity.metadata.projectUrl" target="_blank">{{
+ packageEntity.metadata.projectUrl
}}</gl-link>
</template>
</gl-sprintf>
@@ -54,8 +64,8 @@ export default {
<details-row icon="license" padding="gl-p-4" data-testid="nuget-license">
<gl-sprintf :message="$options.i18n.licenseText">
<template #link>
- <gl-link :href="packageEntity.nuget_metadatum.license_url" target="_blank">{{
- packageEntity.nuget_metadatum.license_url
+ <gl-link :href="packageEntity.metadata.licenseUrl" target="_blank">{{
+ packageEntity.metadata.licenseUrl
}}</gl-link>
</template>
</gl-sprintf>
@@ -63,28 +73,28 @@ export default {
</template>
<details-row
- v-else-if="packageEntity.conan_metadatum"
+ v-else-if="showConanMetadata"
icon="information-o"
padding="gl-p-4"
data-testid="conan-recipe"
>
<gl-sprintf :message="$options.i18n.recipeText">
- <template #recipe>{{ packageEntity.name }}</template>
+ <template #recipe>{{ packageEntity.metadata.recipe }}</template>
</gl-sprintf>
</details-row>
- <template v-else-if="packageEntity.maven_metadatum">
+ <template v-else-if="showMavenMetadata">
<details-row icon="information-o" padding="gl-p-4" dashed data-testid="maven-app">
<gl-sprintf :message="$options.i18n.appName">
<template #name>
- <strong>{{ packageEntity.maven_metadatum.app_name }}</strong>
+ <strong>{{ packageEntity.metadata.appName }}</strong>
</template>
</gl-sprintf>
</details-row>
<details-row icon="information-o" padding="gl-p-4" data-testid="maven-group">
<gl-sprintf :message="$options.i18n.appGroup">
<template #group>
- <strong>{{ packageEntity.maven_metadatum.app_group }}</strong>
+ <strong>{{ packageEntity.metadata.appGroup }}</strong>
</template>
</gl-sprintf>
</details-row>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
index 903d7ae25ae..ae5558111e6 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -19,13 +19,13 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
-// import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
// import DependencyRow from '~/packages/details/components/dependency_row.vue';
// import InstallationCommands from '~/packages/details/components/installation_commands.vue';
// import PackageFiles from '~/packages/details/components/package_files.vue';
// import PackageListRow from '~/packages/shared/components/package_list_row.vue';
import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import {
PACKAGE_TYPE_NUGET,
@@ -61,7 +61,7 @@ export default {
// PackageListRow,
// DependencyRow,
PackageHistory,
- // AdditionalMetadata,
+ AdditionalMetadata,
// InstallationCommands,
// PackageFiles,
},
@@ -244,9 +244,9 @@ export default {
:package-entity="packageEntity"
:npm-path="npmPath"
:npm-help-path="npmHelpPath"
- />
+ /> -->
- <additional-metadata :package-entity="packageEntity" /> -->
+ <additional-metadata :package-entity="packageEntity" />
</div>
<!-- <package-files
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..c61a653d10b
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragmentTypes.json
@@ -0,0 +1,17 @@
+{
+ "__schema": {
+ "types": [
+ {
+ "kind": "UNION",
+ "name": "PackageMetadata",
+ "possibleTypes": [
+ { "name": "ComposerMetadata" },
+ { "name": "ConanMetadata" },
+ { "name": "MavenMetadata" },
+ { "name": "NugetMetadata" },
+ { "name": "PypiMetadata" }
+ ]
+ }
+ ]
+ }
+}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
index 16152eb81f6..f8cb5c516e2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js
@@ -1,6 +1,12 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import introspectionQueryResultData from './fragmentTypes.json';
+
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
Vue.use(VueApollo);
@@ -8,6 +14,9 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
+ cacheConfig: {
+ fragmentMatcher,
+ },
assumeImmutableResults: true,
},
),
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 564476ba26a..2dff256525a 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -40,5 +40,35 @@ query getPackageDetails($id: ID!) {
size
}
}
+ metadata {
+ ... on ComposerMetadata {
+ targetSha
+ composerJson {
+ license
+ version
+ }
+ }
+ ... on PypiMetadata {
+ requiredPython
+ }
+ ... on ConanMetadata {
+ packageChannel
+ packageUsername
+ recipe
+ recipePath
+ }
+ ... on MavenMetadata {
+ appName
+ appGroup
+ appVersion
+ path
+ }
+
+ ... on NugetMetadata {
+ iconUrl
+ licenseUrl
+ projectUrl
+ }
+ }
}
}
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 9ef1e414872..683e70248b6 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -15,26 +15,22 @@
checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) }
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group), class: 'custom-control-input'
- = f.label :share_with_group_lock, class: 'custom-control-label' do
- %span
- = s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: link_to_group(@group) }
- %p.js-descr.help-text= share_with_group_lock_help_text(@group)
+ = f.gitlab_ui_checkbox_component :share_with_group_lock,
+ s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: link_to_group(@group) },
+ checkbox_options: { disabled: !can_change_share_with_group_lock?(@group) },
+ help_text: share_with_group_lock_help_text(@group)
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :emails_disabled, checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group), class: 'custom-control-input'
- = f.label :emails_disabled, class: 'custom-control-label' do
- %span= s_('GroupSettings|Disable email notifications')
- %p.help-text= s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
+ = f.gitlab_ui_checkbox_component :emails_disabled,
+ s_('GroupSettings|Disable email notifications'),
+ checkbox_options: { checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group) },
+ help_text: s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
.form-group.gl-mb-3
- .gl-form-checkbox.custom-control.custom-checkbox
- = f.check_box :mentions_disabled, checked: @group.mentions_disabled?, class: 'custom-control-input'
- = f.label :mentions_disabled, class: 'custom-control-label' do
- %span= s_('GroupSettings|Disable group mentions')
- %p.help-text= s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
+ = f.gitlab_ui_checkbox_component :mentions_disabled,
+ s_('GroupSettings|Disable group mentions'),
+ checkbox_options: { checked: @group.mentions_disabled? },
+ help_text: s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
diff --git a/db/migrate/20210729161242_remove_foreign_keys_from_ci_test_case_failures.rb b/db/migrate/20210729161242_remove_foreign_keys_from_ci_test_case_failures.rb
new file mode 100644
index 00000000000..2193a698272
--- /dev/null
+++ b/db/migrate/20210729161242_remove_foreign_keys_from_ci_test_case_failures.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class RemoveForeignKeysFromCiTestCaseFailures < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ TABLE_NAME = :ci_test_case_failures
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(TABLE_NAME, column: :build_id)
+ end
+
+ with_lock_retries do
+ remove_foreign_key_if_exists(TABLE_NAME, column: :test_case_id)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(TABLE_NAME, :ci_builds, column: :build_id, on_delete: :cascade)
+ add_concurrent_foreign_key(TABLE_NAME, :ci_test_cases, column: :test_case_id, on_delete: :cascade)
+ end
+end
diff --git a/db/migrate/20210729192148_remove_foreign_keys_from_ci_test_cases.rb b/db/migrate/20210729192148_remove_foreign_keys_from_ci_test_cases.rb
new file mode 100644
index 00000000000..1d0a5f4fd64
--- /dev/null
+++ b/db/migrate/20210729192148_remove_foreign_keys_from_ci_test_cases.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class RemoveForeignKeysFromCiTestCases < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ TABLE_NAME = :ci_test_cases
+
+ disable_ddl_transaction!
+
+ def up
+ with_lock_retries do
+ remove_foreign_key_if_exists(TABLE_NAME, column: :project_id)
+ end
+ end
+
+ def down
+ add_concurrent_foreign_key(TABLE_NAME, :projects, column: :project_id, on_delete: :cascade)
+ end
+end
diff --git a/db/post_migrate/20210729192959_drop_ci_test_case_failures_table.rb b/db/post_migrate/20210729192959_drop_ci_test_case_failures_table.rb
new file mode 100644
index 00000000000..ad6676a1704
--- /dev/null
+++ b/db/post_migrate/20210729192959_drop_ci_test_case_failures_table.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class DropCiTestCaseFailuresTable < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ drop_table :ci_test_case_failures
+ end
+
+ def down
+ create_table :ci_test_case_failures do |t|
+ t.datetime_with_timezone :failed_at
+ t.bigint :test_case_id, null: false
+ t.bigint :build_id, null: false
+
+ t.index [:test_case_id, :failed_at, :build_id], name: 'index_test_case_failures_unique_columns', unique: true, order: { failed_at: :desc }
+ t.index :build_id
+ end
+ end
+end
diff --git a/db/post_migrate/20210729193056_drop_ci_test_cases_table.rb b/db/post_migrate/20210729193056_drop_ci_test_cases_table.rb
new file mode 100644
index 00000000000..2de1749721d
--- /dev/null
+++ b/db/post_migrate/20210729193056_drop_ci_test_cases_table.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class DropCiTestCasesTable < ActiveRecord::Migration[6.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ drop_table :ci_test_cases
+ end
+
+ def down
+ create_table_with_constraints :ci_test_cases do |t|
+ t.bigint :project_id, null: false
+ t.text :key_hash, null: false
+ t.text_limit :key_hash, 64
+
+ t.index [:project_id, :key_hash], unique: true
+ end
+ end
+end
diff --git a/db/schema_migrations/20210729161242 b/db/schema_migrations/20210729161242
new file mode 100644
index 00000000000..38769ac4ff0
--- /dev/null
+++ b/db/schema_migrations/20210729161242
@@ -0,0 +1 @@
+22a64ce9a8cbebd2024908cc74cc92a50fb6ccaa1580ebea3be60d3659c48fa0 \ No newline at end of file
diff --git a/db/schema_migrations/20210729192148 b/db/schema_migrations/20210729192148
new file mode 100644
index 00000000000..8cf650a223a
--- /dev/null
+++ b/db/schema_migrations/20210729192148
@@ -0,0 +1 @@
+6ed7827f6f911dbb40637ac056298877b709fb7356bc9ee3a366cceb48268646 \ No newline at end of file
diff --git a/db/schema_migrations/20210729192959 b/db/schema_migrations/20210729192959
new file mode 100644
index 00000000000..df4f4ed2c71
--- /dev/null
+++ b/db/schema_migrations/20210729192959
@@ -0,0 +1 @@
+3cb0c88fddfec66c0d89c4c1f34d0538be88a44f2039e6c542c5282b293ce019 \ No newline at end of file
diff --git a/db/schema_migrations/20210729193056 b/db/schema_migrations/20210729193056
new file mode 100644
index 00000000000..fea83eb2750
--- /dev/null
+++ b/db/schema_migrations/20210729193056
@@ -0,0 +1 @@
+d983a765482b368bd7a238b3b75fc9b0a45310f295953ea053ee4c42785e8684 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 2e31660b31e..8496f869c32 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -11340,38 +11340,6 @@ CREATE SEQUENCE ci_subscriptions_projects_id_seq
ALTER SEQUENCE ci_subscriptions_projects_id_seq OWNED BY ci_subscriptions_projects.id;
-CREATE TABLE ci_test_case_failures (
- id bigint NOT NULL,
- failed_at timestamp with time zone,
- test_case_id bigint NOT NULL,
- build_id bigint NOT NULL
-);
-
-CREATE SEQUENCE ci_test_case_failures_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE ci_test_case_failures_id_seq OWNED BY ci_test_case_failures.id;
-
-CREATE TABLE ci_test_cases (
- id bigint NOT NULL,
- project_id bigint NOT NULL,
- key_hash text NOT NULL,
- CONSTRAINT check_dd3c5d1c15 CHECK ((char_length(key_hash) <= 64))
-);
-
-CREATE SEQUENCE ci_test_cases_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE ci_test_cases_id_seq OWNED BY ci_test_cases.id;
-
CREATE TABLE ci_trigger_requests (
id integer NOT NULL,
trigger_id integer NOT NULL,
@@ -20074,10 +20042,6 @@ ALTER TABLE ONLY ci_stages ALTER COLUMN id SET DEFAULT nextval('ci_stages_id_seq
ALTER TABLE ONLY ci_subscriptions_projects ALTER COLUMN id SET DEFAULT nextval('ci_subscriptions_projects_id_seq'::regclass);
-ALTER TABLE ONLY ci_test_case_failures ALTER COLUMN id SET DEFAULT nextval('ci_test_case_failures_id_seq'::regclass);
-
-ALTER TABLE ONLY ci_test_cases ALTER COLUMN id SET DEFAULT nextval('ci_test_cases_id_seq'::regclass);
-
ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id SET DEFAULT nextval('ci_trigger_requests_id_seq'::regclass);
ALTER TABLE ONLY ci_triggers ALTER COLUMN id SET DEFAULT nextval('ci_triggers_id_seq'::regclass);
@@ -21311,12 +21275,6 @@ ALTER TABLE ONLY ci_stages
ALTER TABLE ONLY ci_subscriptions_projects
ADD CONSTRAINT ci_subscriptions_projects_pkey PRIMARY KEY (id);
-ALTER TABLE ONLY ci_test_case_failures
- ADD CONSTRAINT ci_test_case_failures_pkey PRIMARY KEY (id);
-
-ALTER TABLE ONLY ci_test_cases
- ADD CONSTRAINT ci_test_cases_pkey PRIMARY KEY (id);
-
ALTER TABLE ONLY ci_trigger_requests
ADD CONSTRAINT ci_trigger_requests_pkey PRIMARY KEY (id);
@@ -23397,10 +23355,6 @@ CREATE INDEX index_ci_subscriptions_projects_on_upstream_project_id ON ci_subscr
CREATE UNIQUE INDEX index_ci_subscriptions_projects_unique_subscription ON ci_subscriptions_projects USING btree (downstream_project_id, upstream_project_id);
-CREATE INDEX index_ci_test_case_failures_on_build_id ON ci_test_case_failures USING btree (build_id);
-
-CREATE UNIQUE INDEX index_ci_test_cases_on_project_id_and_key_hash ON ci_test_cases USING btree (project_id, key_hash);
-
CREATE INDEX index_ci_trigger_requests_on_commit_id ON ci_trigger_requests USING btree (commit_id);
CREATE INDEX index_ci_trigger_requests_on_trigger_id_and_id ON ci_trigger_requests USING btree (trigger_id, id DESC);
@@ -25205,8 +25159,6 @@ CREATE UNIQUE INDEX index_terraform_states_on_project_id_and_name ON terraform_s
CREATE UNIQUE INDEX index_terraform_states_on_uuid ON terraform_states USING btree (uuid);
-CREATE UNIQUE INDEX index_test_case_failures_unique_columns ON ci_test_case_failures USING btree (test_case_id, failed_at DESC, build_id);
-
CREATE INDEX index_timelogs_on_issue_id ON timelogs USING btree (issue_id);
CREATE INDEX index_timelogs_on_merge_request_id ON timelogs USING btree (merge_request_id);
@@ -25913,9 +25865,6 @@ ALTER TABLE ONLY design_management_designs_versions
ALTER TABLE ONLY terraform_state_versions
ADD CONSTRAINT fk_04b91e4a9f FOREIGN KEY (ci_build_id) REFERENCES ci_builds(id) ON DELETE SET NULL;
-ALTER TABLE ONLY ci_test_cases
- ADD CONSTRAINT fk_0526c30ded FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_05f1e72feb FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
@@ -26537,9 +26486,6 @@ ALTER TABLE ONLY ci_sources_pipelines
ALTER TABLE ONLY geo_event_log
ADD CONSTRAINT fk_d5af95fcd9 FOREIGN KEY (lfs_object_deleted_event_id) REFERENCES geo_lfs_object_deleted_events(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_test_case_failures
- ADD CONSTRAINT fk_d69404d827 FOREIGN KEY (build_id) REFERENCES ci_builds(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@@ -28142,9 +28088,6 @@ ALTER TABLE ONLY vulnerability_finding_evidence_sources
ALTER TABLE ONLY protected_branch_unprotect_access_levels
ADD CONSTRAINT fk_rails_e9eb8dc025 FOREIGN KEY (protected_branch_id) REFERENCES protected_branches(id) ON DELETE CASCADE;
-ALTER TABLE ONLY ci_test_case_failures
- ADD CONSTRAINT fk_rails_eab6349715 FOREIGN KEY (test_case_id) REFERENCES ci_test_cases(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
diff --git a/doc/administration/monitoring/ip_whitelist.md b/doc/administration/monitoring/ip_whitelist.md
index 20c97a0df8f..b6df176ea87 100644
--- a/doc/administration/monitoring/ip_whitelist.md
+++ b/doc/administration/monitoring/ip_whitelist.md
@@ -9,7 +9,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> Introduced in GitLab 9.4.
NOTE:
-We intend to [rename IP whitelist as `IP allowlist`](https://gitlab.com/gitlab-org/gitlab/-/issues/7554).
+We intend to [rename IP whitelist as `IP allowlist`](https://gitlab.com/groups/gitlab-org/-/epics/3478).
GitLab provides some [monitoring endpoints](../../user/admin_area/monitoring/health_check.md)
that provide health check information when probed.
diff --git a/doc/api/packages/debian.md b/doc/api/packages/debian.md
index 797955ea600..0912f894fa3 100644
--- a/doc/api/packages/debian.md
+++ b/doc/api/packages/debian.md
@@ -74,6 +74,38 @@ curl --request PUT \
"https://gitlab.example.com/api/v4/projects/1/packages/debian/mypkg.deb"
```
+## Download a package
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64923) in GitLab 14.2.
+
+Download a package file.
+
+```plaintext
+GET projects/:id/packages/debian/pool/:distribution/:letter/:package_name/:package_version/:file_name
+```
+
+| Attribute | Type | Required | Description |
+| ----------------- | ------ | -------- | ----------- |
+| `distribution` | string | yes | The codename or suite of the Debian distribution. |
+| `letter` | string | yes | The Debian Classification (first-letter or lib-first-letter). |
+| `package_name` | string | yes | The source package name. |
+| `package_version` | string | yes | The source package version. |
+| `file_name` | string | yes | The file name. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/pool/my-distro/a/my-pkg/1.0.0/example_1.0.0~alpha2_amd64.deb"
+```
+
+Write the output to a file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/packages/pool/my-distro/a/my-pkg/1.0.0/example_1.0.0~alpha2_amd64.deb" \
+ --remote-name
+```
+
+This writes the downloaded file using the remote file name in the current directory.
+
## Route prefix
The remaining endpoints described are two sets of identical routes that each make requests in
@@ -108,7 +140,7 @@ The examples in this document all use the project-level prefix.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64067) in GitLab 14.1.
-Download a Debian package file.
+Download a Debian distribution file.
```plaintext
GET <route-prefix>/dists/*distribution/Release
@@ -130,16 +162,13 @@ curl --header "Private-Token: <personal_access_token>" \
--remote-name
```
-This writes the downloaded file to `Release` in the current directory.
+This writes the downloaded file using the remote file name in the current directory.
## Download a signed distribution Release file
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64067) in GitLab 14.1.
-Download a Debian package file.
-
-Signed releases are [not supported](https://gitlab.com/groups/gitlab-org/-/epics/6057#note_582697034).
-Therefore, this endpoint downloads the unsigned release file.
+Download a signed Debian distribution file.
```plaintext
GET <route-prefix>/dists/*distribution/InRelease
@@ -161,4 +190,62 @@ curl --header "Private-Token: <personal_access_token>" \
--remote-name
```
-This writes the downloaded file to `InRelease` in the current directory.
+This writes the downloaded file using the remote file name in the current directory.
+
+## Download a release file signature
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64923) in GitLab 14.2.
+
+Download a Debian release file signature.
+
+```plaintext
+GET <route-prefix>/dists/*distribution/Release.gpg
+```
+
+| Attribute | Type | Required | Description |
+| ----------------- | ------ | -------- | ----------- |
+| `distribution` | string | yes | The codename or suite of the Debian distribution. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/Release.gpg"
+```
+
+Write the output to a file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/Release.gpg" \
+ --remote-name
+```
+
+This writes the downloaded file using the remote file name in the current directory.
+
+## Download a binary file's index
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64923) in GitLab 14.2.
+
+Download a distribution index.
+
+```plaintext
+GET <route-prefix>/dists/*distribution/:component/binary-:architecture/Packages
+```
+
+| Attribute | Type | Required | Description |
+| ----------------- | ------ | -------- | ----------- |
+| `distribution` | string | yes | The codename or suite of the Debian distribution. |
+| `component` | string | yes | The distribution component name. |
+| `architecture` | string | yes | The distribution architecture type. |
+
+```shell
+curl --header "Private-Token: <personal_access_token>" "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/amd64/Packages"
+```
+
+Write the output to a file:
+
+```shell
+curl --header "Private-Token: <personal_access_token>" \
+ "https://gitlab.example.com/api/v4/projects/1/packages/debian/dists/my-distro/main/amd64/Packages" \
+ --remote-name
+```
+
+This writes the downloaded file using the remote file name in the current directory.
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 240cb844087..fc39036891c 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -149,7 +149,8 @@ When the user is authenticated and `simple` is not set this returns something li
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -238,7 +239,8 @@ When the user is authenticated and `simple` is not set this returns something li
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -425,7 +427,8 @@ GET /users/:user_id/projects
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -514,7 +517,8 @@ GET /users/:user_id/projects
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -663,7 +667,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -745,7 +750,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -871,7 +877,8 @@ GET /projects/:id
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"container_expiration_policy": {
"cadence": "7d",
"enabled": false,
@@ -1181,7 +1188,8 @@ POST /projects
| `builds_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). Valid values for `cadence` are: `1d` (every day), `7d` (every week), `14d` (every two weeks), `1month` (every month), or `3month` (every quarter). |
-| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
+| `container_registry_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
+| `container_registry_access_level` | string | **{dotted-circle}** No | Set visibility of container registry, for this project, to one of `disabled`, `private` or `enabled`. |
| `default_branch` | string | **{dotted-circle}** No | The [default branch](../user/project/repository/branches/default.md) name. Requires `initialize_with_readme` to be `true`. |
| `description` | string | **{dotted-circle}** No | Short project description. |
| `emails_disabled` | boolean | **{dotted-circle}** No | Disable email notifications. |
@@ -1256,7 +1264,8 @@ POST /projects/user/:user_id
| `build_timeout` | integer | **{dotted-circle}** No | The maximum amount of time, in seconds, that a job can run. |
| `builds_access_level` | string | **{dotted-circle}** No | One of `disabled`, `private`, or `enabled`. |
| `ci_config_path` | string | **{dotted-circle}** No | The path to CI configuration file. |
-| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
+| `container_registry_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
+| `container_registry_access_level` | string | **{dotted-circle}** No | Set visibility of container registry, for this project, to one of `disabled`, `private` or `enabled`. |
| `description` | string | **{dotted-circle}** No | Short project description. |
| `default_branch` | string | **{dotted-circle}** No | The [default branch](../user/project/repository/branches/default.md) name. Requires `initialize_with_readme` to be `true`. |
| `emails_disabled` | boolean | **{dotted-circle}** No | Disable email notifications. |
@@ -1333,7 +1342,8 @@ PUT /projects/:id
| `ci_default_git_depth` | integer | **{dotted-circle}** No | Default number of revisions for [shallow cloning](../ci/pipelines/settings.md#limit-the-number-of-changes-fetched-during-clone). |
| `ci_forward_deployment_enabled` | boolean | **{dotted-circle}** No | When a new deployment job starts, [skip older deployment jobs](../ci/pipelines/settings.md#skip-outdated-deployment-jobs) that are still pending |
| `container_expiration_policy_attributes` | hash | **{dotted-circle}** No | Update the image cleanup policy for this project. Accepts: `cadence` (string), `keep_n` (integer), `older_than` (string), `name_regex` (string), `name_regex_delete` (string), `name_regex_keep` (string), `enabled` (boolean). |
-| `container_registry_enabled` | boolean | **{dotted-circle}** No | Enable container registry for this project. |
+| `container_registry_enabled` | boolean | **{dotted-circle}** No | _(Deprecated)_ Enable container registry for this project. Use `container_registry_access_level` instead. |
+| `container_registry_access_level` | string | **{dotted-circle}** No | Set visibility of container registry, for this project, to one of `disabled`, `private` or `enabled`. |
| `default_branch` | string | **{dotted-circle}** No | The [default branch](../user/project/repository/branches/default.md) name. |
| `description` | string | **{dotted-circle}** No | Short project description. |
| `emails_disabled` | boolean | **{dotted-circle}** No | Disable email notifications. |
@@ -1473,7 +1483,8 @@ Example responses:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -1565,7 +1576,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -1663,7 +1675,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -1841,7 +1854,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -1960,7 +1974,8 @@ Example response:
"snippets_enabled": false,
"can_create_merge_request_in": true,
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": false,
+ "container_registry_enabled": false, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "disabled",
"created_at": "2013-09-30T13:46:02Z",
"last_activity_at": "2013-09-30T13:46:02Z",
"creator_id": 3,
@@ -2563,7 +2578,8 @@ Example response:
"archived": false,
"visibility": "private",
"resolve_outdated_diff_discussions": false,
- "container_registry_enabled": true,
+ "container_registry_enabled": true, // deprecated, use container_registry_access_level instead
+ "container_registry_access_level": "enabled",
"container_expiration_policy": {
"cadence": "7d",
"enabled": false,
diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md
index ac934673ee2..a9b93997906 100644
--- a/doc/development/documentation/structure.md
+++ b/doc/development/documentation/structure.md
@@ -183,8 +183,8 @@ A paragraph that explains what the tutorial does, and the expected outcome.
To create a website:
-- [Step 1: Do the first task](#do-the-first-task)
-- [Step 2: Do the second task](#do-the-second-task)
+1. [Do the first task](#do-the-first-task)
+1. [Do the second task](#do-the-second-task)
Prerequisites (optional):
@@ -197,8 +197,8 @@ Prerequisites (optional):
To do step 1:
1. First step.
-2. Another step.
-3. Another step.
+1. Another step.
+1. Another step.
## Do the second task
@@ -207,8 +207,8 @@ Before you begin, make sure you have [done the first task](#do-the-first-task).
To do step 2:
1. First step.
-2. Another step.
-3. Another step.
+1. Another step.
+1. Another step.
```
### Get started
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index 4c43bcbe871..2a4db4102f1 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -442,6 +442,18 @@ it('passes', () => {
});
```
+NOTE:
+To modify only the hash, use either the `setWindowLocation` helper, or assign
+directly to `window.location.hash`, e.g.:
+
+```javascript
+it('passes', () => {
+ window.location.hash = '#foo';
+
+ expect(window.location.href).toBe('http://test.host/#foo');
+});
+```
+
If your tests need to assert that certain `window.location` methods were
called, use the `useMockLocationHelper` helper:
diff --git a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v14_2.png b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v14_2.png
index f4afc5a3fb4..2ad08919f86 100644
--- a/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v14_2.png
+++ b/doc/user/compliance/license_compliance/img/policies_maintainer_edit_v14_2.png
Binary files differ
diff --git a/doc/user/gitlab_com/index.md b/doc/user/gitlab_com/index.md
index ef458db67f0..28b73e6991c 100644
--- a/doc/user/gitlab_com/index.md
+++ b/doc/user/gitlab_com/index.md
@@ -148,7 +148,7 @@ from those IPs and allow them.
GitLab.com is fronted by Cloudflare. For incoming connections to GitLab.com, you might need to allow CIDR blocks of Cloudflare ([IPv4](https://www.cloudflare.com/ips-v4) and [IPv6](https://www.cloudflare.com/ips-v6)).
For outgoing connections from CI/CD runners, we are not providing static IP
-addresses. All GitLab runners are deployed into Google Cloud Platform (GCP). Any
+addresses. All GitLab.com shared runners are deployed into Google Cloud Platform (GCP). Any
IP-based firewall can be configured by looking up all
[IP address ranges or CIDR blocks for GCP](https://cloud.google.com/compute/docs/faq#find_ip_range).
diff --git a/doc/user/packages/debian_repository/index.md b/doc/user/packages/debian_repository/index.md
index 9c731d76c4e..789902c03e3 100644
--- a/doc/user/packages/debian_repository/index.md
+++ b/doc/user/packages/debian_repository/index.md
@@ -61,6 +61,15 @@ Feature.disable(:debian_group_packages)
Creating a Debian package is documented [on the Debian Wiki](https://wiki.debian.org/Packaging).
+## Authenticate to the Package Registry
+
+To create a distribution, publish a package, or install a private package, you need one of the
+following:
+
+- [Personal access token](../../../api/index.md#personalproject-access-tokens)
+- [CI/CD job token](../../../api/index.md#gitlab-cicd-job-token)
+- [Deploy token](../../project/deploy_tokens/index.md)
+
## Create a Distribution
On the project-level, Debian packages are published using *Debian Distributions*. To publish
@@ -116,7 +125,7 @@ To upload these files, you can use `dput-ng >= 1.32` (Debian bullseye):
cat <<EOF > dput.cf
[gitlab]
method = https
-fqdn = <login>:<your_access_token>@gitlab.example.com
+fqdn = <username>:<your_access_token>@gitlab.example.com
incoming = /api/v4/projects/<project_id>/packages/debian
EOF
@@ -125,5 +134,27 @@ dput --config=dput.cf --unchecked --no-upload-log gitlab <your_package>.changes
## Install a package
-The Debian package registry for GitLab is under development, and isn't ready for production use. You
-cannot install packages from the registry. However, you can download files directly from the UI.
+To install a package:
+
+1. Configure the repository:
+
+ If you are using a private project, add your [credentials](#authenticate-to-the-package-registry) to your apt config:
+
+ ```shell
+ echo 'machine gitlab.example.com login <username> password <your_access_token>' \
+ | sudo tee /etc/apt/auth.conf.d/gitlab_project.conf
+ ```
+
+ Add your project as a source:
+
+ ```shell
+ echo 'deb [trusted=yes] https://gitlab.example.com/api/v4/projects/<project_id>/packages/debian <codename> <component1> <component2>' \
+ | sudo tee /etc/apt/sources.list.d/gitlab_project.list
+ sudo apt-get update
+ ```
+
+1. Install the package:
+
+ ```shell
+ sudo apt-get -y install -t <codename> <package-name>
+ ```
diff --git a/lib/api/entities/project.rb b/lib/api/entities/project.rb
index f5f565e5b07..890b42ed8c8 100644
--- a/lib/api/entities/project.rb
+++ b/lib/api/entities/project.rb
@@ -71,6 +71,7 @@ module API
expose(:pages_access_level) { |project, options| project.project_feature.string_access_level(:pages) }
expose(:operations_access_level) { |project, options| project.project_feature.string_access_level(:operations) }
expose(:analytics_access_level) { |project, options| project.project_feature.string_access_level(:analytics) }
+ expose(:container_registry_access_level) { |project, options| project.project_feature.string_access_level(:container_registry) }
expose :emails_disabled
expose :shared_runners_enabled
diff --git a/lib/api/helpers/projects_helpers.rb b/lib/api/helpers/projects_helpers.rb
index 0fdd6c141a9..becd25595a6 100644
--- a/lib/api/helpers/projects_helpers.rb
+++ b/lib/api/helpers/projects_helpers.rb
@@ -35,13 +35,14 @@ module API
optional :pages_access_level, type: String, values: %w(disabled private enabled public), desc: 'Pages access level. One of `disabled`, `private`, `enabled` or `public`'
optional :operations_access_level, type: String, values: %w(disabled private enabled), desc: 'Operations access level. One of `disabled`, `private` or `enabled`'
optional :analytics_access_level, type: String, values: %w(disabled private enabled), desc: 'Analytics access level. One of `disabled`, `private` or `enabled`'
+ optional :container_registry_access_level, type: String, values: %w(disabled private enabled), desc: 'Controls visibility of the container registry. One of `disabled`, `private` or `enabled`. `private` will make the container registry accessible only to project members (reporter role and above). `enabled` will make the container registry accessible to everyone who has access to the project. `disabled` will disable the container registry'
optional :emails_disabled, type: Boolean, desc: 'Disable email notifications'
optional :show_default_award_emojis, type: Boolean, desc: 'Show default award emojis'
optional :shared_runners_enabled, type: Boolean, desc: 'Flag indication if shared runners are enabled for that project'
optional :resolve_outdated_diff_discussions, type: Boolean, desc: 'Automatically resolve merge request diffs discussions on lines changed with a push'
optional :remove_source_branch_after_merge, type: Boolean, desc: 'Remove the source branch by default after merge'
- optional :container_registry_enabled, type: Boolean, desc: 'Flag indication if the container registry is enabled for that project'
+ optional :container_registry_enabled, type: Boolean, desc: 'Deprecated: Use :container_registry_access_level instead. Flag indication if the container registry is enabled for that project'
optional :container_expiration_policy_attributes, type: Hash do
use :optional_container_expiration_policy_params
end
@@ -124,7 +125,7 @@ module API
:ci_config_path,
:ci_default_git_depth,
:ci_forward_deployment_enabled,
- :container_registry_enabled,
+ :container_registry_access_level,
:container_expiration_policy_attributes,
:default_branch,
:description,
@@ -169,7 +170,8 @@ module API
:jobs_enabled,
:merge_requests_enabled,
:wiki_enabled,
- :snippets_enabled
+ :snippets_enabled,
+ :container_registry_enabled
]
end
diff --git a/lib/gitlab/pagination/keyset/column_condition_builder.rb b/lib/gitlab/pagination/keyset/column_condition_builder.rb
new file mode 100644
index 00000000000..ca436000abe
--- /dev/null
+++ b/lib/gitlab/pagination/keyset/column_condition_builder.rb
@@ -0,0 +1,206 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Pagination
+ module Keyset
+ class ColumnConditionBuilder
+ # This class builds the WHERE conditions for the keyset pagination library.
+ # It produces WHERE conditions for one column at a time.
+ #
+ # Requisite 1: Only the last column (columns.last) is non-nullable and distinct.
+ # Requisite 2: Only one column is distinct and non-nullable.
+ #
+ # Scenario: We want to order by columns named X, Y and Z and build the conditions
+ # used in the WHERE clause of a pagination query using a set of cursor values.
+ # X is the column definition for a nullable column
+ # Y is the column definition for a non-nullable but not distinct column
+ # Z is the column definition for a distinct, non-nullable column used as a tie breaker.
+ #
+ # Then the method is initially invoked with these arguments:
+ # columns = [ColumnDefinition for X, ColumnDefinition for Y, ColumnDefinition for Z]
+ # values = { X: x, Y: y, Z: z } => these represent cursor values for pagination
+ # (x could be nil since X is nullable)
+ # current_conditions is initialized to [] to store the result during the iteration calls
+ # invoked within the Order#build_where_values method.
+ #
+ # The elements of current_conditions are instances of Arel::Nodes and -
+ # will be concatenated using OR or UNION to be used in the WHERE clause.
+ #
+ # Example: Let's say we want to build WHERE clause conditions for
+ # ORDER BY X DESC NULLS LAST, Y ASC, Z DESC
+ #
+ # Iteration 1:
+ # columns = [X, Y, Z]
+ # At the end, current_conditions should be:
+ # [(Z < z)]
+ #
+ # Iteration 2:
+ # columns = [X, Y]
+ # At the end, current_conditions should be:
+ # [(Y > y) OR (Y = y AND Z < z)]
+ #
+ # Iteration 3:
+ # columns = [X]
+ # At the end, current_conditions should be:
+ # [((X IS NOT NULL AND Y > y) OR (X IS NOT NULL AND Y = y AND Z < z))
+ # OR
+ # ((x IS NULL) OR (X IS NULL))]
+ #
+ # Parameters:
+ #
+ # - columns: instance of ColumnOrderDefinition
+ # - value: cursor value for the column
+ def initialize(column, value)
+ @column = column
+ @value = value
+ end
+
+ def where_conditions(current_conditions)
+ return not_nullable_conditions(current_conditions) if column.not_nullable?
+ return nulls_first_conditions(current_conditions) if column.nulls_first?
+
+ # Here we are dealing with the case of column_definition.nulls_last?
+ # Suppose ORDER BY X DESC NULLS FIRST, Y ASC, Z DESC is the ordering clause
+ # and we already have built the conditions for columns Y and Z.
+ #
+ # We first need a set of conditions to use when x (the value for X) is NULL:
+ # null_conds = [
+ # (x IS NULL AND X IS NULL AND Y<y),
+ # (x IS NULL AND X IS NULL AND Y=y AND Z<z),
+ null_conds = current_conditions.map do |conditional|
+ Arel::Nodes::And.new([value_is_null, column_is_null, conditional])
+ end
+
+ # We then need a set of conditions to use when m has an actual value:
+ # non_null_conds = [
+ # (x IS NOT NULL AND X IS NULL),
+ # (x IS NOT NULL AND X < x)
+ # (x IS NOT NULL AND X = x AND Y > y),
+ # (x IS NOT NULL AND X = x AND Y = y AND Z < z),
+ tie_breaking_conds = current_conditions.map do |conditional|
+ Arel::Nodes::And.new([column_equals_to_value, conditional])
+ end
+
+ non_null_conds = [column_is_null, compare_column_with_value, *tie_breaking_conds].map do |conditional|
+ Arel::Nodes::And.new([value_is_not_null, conditional])
+ end
+
+ [*null_conds, *non_null_conds]
+ end
+
+ private
+
+ # WHEN THE COLUMN IS NON-NULLABLE AND DISTINCT
+ # Per Assumption 1, only the last column can be non-nullable and distinct
+ # (column Z is non-nullable/distinct and comes last in the example).
+ # So the Order#build_where_conditions is being called for the first time with current_conditions = [].
+ #
+ # At the end of the call, we should expect:
+ # current_conditions should be [(Z < z)]
+ #
+ # WHEN THE COLUMN IS NON-NULLABLE BUT NOT DISTINCT
+ # Let's say Z has been processed and we are about to process the column Y next.
+ # (per requisite 1, if a non-nullable but not distinct column is being processed,
+ # at the least, the conditional for the non-nullable/distinct column exists)
+ #
+ # At the start of the method call:
+ # current_conditions = [(Z < z)]
+ # comparison_node = (Y < y)
+ # eqaulity_node = (Y = y)
+ #
+ # We should add a comparison node for the next column Y, (Y < y)
+ # then break a tie using the previous conditionals, (Y = y AND Z < z)
+ #
+ # At the end of the call, we should expect:
+ # current_conditions = [(Y < y), (Y = y AND Z < z)]
+ def not_nullable_conditions(current_conditions)
+ tie_break_conds = current_conditions.map do |conditional|
+ Arel::Nodes::And.new([column_equals_to_value, conditional])
+ end
+
+ [compare_column_with_value, *tie_break_conds]
+ end
+
+ def nulls_first_conditions(current_conditions)
+ # Using the same scenario described earlier,
+ # suppose the ordering clause is ORDER BY X DESC NULLS FIRST, Y ASC, Z DESC
+ # and we have built the conditions for columns Y and Z in previous iterations:
+ #
+ # current_conditions = [(Y > y), (Y = y AND Z < z)]
+ #
+ # In this branch of the iteration,
+ # we first need a set of conditions to use when m (the value for M) is NULL:
+ # null_conds = [
+ # (x IS NULL AND X IS NULL AND Y > y),
+ # (x IS NULL AND X IS NULL AND Y = y AND Z < z),
+ # (x IS NULL AND X IS NOT NULL)]
+ #
+ # Note that when x has an actual value, say x = 3, null_conds evalutes to FALSE.
+ tie_breaking_conds = current_conditions.map do |conditional|
+ Arel::Nodes::And.new([column_is_null, conditional])
+ end
+
+ null_conds = [*tie_breaking_conds, column_is_not_null].map do |conditional|
+ Arel::Nodes::And.new([value_is_null, conditional])
+ end
+
+ # We then need a set of conditions to use when m has an actual value:
+ # non_null_conds = [
+ # (x IS NOT NULL AND X < x),
+ # (x IS NOT NULL AND X = x AND Y > y),
+ # (x IS NOT NULL AND X = x AND Y = y AND Z < z)]
+ #
+ # Note again that when x IS NULL, non_null_conds evaluates to FALSE.
+ tie_breaking_conds = current_conditions.map do |conditional|
+ Arel::Nodes::And.new([column_equals_to_value, conditional])
+ end
+
+ # The combined OR condition (null_where_cond OR non_null_where_cond) will return a correct result -
+ # without having to account for whether x is nil or an actual value at the application level.
+ non_null_conds = [compare_column_with_value, *tie_breaking_conds].map do |conditional|
+ Arel::Nodes::And.new([value_is_not_null, conditional])
+ end
+
+ [*null_conds, *non_null_conds]
+ end
+
+ def column_equals_to_value
+ @equality_node ||= column.column_expression.eq(value)
+ end
+
+ def column_is_null
+ @column_is_null ||= column.column_expression.eq(nil)
+ end
+
+ def column_is_not_null
+ @column_is_not_null ||= column.column_expression.not_eq(nil)
+ end
+
+ def value_is_null
+ @value_is_null ||= build_quoted_value.eq(nil)
+ end
+
+ def value_is_not_null
+ @value_is_not_null ||= build_quoted_value.not_eq(nil)
+ end
+
+ def compare_column_with_value
+ if column.descending_order?
+ column.column_expression.lt(value)
+ else
+ column.column_expression.gt(value)
+ end
+ end
+
+ # Turns the given value to an SQL literal by casting it to the proper format.
+ def build_quoted_value
+ return value if value.instance_of?(Arel::Nodes::SqlLiteral)
+
+ Arel::Nodes.build_quoted(value, column.column_expression)
+ end
+
+ attr_reader :column, :value
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/pagination/keyset/order.rb b/lib/gitlab/pagination/keyset/order.rb
index 19d44ee69dd..ccfa9334a12 100644
--- a/lib/gitlab/pagination/keyset/order.rb
+++ b/lib/gitlab/pagination/keyset/order.rb
@@ -141,24 +141,10 @@ module Gitlab
return use_composite_row_comparison(values) if composite_row_comparison_possible?
- where_values = []
-
- reversed_column_definitions = column_definitions.reverse
- reversed_column_definitions.each_with_index do |column_definition, i|
- value = values[column_definition.attribute_name]
-
- conditions_for_column(column_definition, value).each do |condition|
- column_definitions_after_index = reversed_column_definitions.last(column_definitions.reverse.size - i - 1)
-
- equal_conditon_for_rest = column_definitions_after_index.map do |definition|
- definition.column_expression.eq(values[definition.attribute_name])
- end
-
- where_values << Arel::Nodes::Grouping.new(Arel::Nodes::And.new([condition, *equal_conditon_for_rest].compact))
- end
- end
-
- where_values
+ column_definitions
+ .map { ColumnConditionBuilder.new(_1, values[_1.attribute_name]) }
+ .reverse
+ .reduce([]) { |where_conditions, column| column.where_conditions(where_conditions) }
end
def where_values_with_or_query(values)
@@ -222,32 +208,6 @@ module Gitlab
scope
end
- def conditions_for_column(column_definition, value)
- conditions = []
- # Depending on the order, build a query condition fragment for taking the next rows
- if column_definition.distinct? || (!column_definition.distinct? && value.present?)
- conditions << compare_column_with_value(column_definition, value)
- end
-
- # When the column is nullable, additional conditions for NULL a NOT NULL values are necessary.
- # This depends on the position of the nulls (top or bottom of the resultset).
- if column_definition.nulls_first? && value.blank?
- conditions << column_definition.column_expression.not_eq(nil)
- elsif column_definition.nulls_last? && value.present?
- conditions << column_definition.column_expression.eq(nil)
- end
-
- conditions
- end
-
- def compare_column_with_value(column_definition, value)
- if column_definition.descending_order?
- column_definition.column_expression.lt(value)
- else
- column_definition.column_expression.gt(value)
- end
- end
-
def build_or_query(expressions)
return [] if expressions.blank?
diff --git a/spec/frontend/authentication/two_factor_auth/index_spec.js b/spec/frontend/authentication/two_factor_auth/index_spec.js
index f5345139021..0ff9d60f409 100644
--- a/spec/frontend/authentication/two_factor_auth/index_spec.js
+++ b/spec/frontend/authentication/two_factor_auth/index_spec.js
@@ -1,5 +1,6 @@
import { getByTestId, fireEvent } from '@testing-library/dom';
import { createWrapper } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { initRecoveryCodes, initClose2faSuccessMessage } from '~/authentication/two_factor_auth';
import RecoveryCodes from '~/authentication/two_factor_auth/components/recovery_codes.vue';
import * as urlUtils from '~/lib/utils/url_utility';
@@ -53,8 +54,7 @@ describe('initClose2faSuccessMessage', () => {
describe('when alert is closed', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL(
+ setWindowLocation(
'https://localhost/-/profile/account?two_factor_auth_enabled_successfully=true',
);
diff --git a/spec/frontend/authentication/webauthn/error_spec.js b/spec/frontend/authentication/webauthn/error_spec.js
index 26f1ca5e27d..9b71f77dde2 100644
--- a/spec/frontend/authentication/webauthn/error_spec.js
+++ b/spec/frontend/authentication/webauthn/error_spec.js
@@ -1,3 +1,4 @@
+import setWindowLocation from 'helpers/set_window_location_helper';
import WebAuthnError from '~/authentication/webauthn/error';
describe('WebAuthnError', () => {
@@ -17,19 +18,8 @@ describe('WebAuthnError', () => {
});
describe('SecurityError', () => {
- const { location } = window;
-
- beforeEach(() => {
- delete window.location;
- window.location = {};
- });
-
- afterEach(() => {
- window.location = location;
- });
-
it('returns a descriptive error if https is disabled', () => {
- window.location.protocol = 'http:';
+ setWindowLocation('http://localhost');
const expectedMessage =
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.';
@@ -39,7 +29,7 @@ describe('WebAuthnError', () => {
});
it('returns a generic error if https is enabled', () => {
- window.location.protocol = 'https:';
+ setWindowLocation('https://localhost');
const expectedMessage = 'There was a problem communicating with your device.';
expect(
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 43cd3d7ca34..0f8ea2b635f 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnRegister from '~/authentication/webauthn/register';
import MockWebAuthnDevice from './mock_webauthn_device';
@@ -50,17 +51,14 @@ describe('WebAuthnRegister', () => {
});
describe('when unsupported', () => {
- const { location, PublicKeyCredential } = window;
+ const { PublicKeyCredential } = window;
beforeEach(() => {
- delete window.location;
delete window.credentials;
- window.location = {};
window.PublicKeyCredential = undefined;
});
afterEach(() => {
- window.location = location;
window.PublicKeyCredential = PublicKeyCredential;
});
@@ -69,7 +67,7 @@ describe('WebAuthnRegister', () => {
${false} | ${'WebAuthn only works with HTTPS-enabled websites'}
${true} | ${'Please use a supported browser, e.g. Chrome (67+) or Firefox'}
`('when https is $httpsEnabled', ({ httpsEnabled, expectedText }) => {
- window.location.protocol = httpsEnabled ? 'https:' : 'http:';
+ setWindowLocation(`${httpsEnabled ? 'https:' : 'http:'}//localhost`);
component.start();
expect(findMessage().text()).toContain(expectedText);
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 87f9a68f5dd..555767dd549 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,6 +1,7 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import BoardBlockedIcon from '~/boards/components/board_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
@@ -363,8 +364,6 @@ describe('Board card component', () => {
describe('filterByLabel method', () => {
beforeEach(() => {
- delete window.location;
-
wrapper.setProps({
updateFilters: true,
});
@@ -373,7 +372,7 @@ describe('Board card component', () => {
describe('when selected label is not in the filter', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
- window.location = { search: '' };
+ setWindowLocation('?');
wrapper.vm.filterByLabel(label1);
});
@@ -394,7 +393,7 @@ describe('Board card component', () => {
describe('when selected label is already in the filter', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'performSearch').mockImplementation(() => {});
- window.location = { search: '?label_name[]=testing%20123' };
+ setWindowLocation('?label_name[]=testing%20123');
wrapper.vm.filterByLabel(label1);
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 3966c3e6b87..52f1907654a 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
@@ -75,10 +76,6 @@ describe('BoardForm', () => {
});
};
- beforeEach(() => {
- delete window.location;
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -244,7 +241,7 @@ describe('BoardForm', () => {
updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
},
});
- window.location = new URL('https://test/boards/1');
+ setWindowLocation('https://test/boards/1');
createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
@@ -270,7 +267,7 @@ describe('BoardForm', () => {
updateBoard: { board: { id: 'gid://gitlab/Board/321', webPath: 'test-path' } },
},
});
- window.location = new URL('https://test/boards/1?group_by=epic');
+ setWindowLocation('https://test/boards/1?group_by=epic');
createComponent({ canAdminBoard: true, currentPage: formType.edit });
findInput().trigger('keyup.enter', { metaKey: true });
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index 42990334f0a..2a0610b1b0a 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture } from 'helpers/fixtures';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
import axios from '~/lib/utils/axios_utils';
@@ -8,6 +9,8 @@ import initProjectSelectDropdown from '~/project_select';
jest.mock('~/lib/utils/poll');
jest.mock('~/project_select');
+useMockLocationHelper();
+
describe('Clusters', () => {
setTestTimeout(1000);
@@ -55,20 +58,6 @@ describe('Clusters', () => {
});
describe('updateContainer', () => {
- const { location } = window;
-
- beforeEach(() => {
- delete window.location;
- window.location = {
- reload: jest.fn(),
- hash: location.hash,
- };
- });
-
- afterEach(() => {
- window.location = location;
- });
-
describe('when creating cluster', () => {
it('should show the creating container', () => {
cluster.updateContainer(null, 'creating');
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index b5eb3e1713c..bc06990e03a 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import Mousetrap from 'mousetrap';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
@@ -428,12 +429,10 @@ describe('diffs/components/app', () => {
jest.spyOn(wrapper.vm, 'refetchDiffData').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'adjustView').mockImplementation(() => {});
};
- let location;
+ const location = window.location.href;
beforeAll(() => {
- location = window.location;
- delete window.location;
- window.location = COMMIT_URL;
+ setWindowLocation(COMMIT_URL);
document.title = 'My Title';
});
@@ -442,7 +441,7 @@ describe('diffs/components/app', () => {
});
afterAll(() => {
- window.location = location;
+ setWindowLocation(location);
});
it('when the commit changes and the app is not loading it should update the history, refetch the diff data, and update the view', async () => {
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 1697ea3e952..1c0cb1193fa 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -220,7 +220,7 @@ describe('CompareVersions', () => {
describe('prev commit', () => {
beforeAll(() => {
- setWindowLocation(`${TEST_HOST}?commit_id=${mrCommit.id}`);
+ setWindowLocation(`?commit_id=${mrCommit.id}`);
});
beforeEach(() => {
@@ -255,7 +255,7 @@ describe('CompareVersions', () => {
describe('next commit', () => {
beforeAll(() => {
- setWindowLocation(`${TEST_HOST}?commit_id=${mrCommit.id}`);
+ setWindowLocation(`?commit_id=${mrCommit.id}`);
});
beforeEach(() => {
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 352db9d0d51..2c06ae03892 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -1,5 +1,6 @@
import { Range } from 'monaco-editor';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
+import setWindowLocation from 'helpers/set_window_location_helper';
import {
ERROR_INSTANCE_REQUIRED_FOR_EXTENSION,
EDITOR_TYPE_CODE,
@@ -152,12 +153,7 @@ describe('The basis for an Source Editor extension', () => {
useFakeRequestAnimationFrame();
beforeEach(() => {
- delete window.location;
- window.location = new URL(`https://localhost`);
- });
-
- afterEach(() => {
- window.location.hash = '';
+ setWindowLocation('https://localhost');
});
it.each`
diff --git a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
index a3b91cb20bb..3f47fa024bc 100644
--- a/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
+++ b/spec/frontend/members/components/filter_sort/members_filtered_search_bar_spec.js
@@ -1,11 +1,23 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
import MembersFilteredSearchBar from '~/members/components/filter_sort/members_filtered_search_bar.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+jest.mock('~/lib/utils/url_utility', () => {
+ const urlUtility = jest.requireActual('~/lib/utils/url_utility');
+
+ return {
+ __esModule: true,
+ ...urlUtility,
+ redirectTo: jest.fn(),
+ };
+});
+
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -113,12 +125,11 @@ describe('MembersFilteredSearchBar', () => {
describe('when filters are set via query params', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL('https://localhost');
+ setWindowLocation('https://localhost');
});
it('parses and passes tokens to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
- window.location.search = '?two_factor=enabled&token_not_available=foobar';
+ setWindowLocation('?two_factor=enabled&token_not_available=foobar');
createComponent();
@@ -134,7 +145,7 @@ describe('MembersFilteredSearchBar', () => {
});
it('parses and passes search param to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
- window.location.search = '?search=foobar';
+ setWindowLocation('?search=foobar');
createComponent();
@@ -149,7 +160,7 @@ describe('MembersFilteredSearchBar', () => {
});
it('parses and passes search param with multiple words to `FilteredSearchBar` component as `initialFilterValue` prop', () => {
- window.location.search = '?search=foo+bar+baz';
+ setWindowLocation('?search=foo+bar+baz');
createComponent();
@@ -166,8 +177,7 @@ describe('MembersFilteredSearchBar', () => {
describe('when filter bar is submitted', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL('https://localhost');
+ setWindowLocation('https://localhost');
});
it('adds correct filter query params', () => {
@@ -177,7 +187,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: 'two_factor', value: { data: 'enabled', operator: '=' } },
]);
- expect(window.location.href).toBe('https://localhost/?two_factor=enabled');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?two_factor=enabled');
});
it('adds search query param', () => {
@@ -188,7 +198,9 @@ describe('MembersFilteredSearchBar', () => {
{ type: 'filtered-search-term', value: { data: 'foobar' } },
]);
- expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foobar');
+ expect(redirectTo).toHaveBeenCalledWith(
+ 'https://localhost/?two_factor=enabled&search=foobar',
+ );
});
it('adds search query param with multiple words', () => {
@@ -199,11 +211,13 @@ describe('MembersFilteredSearchBar', () => {
{ type: 'filtered-search-term', value: { data: 'foo bar baz' } },
]);
- expect(window.location.href).toBe('https://localhost/?two_factor=enabled&search=foo+bar+baz');
+ expect(redirectTo).toHaveBeenCalledWith(
+ 'https://localhost/?two_factor=enabled&search=foo+bar+baz',
+ );
});
it('adds sort query param', () => {
- window.location.search = '?sort=name_asc';
+ setWindowLocation('?sort=name_asc');
createComponent();
@@ -212,13 +226,13 @@ describe('MembersFilteredSearchBar', () => {
{ type: 'filtered-search-term', value: { data: 'foobar' } },
]);
- expect(window.location.href).toBe(
+ expect(redirectTo).toHaveBeenCalledWith(
'https://localhost/?two_factor=enabled&search=foobar&sort=name_asc',
);
});
it('adds active tab query param', () => {
- window.location.search = '?tab=invited';
+ setWindowLocation('?tab=invited');
createComponent();
@@ -226,7 +240,7 @@ describe('MembersFilteredSearchBar', () => {
{ type: 'filtered-search-term', value: { data: 'foobar' } },
]);
- expect(window.location.href).toBe('https://localhost/?search=foobar&tab=invited');
+ expect(redirectTo).toHaveBeenCalledWith('https://localhost/?search=foobar&tab=invited');
});
});
});
diff --git a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
index 4b335755980..d0684acd487 100644
--- a/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
+++ b/spec/frontend/members/components/filter_sort/sort_dropdown_spec.js
@@ -1,6 +1,7 @@
import { GlSorting, GlSortingItem } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import * as urlUtilities from '~/lib/utils/url_utility';
import SortDropdown from '~/members/components/filter_sort/sort_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
@@ -52,17 +53,16 @@ describe('SortDropdown', () => {
.findAll(GlSortingItem)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.text() === text);
- describe('dropdown options', () => {
- beforeEach(() => {
- delete window.location;
- window.location = new URL(URL_HOST);
- });
+ beforeEach(() => {
+ setWindowLocation(URL_HOST);
+ });
+ describe('dropdown options', () => {
it('adds dropdown items for all the sortable fields', () => {
const URL_FILTER_PARAMS = '?two_factor=enabled&search=foobar';
const EXPECTED_BASE_URL = `${URL_HOST}${URL_FILTER_PARAMS}&sort=`;
- window.location.search = URL_FILTER_PARAMS;
+ setWindowLocation(URL_FILTER_PARAMS);
const expectedDropdownItems = [
{
@@ -94,7 +94,7 @@ describe('SortDropdown', () => {
});
it('checks selected sort option', () => {
- window.location.search = '?sort=access_level_asc';
+ setWindowLocation('?sort=access_level_asc');
createComponent();
@@ -103,11 +103,6 @@ describe('SortDropdown', () => {
});
describe('dropdown toggle', () => {
- beforeEach(() => {
- delete window.location;
- window.location = new URL(URL_HOST);
- });
-
it('defaults to sorting by "Account" in ascending order', () => {
createComponent();
@@ -116,7 +111,7 @@ describe('SortDropdown', () => {
});
it('sets text as selected sort option', () => {
- window.location.search = '?sort=access_level_asc';
+ setWindowLocation('?sort=access_level_asc');
createComponent();
@@ -126,15 +121,12 @@ describe('SortDropdown', () => {
describe('sort direction toggle', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL(URL_HOST);
-
- jest.spyOn(urlUtilities, 'visitUrl');
+ jest.spyOn(urlUtilities, 'visitUrl').mockImplementation();
});
describe('when current sort direction is ascending', () => {
beforeEach(() => {
- window.location.search = '?sort=access_level_asc';
+ setWindowLocation('?sort=access_level_asc');
createComponent();
});
@@ -152,7 +144,7 @@ describe('SortDropdown', () => {
describe('when current sort direction is descending', () => {
beforeEach(() => {
- window.location.search = '?sort=access_level_desc';
+ setWindowLocation('?sort=access_level_desc');
createComponent();
});
diff --git a/spec/frontend/members/components/members_tabs_spec.js b/spec/frontend/members/components/members_tabs_spec.js
index 33d8eebf7eb..68f25bcb619 100644
--- a/spec/frontend/members/components/members_tabs_spec.js
+++ b/spec/frontend/members/components/members_tabs_spec.js
@@ -1,6 +1,7 @@
import { GlTabs } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MembersApp from '~/members/components/app.vue';
import MembersTabs from '~/members/components/members_tabs.vue';
@@ -90,8 +91,7 @@ describe('MembersTabs', () => {
const findActiveTab = () => wrapper.findByRole('tab', { selected: true });
beforeEach(() => {
- delete window.location;
- window.location = new URL('https://localhost');
+ setWindowLocation('https://localhost');
});
afterEach(() => {
@@ -151,7 +151,7 @@ describe('MembersTabs', () => {
describe('when url param matches `filteredSearchBar.searchParam`', () => {
beforeEach(() => {
- window.location.search = '?search_groups=foo+bar';
+ setWindowLocation('?search_groups=foo+bar');
});
it('shows tab that corresponds to search param', async () => {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 6e7d9bc6b6e..6885da53b26 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -6,6 +6,7 @@ import {
} from '@testing-library/dom';
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
@@ -243,12 +244,8 @@ describe('MembersTable', () => {
});
describe('when required pagination data is provided', () => {
- beforeEach(() => {
- delete window.location;
- });
-
it('renders `gl-pagination` component with correct props', () => {
- window.location = new URL(url);
+ setWindowLocation(url);
createComponent();
@@ -268,7 +265,7 @@ describe('MembersTable', () => {
});
it('uses `pagination.paramName` to generate the pagination links', () => {
- window.location = new URL(url);
+ setWindowLocation(url);
createComponent({
pagination: {
@@ -283,7 +280,7 @@ describe('MembersTable', () => {
});
it('removes any url params defined as `null` in the `params` attribute', () => {
- window.location = new URL(`${url}&search_groups=foo`);
+ setWindowLocation(`${url}&search_groups=foo`);
createComponent({
pagination: {
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 9740e1c2edb..a157cfa1c1d 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -1,3 +1,4 @@
+import setWindowLocation from 'helpers/set_window_location_helper';
import { DEFAULT_SORT, MEMBER_TYPES } from '~/members/constants';
import {
generateBadges,
@@ -150,21 +151,18 @@ describe('Members Utils', () => {
describe('parseSortParam', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL(URL_HOST);
+ setWindowLocation(URL_HOST);
});
describe('when `sort` param is not present', () => {
it('returns default sort options', () => {
- window.location.search = '';
-
expect(parseSortParam(['account'])).toEqual(DEFAULT_SORT);
});
});
describe('when field passed in `sortableFields` argument does not have `sort` key defined', () => {
it('returns default sort options', () => {
- window.location.search = '?sort=source_asc';
+ setWindowLocation('?sort=source_asc');
expect(parseSortParam(['source'])).toEqual(DEFAULT_SORT);
});
@@ -182,7 +180,7 @@ describe('Members Utils', () => {
${'oldest_sign_in'} | ${{ sortByKey: 'lastSignIn', sortDesc: true }}
`('when `sort` query string param is `$sortParam`', ({ sortParam, expected }) => {
it(`returns ${JSON.stringify(expected)}`, async () => {
- window.location.search = `?sort=${sortParam}`;
+ setWindowLocation(`?sort=${sortParam}`);
expect(parseSortParam(['account', 'granted', 'expires', 'maxRole', 'lastSignIn'])).toEqual(
expected,
@@ -193,8 +191,7 @@ describe('Members Utils', () => {
describe('buildSortHref', () => {
beforeEach(() => {
- delete window.location;
- window.location = new URL(URL_HOST);
+ setWindowLocation(URL_HOST);
});
describe('when field passed in `sortBy` argument does not have `sort` key defined', () => {
@@ -225,7 +222,7 @@ describe('Members Utils', () => {
describe('when filter params are set', () => {
it('merges the `sort` param with the filter params', () => {
- window.location.search = '?two_factor=enabled&with_inherited_permissions=exclude';
+ setWindowLocation('?two_factor=enabled&with_inherited_permissions=exclude');
expect(
buildSortHref({
@@ -240,7 +237,7 @@ describe('Members Utils', () => {
describe('when search param is set', () => {
it('merges the `sort` param with the search param', () => {
- window.location.search = '?search=foobar';
+ setWindowLocation('?search=foobar');
expect(
buildSortHref({
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index dbb9fd5f603..f2116c1f478 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -301,9 +301,6 @@ describe('Actions menu', () => {
});
it('redirects to the newly created dashboard', () => {
- delete window.location;
- window.location = new URL('https://localhost');
-
const newDashboard = dashboardGitResponse[1];
const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 7ca1b97d849..f899580b3df 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import VueDraggable from 'vuedraggable';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
@@ -226,32 +227,25 @@ describe('Dashboard', () => {
});
describe('when the URL contains a reference to a panel', () => {
- let location;
+ const location = window.location.href;
- const setSearch = (search) => {
- window.location = { ...location, search };
+ const setSearch = (searchParams) => {
+ setWindowLocation(`?${objectToQuery(searchParams)}`);
};
- beforeEach(() => {
- location = window.location;
- delete window.location;
- });
-
afterEach(() => {
- window.location = location;
+ setWindowLocation(location);
});
it('when the URL points to a panel it expands', () => {
const panelGroup = metricsDashboardViewModel.panelGroups[0];
const panel = panelGroup.panels[0];
- setSearch(
- objectToQuery({
- group: panelGroup.group,
- title: panel.title,
- y_label: panel.y_label,
- }),
- );
+ setSearch({
+ group: panelGroup.group,
+ title: panel.title,
+ y_label: panel.y_label,
+ });
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
@@ -268,7 +262,7 @@ describe('Dashboard', () => {
});
it('when the URL does not link to any panel, no panel is expanded', () => {
- setSearch('');
+ setSearch();
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
@@ -285,13 +279,11 @@ describe('Dashboard', () => {
const panelGroup = metricsDashboardViewModel.panelGroups[0];
const panel = panelGroup.panels[0];
- setSearch(
- objectToQuery({
- group: panelGroup.group,
- title: 'incorrect',
- y_label: panel.y_label,
- }),
- );
+ setSearch({
+ group: panelGroup.group,
+ title: 'incorrect',
+ y_label: panel.y_label,
+ });
createMountedWrapper({ hasMetrics: true });
setupStoreWithData(store);
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
index 3132ec61942..377e7e05f09 100644
--- a/spec/frontend/packages/details/components/app_spec.js
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -2,6 +2,7 @@ import { GlEmptyState } from '@gitlab/ui';
import { mount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import stubChildren from 'helpers/stub_children';
import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
@@ -30,6 +31,8 @@ import {
const localVue = createLocalVue();
localVue.use(Vuex);
+useMockLocationHelper();
+
describe('PackagesApp', () => {
let wrapper;
let store;
@@ -37,7 +40,6 @@ describe('PackagesApp', () => {
const deletePackage = jest.fn();
const deletePackageFile = jest.fn();
const defaultProjectName = 'bar';
- const { location } = window;
function createComponent({
packageEntity = mavenPackage,
@@ -100,14 +102,8 @@ describe('PackagesApp', () => {
const findInstallationCommands = () => wrapper.find(InstallationCommands);
const findPackageFiles = () => wrapper.find(PackageFiles);
- beforeEach(() => {
- delete window.location;
- window.location = { replace: jest.fn() };
- });
-
afterEach(() => {
wrapper.destroy();
- window.location = location;
});
it('renders the app and displays the package title', async () => {
diff --git a/spec/frontend/packages/list/components/packages_list_app_spec.js b/spec/frontend/packages/list/components/packages_list_app_spec.js
index 4de2dd0789e..b94192c531c 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -1,6 +1,7 @@
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import setWindowLocation from 'helpers/set_window_location_helper';
import createFlash from '~/flash';
import * as commonUtils from '~/lib/utils/common_utils';
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
@@ -233,21 +234,17 @@ describe('packages_list_app', () => {
});
describe('delete alert handling', () => {
- const { location } = window.location;
+ const originalLocation = window.location.href;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => {
createStore();
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
- delete window.location;
- window.location = {
- href: `foo_bar_baz${search}`,
- search,
- };
+ setWindowLocation(search);
});
afterEach(() => {
- window.location = location;
+ setWindowLocation(originalLocation);
});
it(`creates a flash if the query string contains ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
@@ -262,11 +259,11 @@ describe('packages_list_app', () => {
it('calls historyReplaceState with a clean url', () => {
mountComponent();
- expect(commonUtils.historyReplaceState).toHaveBeenCalledWith('foo_bar_baz');
+ expect(commonUtils.historyReplaceState).toHaveBeenCalledWith(originalLocation);
});
it(`does nothing if the query string does not contain ${SHOW_DELETE_SUCCESS_ALERT}`, () => {
- window.location.search = '';
+ setWindowLocation('?');
mountComponent();
expect(createFlash).not.toHaveBeenCalled();
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
index 2b3acbf99f3..0504a42dfcf 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/additional_metadata_spec.js
@@ -1,17 +1,35 @@
import { GlLink, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { mavenPackage, conanPackage, nugetPackage, npmPackage } from 'jest/packages/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ conanMetadata,
+ mavenMetadata,
+ nugetMetadata,
+ packageData,
+} from 'jest/packages_and_registries/package_registry/mock_data';
import component from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
+import {
+ PACKAGE_TYPE_NUGET,
+ PACKAGE_TYPE_CONAN,
+ PACKAGE_TYPE_MAVEN,
+ PACKAGE_TYPE_NPM,
+} from '~/packages_and_registries/package_registry/constants';
import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
+const mavenPackage = { packageType: PACKAGE_TYPE_MAVEN, metadata: mavenMetadata() };
+const conanPackage = { packageType: PACKAGE_TYPE_CONAN, metadata: conanMetadata() };
+const nugetPackage = { packageType: PACKAGE_TYPE_NUGET, metadata: nugetMetadata() };
+const npmPackage = { packageType: PACKAGE_TYPE_NPM, metadata: {} };
+
describe('Package Additional Metadata', () => {
let wrapper;
const defaultProps = {
- packageEntity: { ...mavenPackage },
+ packageEntity: {
+ ...packageData(mavenPackage),
+ },
};
const mountComponent = (props) => {
- wrapper = shallowMount(component, {
+ wrapper = shallowMountExtended(component, {
propsData: { ...defaultProps, ...props },
stubs: {
DetailsRow,
@@ -25,14 +43,14 @@ describe('Package Additional Metadata', () => {
wrapper = null;
});
- const findTitle = () => wrapper.find('[data-testid="title"]');
- const findMainArea = () => wrapper.find('[data-testid="main"]');
- const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
- const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
- const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
- const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
- const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
- const findElementLink = (container) => container.find(GlLink);
+ const findTitle = () => wrapper.findByTestId('title');
+ const findMainArea = () => wrapper.findByTestId('main');
+ const findNugetSource = () => wrapper.findByTestId('nuget-source');
+ const findNugetLicense = () => wrapper.findByTestId('nuget-license');
+ const findConanRecipe = () => wrapper.findByTestId('conan-recipe');
+ const findMavenApp = () => wrapper.findByTestId('maven-app');
+ const findMavenGroup = () => wrapper.findByTestId('maven-group');
+ const findElementLink = (container) => container.findComponent(GlLink);
it('has the correct title', () => {
mountComponent();
@@ -43,27 +61,21 @@ describe('Package Additional Metadata', () => {
expect(title.text()).toBe('Additional Metadata');
});
- describe.each`
- packageEntity | visible | metadata
- ${mavenPackage} | ${true} | ${'maven_metadatum'}
- ${conanPackage} | ${true} | ${'conan_metadatum'}
- ${nugetPackage} | ${true} | ${'nuget_metadatum'}
- ${npmPackage} | ${false} | ${null}
- `('Component visibility', ({ packageEntity, visible, metadata }) => {
- it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
+ it.each`
+ packageEntity | visible | packageType
+ ${mavenPackage} | ${true} | ${PACKAGE_TYPE_MAVEN}
+ ${conanPackage} | ${true} | ${PACKAGE_TYPE_CONAN}
+ ${nugetPackage} | ${true} | ${PACKAGE_TYPE_NUGET}
+ ${npmPackage} | ${false} | ${PACKAGE_TYPE_NPM}
+ `(
+ `It is $visible that the component is visible when the package is $packageType`,
+ ({ packageEntity, visible }) => {
mountComponent({ packageEntity });
expect(findTitle().exists()).toBe(visible);
expect(findMainArea().exists()).toBe(visible);
- });
-
- it(`The component is hidden if ${metadata} is missing`, () => {
- mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
-
- expect(findTitle().exists()).toBe(false);
- expect(findMainArea().exists()).toBe(false);
- });
- });
+ },
+ );
describe('nuget metadata', () => {
beforeEach(() => {
@@ -71,15 +83,15 @@ describe('Package Additional Metadata', () => {
});
it.each`
- name | finderFunction | text | link | icon
- ${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
- ${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'}
+ name | finderFunction | text | link | icon
+ ${'source'} | ${findNugetSource} | ${'Source project located at projectUrl'} | ${'projectUrl'} | ${'project'}
+ ${'license'} | ${findNugetLicense} | ${'License information located at licenseUrl'} | ${'licenseUrl'} | ${'license'}
`('$name element', ({ finderFunction, text, link, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
expect(element.text()).toBe(text);
expect(element.props('icon')).toBe(icon);
- expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]);
+ expect(findElementLink(element).attributes('href')).toBe(nugetPackage.metadata[link]);
});
});
@@ -89,8 +101,8 @@ describe('Package Additional Metadata', () => {
});
it.each`
- name | finderFunction | text | icon
- ${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'}
+ name | finderFunction | text | icon
+ ${'recipe'} | ${findConanRecipe} | ${'Recipe: package-8/1.0.0@gitlab-org+gitlab-test/stable'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
@@ -105,9 +117,9 @@ describe('Package Additional Metadata', () => {
});
it.each`
- name | finderFunction | text | icon
- ${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'}
- ${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'}
+ name | finderFunction | text | icon
+ ${'app'} | ${findMavenApp} | ${'App name: appName'} | ${'information-o'}
+ ${'group'} | ${findMavenGroup} | ${'App group: appGroup'} | ${'information-o'}
`('$name element', ({ finderFunction, text, icon }) => {
const element = finderFunction();
expect(element.exists()).toBe(true);
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
index 4be4733b3b0..e6d12f5e479 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/app_spec.js
@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
+import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue';
import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
@@ -48,6 +49,7 @@ describe('PackagesApp', () => {
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findPackageHistory = () => wrapper.findComponent(PackageHistory);
+ const findAdditionalMetadata = () => wrapper.findComponent(AdditionalMetadata);
afterEach(() => {
wrapper.destroy();
@@ -95,4 +97,15 @@ describe('PackagesApp', () => {
projectName: provide.projectName,
});
});
+
+ it('renders additional metadata and has the right props', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findAdditionalMetadata().exists()).toBe(true);
+ expect(findAdditionalMetadata().props()).toMatchObject({
+ packageEntity: expect.objectContaining(packageData()),
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 9de3dee0738..d1b81aa8b5f 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -58,10 +58,49 @@ export const packageData = (extend) => ({
...extend,
});
+export const conanMetadata = () => ({
+ packageChannel: 'stable',
+ packageUsername: 'gitlab-org+gitlab-test',
+ recipe: 'package-8/1.0.0@gitlab-org+gitlab-test/stable',
+ recipePath: 'package-8/1.0.0/gitlab-org+gitlab-test/stable',
+});
+
+export const composerMetadata = () => ({
+ targetSha: 'b83d6e391c22777fca1ed3012fce84f633d7fed0',
+ composerJson: {
+ license: 'MIT',
+ version: '1.0.0',
+ },
+});
+
+export const pypyMetadata = () => ({
+ requiredPython: '1.0.0',
+});
+
+export const mavenMetadata = () => ({
+ appName: 'appName',
+ appGroup: 'appGroup',
+ appVersion: 'appVersion',
+ path: 'path',
+});
+
+export const nugetMetadata = () => ({
+ iconUrl: 'iconUrl',
+ licenseUrl: 'licenseUrl',
+ projectUrl: 'projectUrl',
+});
+
export const packageDetailsQuery = () => ({
data: {
package: {
...packageData(),
+ metadata: {
+ ...conanMetadata(),
+ ...composerMetadata(),
+ ...pypyMetadata(),
+ ...mavenMetadata(),
+ ...nugetMetadata(),
+ },
tags: {
nodes: packageTags(),
__typename: 'PackageTagConnection',
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 1e51ddf909a..1db255106ed 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -166,6 +167,8 @@ describe('PersistentUserCallout', () => {
let mockAxios;
let persistentUserCallout;
+ useMockLocationHelper();
+
beforeEach(() => {
const fixture = createFollowLinkFixture();
const container = fixture.querySelector('.container');
@@ -174,9 +177,6 @@ describe('PersistentUserCallout', () => {
persistentUserCallout = new PersistentUserCallout(container);
jest.spyOn(persistentUserCallout.container, 'remove').mockImplementation(() => {});
-
- delete window.location;
- window.location = { assign: jest.fn() };
});
afterEach(() => {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index b0d1a69ee56..0c5c08d7190 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -2,6 +2,7 @@ import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import TextEditor from '~/pipeline_editor/components/editor/text_editor.vue';
@@ -348,15 +349,14 @@ describe('Pipeline editor app component', () => {
});
describe('when a template parameter is present in the URL', () => {
- const { location } = window;
+ const originalLocation = window.location.href;
beforeEach(() => {
- delete window.location;
- window.location = new URL('https://localhost?template=Android');
+ setWindowLocation('?template=Android');
});
afterEach(() => {
- window.location = location;
+ setWindowLocation(originalLocation);
});
it('renders the given template', async () => {
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index f1172a73d36..4d2dcf83d3b 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createFlash from '~/flash';
import IntegrationView from '~/profile/preferences/components/integration_view.vue';
@@ -19,6 +20,8 @@ import {
jest.mock('~/flash');
const expectedUrl = '/foo';
+useMockLocationHelper();
+
describe('ProfilePreferences component', () => {
let wrapper;
const defaultProvide = {
@@ -174,8 +177,6 @@ describe('ProfilePreferences component', () => {
});
describe('theme changes', () => {
- const { location } = window;
-
let themeInput;
let form;
@@ -197,18 +198,6 @@ describe('ProfilePreferences component', () => {
form.dispatchEvent(successEvent);
}
- beforeAll(() => {
- delete window.location;
- window.location = {
- ...location,
- reload: jest.fn(),
- };
- });
-
- afterAll(() => {
- window.location = location;
- });
-
beforeEach(() => {
setupBody();
themeInput = createThemeInput();
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 585614a6b79..e77bae79f68 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,12 +1,15 @@
import { GlButton, GlModal } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { differenceInMilliseconds } from '~/lib/utils/datetime_utility';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+useMockLocationHelper();
+
describe('Snippet header component', () => {
let wrapper;
let snippet;
@@ -200,19 +203,6 @@ describe('Snippet header component', () => {
});
describe('Delete mutation', () => {
- const { location } = window;
-
- beforeEach(() => {
- delete window.location;
- window.location = {
- pathname: '',
- };
- });
-
- afterEach(() => {
- window.location = location;
- });
-
it('dispatches a mutation to delete the snippet with correct variables', () => {
createComponent();
wrapper.vm.deleteSnippet();
diff --git a/spec/lib/gitlab/pagination/keyset/order_spec.rb b/spec/lib/gitlab/pagination/keyset/order_spec.rb
index a2179b4e902..b867dd533e0 100644
--- a/spec/lib/gitlab/pagination/keyset/order_spec.rb
+++ b/spec/lib/gitlab/pagination/keyset/order_spec.rb
@@ -6,32 +6,67 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
describe 'paginate over items correctly' do
let(:table) { Arel::Table.new(:my_table) }
let(:order) { nil }
+ let(:default_limit) { 999 }
+ let(:query_building_method) { :build_query }
def run_query(query)
ApplicationRecord.connection.execute(query).to_a
end
- def build_query(order:, where_conditions: nil, limit: nil)
+ def where_conditions_as_sql(where_conditions)
+ "WHERE #{Array(where_conditions).map(&:to_sql).join(' OR ')}"
+ end
+
+ def build_query(order:, where_conditions: [], limit: nil)
+ where_string = where_conditions_as_sql(where_conditions)
+
+ <<-SQL
+ SELECT id, year, month
+ FROM (#{table_data}) my_table (id, year, month)
+ #{where_string if where_conditions.present?}
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit};
+ SQL
+ end
+
+ def build_union_query(order:, where_conditions: [], limit: nil)
+ return build_query(order: order, where_conditions: where_conditions, limit: limit) if where_conditions.blank?
+
+ union_queries = Array(where_conditions).map do |where_condition|
+ <<-SQL
+ (SELECT id, year, month
+ FROM (#{table_data}) my_table (id, year, month)
+ WHERE #{where_condition.to_sql}
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit})
+ SQL
+ end
+
+ union_query = union_queries.join(" UNION ALL ")
+
<<-SQL
- SELECT id, year, month
- FROM (#{table_data}) my_table (id, year, month)
- WHERE #{where_conditions || '1=1'}
- ORDER BY #{order}
- LIMIT #{limit || 999};
+ SELECT id, year, month
+ FROM (#{union_query}) as my_table
+ ORDER BY #{order}
+ LIMIT #{limit || default_limit};
SQL
end
+ def cursor_attributes_for_node(node)
+ order.cursor_attributes_for_node(node)
+ end
+
def iterate_and_collect(order:, page_size:, where_conditions: nil)
all_items = []
loop do
- paginated_items = run_query(build_query(order: order, where_conditions: where_conditions, limit: page_size))
+ paginated_items = run_query(send(query_building_method, order: order, where_conditions: where_conditions, limit: page_size))
break if paginated_items.empty?
all_items.concat(paginated_items)
last_item = paginated_items.last
- cursor_attributes = order.cursor_attributes_for_node(last_item)
- where_conditions = order.where_values_with_or_query(cursor_attributes).to_sql
+ cursor_attributes = cursor_attributes_for_node(last_item)
+ where_conditions = order.build_where_values(cursor_attributes)
end
all_items
@@ -54,15 +89,41 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
it { expect(subject).to eq(expected) }
end
+
+ context 'when using the conditions in an UNION query' do
+ let(:query_building_method) { :build_union_query }
+
+ it { expect(subject).to eq(expected) }
+ end
+
+ context 'when the cursor attributes are SQL literals' do
+ def cursor_attributes_for_node(node)
+ # Simulate the scenario where the cursor attributes are SQL literals
+ order.cursor_attributes_for_node(node).transform_values.each_with_index do |value, i|
+ index = i + 1
+ value_sql = value.nil? ? 'NULL::integer' : value
+ values = [value_sql] * index
+ Arel.sql("(ARRAY[#{values.join(',')}])[#{index}]") # example: ARRAY[cursor_value][1] will return cursor_value
+ end
+ end
+
+ it { expect(subject).to eq(expected) }
+
+ context 'when using the conditions in an UNION query' do
+ let(:query_building_method) { :build_union_query }
+
+ it { expect(subject).to eq(expected) }
+ end
+ end
end
context 'when paginating backwards' do
subject do
last_item = expected.last
cursor_attributes = order.cursor_attributes_for_node(last_item)
- where_conditions = order.reversed_order.where_values_with_or_query(cursor_attributes)
+ where_conditions = order.reversed_order.build_where_values(cursor_attributes)
- iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions.to_sql)
+ iterate_and_collect(order: order.reversed_order, page_size: 2, where_conditions: where_conditions)
end
it do
@@ -371,7 +432,7 @@ RSpec.describe Gitlab::Pagination::Keyset::Order do
reversed = order.reversed_order
before_conditions = reversed.where_values_with_or_query(before_cursor)
- query = build_query(order: order, where_conditions: "(#{after_conditions.to_sql}) AND (#{before_conditions.to_sql})", limit: 100)
+ query = build_query(order: order, where_conditions: [Arel::Nodes::And.new([after_conditions, before_conditions])], limit: 100)
expect(run_query(query)).to eq([
{ "id" => 2, "year" => 2011, "month" => 0 },
diff --git a/spec/requests/api/project_attributes.yml b/spec/requests/api/project_attributes.yml
index 822941e7c36..c5bcedd491a 100644
--- a/spec/requests/api/project_attributes.yml
+++ b/spec/requests/api/project_attributes.yml
@@ -121,7 +121,6 @@ project_feature:
- project_id
- requirements_access_level
- security_and_compliance_access_level
- - container_registry_access_level
- updated_at
computed_attributes:
- issues_enabled
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index c2e564b2759..06d27dcb87f 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -207,6 +207,18 @@ RSpec.describe API::Projects do
let(:current_user) { user }
end
+ it 'includes container_registry_access_level', :aggregate_failures do
+ project.project_feature.update!(container_registry_access_level: ProjectFeature::DISABLED)
+
+ get api('/projects', user)
+ project_response = json_response.find { |p| p['id'] == project.id }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(project_response['container_registry_access_level']).to eq('disabled')
+ expect(project_response['container_registry_enabled']).to eq(false)
+ end
+
context 'when some projects are in a group' do
before do
create(:project, :public, group: create(:group))
@@ -1042,7 +1054,7 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:bad_request)
end
- it "assigns attributes to project" do
+ it "assigns attributes to project", :aggregate_failures do
project = attributes_for(:project, {
path: 'camelCasePath',
issues_enabled: false,
@@ -1064,6 +1076,7 @@ RSpec.describe API::Projects do
}).tap do |attrs|
attrs[:operations_access_level] = 'disabled'
attrs[:analytics_access_level] = 'disabled'
+ attrs[:container_registry_access_level] = 'private'
end
post api('/projects', user), params: project
@@ -1071,7 +1084,10 @@ RSpec.describe API::Projects do
expect(response).to have_gitlab_http_status(:created)
project.each_pair do |k, v|
- next if %i[has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version].include?(k)
+ next if %i[
+ has_external_issue_tracker has_external_wiki issues_enabled merge_requests_enabled wiki_enabled storage_version
+ container_registry_access_level
+ ].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
@@ -1083,6 +1099,18 @@ RSpec.describe API::Projects do
expect(project.project_feature.wiki_access_level).to eq(ProjectFeature::DISABLED)
expect(project.operations_access_level).to eq(ProjectFeature::DISABLED)
expect(project.project_feature.analytics_access_level).to eq(ProjectFeature::DISABLED)
+ expect(project.project_feature.container_registry_access_level).to eq(ProjectFeature::PRIVATE)
+ end
+
+ it 'assigns container_registry_enabled to project', :aggregate_failures do
+ project = attributes_for(:project, { container_registry_enabled: true })
+
+ post api('/projects', user), params: project
+
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['container_registry_enabled']).to eq(true)
+ expect(json_response['container_registry_access_level']).to eq('enabled')
+ expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::ENABLED)
end
it 'creates a project using a template' do
@@ -1340,6 +1368,14 @@ RSpec.describe API::Projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
+ it 'includes container_registry_access_level', :aggregate_failures do
+ get api("/users/#{user4.id}/projects/", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('container_registry_access_level')
+ end
+
context 'and using id_after' do
let!(:another_public_project) { create(:project, :public, name: 'another_public_project', creator_id: user4.id, namespace: user4.namespace) }
@@ -1649,6 +1685,59 @@ RSpec.describe API::Projects do
expect(json_response['only_allow_merge_if_all_discussions_are_resolved']).to be_truthy
end
+
+ context 'container_registry_enabled' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:container_registry_enabled, :container_registry_access_level) do
+ true | ProjectFeature::ENABLED
+ false | ProjectFeature::DISABLED
+ end
+
+ with_them do
+ it 'setting container_registry_enabled also sets container_registry_access_level', :aggregate_failures do
+ project_attributes = attributes_for(:project).tap do |attrs|
+ attrs[:container_registry_enabled] = container_registry_enabled
+ end
+
+ post api("/projects/user/#{user.id}", admin), params: project_attributes
+
+ project = Project.find_by(path: project_attributes[:path])
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['container_registry_access_level']).to eq(ProjectFeature.str_from_access_level(container_registry_access_level))
+ expect(json_response['container_registry_enabled']).to eq(container_registry_enabled)
+ expect(project.container_registry_access_level).to eq(container_registry_access_level)
+ expect(project.container_registry_enabled).to eq(container_registry_enabled)
+ end
+ end
+ end
+
+ context 'container_registry_access_level' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:container_registry_access_level, :container_registry_enabled) do
+ 'enabled' | true
+ 'private' | true
+ 'disabled' | false
+ end
+
+ with_them do
+ it 'setting container_registry_access_level also sets container_registry_enabled', :aggregate_failures do
+ project_attributes = attributes_for(:project).tap do |attrs|
+ attrs[:container_registry_access_level] = container_registry_access_level
+ end
+
+ post api("/projects/user/#{user.id}", admin), params: project_attributes
+
+ project = Project.find_by(path: project_attributes[:path])
+ expect(response).to have_gitlab_http_status(:created)
+ expect(json_response['container_registry_access_level']).to eq(container_registry_access_level)
+ expect(json_response['container_registry_enabled']).to eq(container_registry_enabled)
+ expect(project.container_registry_access_level).to eq(ProjectFeature.access_level_from_str(container_registry_access_level))
+ expect(project.container_registry_enabled).to eq(container_registry_enabled)
+ end
+ end
+ end
end
describe "POST /projects/:id/uploads/authorize" do
@@ -2034,6 +2123,7 @@ RSpec.describe API::Projects do
expect(json_response['jobs_enabled']).to be_present
expect(json_response['snippets_enabled']).to be_present
expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['container_registry_access_level']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
@@ -2125,6 +2215,7 @@ RSpec.describe API::Projects do
expect(json_response['resolve_outdated_diff_discussions']).to eq(project.resolve_outdated_diff_discussions)
expect(json_response['remove_source_branch_after_merge']).to be_truthy
expect(json_response['container_registry_enabled']).to be_present
+ expect(json_response['container_registry_access_level']).to be_present
expect(json_response['created_at']).to be_present
expect(json_response['last_activity_at']).to be_present
expect(json_response['shared_runners_enabled']).to be_present
@@ -2951,6 +3042,14 @@ RSpec.describe API::Projects do
end
end
+ it 'sets container_registry_access_level', :aggregate_failures do
+ put api("/projects/#{project.id}", user), params: { container_registry_access_level: 'private' }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response['container_registry_access_level']).to eq('private')
+ expect(Project.find_by(path: project[:path]).container_registry_access_level).to eq(ProjectFeature::PRIVATE)
+ end
+
it 'returns 400 when nothing sent' do
project_param = {}
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
index f40b03fda2a..43e11d31611 100644
--- a/spec/views/groups/edit.html.haml_spec.rb
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'groups/edit.html.haml' do
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
- expect(rendered).to have_css('.js-descr', text: 'help text here')
+ expect(rendered).to have_content('help text here')
expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
end
end
diff --git a/spec/views/groups/show.html.haml_spec.rb b/spec/views/groups/show.html.haml_spec.rb
index f40b03fda2a..43e11d31611 100644
--- a/spec/views/groups/show.html.haml_spec.rb
+++ b/spec/views/groups/show.html.haml_spec.rb
@@ -25,7 +25,7 @@ RSpec.describe 'groups/edit.html.haml' do
render
expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
- expect(rendered).to have_css('.js-descr', text: 'help text here')
+ expect(rendered).to have_content('help text here')
expect(rendered).to have_field('group_share_with_group_lock', **checkbox_options)
end
end