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:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-10-01 09:09:59 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-10-01 09:09:59 +0300
commita7e81add72ecfbbf888ec4f73debdc2647b059de (patch)
tree3139b36451667bdd07181ef35044bd9903424fb1
parent4ed4dc08a806773e5dc326fc0c18bda6f6ea7af7 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue22
-rw-r--r--app/assets/javascripts/registry/settings/registry_settings_bundle.js3
-rw-r--r--app/assets/javascripts/registry/settings/store/actions.js30
-rw-r--r--app/assets/javascripts/registry/settings/store/getters.js26
-rw-r--r--app/assets/javascripts/registry/settings/store/index.js18
-rw-r--r--app/assets/javascripts/registry/settings/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/registry/settings/store/mutations.js29
-rw-r--r--app/assets/javascripts/registry/settings/store/state.js42
-rw-r--r--app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue31
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/members_table.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/members/utils.js19
-rw-r--r--app/finders/packages/generic/package_finder.rb22
-rw-r--r--app/models/application_setting/term.rb2
-rw-r--r--app/models/packages/package.rb9
-rw-r--r--app/models/user.rb2
-rw-r--r--app/views/layouts/group.html.haml4
-rw-r--r--app/views/layouts/project.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml5
-rw-r--r--changelogs/unreleased/okr-ingress-modsecurity-settings.yml5
-rw-r--r--changelogs/unreleased/pat-bot-terms.yml5
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql38
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json133
-rw-r--r--doc/api/graphql/reference/index.md10
-rw-r--r--doc/ci/introduction/index.md2
-rw-r--r--doc/ci/migration/jenkins.md2
-rw-r--r--doc/ci/yaml/README.md10
-rw-r--r--doc/topics/autodevops/index.md6
-rw-r--r--doc/topics/autodevops/stages.md15
-rw-r--r--lib/api/generic_packages.rb27
-rw-r--r--lib/gitlab/regex.rb6
-rw-r--r--locale/gitlab.pot3
-rw-r--r--spec/factories/packages/package_file.rb8
-rw-r--r--spec/features/groups/show_spec.rb13
-rw-r--r--spec/features/issues/user_views_issue_spec.rb4
-rw-r--r--spec/features/merge_request/user_sees_page_metadata_spec.rb17
-rw-r--r--spec/features/milestones/user_views_milestone_spec.rb10
-rw-r--r--spec/features/projects_spec.rb9
-rw-r--r--spec/features/users/show_spec.rb10
-rw-r--r--spec/features/users/terms_spec.rb15
-rw-r--r--spec/finders/packages/generic/package_finder_spec.rb31
-rw-r--r--spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js6
-rw-r--r--spec/frontend/registry/settings/store/actions_spec.js90
-rw-r--r--spec/frontend/registry/settings/store/getters_spec.js72
-rw-r--r--spec/frontend/registry/settings/store/mutations_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js34
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_avatar_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js38
-rw-r--r--spec/frontend/vue_shared/components/members/utils_spec.js29
-rw-r--r--spec/lib/gitlab/regex_spec.rb14
-rw-r--r--spec/models/application_setting/term_spec.rb5
-rw-r--r--spec/models/ci/job_artifact_spec.rb2
-rw-r--r--spec/models/packages/package_spec.rb14
-rw-r--r--spec/models/user_spec.rb22
-rw-r--r--spec/requests/api/generic_packages_spec.rb206
-rw-r--r--spec/support/shared_examples/features/page_description_shared_examples.rb9
62 files changed, 818 insertions, 490 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ac663669a67..5b06b4f3f5b 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-87acab96b9eb16381a49f2c08a2eaa9664a2fa75
+3f5e218def93024f3aafe590c22cd1b29f744105
diff --git a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
index 5e8e1a76182..ec252878e93 100644
--- a/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
+++ b/app/assets/javascripts/clusters/components/ingress_modsecurity_settings.vue
@@ -5,7 +5,7 @@ import {
GlSprintf,
GlLink,
GlToggle,
- GlDeprecatedButton,
+ GlButton,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlIcon,
@@ -25,7 +25,7 @@ export default {
GlSprintf,
GlLink,
GlToggle,
- GlDeprecatedButton,
+ GlButton,
GlDeprecatedDropdown,
GlDeprecatedDropdownItem,
GlIcon,
@@ -232,18 +232,24 @@ export default {
</gl-deprecated-dropdown>
</div>
</div>
- <div v-if="showButtons" class="mt-3">
- <gl-deprecated-button
- class="btn-success inline mr-1"
+ <div v-if="showButtons" class="gl-mt-5 gl-display-flex">
+ <gl-button
+ variant="success"
+ category="primary"
+ data-qa-selector="save_ingress_modsecurity_settings"
:loading="saving"
:disabled="saveButtonDisabled"
@click="updateApplication"
>
{{ saveButtonLabel }}
- </gl-deprecated-button>
- <gl-deprecated-button :disabled="saveButtonDisabled" @click="resetStatus">
+ </gl-button>
+ <gl-button
+ data-qa-selector="cancel_ingress_modsecurity_settings"
+ :disabled="saveButtonDisabled"
+ @click="resetStatus"
+ >
{{ __('Cancel') }}
- </gl-deprecated-button>
+ </gl-button>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/registry/settings/registry_settings_bundle.js b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
index 418483fdb41..5f25d508e2f 100644
--- a/app/assets/javascripts/registry/settings/registry_settings_bundle.js
+++ b/app/assets/javascripts/registry/settings/registry_settings_bundle.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate';
-import store from './store';
import RegistrySettingsApp from './components/registry_settings_app.vue';
import { apolloProvider } from './graphql/index';
@@ -13,11 +12,9 @@ export default () => {
if (!el) {
return null;
}
- store.dispatch('setInitialState', el.dataset);
const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
return new Vue({
el,
- store,
apolloProvider,
components: {
RegistrySettingsApp,
diff --git a/app/assets/javascripts/registry/settings/store/actions.js b/app/assets/javascripts/registry/settings/store/actions.js
deleted file mode 100644
index 0530a870ecc..00000000000
--- a/app/assets/javascripts/registry/settings/store/actions.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Api from '~/api';
-import * as types from './mutation_types';
-
-export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data);
-export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
-export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
-export const receiveSettingsSuccess = ({ commit }, data) => {
- commit(types.SET_SETTINGS, data);
-};
-export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
-
-export const fetchSettings = ({ dispatch, state }) => {
- dispatch('toggleLoading');
- return Api.project(state.projectId)
- .then(({ data: { container_expiration_policy } }) =>
- dispatch('receiveSettingsSuccess', container_expiration_policy),
- )
- .finally(() => dispatch('toggleLoading'));
-};
-
-export const saveSettings = ({ dispatch, state }) => {
- dispatch('toggleLoading');
- return Api.updateProject(state.projectId, {
- container_expiration_policy_attributes: state.settings,
- })
- .then(({ data: { container_expiration_policy } }) =>
- dispatch('receiveSettingsSuccess', container_expiration_policy),
- )
- .finally(() => dispatch('toggleLoading'));
-};
diff --git a/app/assets/javascripts/registry/settings/store/getters.js b/app/assets/javascripts/registry/settings/store/getters.js
deleted file mode 100644
index ac1a931d8e0..00000000000
--- a/app/assets/javascripts/registry/settings/store/getters.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { isEqual } from 'lodash';
-import { findDefaultOption } from '../../shared/utils';
-
-export const getCadence = state =>
- state.settings.cadence || findDefaultOption(state.formOptions.cadence);
-
-export const getKeepN = state =>
- state.settings.keep_n || findDefaultOption(state.formOptions.keepN);
-
-export const getOlderThan = state =>
- state.settings.older_than || findDefaultOption(state.formOptions.olderThan);
-
-export const getSettings = (state, getters) => ({
- enabled: state.settings.enabled,
- cadence: getters.getCadence,
- older_than: getters.getOlderThan,
- keep_n: getters.getKeepN,
- name_regex: state.settings.name_regex,
- name_regex_keep: state.settings.name_regex_keep,
-});
-
-export const getIsEdited = state => !isEqual(state.original, state.settings);
-
-export const getIsDisabled = state => {
- return !(state.original || state.enableHistoricEntries);
-};
diff --git a/app/assets/javascripts/registry/settings/store/index.js b/app/assets/javascripts/registry/settings/store/index.js
deleted file mode 100644
index c2500454d8e..00000000000
--- a/app/assets/javascripts/registry/settings/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import * as actions from './actions';
-import mutations from './mutations';
-import * as getters from './getters';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const createStore = () =>
- new Vuex.Store({
- state,
- actions,
- mutations,
- getters,
- });
-
-export default createStore();
diff --git a/app/assets/javascripts/registry/settings/store/mutation_types.js b/app/assets/javascripts/registry/settings/store/mutation_types.js
deleted file mode 100644
index db499ffa761..00000000000
--- a/app/assets/javascripts/registry/settings/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
-export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
-export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_SETTINGS = 'SET_SETTINGS';
-export const RESET_SETTINGS = 'RESET_SETTINGS';
diff --git a/app/assets/javascripts/registry/settings/store/mutations.js b/app/assets/javascripts/registry/settings/store/mutations.js
deleted file mode 100644
index 3ba13419b98..00000000000
--- a/app/assets/javascripts/registry/settings/store/mutations.js
+++ /dev/null
@@ -1,29 +0,0 @@
-import { parseBoolean } from '~/lib/utils/common_utils';
-import * as types from './mutation_types';
-
-export default {
- [types.SET_INITIAL_STATE](state, initialState) {
- state.projectId = initialState.projectId;
- state.formOptions = {
- cadence: JSON.parse(initialState.cadenceOptions),
- keepN: JSON.parse(initialState.keepNOptions),
- olderThan: JSON.parse(initialState.olderThanOptions),
- };
- state.enableHistoricEntries = parseBoolean(initialState.enableHistoricEntries);
- state.isAdmin = parseBoolean(initialState.isAdmin);
- state.adminSettingsPath = initialState.adminSettingsPath;
- },
- [types.UPDATE_SETTINGS](state, data) {
- state.settings = { ...state.settings, ...data.settings };
- },
- [types.SET_SETTINGS](state, settings) {
- state.settings = settings ?? state.settings;
- state.original = Object.freeze(settings);
- },
- [types.RESET_SETTINGS](state) {
- state.settings = { ...state.original };
- },
- [types.TOGGLE_LOADING](state) {
- state.isLoading = !state.isLoading;
- },
-};
diff --git a/app/assets/javascripts/registry/settings/store/state.js b/app/assets/javascripts/registry/settings/store/state.js
deleted file mode 100644
index fccc0991c1c..00000000000
--- a/app/assets/javascripts/registry/settings/store/state.js
+++ /dev/null
@@ -1,42 +0,0 @@
-export default () => ({
- /*
- * Project Id used to build the API call
- */
- projectId: '',
- /*
- * Boolean to determine if the UI is loading data from the API
- */
- isLoading: false,
- /*
- * Boolean to determine if the user is an admin
- */
- isAdmin: false,
- /*
- * String containing the full path to the admin config page for CI/CD
- */
- adminSettingsPath: '',
- /*
- * Boolean to determine if project created before 12.8 can use this feature
- */
- enableHistoricEntries: false,
- /*
- * This contains the data shown and manipulated in the UI
- * Has the following structure:
- * {
- * enabled: Boolean
- * cadence: String,
- * older_than: String,
- * keep_n: String,
- * name_regex: String
- * }
- */
- settings: {},
- /*
- * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel', initialized to null
- */
- original: null,
- /*
- * Contains the options used to populate the form selects
- */
- formOptions: {},
-});
diff --git a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue b/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
index 3d61cdff747..4cd74305450 100644
--- a/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/members/avatars/user_avatar.vue
@@ -1,5 +1,11 @@
<script>
-import { GlAvatarLink, GlAvatarLabeled, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import {
+ GlAvatarLink,
+ GlAvatarLabeled,
+ GlBadge,
+ GlSafeHtmlDirective as SafeHtml,
+} from '@gitlab/ui';
+import { generateBadges } from 'ee_else_ce/vue_shared/components/members/utils';
import { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants';
@@ -7,7 +13,11 @@ export default {
name: 'UserAvatar',
avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'),
- components: { GlAvatarLink, GlAvatarLabeled },
+ components: {
+ GlAvatarLink,
+ GlAvatarLabeled,
+ GlBadge,
+ },
directives: {
SafeHtml,
},
@@ -16,11 +26,18 @@ export default {
type: Object,
required: true,
},
+ isCurrentUser: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
user() {
return this.member.user;
},
+ badges() {
+ return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
+ },
},
};
</script>
@@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize"
:entity-name="user.name"
:entity-id="user.id"
- />
+ >
+ <template #meta>
+ <div v-for="badge in badges" :key="badge.text" class="gl-p-1">
+ <gl-badge size="sm" :variant="badge.variant">
+ {{ badge.text }}
+ </gl-badge>
+ </div>
+ </template>
+ </gl-avatar-labeled>
</gl-avatar-link>
<gl-avatar-labeled
diff --git a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue b/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
index 3abd86fa37f..a1f98d4008a 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/members/table/member_avatar.vue
@@ -12,6 +12,10 @@ export default {
type: String,
required: true,
},
+ isCurrentUser: {
+ type: Boolean,
+ required: true,
+ },
member: {
type: Object,
required: true,
@@ -27,5 +31,5 @@ export default {
</script>
<template>
- <component :is="avatarComponent" :member="member" />
+ <component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template>
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
index 4401250a665..b72633f0cee 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
+++ b/app/assets/javascripts/vue_shared/components/members/table/members_table.vue
@@ -44,8 +44,12 @@ export default {
show-empty
>
<template #cell(account)="{ item: member }">
- <members-table-cell #default="{ memberType }" :member="member">
- <member-avatar :member-type="memberType" :member="member" />
+ <members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
+ <member-avatar
+ :member-type="memberType"
+ :is-current-user="isCurrentUser"
+ :member="member"
+ />
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
index c859217c96f..0688c5d3c9d 100644
--- a/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
+++ b/app/assets/javascripts/vue_shared/components/members/table/members_table_cell.vue
@@ -11,7 +11,7 @@ export default {
},
},
computed: {
- ...mapState(['sourceId']),
+ ...mapState(['sourceId', 'currentUserId']),
isGroup() {
return Boolean(this.member.sharedWithGroup);
},
@@ -35,11 +35,15 @@ export default {
isDirectMember() {
return this.member.source?.id === this.sourceId;
},
+ isCurrentUser() {
+ return this.member.user?.id === this.currentUserId;
+ },
},
render() {
return this.$scopedSlots.default({
memberType: this.memberType,
isDirectMember: this.isDirectMember,
+ isCurrentUser: this.isCurrentUser,
});
},
};
diff --git a/app/assets/javascripts/vue_shared/components/members/utils.js b/app/assets/javascripts/vue_shared/components/members/utils.js
new file mode 100644
index 00000000000..782a0b7f96b
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/members/utils.js
@@ -0,0 +1,19 @@
+import { __ } from '~/locale';
+
+export const generateBadges = (member, isCurrentUser) => [
+ {
+ show: isCurrentUser,
+ text: __("It's you"),
+ variant: 'success',
+ },
+ {
+ show: member.user?.blocked,
+ text: __('Blocked'),
+ variant: 'danger',
+ },
+ {
+ show: member.user?.twoFactorEnabled,
+ text: __('2FA'),
+ variant: 'info',
+ },
+];
diff --git a/app/finders/packages/generic/package_finder.rb b/app/finders/packages/generic/package_finder.rb
new file mode 100644
index 00000000000..3a260e11fa3
--- /dev/null
+++ b/app/finders/packages/generic/package_finder.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module Packages
+ module Generic
+ class PackageFinder
+ def initialize(project)
+ @project = project
+ end
+
+ def execute!(package_name, package_version)
+ project
+ .packages
+ .generic
+ .by_name_and_version!(package_name, package_version)
+ end
+
+ private
+
+ attr_reader :project
+ end
+ end
+end
diff --git a/app/models/application_setting/term.rb b/app/models/application_setting/term.rb
index 723540c9b91..bab036f5697 100644
--- a/app/models/application_setting/term.rb
+++ b/app/models/application_setting/term.rb
@@ -14,6 +14,8 @@ class ApplicationSetting
end
def accepted_by_user?(user)
+ return true if user.project_bot?
+
user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists?
end
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 61167546b25..caf2522e3dd 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -26,7 +26,7 @@ class Packages::Package < ApplicationRecord
validates :project, presence: true
validates :name, presence: true
- validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: :conan?
+ validates :name, format: { with: Gitlab::Regex.package_name_regex }, unless: -> { conan? || generic? }
validates :name,
uniqueness: { scope: %i[project_id version package_type] }, unless: :conan?
@@ -35,8 +35,9 @@ class Packages::Package < ApplicationRecord
validate :valid_npm_package_name, if: :npm?
validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm?
- validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
validates :name, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
+ validates :name, format: { with: Gitlab::Regex.generic_package_name_regex }, if: :generic?
+ validates :version, format: { with: Gitlab::Regex.semver_regex }, if: -> { npm? || nuget? }
validates :version, format: { with: Gitlab::Regex.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi?
@@ -120,6 +121,10 @@ class Packages::Package < ApplicationRecord
.where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
end
+ def self.by_name_and_version!(name, version)
+ find_by!(name: name, version: version)
+ end
+
def self.pluck_names
pluck(:name)
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 9305e2518c1..e229c270e83 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1674,6 +1674,8 @@ class User < ApplicationRecord
end
def terms_accepted?
+ return true if project_bot?
+
accepted_term_id.present?
end
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index 8f4c89a9e77..6d2c5870e43 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,6 +1,6 @@
- page_title @group.name
-- page_description @group.description unless page_description
-- header_title group_title(@group) unless header_title
+- page_description @group.description_html unless page_description
+- header_title group_title(@group) unless header_title
- nav "group"
- display_subscription_banner!
- display_namespace_storage_limit_alert!
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 222ca02b1df..a0c82380023 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,6 +1,6 @@
- page_title @project.full_name
-- page_description @project.description unless page_description
-- header_title project_title(@project) unless header_title
+- page_description @project.description_html unless page_description
+- header_title project_title(@project) unless header_title
- nav "project"
- display_subscription_banner!
- display_namespace_storage_limit_alert!
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index c16ebd95429..7ee6c2b137a 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -2,7 +2,7 @@
- add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
-- page_description @issue.description
+- page_description @issue.description_html
- page_card_attributes @issue.card_attributes
- if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index fcb79782acf..84b108d69ad 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -3,7 +3,7 @@
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests")
-- page_description @merge_request.description
+- page_description @merge_request.description_html
- page_card_attributes @merge_request.card_attributes
- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
- number_of_pipelines = @pipelines.size
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index e7cc75e871a..2514d2cce32 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,7 +1,7 @@
- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
- breadcrumb_title @milestone.title
- page_title @milestone.title, _('Milestones')
-- page_description @milestone.description
+- page_description @milestone.description_html
- add_page_specific_style 'page_bundles/milestone'
= render 'shared/milestones/header', milestone: @milestone
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index fbda9b79e82..f1733ce2b51 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -2,7 +2,7 @@
- @hide_breadcrumbs = true
- @no_container = true
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name
-- page_description @user.bio
+- page_description @user.bio_html
- header_title @user.name, user_path(@user)
- link_classes = "flex-grow-1 mx-1 "
diff --git a/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml b/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml
new file mode 100644
index 00000000000..75d53bf26ed
--- /dev/null
+++ b/changelogs/unreleased/nfriend-strip-markdown-from-og-description.yml
@@ -0,0 +1,5 @@
+---
+title: Strip markdown from og:description meta tags
+merge_request: 42918
+author:
+type: added
diff --git a/changelogs/unreleased/okr-ingress-modsecurity-settings.yml b/changelogs/unreleased/okr-ingress-modsecurity-settings.yml
new file mode 100644
index 00000000000..b5b22d25a0a
--- /dev/null
+++ b/changelogs/unreleased/okr-ingress-modsecurity-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate deprecated button to GlButton in ingress_modsecurity_settings.vue
+merge_request: 43717
+author:
+type: other
diff --git a/changelogs/unreleased/pat-bot-terms.yml b/changelogs/unreleased/pat-bot-terms.yml
new file mode 100644
index 00000000000..dceb285e5e0
--- /dev/null
+++ b/changelogs/unreleased/pat-bot-terms.yml
@@ -0,0 +1,5 @@
+---
+title: Auto-accept TOS if project bot
+merge_request: 43067
+author:
+type: fixed
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 08eb9a5d37a..917b94a4f62 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -11641,7 +11641,7 @@ type Mutation {
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload
- revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload
+ revertVulnerabilityToDetected(input: RevertVulnerabilityToDetectedInput!): RevertVulnerabilityToDetectedPayload @deprecated(reason: "Use vulnerabilityRevertToDetected. Deprecated in 13.5")
runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4")
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload
@@ -11673,6 +11673,7 @@ type Mutation {
vulnerabilityConfirm(input: VulnerabilityConfirmInput!): VulnerabilityConfirmPayload
vulnerabilityDismiss(input: VulnerabilityDismissInput!): VulnerabilityDismissPayload
vulnerabilityResolve(input: VulnerabilityResolveInput!): VulnerabilityResolvePayload
+ vulnerabilityRevertToDetected(input: VulnerabilityRevertToDetectedInput!): VulnerabilityRevertToDetectedPayload
}
"""
@@ -20471,6 +20472,41 @@ type VulnerabilityResolvePayload {
}
"""
+Autogenerated input type of VulnerabilityRevertToDetected
+"""
+input VulnerabilityRevertToDetectedInput {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ ID of the vulnerability to be reverted
+ """
+ id: VulnerabilityID!
+}
+
+"""
+Autogenerated return type of VulnerabilityRevertToDetected
+"""
+type VulnerabilityRevertToDetectedPayload {
+ """
+ A unique identifier for the client performing the mutation.
+ """
+ clientMutationId: String
+
+ """
+ Errors encountered during execution of the mutation.
+ """
+ errors: [String!]!
+
+ """
+ The vulnerability after revert
+ """
+ vulnerability: Vulnerability
+}
+
+"""
Represents a vulnerability scanner
"""
type VulnerabilityScanner {
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 54330f60941..0a8fabb1e5b 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -33961,8 +33961,8 @@
"name": "RevertVulnerabilityToDetectedPayload",
"ofType": null
},
- "isDeprecated": false,
- "deprecationReason": null
+ "isDeprecated": true,
+ "deprecationReason": "Use vulnerabilityRevertToDetected. Deprecated in 13.5"
},
{
"name": "runDastScan",
@@ -34503,6 +34503,33 @@
},
"isDeprecated": false,
"deprecationReason": null
+ },
+ {
+ "name": "vulnerabilityRevertToDetected",
+ "description": null,
+ "args": [
+ {
+ "name": "input",
+ "description": null,
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "INPUT_OBJECT",
+ "name": "VulnerabilityRevertToDetectedInput",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ }
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "VulnerabilityRevertToDetectedPayload",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
}
],
"inputFields": null,
@@ -59576,6 +59603,108 @@
"possibleTypes": null
},
{
+ "kind": "INPUT_OBJECT",
+ "name": "VulnerabilityRevertToDetectedInput",
+ "description": "Autogenerated input type of VulnerabilityRevertToDetected",
+ "fields": null,
+ "inputFields": [
+ {
+ "name": "id",
+ "description": "ID of the vulnerability to be reverted",
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "VulnerabilityID",
+ "ofType": null
+ }
+ },
+ "defaultValue": null
+ },
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "defaultValue": null
+ }
+ ],
+ "interfaces": null,
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
+ "kind": "OBJECT",
+ "name": "VulnerabilityRevertToDetectedPayload",
+ "description": "Autogenerated return type of VulnerabilityRevertToDetected",
+ "fields": [
+ {
+ "name": "clientMutationId",
+ "description": "A unique identifier for the client performing the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "errors",
+ "description": "Errors encountered during execution of the mutation.",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "LIST",
+ "name": null,
+ "ofType": {
+ "kind": "NON_NULL",
+ "name": null,
+ "ofType": {
+ "kind": "SCALAR",
+ "name": "String",
+ "ofType": null
+ }
+ }
+ }
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "vulnerability",
+ "description": "The vulnerability after revert",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "Vulnerability",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "OBJECT",
"name": "VulnerabilityScanner",
"description": "Represents a vulnerability scanner",
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 041ab8587b2..7496c3272d7 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -2947,6 +2947,16 @@ Autogenerated return type of VulnerabilityResolve.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `vulnerability` | Vulnerability | The vulnerability after state change |
+### VulnerabilityRevertToDetectedPayload
+
+Autogenerated return type of VulnerabilityRevertToDetected.
+
+| Field | Type | Description |
+| ----- | ---- | ----------- |
+| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
+| `errors` | String! => Array | Errors encountered during execution of the mutation. |
+| `vulnerability` | Vulnerability | The vulnerability after revert |
+
### VulnerabilityScanner
Represents a vulnerability scanner.
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index db2749233e8..12df0751c47 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -154,7 +154,7 @@ commits to a feature branch in a remote repository in GitLab,
the CI/CD pipeline set for your project is triggered. By doing
so, GitLab CI/CD:
-- Runs automated scripts (sequential or parallel) to:
+- Runs automated scripts (sequentially or in parallel) to:
- Build and test your app.
- Preview the changes per merge request with Review Apps, as you
would see in your `localhost`.
diff --git a/doc/ci/migration/jenkins.md b/doc/ci/migration/jenkins.md
index a7ec085a6b2..1130c11f472 100644
--- a/doc/ci/migration/jenkins.md
+++ b/doc/ci/migration/jenkins.md
@@ -83,7 +83,7 @@ There are some high level differences between the products worth mentioning:
- You can control which jobs run in which cases, depending on how they are triggered,
with the [`rules` syntax](../yaml/README.md#rules).
-- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different than with Jenkins.
+- GitLab [pipeline scheduling concepts](../pipelines/schedules.md) are also different from Jenkins.
- You can reuse pipeline configurations using the [`include` keyword](../yaml/README.md#include)
and [templates](#templates). Your templates can be kept in a central repository (with different
permissions), and then any project can use them. This central project could also
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 9ad21eceb05..ccae41017c4 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -33,11 +33,8 @@ We have complete examples of configuring pipelines:
> - <i class="fa fa-youtube-play youtube" aria-hidden="true"></i>&nbsp;Learn how [Verizon reduced rebuilds](https://about.gitlab.com/blog/2019/02/14/verizon-customer-story/)
> from 30 days to under 8 hours with GitLab.
-NOTE: **Note:**
If you have a [mirrored repository that GitLab pulls from](../../user/project/repository/repository_mirroring.md#pulling-from-a-remote-repository),
-you may need to enable pipeline triggering. Go to your project's
-
-**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
+you may need to enable pipeline triggering. Go to your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
## Introduction
@@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab:
- Merges the `rspec` contents into `.tests` recursively.
- Doesn't merge the values of the keys.
-The result is this `rspec` job:
+The result is this `rspec` job, where `script: rake test` is overwritten by `script: rake rspec`:
```yaml
rspec:
@@ -974,9 +971,6 @@ rspec:
- $RSPEC
```
-NOTE: **Note:**
-Note that `script: rake test` has been overwritten by `script: rake rspec`.
-
If you do want to include the `rake test`, see [`before_script` and `after_script`](#before_script-and-after_script).
`.tests` in this example is a [hidden job](#hide-jobs), but it's
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index a39f93a26e1..cc6777a1ce7 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -83,9 +83,9 @@ project in a simple and automatic way:
1. [Auto Build](stages.md#auto-build)
1. [Auto Test](stages.md#auto-test)
-1. [Auto Code Quality](stages.md#auto-code-quality) **(STARTER)**
-1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast) **(ULTIMATE)**
-1. [Auto Secret Detection](stages.md#auto-secret-detection) **(ULTIMATE)**
+1. [Auto Code Quality](stages.md#auto-code-quality)
+1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
+1. [Auto Secret Detection](stages.md#auto-secret-detection)
1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)**
1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)**
1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)**
diff --git a/doc/topics/autodevops/stages.md b/doc/topics/autodevops/stages.md
index 0e9f0812a9a..44eebb748a6 100644
--- a/doc/topics/autodevops/stages.md
+++ b/doc/topics/autodevops/stages.md
@@ -124,7 +124,10 @@ The supported buildpacks are:
If your application needs a buildpack that is not in the above list, you
might want to use a [custom buildpack](customize.md#custom-buildpacks).
-## Auto Code Quality **(STARTER)**
+## Auto Code Quality
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
+> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in GitLab 13.2.
Auto Code Quality uses the
[Code Quality image](https://gitlab.com/gitlab-org/ci-cd/codequality) to run
@@ -133,9 +136,10 @@ report, it's uploaded as an artifact which you can later download and check
out. The merge request widget also displays any
[differences between the source and target branches](../../user/project/merge_requests/code_quality.md).
-## Auto SAST **(ULTIMATE)**
+## Auto SAST
-> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3.
+> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.3.
+> - Select functionality made available in all tiers beginning in 13.1
Static Application Security Testing (SAST) uses the
[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static
@@ -151,9 +155,10 @@ warnings.
To learn more about [how SAST works](../../user/application_security/sast/index.md),
see the documentation.
-## Auto Secret Detection **(ULTIMATE)**
+## Auto Secret Detection
-> Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1.
+> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 13.1.
+> - [Select functionality made available in all tiers](../../user/application_security/secret_detection/#making-secret-detection-available-to-all-gitlab-tiers) in 13.3
Secret Detection uses the
[Secret Detection Docker image](https://gitlab.com/gitlab-org/security-products/analyzers/secrets) to run Secret Detection on the current code, and checks for leaked secrets. The
diff --git a/lib/api/generic_packages.rb b/lib/api/generic_packages.rb
index a24580b358a..a0c29ada950 100644
--- a/lib/api/generic_packages.rb
+++ b/lib/api/generic_packages.rb
@@ -30,7 +30,7 @@ module API
route_setting :authentication, job_token_allowed: true
params do
- requires :package_name, type: String, desc: 'Package name'
+ requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
end
@@ -44,7 +44,7 @@ module API
end
params do
- requires :package_name, type: String, desc: 'Package name'
+ requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
requires :file, type: ::API::Validations::Types::WorkhorseFile, desc: 'The package file to be published (generated by Multipart middleware)'
@@ -69,6 +69,29 @@ module API
forbidden!
end
+
+ desc 'Download package file' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ params do
+ requires :package_name, type: String, desc: 'Package name', regexp: Gitlab::Regex.generic_package_name_regex, file_path: true
+ requires :package_version, type: String, desc: 'Package version', regexp: Gitlab::Regex.generic_package_version_regex
+ requires :file_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
+ end
+
+ route_setting :authentication, job_token_allowed: true
+
+ get do
+ authorize_read_package!(project)
+
+ package = ::Packages::Generic::PackageFinder.new(project).execute!(params[:package_name], params[:package_version])
+ package_file = ::Packages::PackageFileFinder.new(package, params[:file_name]).execute!
+
+ track_event('pull_package')
+
+ present_carrierwave_file!(package_file.file)
+ end
end
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 245621d20a8..6511b84e947 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -142,9 +142,13 @@ module Gitlab
/\A\d+\.\d+\.\d+\z/
end
- def generic_package_file_name_regex
+ def generic_package_name_regex
maven_file_name_regex
end
+
+ def generic_package_file_name_regex
+ generic_package_name_regex
+ end
end
extend self
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index ee042a2a960..da36d4e9f96 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -22175,6 +22175,9 @@ msgstr ""
msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr ""
+msgid "SAML"
+msgstr ""
+
msgid "SAML SSO"
msgstr ""
diff --git a/spec/factories/packages/package_file.rb b/spec/factories/packages/package_file.rb
index da35f246ea0..bcca48fb086 100644
--- a/spec/factories/packages/package_file.rb
+++ b/spec/factories/packages/package_file.rb
@@ -140,6 +140,14 @@ FactoryBot.define do
size { 1149.bytes }
end
+ trait(:generic) do
+ package
+ file_fixture { 'spec/fixtures/packages/generic/myfile.tar.gz' }
+ file_name { "#{package.name}.tar.gz" }
+ file_sha256 { '440e5e148a25331bbd7991575f7d54933c0ebf6cc735a18ee5066ac1381bb590' }
+ size { 1149.bytes }
+ end
+
trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE }
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index ec30f34199d..304573ecd6e 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -184,4 +184,17 @@ RSpec.describe 'Group show page' do
expect(page).to have_selector('.notifications-btn.disabled', visible: true)
end
end
+
+ context 'page og:description' do
+ let(:group) { create(:group, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
+ let(:maintainer) { create(:user) }
+
+ before do
+ group.add_maintainer(maintainer)
+ sign_in(maintainer)
+ visit path
+ end
+
+ it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
+ end
end
diff --git a/spec/features/issues/user_views_issue_spec.rb b/spec/features/issues/user_views_issue_spec.rb
index 3f18764aa58..9b1c8be1513 100644
--- a/spec/features/issues/user_views_issue_spec.rb
+++ b/spec/features/issues/user_views_issue_spec.rb
@@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe "User views issue" do
let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) }
- let_it_be(:issue) { create(:issue, project: project, description: "# Description header", author: user) }
+ let_it_be(:issue) { create(:issue, project: project, description: "# Description header\n\n**Lorem** _ipsum_ dolor sit [amet](https://example.com)", author: user) }
let_it_be(:note) { create(:note, noteable: issue, project: project, author: user) }
before_all do
@@ -20,6 +20,8 @@ RSpec.describe "User views issue" do
it { expect(page).to have_header_with_correct_id_and_link(1, "Description header", "description-header") }
+ it_behaves_like 'page meta description', ' Description header Lorem ipsum dolor sit amet'
+
it 'shows the merge request and issue actions', :aggregate_failures do
expect(page).to have_link('New issue')
expect(page).to have_button('Create merge request')
diff --git a/spec/features/merge_request/user_sees_page_metadata_spec.rb b/spec/features/merge_request/user_sees_page_metadata_spec.rb
new file mode 100644
index 00000000000..7b3e07152a0
--- /dev/null
+++ b/spec/features/merge_request/user_sees_page_metadata_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Merge request > User sees page metadata' do
+ let(:merge_request) { create(:merge_request, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
+ let(:project) { merge_request.target_project }
+ let(:user) { project.creator }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
+end
diff --git a/spec/features/milestones/user_views_milestone_spec.rb b/spec/features/milestones/user_views_milestone_spec.rb
index 11c6fa521d5..9c19f842427 100644
--- a/spec/features/milestones/user_views_milestone_spec.rb
+++ b/spec/features/milestones/user_views_milestone_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe "User views milestone" do
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: group) }
- let_it_be(:milestone) { create(:milestone, project: project) }
+ let_it_be(:milestone) { create(:milestone, project: project, description: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
let_it_be(:labels) { create_list(:label, 2, project: project) }
before_all do
@@ -17,6 +17,14 @@ RSpec.describe "User views milestone" do
sign_in(user)
end
+ context 'page description' do
+ before do
+ visit(project_milestone_path(project, milestone))
+ end
+
+ it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
+ end
+
it "avoids N+1 database queries" do
issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 970500985ae..6c1e1eab968 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -99,6 +99,15 @@ RSpec.describe 'Project' do
expect(page).to have_css('.home-panel-description .is-expanded')
end
end
+
+ context 'page description' do
+ before do
+ project.update_attribute(:description, '**Lorem** _ipsum_ dolor sit [amet](https://example.com)')
+ visit path
+ end
+
+ it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
+ end
end
describe 'project topics' do
diff --git a/spec/features/users/show_spec.rb b/spec/features/users/show_spec.rb
index dd5c2442d00..b3c8cf8d326 100644
--- a/spec/features/users/show_spec.rb
+++ b/spec/features/users/show_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User page' do
include ExternalAuthorizationServiceHelpers
- let(:user) { create(:user) }
+ let(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
context 'with public profile' do
it 'shows all the tabs' do
@@ -174,4 +174,12 @@ RSpec.describe 'User page' do
end
end
end
+
+ context 'page description' do
+ before do
+ visit(user_path(user))
+ end
+
+ it_behaves_like 'page meta description', 'Lorem ipsum dolor sit amet'
+ end
end
diff --git a/spec/features/users/terms_spec.rb b/spec/features/users/terms_spec.rb
index 5275845fe5b..7500f2fe59a 100644
--- a/spec/features/users/terms_spec.rb
+++ b/spec/features/users/terms_spec.rb
@@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do
expect(page).not_to have_content('Continue')
end
+ context 'when user is a project bot' do
+ let(:project_bot) { create(:user, :project_bot) }
+
+ before do
+ enforce_terms
+ end
+
+ it 'auto accepts the terms' do
+ visit terms_path
+
+ expect(page).not_to have_content('Accept terms')
+ expect(project_bot.terms_accepted?).to be(true)
+ end
+ end
+
context 'when signed in' do
let(:user) { create(:user) }
diff --git a/spec/finders/packages/generic/package_finder_spec.rb b/spec/finders/packages/generic/package_finder_spec.rb
new file mode 100644
index 00000000000..ed34268e7a9
--- /dev/null
+++ b/spec/finders/packages/generic/package_finder_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe ::Packages::Generic::PackageFinder do
+ let_it_be(:project) { create(:project) }
+ let_it_be(:package) { create(:generic_package, project: project) }
+
+ describe '#execute!' do
+ subject(:finder) { described_class.new(project) }
+
+ it 'finds package by name and version' do
+ found_package = finder.execute!(package.name, package.version)
+
+ expect(found_package).to eq(package)
+ end
+
+ it 'ignores packages with same name but different version' do
+ create(:generic_package, project: project, name: package.name, version: '3.1.4')
+
+ found_package = finder.execute!(package.name, package.version)
+
+ expect(found_package).to eq(package)
+ end
+
+ it 'raises ActiveRecord::RecordNotFound if package is not found' do
+ expect { finder.execute!(package.name, '3.1.4') }
+ .to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+end
diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
index 3a9a608b2e2..0e46693a4bf 100644
--- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
+++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
@@ -28,8 +28,10 @@ describe('IngressModsecuritySettings', () => {
});
};
- const findSaveButton = () => wrapper.find('.btn-success');
- const findCancelButton = () => wrapper.find('[variant="secondary"]');
+ const findSaveButton = () =>
+ wrapper.find('[data-qa-selector="save_ingress_modsecurity_settings"]');
+ const findCancelButton = () =>
+ wrapper.find('[data-qa-selector="cancel_ingress_modsecurity_settings"]');
const findModSecurityToggle = () => wrapper.find(GlToggle);
const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown);
diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js
deleted file mode 100644
index 51b89f96ef2..00000000000
--- a/spec/frontend/registry/settings/store/actions_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import testAction from 'helpers/vuex_action_helper';
-import Api from '~/api';
-import * as actions from '~/registry/settings/store/actions';
-import * as types from '~/registry/settings/store/mutation_types';
-
-describe('Actions Registry Store', () => {
- describe.each`
- actionName | mutationName | payload
- ${'setInitialState'} | ${types.SET_INITIAL_STATE} | ${'foo'}
- ${'updateSettings'} | ${types.UPDATE_SETTINGS} | ${'foo'}
- ${'toggleLoading'} | ${types.TOGGLE_LOADING} | ${undefined}
- ${'resetSettings'} | ${types.RESET_SETTINGS} | ${undefined}
- `(
- '$actionName invokes $mutationName with payload $payload',
- ({ actionName, mutationName, payload }) => {
- it('should set state', done => {
- testAction(actions[actionName], payload, {}, [{ type: mutationName, payload }], [], done);
- });
- },
- );
-
- describe('receiveSettingsSuccess', () => {
- it('calls SET_SETTINGS', () => {
- testAction(
- actions.receiveSettingsSuccess,
- 'foo',
- {},
- [{ type: types.SET_SETTINGS, payload: 'foo' }],
- [],
- );
- });
- });
-
- describe('fetchSettings', () => {
- const state = {
- projectId: 'bar',
- };
-
- const payload = {
- data: {
- container_expiration_policy: 'foo',
- },
- };
-
- it('should fetch the data from the API', done => {
- Api.project = jest.fn().mockResolvedValue(payload);
- testAction(
- actions.fetchSettings,
- null,
- state,
- [],
- [
- { type: 'toggleLoading' },
- { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
- { type: 'toggleLoading' },
- ],
- done,
- );
- });
- });
-
- describe('saveSettings', () => {
- const state = {
- projectId: 'bar',
- settings: 'baz',
- };
-
- const payload = {
- data: {
- tag_expiration_policies: 'foo',
- },
- };
-
- it('should fetch the data from the API', done => {
- Api.updateProject = jest.fn().mockResolvedValue(payload);
- testAction(
- actions.saveSettings,
- null,
- state,
- [],
- [
- { type: 'toggleLoading' },
- { type: 'receiveSettingsSuccess', payload: payload.data.container_expiration_policy },
- { type: 'toggleLoading' },
- ],
- done,
- );
- });
- });
-});
diff --git a/spec/frontend/registry/settings/store/getters_spec.js b/spec/frontend/registry/settings/store/getters_spec.js
deleted file mode 100644
index b781d09466c..00000000000
--- a/spec/frontend/registry/settings/store/getters_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import * as getters from '~/registry/settings/store/getters';
-import * as utils from '~/registry/shared/utils';
-import { formOptions } from '../../shared/mock_data';
-
-describe('Getters registry settings store', () => {
- const settings = {
- enabled: true,
- cadence: 'foo',
- keep_n: 'bar',
- older_than: 'baz',
- name_regex: 'name-foo',
- name_regex_keep: 'name-keep-bar',
- };
-
- describe.each`
- getter | variable | formOption
- ${'getCadence'} | ${'cadence'} | ${'cadence'}
- ${'getKeepN'} | ${'keep_n'} | ${'keepN'}
- ${'getOlderThan'} | ${'older_than'} | ${'olderThan'}
- `('Options getter', ({ getter, variable, formOption }) => {
- beforeEach(() => {
- utils.findDefaultOption = jest.fn();
- });
-
- it(`${getter} returns ${variable} when ${variable} exists in settings`, () => {
- expect(getters[getter]({ settings })).toBe(settings[variable]);
- });
-
- it(`${getter} calls findDefaultOption when ${variable} does not exists in settings`, () => {
- getters[getter]({ settings: {}, formOptions });
- expect(utils.findDefaultOption).toHaveBeenCalledWith(formOptions[formOption]);
- });
- });
-
- describe('getSettings', () => {
- it('returns the content of settings', () => {
- const computedGetters = {
- getCadence: settings.cadence,
- getOlderThan: settings.older_than,
- getKeepN: settings.keep_n,
- };
- expect(getters.getSettings({ settings }, computedGetters)).toEqual(settings);
- });
- });
-
- describe('getIsEdited', () => {
- it('returns false when original is equal to settings', () => {
- const same = { foo: 'bar' };
- expect(getters.getIsEdited({ original: same, settings: same })).toBe(false);
- });
-
- it('returns true when original is different from settings', () => {
- expect(getters.getIsEdited({ original: { foo: 'bar' }, settings: { foo: 'baz' } })).toBe(
- true,
- );
- });
- });
-
- describe('getIsDisabled', () => {
- it.each`
- original | enableHistoricEntries | result
- ${undefined} | ${false} | ${true}
- ${{ foo: 'bar' }} | ${undefined} | ${false}
- ${{}} | ${false} | ${false}
- `(
- 'returns $result when original is $original and enableHistoricEntries is $enableHistoricEntries',
- ({ original, enableHistoricEntries, result }) => {
- expect(getters.getIsDisabled({ original, enableHistoricEntries })).toBe(result);
- },
- );
- });
-});
diff --git a/spec/frontend/registry/settings/store/mutations_spec.js b/spec/frontend/registry/settings/store/mutations_spec.js
deleted file mode 100644
index 1d85e38eb36..00000000000
--- a/spec/frontend/registry/settings/store/mutations_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import mutations from '~/registry/settings/store/mutations';
-import * as types from '~/registry/settings/store/mutation_types';
-import createState from '~/registry/settings/store/state';
-import { formOptions, stringifiedFormOptions } from '../../shared/mock_data';
-
-describe('Mutations Registry Store', () => {
- let mockState;
-
- beforeEach(() => {
- mockState = createState();
- });
-
- describe('SET_INITIAL_STATE', () => {
- it('should set the initial state', () => {
- const payload = {
- projectId: 'foo',
- enableHistoricEntries: false,
- adminSettingsPath: 'foo',
- isAdmin: true,
- };
- const expectedState = { ...mockState, ...payload, formOptions };
- mutations[types.SET_INITIAL_STATE](mockState, {
- ...payload,
- ...stringifiedFormOptions,
- });
-
- expect(mockState).toEqual(expectedState);
- });
- });
-
- describe('UPDATE_SETTINGS', () => {
- it('should update the settings', () => {
- mockState.settings = { foo: 'bar' };
- const payload = { foo: 'baz' };
- const expectedState = { ...mockState, settings: payload };
- mutations[types.UPDATE_SETTINGS](mockState, { settings: payload });
- expect(mockState.settings).toEqual(expectedState.settings);
- });
- });
-
- describe('SET_SETTINGS', () => {
- it('should set the settings and original', () => {
- const payload = { foo: 'baz' };
- const expectedState = { ...mockState, settings: payload };
- mutations[types.SET_SETTINGS](mockState, payload);
- expect(mockState.settings).toEqual(expectedState.settings);
- expect(mockState.original).toEqual(expectedState.settings);
- });
-
- it('should keep the default state when settings is not present', () => {
- const originalSettings = { ...mockState.settings };
- mutations[types.SET_SETTINGS](mockState);
- expect(mockState.settings).toEqual(originalSettings);
- expect(mockState.original).toEqual(undefined);
- });
- });
-
- describe('RESET_SETTINGS', () => {
- it('should copy original over settings', () => {
- mockState.settings = { foo: 'bar' };
- mockState.original = { foo: 'baz' };
- mutations[types.RESET_SETTINGS](mockState);
- expect(mockState.settings).toEqual(mockState.original);
- });
-
- it('if original is undefined it should initialize to empty object', () => {
- mockState.settings = { foo: 'bar' };
- mockState.original = undefined;
- mutations[types.RESET_SETTINGS](mockState);
- expect(mockState.settings).toEqual({});
- });
- });
-
- describe('TOGGLE_LOADING', () => {
- it('should toggle the loading', () => {
- mutations[types.TOGGLE_LOADING](mockState);
- expect(mockState.isLoading).toEqual(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
index f688e5789cb..79d5129b5ef 100644
--- a/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/members/avatars/user_avatar_spec.js
@@ -1,25 +1,26 @@
import { mount, createWrapper } from '@vue/test-utils';
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { GlAvatarLink } from '@gitlab/ui';
-import { member, orphanedMember } from '../mock_data';
+import { within } from '@testing-library/dom';
+import { GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => {
let wrapper;
- const { user } = member;
+ const { user } = memberMock;
const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, {
propsData: {
- member,
+ member: memberMock,
+ isCurrentUser: false,
...propsData,
},
});
};
const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
+ createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => {
wrapper.destroy();
@@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true);
});
});
+
+ describe('badges', () => {
+ it.each`
+ member | badgeText
+ ${{ ...memberMock, usingLicense: true }} | ${'Is using seat'}
+ ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${'Blocked'}
+ ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${'2FA'}
+ ${{ ...memberMock, groupSso: true }} | ${'SAML'}
+ ${{ ...memberMock, groupManagedAccount: true }} | ${'Managed Account'}
+ `('renders the "$badgeText" badge', ({ member, badgeText }) => {
+ createComponent({ member });
+
+ expect(wrapper.find(GlBadge).text()).toBe(badgeText);
+ });
+
+ it('renders the "It\'s you" badge when member is current user', () => {
+ createComponent({ isCurrentUser: true });
+
+ expect(getByText("It's you").exists()).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
index cf3b28f7bf6..a171dd830c1 100644
--- a/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
+++ b/spec/frontend/vue_shared/components/members/table/member_avatar_spec.js
@@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, {
- propsData,
+ propsData: {
+ isCurrentUser: false,
+ ...propsData,
+ },
});
};
diff --git a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
index 372e07d5e27..960d9bc164c 100644
--- a/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
+++ b/spec/frontend/vue_shared/components/members/table/member_table_cell_spec.js
@@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean,
required: true,
},
+ isCurrentUser: {
+ type: Boolean,
+ required: true,
+ },
},
render(createElement) {
return createElement('div', this.memberType);
@@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({
state: {
sourceId: 1,
+ currentUserId: 1,
...state,
},
});
@@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData,
store: createStore(state),
scopedSlots: {
- default:
- '<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />',
+ default: `
+ <wrapped-component
+ :member-type="props.memberType"
+ :is-direct-member="props.isDirectMember"
+ :is-current-user="props.isCurrentUser"
+ />
+ `,
},
});
};
@@ -93,4 +103,28 @@ describe('MemberList', () => {
expect(findWrappedComponent().props('isDirectMember')).toBe(false);
});
});
+
+ describe('isCurrentUser', () => {
+ it('returns `true` when `member.user` has the same ID as `currentUserId`', () => {
+ createComponent({
+ member: {
+ ...memberMock,
+ user: {
+ ...memberMock.user,
+ id: 1,
+ },
+ },
+ });
+
+ expect(findWrappedComponent().props('isCurrentUser')).toBe(true);
+ });
+
+ it('returns `false` when `member.user` does not have the same ID as `currentUserId`', () => {
+ createComponent({
+ member: memberMock,
+ });
+
+ expect(findWrappedComponent().props('isCurrentUser')).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/members/utils_spec.js b/spec/frontend/vue_shared/components/members/utils_spec.js
new file mode 100644
index 00000000000..f183abc08d6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/members/utils_spec.js
@@ -0,0 +1,29 @@
+import { generateBadges } from '~/vue_shared/components/members/utils';
+import { member as memberMock } from './mock_data';
+
+describe('Members Utils', () => {
+ describe('generateBadges', () => {
+ it('has correct properties for each badge', () => {
+ const badges = generateBadges(memberMock, true);
+
+ badges.forEach(badge => {
+ expect(badge).toEqual(
+ expect.objectContaining({
+ show: expect.any(Boolean),
+ text: expect.any(String),
+ variant: expect.stringMatching(/muted|neutral|info|success|danger|warning/),
+ }),
+ );
+ });
+ });
+
+ it.each`
+ member | expected
+ ${memberMock} | ${{ show: true, text: "It's you", variant: 'success' }}
+ ${{ ...memberMock, user: { ...memberMock.user, blocked: true } }} | ${{ show: true, text: 'Blocked', variant: 'danger' }}
+ ${{ ...memberMock, user: { ...memberMock.user, twoFactorEnabled: true } }} | ${{ show: true, text: '2FA', variant: 'info' }}
+ `('returns expected output for "$expected.text" badge', ({ member, expected }) => {
+ expect(generateBadges(member, true)).toContainEqual(expect.objectContaining(expected));
+ });
+ });
+});
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index d1fde517488..704a4e7b224 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -599,6 +599,20 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('') }
end
+ describe '.generic_package_name_regex' do
+ subject { described_class.generic_package_name_regex }
+
+ it { is_expected.to match('123') }
+ it { is_expected.to match('foo') }
+ it { is_expected.to match('foo.bar.baz-2.0-20190901.47283-1') }
+ it { is_expected.not_to match('../../foo') }
+ it { is_expected.not_to match('..\..\foo') }
+ it { is_expected.not_to match('%2f%2e%2e%2f%2essh%2fauthorized_keys') }
+ it { is_expected.not_to match('$foo/bar') }
+ it { is_expected.not_to match('my file name') }
+ it { is_expected.not_to match('!!()()') }
+ end
+
describe '.generic_package_file_name_regex' do
subject { described_class.generic_package_file_name_regex }
diff --git a/spec/models/application_setting/term_spec.rb b/spec/models/application_setting/term_spec.rb
index 82347453437..51a6027698f 100644
--- a/spec/models/application_setting/term_spec.rb
+++ b/spec/models/application_setting/term_spec.rb
@@ -17,6 +17,7 @@ RSpec.describe ApplicationSetting::Term do
describe '#accepted_by_user?' do
let(:user) { create(:user) }
+ let(:project_bot) { create(:user, :project_bot) }
let(:term) { create(:term) }
it 'is true when the user accepted the terms' do
@@ -25,6 +26,10 @@ RSpec.describe ApplicationSetting::Term do
expect(term.accepted_by_user?(user)).to be(true)
end
+ it 'is true when user is a bot' do
+ expect(term.accepted_by_user?(project_bot)).to be(true)
+ end
+
it 'is false when the user declined the terms' do
decline_terms(term, user)
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index a66e96d8a19..26851c93ac3 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -44,7 +44,7 @@ RSpec.describe Ci::JobArtifact do
let!(:metrics_report) { create(:ci_job_artifact, :junit) }
let!(:codequality_report) { create(:ci_job_artifact, :codequality) }
- it { is_expected.to eq([metrics_report, codequality_report]) }
+ it { is_expected.to match_array([metrics_report, codequality_report]) }
end
end
diff --git a/spec/models/packages/package_spec.rb b/spec/models/packages/package_spec.rb
index ea1f75d04e7..6a3969802f3 100644
--- a/spec/models/packages/package_spec.rb
+++ b/spec/models/packages/package_spec.rb
@@ -108,6 +108,20 @@ RSpec.describe Packages::Package, type: :model do
it { is_expected.not_to allow_value('.foobar').for(:name) }
it { is_expected.not_to allow_value('%foo%bar').for(:name) }
end
+
+ context 'generic package' do
+ subject { build_stubbed(:generic_package) }
+
+ it { is_expected.to allow_value('123').for(:name) }
+ it { is_expected.to allow_value('foo').for(:name) }
+ it { is_expected.to allow_value('foo.bar.baz-2.0-20190901.47283-1').for(:name) }
+ it { is_expected.not_to allow_value('../../foo').for(:name) }
+ it { is_expected.not_to allow_value('..\..\foo').for(:name) }
+ it { is_expected.not_to allow_value('%2f%2e%2e%2f%2essh%2fauthorized_keys').for(:name) }
+ it { is_expected.not_to allow_value('$foo/bar').for(:name) }
+ it { is_expected.not_to allow_value('my file name').for(:name) }
+ it { is_expected.not_to allow_value('!!().for(:name)().for(:name)').for(:name) }
+ end
end
describe '#version' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 1841288cd4b..0f71c7790d4 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -4330,28 +4330,32 @@ RSpec.describe User do
describe '#required_terms_not_accepted?' do
let(:user) { build(:user) }
+ let(:project_bot) { create(:user, :project_bot) }
subject { user.required_terms_not_accepted? }
context "when terms are not enforced" do
- it { is_expected.to be_falsy }
+ it { is_expected.to be_falsey }
end
- context "when terms are enforced and accepted by the user" do
+ context "when terms are enforced" do
before do
enforce_terms
- accept_terms(user)
end
- it { is_expected.to be_falsy }
- end
+ it "is not accepted by the user" do
+ expect(subject).to be_truthy
+ end
- context "when terms are enforced but the user has not accepted" do
- before do
- enforce_terms
+ it "is accepted by the user" do
+ accept_terms(user)
+
+ expect(subject).to be_falsey
end
- it { is_expected.to be_truthy }
+ it "auto accepts the term for project bots" do
+ expect(project_bot.required_terms_not_accepted?).to be_falsey
+ end
end
end
diff --git a/spec/requests/api/generic_packages_spec.rb b/spec/requests/api/generic_packages_spec.rb
index bebeed9402e..2cb686167f1 100644
--- a/spec/requests/api/generic_packages_spec.rb
+++ b/spec/requests/api/generic_packages_spec.rb
@@ -33,7 +33,19 @@ RSpec.describe API::GenericPackages do
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token }
end
- describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize' do
+ shared_examples 'secure endpoint' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'rejects malicious request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name/authorize' do
context 'with valid project' do
using RSpec::Parameterized::TableSyntax
@@ -73,41 +85,49 @@ RSpec.describe API::GenericPackages do
end
it "responds with #{params[:expected_status]}" do
- headers = workhorse_header.merge(auth_header)
- url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize"
-
- put api(url), headers: headers
+ authorize_upload_file(workhorse_header.merge(auth_header))
expect(response).to have_gitlab_http_status(expected_status)
end
end
end
- it 'rejects a malicious request' do
- project.add_developer(user)
- headers = workhorse_header.merge(personal_access_token_header)
- url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/%2e%2e%2f.ssh%2fauthorized_keys/authorize"
+ context 'application security' do
+ using RSpec::Parameterized::TableSyntax
- put api(url), headers: headers
+ where(:param_name, :param_value) do
+ :package_name | 'my-package/../'
+ :package_name | 'my-package%2f%2e%2e%2f'
+ :file_name | '../.ssh%2fauthorized_keys'
+ :file_name | '%2e%2e%2f.ssh%2fauthorized_keys'
+ end
- expect(response).to have_gitlab_http_status(:bad_request)
+ with_them do
+ subject { authorize_upload_file(workhorse_header.merge(personal_access_token_header), param_name => param_value) }
+
+ it_behaves_like 'secure endpoint'
+ end
end
context 'generic_packages feature flag is disabled' do
it 'responds with 404 Not Found' do
stub_feature_flags(generic_packages: false)
project.add_developer(user)
- headers = workhorse_header.merge(personal_access_token_header)
- url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize"
- put api(url), headers: headers
+ authorize_upload_file(workhorse_header.merge(personal_access_token_header))
expect(response).to have_gitlab_http_status(:not_found)
end
end
+
+ def authorize_upload_file(request_headers, package_name: 'mypackage', file_name: 'myfile.tar.gz')
+ url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}/authorize"
+
+ put api(url), headers: request_headers
+ end
end
- describe 'PUT /api/v4/projects/:id/packages/generic/mypackage/0.0.1/myfile.tar.gz' do
+ describe 'PUT /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do
include WorkhorseHelpers
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') }
@@ -246,17 +266,27 @@ RSpec.describe API::GenericPackages do
expect(response).to have_gitlab_http_status(:forbidden)
end
+ end
- it 'rejects a malicious request' do
- headers = workhorse_header.merge(personal_access_token_header)
- upload_file(params, headers, file_name: '%2e%2e%2f.ssh%2fauthorized_keys')
+ context 'application security' do
+ using RSpec::Parameterized::TableSyntax
- expect(response).to have_gitlab_http_status(:bad_request)
+ where(:param_name, :param_value) do
+ :package_name | 'my-package/../'
+ :package_name | 'my-package%2f%2e%2e%2f'
+ :file_name | '../.ssh%2fauthorized_keys'
+ :file_name | '%2e%2e%2f.ssh%2fauthorized_keys'
+ end
+
+ with_them do
+ subject { upload_file(params, workhorse_header.merge(personal_access_token_header), param_name => param_value) }
+
+ it_behaves_like 'secure endpoint'
end
end
- def upload_file(params, request_headers, send_rewritten_field: true, file_name: 'myfile.tar.gz')
- url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/#{file_name}"
+ def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz')
+ url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}"
workhorse_finalize(
api(url),
@@ -268,4 +298,138 @@ RSpec.describe API::GenericPackages do
)
end
end
+
+ describe 'GET /api/v4/projects/:id/packages/generic/:package_name/:package_version/:file_name' do
+ using RSpec::Parameterized::TableSyntax
+
+ let_it_be(:package) { create(:generic_package, project: project) }
+ let_it_be(:package_file) { create(:package_file, :generic, package: package) }
+
+ context 'authentication' do
+ where(:project_visibility, :user_role, :member?, :authenticate_with, :expected_status) do
+ 'PUBLIC' | :developer | true | :personal_access_token | :success
+ 'PUBLIC' | :guest | true | :personal_access_token | :success
+ 'PUBLIC' | :developer | true | :invalid_personal_access_token | :unauthorized
+ 'PUBLIC' | :guest | true | :invalid_personal_access_token | :unauthorized
+ 'PUBLIC' | :developer | false | :personal_access_token | :success
+ 'PUBLIC' | :guest | false | :personal_access_token | :success
+ 'PUBLIC' | :developer | false | :invalid_personal_access_token | :unauthorized
+ 'PUBLIC' | :guest | false | :invalid_personal_access_token | :unauthorized
+ 'PUBLIC' | :anonymous | false | :none | :unauthorized
+ 'PRIVATE' | :developer | true | :personal_access_token | :success
+ 'PRIVATE' | :guest | true | :personal_access_token | :forbidden
+ 'PRIVATE' | :developer | true | :invalid_personal_access_token | :unauthorized
+ 'PRIVATE' | :guest | true | :invalid_personal_access_token | :unauthorized
+ 'PRIVATE' | :developer | false | :personal_access_token | :not_found
+ 'PRIVATE' | :guest | false | :personal_access_token | :not_found
+ 'PRIVATE' | :developer | false | :invalid_personal_access_token | :unauthorized
+ 'PRIVATE' | :guest | false | :invalid_personal_access_token | :unauthorized
+ 'PRIVATE' | :anonymous | false | :none | :unauthorized
+ 'PUBLIC' | :developer | true | :job_token | :success
+ 'PUBLIC' | :developer | true | :invalid_job_token | :unauthorized
+ 'PUBLIC' | :developer | false | :job_token | :success
+ 'PUBLIC' | :developer | false | :invalid_job_token | :unauthorized
+ 'PRIVATE' | :developer | true | :job_token | :success
+ 'PRIVATE' | :developer | true | :invalid_job_token | :unauthorized
+ 'PRIVATE' | :developer | false | :job_token | :not_found
+ 'PRIVATE' | :developer | false | :invalid_job_token | :unauthorized
+ end
+
+ with_them do
+ before do
+ project.update!(visibility_level: Gitlab::VisibilityLevel.const_get(project_visibility, false))
+ project.send("add_#{user_role}", user) if member? && user_role != :anonymous
+ end
+
+ it "responds with #{params[:expected_status]}" do
+ download_file(auth_header)
+
+ expect(response).to have_gitlab_http_status(expected_status)
+ end
+ end
+ end
+
+ context 'event tracking' do
+ before do
+ project.add_developer(user)
+ end
+
+ subject { download_file(personal_access_token_header) }
+
+ it_behaves_like 'a gitlab tracking event', described_class.name, 'pull_package'
+ end
+
+ it 'rejects a malicious file name request' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, file_name: '../.ssh%2fauthorized_keys')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'rejects a malicious file name request' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, file_name: '%2e%2e%2f.ssh%2fauthorized_keys')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'rejects a malicious package name request' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, package_name: 'my-package/../')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'rejects a malicious package name request' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, package_name: 'my-package%2f%2e%2e%2f')
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ context 'application security' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:param_name, :param_value) do
+ :package_name | 'my-package/../'
+ :package_name | 'my-package%2f%2e%2e%2f'
+ :file_name | '../.ssh%2fauthorized_keys'
+ :file_name | '%2e%2e%2f.ssh%2fauthorized_keys'
+ end
+
+ with_them do
+ subject { download_file(personal_access_token_header, param_name => param_value) }
+
+ it_behaves_like 'secure endpoint'
+ end
+ end
+
+ it 'responds with 404 Not Found for non existing package' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, package_name: 'no-such-package')
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ it 'responds with 404 Not Found for non existing package file' do
+ project.add_developer(user)
+
+ download_file(personal_access_token_header, file_name: 'no-such-file')
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ def download_file(request_headers, package_name: nil, file_name: nil)
+ package_name ||= package.name
+ file_name ||= package_file.file_name
+ url = "/projects/#{project.id}/packages/generic/#{package_name}/#{package.version}/#{file_name}"
+
+ get api(url), headers: request_headers
+ end
+ end
end
diff --git a/spec/support/shared_examples/features/page_description_shared_examples.rb b/spec/support/shared_examples/features/page_description_shared_examples.rb
new file mode 100644
index 00000000000..81653220b4c
--- /dev/null
+++ b/spec/support/shared_examples/features/page_description_shared_examples.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'page meta description' do |expected_description|
+ it 'renders the page with description, og:description, and twitter:description meta tags that contains a plain-text version of the markdown', :aggregate_failures do
+ %w(name='description' property='og:description' property='twitter:description').each do |selector|
+ expect(page).to have_selector("meta[#{selector}][content='#{expected_description}']", visible: false)
+ end
+ end
+end