Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2020-10-01 06:09:59 +00:00
parent 4ed4dc08a8
commit a7e81add72
62 changed files with 819 additions and 491 deletions

View file

@ -1 +1 @@
87acab96b9eb16381a49f2c08a2eaa9664a2fa75 3f5e218def93024f3aafe590c22cd1b29f744105

View file

@ -5,7 +5,7 @@ import {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlToggle, GlToggle,
GlDeprecatedButton, GlButton,
GlDeprecatedDropdown, GlDeprecatedDropdown,
GlDeprecatedDropdownItem, GlDeprecatedDropdownItem,
GlIcon, GlIcon,
@ -25,7 +25,7 @@ export default {
GlSprintf, GlSprintf,
GlLink, GlLink,
GlToggle, GlToggle,
GlDeprecatedButton, GlButton,
GlDeprecatedDropdown, GlDeprecatedDropdown,
GlDeprecatedDropdownItem, GlDeprecatedDropdownItem,
GlIcon, GlIcon,
@ -232,18 +232,24 @@ export default {
</gl-deprecated-dropdown> </gl-deprecated-dropdown>
</div> </div>
</div> </div>
<div v-if="showButtons" class="mt-3"> <div v-if="showButtons" class="gl-mt-5 gl-display-flex">
<gl-deprecated-button <gl-button
class="btn-success inline mr-1" variant="success"
category="primary"
data-qa-selector="save_ingress_modsecurity_settings"
:loading="saving" :loading="saving"
:disabled="saveButtonDisabled" :disabled="saveButtonDisabled"
@click="updateApplication" @click="updateApplication"
> >
{{ saveButtonLabel }} {{ saveButtonLabel }}
</gl-deprecated-button> </gl-button>
<gl-deprecated-button :disabled="saveButtonDisabled" @click="resetStatus"> <gl-button
data-qa-selector="cancel_ingress_modsecurity_settings"
:disabled="saveButtonDisabled"
@click="resetStatus"
>
{{ __('Cancel') }} {{ __('Cancel') }}
</gl-deprecated-button> </gl-button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -1,7 +1,6 @@
import Vue from 'vue'; import Vue from 'vue';
import { GlToast } from '@gitlab/ui'; import { GlToast } from '@gitlab/ui';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import store from './store';
import RegistrySettingsApp from './components/registry_settings_app.vue'; import RegistrySettingsApp from './components/registry_settings_app.vue';
import { apolloProvider } from './graphql/index'; import { apolloProvider } from './graphql/index';
@ -13,11 +12,9 @@ export default () => {
if (!el) { if (!el) {
return null; return null;
} }
store.dispatch('setInitialState', el.dataset);
const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset; const { projectPath, isAdmin, adminSettingsPath, enableHistoricEntries } = el.dataset;
return new Vue({ return new Vue({
el, el,
store,
apolloProvider, apolloProvider,
components: { components: {
RegistrySettingsApp, RegistrySettingsApp,

View file

@ -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'));
};

View file

@ -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);
};

View file

@ -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();

View file

@ -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';

View file

@ -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;
},
};

View file

@ -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: {},
});

View file

@ -1,5 +1,11 @@
<script> <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 { __ } from '~/locale';
import { AVATAR_SIZE } from '../constants'; import { AVATAR_SIZE } from '../constants';
@ -7,7 +13,11 @@ export default {
name: 'UserAvatar', name: 'UserAvatar',
avatarSize: AVATAR_SIZE, avatarSize: AVATAR_SIZE,
orphanedUserLabel: __('Orphaned member'), orphanedUserLabel: __('Orphaned member'),
components: { GlAvatarLink, GlAvatarLabeled }, components: {
GlAvatarLink,
GlAvatarLabeled,
GlBadge,
},
directives: { directives: {
SafeHtml, SafeHtml,
}, },
@ -16,11 +26,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
computed: { computed: {
user() { user() {
return this.member.user; return this.member.user;
}, },
badges() {
return generateBadges(this.member, this.isCurrentUser).filter(badge => badge.show);
},
}, },
}; };
</script> </script>
@ -41,7 +58,15 @@ export default {
:size="$options.avatarSize" :size="$options.avatarSize"
:entity-name="user.name" :entity-name="user.name"
:entity-id="user.id" :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-link>
<gl-avatar-labeled <gl-avatar-labeled

View file

@ -12,6 +12,10 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
member: { member: {
type: Object, type: Object,
required: true, required: true,
@ -27,5 +31,5 @@ export default {
</script> </script>
<template> <template>
<component :is="avatarComponent" :member="member" /> <component :is="avatarComponent" :member="member" :is-current-user="isCurrentUser" />
</template> </template>

View file

@ -44,8 +44,12 @@ export default {
show-empty show-empty
> >
<template #cell(account)="{ item: member }"> <template #cell(account)="{ item: member }">
<members-table-cell #default="{ memberType }" :member="member"> <members-table-cell #default="{ memberType, isCurrentUser }" :member="member">
<member-avatar :member-type="memberType" :member="member" /> <member-avatar
:member-type="memberType"
:is-current-user="isCurrentUser"
:member="member"
/>
</members-table-cell> </members-table-cell>
</template> </template>

View file

@ -11,7 +11,7 @@ export default {
}, },
}, },
computed: { computed: {
...mapState(['sourceId']), ...mapState(['sourceId', 'currentUserId']),
isGroup() { isGroup() {
return Boolean(this.member.sharedWithGroup); return Boolean(this.member.sharedWithGroup);
}, },
@ -35,11 +35,15 @@ export default {
isDirectMember() { isDirectMember() {
return this.member.source?.id === this.sourceId; return this.member.source?.id === this.sourceId;
}, },
isCurrentUser() {
return this.member.user?.id === this.currentUserId;
},
}, },
render() { render() {
return this.$scopedSlots.default({ return this.$scopedSlots.default({
memberType: this.memberType, memberType: this.memberType,
isDirectMember: this.isDirectMember, isDirectMember: this.isDirectMember,
isCurrentUser: this.isCurrentUser,
}); });
}, },
}; };

View file

@ -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',
},
];

View file

@ -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

View file

@ -14,6 +14,8 @@ class ApplicationSetting
end end
def accepted_by_user?(user) def accepted_by_user?(user)
return true if user.project_bot?
user.accepted_term_id == id || user.accepted_term_id == id ||
term_agreements.accepted.where(user: user).exists? term_agreements.accepted.where(user: user).exists?
end end

View file

@ -26,7 +26,7 @@ class Packages::Package < ApplicationRecord
validates :project, presence: true validates :project, presence: true
validates :name, 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, validates :name,
uniqueness: { scope: %i[project_id version package_type] }, unless: :conan? 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_npm_package_name, if: :npm?
validate :valid_composer_global_name, if: :composer? validate :valid_composer_global_name, if: :composer?
validate :package_already_taken, if: :npm? 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.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.conan_recipe_component_regex }, if: :conan?
validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? } validates :version, format: { with: Gitlab::Regex.maven_version_regex }, if: -> { version? && maven? }
validates :version, format: { with: Gitlab::Regex.pypi_version_regex }, if: :pypi? 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! .where(packages_package_files: { file_name: file_name, file_sha256: sha256 }).last!
end end
def self.by_name_and_version!(name, version)
find_by!(name: name, version: version)
end
def self.pluck_names def self.pluck_names
pluck(:name) pluck(:name)
end end

View file

@ -1674,6 +1674,8 @@ class User < ApplicationRecord
end end
def terms_accepted? def terms_accepted?
return true if project_bot?
accepted_term_id.present? accepted_term_id.present?
end end

View file

@ -1,6 +1,6 @@
- page_title @group.name - page_title @group.name
- page_description @group.description unless page_description - page_description @group.description_html unless page_description
- header_title group_title(@group) unless header_title - header_title group_title(@group) unless header_title
- nav "group" - nav "group"
- display_subscription_banner! - display_subscription_banner!
- display_namespace_storage_limit_alert! - display_namespace_storage_limit_alert!

View file

@ -1,6 +1,6 @@
- page_title @project.full_name - page_title @project.full_name
- page_description @project.description unless page_description - page_description @project.description_html unless page_description
- header_title project_title(@project) unless header_title - header_title project_title(@project) unless header_title
- nav "project" - nav "project"
- display_subscription_banner! - display_subscription_banner!
- display_namespace_storage_limit_alert! - display_namespace_storage_limit_alert!

View file

@ -2,7 +2,7 @@
- add_to_breadcrumbs _("Issues"), project_issues_path(@project) - add_to_breadcrumbs _("Issues"), project_issues_path(@project)
- breadcrumb_title @issue.to_reference - breadcrumb_title @issue.to_reference
- page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues") - page_title "#{@issue.title} (#{@issue.to_reference})", _("Issues")
- page_description @issue.description - page_description @issue.description_html
- page_card_attributes @issue.card_attributes - page_card_attributes @issue.card_attributes
- if @issue.relocation_target - if @issue.relocation_target
- page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url - page_canonical_link @issue.relocation_target.present(current_user: current_user).web_url

View file

@ -3,7 +3,7 @@
- add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project) - add_to_breadcrumbs _("Merge Requests"), project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference - breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge Requests") - 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 - page_card_attributes @merge_request.card_attributes
- suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes') - suggest_changes_help_path = help_page_path('user/discussions/index.md', anchor: 'suggest-changes')
- number_of_pipelines = @pipelines.size - number_of_pipelines = @pipelines.size

View file

@ -1,7 +1,7 @@
- add_to_breadcrumbs _('Milestones'), project_milestones_path(@project) - add_to_breadcrumbs _('Milestones'), project_milestones_path(@project)
- breadcrumb_title @milestone.title - breadcrumb_title @milestone.title
- page_title @milestone.title, _('Milestones') - page_title @milestone.title, _('Milestones')
- page_description @milestone.description - page_description @milestone.description_html
- add_page_specific_style 'page_bundles/milestone' - add_page_specific_style 'page_bundles/milestone'
= render 'shared/milestones/header', milestone: @milestone = render 'shared/milestones/header', milestone: @milestone

View file

@ -2,7 +2,7 @@
- @hide_breadcrumbs = true - @hide_breadcrumbs = true
- @no_container = true - @no_container = true
- page_title @user.blocked? ? s_('UserProfile|Blocked user') : @user.name - 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) - header_title @user.name, user_path(@user)
- link_classes = "flex-grow-1 mx-1 " - link_classes = "flex-grow-1 mx-1 "

View file

@ -0,0 +1,5 @@
---
title: Strip markdown from og:description meta tags
merge_request: 42918
author:
type: added

View file

@ -0,0 +1,5 @@
---
title: Migrate deprecated button to GlButton in ingress_modsecurity_settings.vue
merge_request: 43717
author:
type: other

View file

@ -0,0 +1,5 @@
---
title: Auto-accept TOS if project bot
merge_request: 43067
author:
type: fixed

View file

@ -11641,7 +11641,7 @@ type Mutation {
pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload pipelineRetry(input: PipelineRetryInput!): PipelineRetryPayload
removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2") removeAwardEmoji(input: RemoveAwardEmojiInput!): RemoveAwardEmojiPayload @deprecated(reason: "Use awardEmojiRemove. Deprecated in 13.2")
removeProjectFromSecurityDashboard(input: RemoveProjectFromSecurityDashboardInput!): RemoveProjectFromSecurityDashboardPayload 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") runDastScan(input: RunDASTScanInput!): RunDASTScanPayload @deprecated(reason: "Use DastOnDemandScanCreate. Deprecated in 13.4")
todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload todoMarkDone(input: TodoMarkDoneInput!): TodoMarkDonePayload
todoRestore(input: TodoRestoreInput!): TodoRestorePayload todoRestore(input: TodoRestoreInput!): TodoRestorePayload
@ -11673,6 +11673,7 @@ type Mutation {
vulnerabilityConfirm(input: VulnerabilityConfirmInput!): VulnerabilityConfirmPayload vulnerabilityConfirm(input: VulnerabilityConfirmInput!): VulnerabilityConfirmPayload
vulnerabilityDismiss(input: VulnerabilityDismissInput!): VulnerabilityDismissPayload vulnerabilityDismiss(input: VulnerabilityDismissInput!): VulnerabilityDismissPayload
vulnerabilityResolve(input: VulnerabilityResolveInput!): VulnerabilityResolvePayload vulnerabilityResolve(input: VulnerabilityResolveInput!): VulnerabilityResolvePayload
vulnerabilityRevertToDetected(input: VulnerabilityRevertToDetectedInput!): VulnerabilityRevertToDetectedPayload
} }
""" """
@ -20470,6 +20471,41 @@ type VulnerabilityResolvePayload {
vulnerability: Vulnerability vulnerability: Vulnerability
} }
"""
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 Represents a vulnerability scanner
""" """

View file

@ -33961,8 +33961,8 @@
"name": "RevertVulnerabilityToDetectedPayload", "name": "RevertVulnerabilityToDetectedPayload",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": true,
"deprecationReason": null "deprecationReason": "Use vulnerabilityRevertToDetected. Deprecated in 13.5"
}, },
{ {
"name": "runDastScan", "name": "runDastScan",
@ -34503,6 +34503,33 @@
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "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, "inputFields": null,
@ -59575,6 +59602,108 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "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", "kind": "OBJECT",
"name": "VulnerabilityScanner", "name": "VulnerabilityScanner",

View file

@ -2947,6 +2947,16 @@ Autogenerated return type of VulnerabilityResolve.
| `errors` | String! => Array | Errors encountered during execution of the mutation. | | `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `vulnerability` | Vulnerability | The vulnerability after state change | | `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 ### VulnerabilityScanner
Represents a vulnerability scanner. Represents a vulnerability scanner.

View file

@ -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 the CI/CD pipeline set for your project is triggered. By doing
so, GitLab CI/CD: so, GitLab CI/CD:
- Runs automated scripts (sequential or parallel) to: - Runs automated scripts (sequentially or in parallel) to:
- Build and test your app. - Build and test your app.
- Preview the changes per merge request with Review Apps, as you - Preview the changes per merge request with Review Apps, as you
would see in your `localhost`. would see in your `localhost`.

View file

@ -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, - You can control which jobs run in which cases, depending on how they are triggered,
with the [`rules` syntax](../yaml/README.md#rules). 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) - 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 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 permissions), and then any project can use them. This central project could also

View file

@ -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/) > - <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. > 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), 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 you may need to enable pipeline triggering. Go to your project's **Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
**Settings > Repository > Pull from a remote repository > Trigger pipelines for mirror updates**.
## Introduction ## Introduction
@ -961,7 +958,7 @@ GitLab performs a reverse deep merge based on the keys. GitLab:
- Merges the `rspec` contents into `.tests` recursively. - Merges the `rspec` contents into `.tests` recursively.
- Doesn't merge the values of the keys. - 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 ```yaml
rspec: rspec:
@ -974,9 +971,6 @@ rspec:
- $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). 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 `.tests` in this example is a [hidden job](#hide-jobs), but it's

View file

@ -83,9 +83,9 @@ project in a simple and automatic way:
1. [Auto Build](stages.md#auto-build) 1. [Auto Build](stages.md#auto-build)
1. [Auto Test](stages.md#auto-test) 1. [Auto Test](stages.md#auto-test)
1. [Auto Code Quality](stages.md#auto-code-quality) **(STARTER)** 1. [Auto Code Quality](stages.md#auto-code-quality)
1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast) **(ULTIMATE)** 1. [Auto SAST (Static Application Security Testing)](stages.md#auto-sast)
1. [Auto Secret Detection](stages.md#auto-secret-detection) **(ULTIMATE)** 1. [Auto Secret Detection](stages.md#auto-secret-detection)
1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)** 1. [Auto Dependency Scanning](stages.md#auto-dependency-scanning) **(ULTIMATE)**
1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)** 1. [Auto License Compliance](stages.md#auto-license-compliance) **(ULTIMATE)**
1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)** 1. [Auto Container Scanning](stages.md#auto-container-scanning) **(ULTIMATE)**

View file

@ -124,7 +124,10 @@ The supported buildpacks are:
If your application needs a buildpack that is not in the above list, you 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). 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 Auto Code Quality uses the
[Code Quality image](https://gitlab.com/gitlab-org/ci-cd/codequality) to run [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 out. The merge request widget also displays any
[differences between the source and target branches](../../user/project/merge_requests/code_quality.md). [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 Static Application Security Testing (SAST) uses the
[SAST Docker image](https://gitlab.com/gitlab-org/security-products/sast) to run static [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), To learn more about [how SAST works](../../user/application_security/sast/index.md),
see the documentation. 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 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 [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

View file

@ -30,7 +30,7 @@ module API
route_setting :authentication, job_token_allowed: true route_setting :authentication, job_token_allowed: true
params do 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 :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_name, type: String, desc: 'Package file name', regexp: Gitlab::Regex.generic_package_file_name_regex, file_path: true
end end
@ -44,7 +44,7 @@ module API
end end
params do 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 :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_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)' 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! forbidden!
end 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 end
end end

View file

@ -142,9 +142,13 @@ module Gitlab
/\A\d+\.\d+\.\d+\z/ /\A\d+\.\d+\.\d+\z/
end end
def generic_package_file_name_regex def generic_package_name_regex
maven_file_name_regex maven_file_name_regex
end end
def generic_package_file_name_regex
generic_package_name_regex
end
end end
extend self extend self

View file

@ -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." msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects."
msgstr "" msgstr ""
msgid "SAML"
msgstr ""
msgid "SAML SSO" msgid "SAML SSO"
msgstr "" msgstr ""

View file

@ -140,6 +140,14 @@ FactoryBot.define do
size { 1149.bytes } size { 1149.bytes }
end 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 trait(:object_storage) do
file_store { Packages::PackageFileUploader::Store::REMOTE } file_store { Packages::PackageFileUploader::Store::REMOTE }
end end

View file

@ -184,4 +184,17 @@ RSpec.describe 'Group show page' do
expect(page).to have_selector('.notifications-btn.disabled', visible: true) expect(page).to have_selector('.notifications-btn.disabled', visible: true)
end end
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 end

View file

@ -5,7 +5,7 @@ require "spec_helper"
RSpec.describe "User views issue" do RSpec.describe "User views issue" do
let_it_be(:project) { create(:project_empty_repo, :public) } let_it_be(:project) { create(:project_empty_repo, :public) }
let_it_be(:user) { create(:user) } 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) } let_it_be(:note) { create(:note, noteable: issue, project: project, author: user) }
before_all do 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 { 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 it 'shows the merge request and issue actions', :aggregate_failures do
expect(page).to have_link('New issue') expect(page).to have_link('New issue')
expect(page).to have_button('Create merge request') expect(page).to have_button('Create merge request')

View file

@ -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

View file

@ -6,7 +6,7 @@ RSpec.describe "User views milestone" do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) } let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, :repository, group: 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) } let_it_be(:labels) { create_list(:label, 2, project: project) }
before_all do before_all do
@ -17,6 +17,14 @@ RSpec.describe "User views milestone" do
sign_in(user) sign_in(user)
end 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 it "avoids N+1 database queries" do
issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze issue_params = { project: project, assignees: [user], author: user, milestone: milestone, labels: labels }.freeze

View file

@ -99,6 +99,15 @@ RSpec.describe 'Project' do
expect(page).to have_css('.home-panel-description .is-expanded') expect(page).to have_css('.home-panel-description .is-expanded')
end end
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 end
describe 'project topics' do describe 'project topics' do

View file

@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'User page' do RSpec.describe 'User page' do
include ExternalAuthorizationServiceHelpers include ExternalAuthorizationServiceHelpers
let(:user) { create(:user) } let(:user) { create(:user, bio: '**Lorem** _ipsum_ dolor sit [amet](https://example.com)') }
context 'with public profile' do context 'with public profile' do
it 'shows all the tabs' do it 'shows all the tabs' do
@ -174,4 +174,12 @@ RSpec.describe 'User page' do
end end
end 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 end

View file

@ -26,6 +26,21 @@ RSpec.describe 'Users > Terms' do
expect(page).not_to have_content('Continue') expect(page).not_to have_content('Continue')
end 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 context 'when signed in' do
let(:user) { create(:user) } let(:user) { create(:user) }

View file

@ -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

View file

@ -28,8 +28,10 @@ describe('IngressModsecuritySettings', () => {
}); });
}; };
const findSaveButton = () => wrapper.find('.btn-success'); const findSaveButton = () =>
const findCancelButton = () => wrapper.find('[variant="secondary"]'); 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 findModSecurityToggle = () => wrapper.find(GlToggle);
const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown); const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown);

View file

@ -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,
);
});
});
});

View file

@ -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);
},
);
});
});

View file

@ -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);
});
});
});

View file

@ -1,25 +1,26 @@
import { mount, createWrapper } from '@vue/test-utils'; import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom'; import { within } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui'; import { GlAvatarLink, GlBadge } from '@gitlab/ui';
import { member, orphanedMember } from '../mock_data'; import { member as memberMock, orphanedMember } from '../mock_data';
import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue'; import UserAvatar from '~/vue_shared/components/members/avatars/user_avatar.vue';
describe('MemberList', () => { describe('MemberList', () => {
let wrapper; let wrapper;
const { user } = member; const { user } = memberMock;
const createComponent = (propsData = {}) => { const createComponent = (propsData = {}) => {
wrapper = mount(UserAvatar, { wrapper = mount(UserAvatar, {
propsData: { propsData: {
member, member: memberMock,
isCurrentUser: false,
...propsData, ...propsData,
}, },
}); });
}; };
const getByText = (text, options) => const getByText = (text, options) =>
createWrapper(getByTextHelper(wrapper.element, text, options)); createWrapper(within(wrapper.element).findByText(text, options));
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
@ -63,4 +64,25 @@ describe('MemberList', () => {
expect(getByText('Orphaned member').exists()).toBe(true); 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);
});
});
}); });

View file

@ -11,7 +11,10 @@ describe('MemberList', () => {
const createComponent = propsData => { const createComponent = propsData => {
wrapper = shallowMount(MemberAvatar, { wrapper = shallowMount(MemberAvatar, {
propsData, propsData: {
isCurrentUser: false,
...propsData,
},
}); });
}; };

View file

@ -15,6 +15,10 @@ describe('MemberList', () => {
type: Boolean, type: Boolean,
required: true, required: true,
}, },
isCurrentUser: {
type: Boolean,
required: true,
},
}, },
render(createElement) { render(createElement) {
return createElement('div', this.memberType); return createElement('div', this.memberType);
@ -29,6 +33,7 @@ describe('MemberList', () => {
return new Vuex.Store({ return new Vuex.Store({
state: { state: {
sourceId: 1, sourceId: 1,
currentUserId: 1,
...state, ...state,
}, },
}); });
@ -42,8 +47,13 @@ describe('MemberList', () => {
propsData, propsData,
store: createStore(state), store: createStore(state),
scopedSlots: { scopedSlots: {
default: default: `
'<wrapped-component :member-type="props.memberType" :is-direct-member="props.isDirectMember" />', <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); 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);
});
});
}); });

View file

@ -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));
});
});
});

View file

@ -599,6 +599,20 @@ RSpec.describe Gitlab::Regex do
it { is_expected.not_to match('') } it { is_expected.not_to match('') }
end 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 describe '.generic_package_file_name_regex' do
subject { described_class.generic_package_file_name_regex } subject { described_class.generic_package_file_name_regex }

View file

@ -17,6 +17,7 @@ RSpec.describe ApplicationSetting::Term do
describe '#accepted_by_user?' do describe '#accepted_by_user?' do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project_bot) { create(:user, :project_bot) }
let(:term) { create(:term) } let(:term) { create(:term) }
it 'is true when the user accepted the terms' do 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) expect(term.accepted_by_user?(user)).to be(true)
end 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 it 'is false when the user declined the terms' do
decline_terms(term, user) decline_terms(term, user)

View file

@ -44,7 +44,7 @@ RSpec.describe Ci::JobArtifact do
let!(:metrics_report) { create(:ci_job_artifact, :junit) } let!(:metrics_report) { create(:ci_job_artifact, :junit) }
let!(:codequality_report) { create(:ci_job_artifact, :codequality) } 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
end end

View file

@ -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('.foobar').for(:name) }
it { is_expected.not_to allow_value('%foo%bar').for(:name) } it { is_expected.not_to allow_value('%foo%bar').for(:name) }
end 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 end
describe '#version' do describe '#version' do

View file

@ -4330,28 +4330,32 @@ RSpec.describe User do
describe '#required_terms_not_accepted?' do describe '#required_terms_not_accepted?' do
let(:user) { build(:user) } let(:user) { build(:user) }
let(:project_bot) { create(:user, :project_bot) }
subject { user.required_terms_not_accepted? } subject { user.required_terms_not_accepted? }
context "when terms are not enforced" do context "when terms are not enforced" do
it { is_expected.to be_falsy } it { is_expected.to be_falsey }
end end
context "when terms are enforced and accepted by the user" do context "when terms are enforced" do
before do before do
enforce_terms enforce_terms
end
it "is not accepted by the user" do
expect(subject).to be_truthy
end
it "is accepted by the user" do
accept_terms(user) accept_terms(user)
expect(subject).to be_falsey
end end
it { is_expected.to be_falsy } it "auto accepts the term for project bots" do
end expect(project_bot.required_terms_not_accepted?).to be_falsey
context "when terms are enforced but the user has not accepted" do
before do
enforce_terms
end end
it { is_expected.to be_truthy }
end end
end end

View file

@ -33,7 +33,19 @@ RSpec.describe API::GenericPackages do
{ Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token } { Gitlab::Auth::AuthFinders::JOB_TOKEN_HEADER => value || ci_build.token }
end 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 context 'with valid project' do
using RSpec::Parameterized::TableSyntax using RSpec::Parameterized::TableSyntax
@ -73,41 +85,49 @@ RSpec.describe API::GenericPackages do
end end
it "responds with #{params[:expected_status]}" do it "responds with #{params[:expected_status]}" do
headers = workhorse_header.merge(auth_header) authorize_upload_file(workhorse_header.merge(auth_header))
url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/myfile.tar.gz/authorize"
put api(url), headers: headers
expect(response).to have_gitlab_http_status(expected_status) expect(response).to have_gitlab_http_status(expected_status)
end end
end end
end end
it 'rejects a malicious request' do context 'application security' do
project.add_developer(user) using RSpec::Parameterized::TableSyntax
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"
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 end
context 'generic_packages feature flag is disabled' do context 'generic_packages feature flag is disabled' do
it 'responds with 404 Not Found' do it 'responds with 404 Not Found' do
stub_feature_flags(generic_packages: false) stub_feature_flags(generic_packages: false)
project.add_developer(user) 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) expect(response).to have_gitlab_http_status(:not_found)
end end
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 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 include WorkhorseHelpers
let(:file_upload) { fixture_file_upload('spec/fixtures/packages/generic/myfile.tar.gz') } 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) expect(response).to have_gitlab_http_status(:forbidden)
end end
end
it 'rejects a malicious request' do context 'application security' do
headers = workhorse_header.merge(personal_access_token_header) using RSpec::Parameterized::TableSyntax
upload_file(params, headers, file_name: '%2e%2e%2f.ssh%2fauthorized_keys')
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
end end
def upload_file(params, request_headers, send_rewritten_field: true, file_name: 'myfile.tar.gz') def upload_file(params, request_headers, send_rewritten_field: true, package_name: 'mypackage', file_name: 'myfile.tar.gz')
url = "/projects/#{project.id}/packages/generic/mypackage/0.0.1/#{file_name}" url = "/projects/#{project.id}/packages/generic/#{package_name}/0.0.1/#{file_name}"
workhorse_finalize( workhorse_finalize(
api(url), api(url),
@ -268,4 +298,138 @@ RSpec.describe API::GenericPackages do
) )
end end
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 end

View file

@ -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