Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-03-07 21:13:36 +00:00
parent 3ff3d897d6
commit c19944d997
101 changed files with 755 additions and 179 deletions

2
.gitleaksignore Normal file
View File

@ -0,0 +1,2 @@
7e07fe42d34916b276a7b068f4faa8bdc0ebc984:doc/architecture/blueprints/runner_tokens/index.md:gitlab-rrt:485
f6504b498548380198ad38295d9caa71412115f0:doc/architecture/blueprints/runner_tokens/index.md:generic-api-key:506

View File

@ -1 +1 @@
65769c7a58d3339fe94a809bf6fd34f2f300a700
998d1f6dbf9856c51d548814371cc6b8276086a6

View File

@ -1,3 +1,4 @@
import { WEBAUTHN_AUTHENTICATE } from './constants';
import WebAuthnError from './error';
import WebAuthnFlow from './flow';
import { supported, convertGetParams, convertGetResponse } from './util';
@ -44,7 +45,7 @@ export default class WebAuthnAuthenticate {
this.renderAuthenticated(JSON.stringify(convertedResponse));
})
.catch((err) => {
this.flow.renderError(new WebAuthnError(err, 'authenticate'));
this.flow.renderError(new WebAuthnError(err, WEBAUTHN_AUTHENTICATE));
});
}

View File

@ -30,10 +30,10 @@ import {
STATE_UNSUPPORTED,
STATE_WAITING,
WEBAUTHN_DOCUMENTATION_PATH,
WEBAUTHN_REGISTER,
} from '~/authentication/webauthn/constants';
import WebAuthnError from '~/authentication/webauthn/error';
import {
FLOW_REGISTER,
convertCreateParams,
convertCreateResponse,
isHTTPS,
@ -123,7 +123,7 @@ export default {
this.credentials = JSON.stringify(convertCreateResponse(credentials));
this.state = STATE_SUCCESS;
} catch (error) {
this.errorMessage = new WebAuthnError(error, FLOW_REGISTER).message();
this.errorMessage = new WebAuthnError(error, WEBAUTHN_REGISTER).message();
this.state = STATE_ERROR;
}
},

View File

@ -38,6 +38,8 @@ export const STATE_SUCCESS = 'success';
export const STATE_UNSUPPORTED = 'unsupported';
export const STATE_WAITING = 'waiting';
export const WEBAUTHN_AUTHENTICATE = 'authenticate';
export const WEBAUTHN_REGISTER = 'register';
export const WEBAUTHN_DOCUMENTATION_PATH = helpPagePath(
'user/profile/account/two_factor_authentication',
{ anchor: 'set-up-a-webauthn-device' },

View File

@ -1,5 +1,6 @@
import { __ } from '~/locale';
import { isHTTPS, FLOW_AUTHENTICATE, FLOW_REGISTER } from './util';
import { WEBAUTHN_AUTHENTICATE, WEBAUTHN_REGISTER } from './constants';
import { isHTTPS } from './util';
export default class WebAuthnError {
constructor(error, flowType) {
@ -13,9 +14,9 @@ export default class WebAuthnError {
message() {
if (this.errorName === 'NotSupportedError') {
return __('Your device is not compatible with GitLab. Please try another device');
} else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_AUTHENTICATE) {
} else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_AUTHENTICATE) {
return __('This device has not been registered with us.');
} else if (this.errorName === 'InvalidStateError' && this.flowType === FLOW_REGISTER) {
} else if (this.errorName === 'InvalidStateError' && this.flowType === WEBAUTHN_REGISTER) {
return __('This device has already been registered with us.');
} else if (this.errorName === 'SecurityError' && this.httpsDisabled) {
return __(

View File

@ -2,6 +2,7 @@ import { __ } from '~/locale';
import WebAuthnError from './error';
import WebAuthnFlow from './flow';
import { supported, isHTTPS, convertCreateParams, convertCreateResponse } from './util';
import { WEBAUTHN_REGISTER } from './constants';
// Register WebAuthn devices for users to authenticate with.
//
@ -40,7 +41,7 @@ export default class WebAuthnRegister {
publicKey: this.webauthnOptions,
})
.then((cred) => this.renderRegistered(JSON.stringify(convertCreateResponse(cred))))
.catch((err) => this.flow.renderError(new WebAuthnError(err, 'register')));
.catch((err) => this.flow.renderError(new WebAuthnError(err, WEBAUTHN_REGISTER)));
}
renderSetup() {

View File

@ -8,9 +8,6 @@ export function isHTTPS() {
return window.location.protocol.startsWith('https');
}
export const FLOW_AUTHENTICATE = 'authenticate';
export const FLOW_REGISTER = 'register';
/**
* Converts a base64 string to an ArrayBuffer
*

View File

@ -7,7 +7,7 @@ import {
import confidentialMergeRequestState from '~/confidential_merge_request/state';
import DropLab from '~/filtered_search/droplab/drop_lab_deprecated';
import ISetter from '~/filtered_search/droplab/plugins/input_setter';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';

View File

@ -1,6 +1,6 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';

View File

@ -7,7 +7,7 @@ import IssueCardStatistics from 'ee_else_ce/issues/list/components/issue_card_st
import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
import { createAlert, VARIANT_INFO } from '~/flash';
import { createAlert, VARIANT_INFO } from '~/alert';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';

View File

@ -1,5 +1,5 @@
import Sortable from 'sortablejs';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { getSortableDefaultOptions, sortableStart } from '~/sortable/utils';

View File

@ -1,4 +1,4 @@
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale';

View File

@ -1,7 +1,7 @@
<script>
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import {
IssuableStatusText,
STATUS_CLOSED,
@ -277,7 +277,7 @@ export default {
},
},
created() {
this.flashContainer = null;
this.alert = null;
this.service = new Service(this.endpoint);
this.poll = new Poll({
resource: this.service,
@ -395,7 +395,7 @@ export default {
? { ...formState, issue_type: issueState.issueType }
: formState;
this.clearFlash();
this.alert?.dismiss();
return this.service
.updateIssuable(issuablePayload)
@ -431,7 +431,7 @@ export default {
errMsg += `. ${message}`;
}
this.flashContainer = createAlert({
this.alert = createAlert({
message: errMsg,
});
})
@ -448,13 +448,6 @@ export default {
this.isStickyHeaderShowing = true;
},
clearFlash() {
if (this.flashContainer) {
this.flashContainer.close();
this.flashContainer = null;
}
},
handleSaveDescription(description) {
this.updateFormState();
this.setFormState({ description });

View File

@ -7,7 +7,7 @@ import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_detail
import SafeHtml from '~/vue_shared/directives/safe_html';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_ISSUE, TYPENAME_WORK_ITEM } from '~/graphql_shared/constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, s__, sprintf } from '~/locale';
import { getSortableDefaultOptions, isDragging } from '~/sortable/utils';

View File

@ -10,7 +10,7 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { STATUS_CLOSED, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';

View File

@ -3,7 +3,7 @@ import { produce } from 'immer';
import { sortBy } from 'lodash';
import { GlIcon } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPENAME_ISSUE } from '~/graphql_shared/constants';
import { timelineFormI18n } from './constants';

View File

@ -1,6 +1,6 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import { s__ } from '~/locale';
import Tracking from '~/tracking';

View File

@ -1,6 +1,6 @@
<script>
import { formatDate } from '~/lib/utils/datetime_utility';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { sprintf } from '~/locale';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';

View File

@ -1,4 +1,4 @@
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
export const displayAndLogError = (error) =>

View File

@ -11,6 +11,10 @@ export default {
NewNamespacePage,
},
props: {
groupsUrl: {
type: String,
required: true,
},
parentGroupUrl: {
type: String,
required: false,
@ -39,7 +43,10 @@ export default {
{ text: this.parentGroupName, href: this.parentGroupUrl },
{ text: s__('GroupsNew|New subgroup'), href: '#' },
]
: [{ text: s__('GroupsNew|New group'), href: '#' }];
: [
{ text: s__('GroupsNew|Groups'), href: this.groupsUrl },
{ text: s__('GroupsNew|New group'), href: '#' },
];
},
panels() {
return [

View File

@ -22,6 +22,7 @@ initFilePickers();
function initNewGroupCreation(el) {
const {
hasErrors,
groupsUrl,
parentGroupUrl,
parentGroupName,
importExistingGroupPath,
@ -31,6 +32,7 @@ function initNewGroupCreation(el) {
} = el.dataset;
const props = {
groupsUrl,
parentGroupUrl,
parentGroupName,
importExistingGroupPath,

View File

@ -4,7 +4,7 @@ import Vue from 'vue';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import initDeprecatedNotes from '~/init_deprecated_notes';
import { initDiffStatsDropdown } from '~/init_diff_stats_dropdown';
import axios from '~/lib/utils/axios_utils';

View File

@ -184,6 +184,7 @@ export default class ActivityCalendar {
});
return `translate(${this.daySizeWithSpace * i + 1 + this.daySizeWithSpace}, 18)`;
})
.attr('data-testid', 'user-contrib-cell-group')
.selectAll('rect')
.data((stamp) => stamp)
.enter()
@ -195,6 +196,7 @@ export default class ActivityCalendar {
.attr('data-level', (stamp) => getLevelFromContributions(stamp.count))
.attr('title', (stamp) => formatTooltipText(stamp))
.attr('class', 'user-contrib-cell has-tooltip')
.attr('data-testid', 'user-contrib-cell')
.attr('data-html', true)
.attr('data-container', 'body')
.on('click', this.clickDay);

View File

@ -253,7 +253,7 @@ export default class UserTabs {
timestamps: data,
calendarActivitiesPath,
utcOffset,
firstDayOfTheWeek: gon.first_day_of_week,
firstDayOfWeek: gon.first_day_of_week,
monthsAgo,
});
}

View File

@ -59,6 +59,10 @@ export default {
SafeHtml,
},
props: {
projectsUrl: {
type: String,
required: true,
},
parentGroupUrl: {
type: String,
required: false,
@ -89,9 +93,11 @@ export default {
computed: {
initialBreadcrumbs() {
return [
this.parentGroupUrl && { text: this.parentGroupName, href: this.parentGroupUrl },
this.parentGroupUrl
? { text: this.parentGroupName, href: this.parentGroupUrl }
: { text: s__('ProjectsNew|Projects'), href: this.projectsUrl },
{ text: s__('ProjectsNew|New project'), href: '#' },
].filter(Boolean);
];
},
availablePanels() {
return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL);

View File

@ -17,6 +17,7 @@ export function initNewProjectCreation() {
isCiCdAvailable,
parentGroupUrl,
parentGroupName,
projectsUrl,
} = el.dataset;
const props = {
@ -25,6 +26,7 @@ export function initNewProjectCreation() {
newProjectGuidelines,
parentGroupUrl,
parentGroupName,
projectsUrl,
};
const provide = {

View File

@ -33,7 +33,11 @@ export default {
</script>
<template>
<li :id="noteAnchorId" class="timeline-entry note system-note note-wrapper gl-p-0!">
<li
:id="noteAnchorId"
class="timeline-entry note system-note note-wrapper gl-p-0!"
data-qa-selector="alert_system_note_container"
>
<div class="gl-display-inline-flex gl-align-items-center gl-relative">
<div
class="gl-display-inline gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-box-sizing-content-box gl-p-3 gl-mt-n2 gl-mr-6"

View File

@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';

View File

@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { TYPENAME_CRM_CONTACT } from '~/graphql_shared/constants';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';

View File

@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { OPTIONS_NONE_ANY } from '../constants';

View File

@ -1,7 +1,7 @@
<script>
import { GlAvatar, GlFilteredSearchSuggestion } from '@gitlab/ui';
import { compact } from 'lodash';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { __ } from '~/locale';
import { OPTIONS_NONE_ANY } from '../constants';

View File

@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { followUser, unfollowUser } from '~/rest_api';
import { isUserBusy } from '~/set_status_modal/utils';
import Tracking from '~/tracking';

View File

@ -1,6 +1,6 @@
<script>
import { reportTypeToSecurityReportTypeEnum } from 'ee_else_ce/vue_shared/security_reports/constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';

View File

@ -1,6 +1,6 @@
<script>
import { mapActions, mapGetters } from 'vuex';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import ReportSection from '~/ci/reports/components/report_section.vue';
import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/ci/reports/constants';

View File

@ -0,0 +1,39 @@
# frozen_string_literal: true
module Mutations
module DesignManagement
class Update < ::Mutations::BaseMutation
graphql_name "DesignManagementUpdate"
authorize :update_design
argument :id, ::Types::GlobalIDType[::DesignManagement::Design],
required: true,
description: "ID of the design to update."
argument :description, GraphQL::Types::String,
required: false,
description: copy_field_description(Types::DesignManagement::DesignType, :description)
field :design, Types::DesignManagement::DesignType,
null: false,
description: "Updated design."
def resolve(id:, description:)
design = authorized_find!(id: id)
design.update(description: description)
{
design: design.reset,
errors: errors_on_object(design)
}
end
private
def find_object(id:)
GitlabSchema.find_by_gid(id)
end
end
end
end

View File

@ -15,6 +15,11 @@ module Types
implements(Types::CurrentUserTodos)
implements(Types::TodoableInterface)
field :description,
GraphQL::Types::String,
null: true,
description: 'Description of the design.'
field :web_url,
GraphQL::Types::String,
null: false,
@ -25,6 +30,8 @@ module Types
resolver: Resolvers::DesignManagement::VersionsResolver,
description: "All versions related to this design ordered newest first."
markdown_field :description_html, null: true
# Returns a `DesignManagement::Version` for this query based on the
# `atVersion` argument passed to a parent node if present, or otherwise
# the most recent `Version` for the issue.

View File

@ -117,6 +117,7 @@ module Types
mount_mutation Mutations::DesignManagement::Upload, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Delete, calls_gitaly: true
mount_mutation Mutations::DesignManagement::Move
mount_mutation Mutations::DesignManagement::Update
mount_mutation Mutations::ContainerExpirationPolicies::Update
mount_mutation Mutations::ContainerRepositories::Destroy
mount_mutation Mutations::ContainerRepositories::DestroyTags

View File

@ -59,6 +59,7 @@ class IssuePolicy < IssuablePolicy
rule { ~can?(:read_issue) }.policy do
prevent :read_design
prevent :create_design
prevent :update_design
prevent :destroy_design
end

View File

@ -464,6 +464,7 @@ class ProjectPolicy < BasePolicy
enable :read_alert_management_alert
enable :update_alert_management_alert
enable :create_design
enable :update_design
enable :move_design
enable :destroy_design
enable :read_terraform_state
@ -750,6 +751,7 @@ class ProjectPolicy < BasePolicy
prevent :read_design
prevent :read_design_activity
prevent :create_design
prevent :update_design
prevent :destroy_design
prevent :move_design
end

View File

@ -1,9 +1,9 @@
# frozen_string_literal: true
require 'securerandom'
module ResourceAccessTokens
class CreateService < BaseService
include Gitlab::Utils::StrongMemoize
def initialize(current_user, resource, params = {})
@resource_type = resource.class.name.downcase
@resource = resource
@ -45,6 +45,14 @@ module ResourceAccessTokens
attr_reader :resource_type, :resource
def username_and_email_generator
Gitlab::Utils::UsernameAndEmailGenerator.new(
username_prefix: "#{resource_type}_#{resource.id}_bot",
email_domain: "noreply.#{Gitlab.config.gitlab.host}"
)
end
strong_memoize_attr :username_and_email_generator
def has_permission_to_create?
%w(project group).include?(resource_type) && can?(current_user, :create_resource_access_tokens, resource)
end
@ -65,25 +73,13 @@ module ResourceAccessTokens
def default_user_params
{
name: params[:name] || "#{resource.name.to_s.humanize} bot",
email: generate_email,
username: generate_username,
email: username_and_email_generator.email,
username: username_and_email_generator.username,
user_type: :project_bot,
skip_confirmation: true # Bot users should always have their emails confirmed.
}
end
def generate_username
username
end
def generate_email
"#{username}@noreply.#{Gitlab.config.gitlab.host}"
end
def username
@username ||= "#{resource_type}_#{resource.id}_bot_#{SecureRandom.hex(8)}"
end
def create_personal_access_token(user)
PersonalAccessTokens::CreateService.new(
current_user: user, target_user: user, params: personal_access_token_params

View File

@ -6,7 +6,7 @@
.group-edit-container
.js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s }.merge(subgroup_creation_data(@group),
.js-new-group-creation{ data: { has_errors: @group.errors.any?.to_s, groups_url: dashboard_groups_url }.merge(subgroup_creation_data(@group),
verification_for_group_creation_data) }
.row{ 'v-cloak': true }

View File

@ -8,7 +8,15 @@
.project-edit-errors
= render 'projects/errors'
.js-new-project-creation{ data: { is_ci_cd_available: remote_mirror_setting_enabled?.to_s, has_errors: @project.errors.any?.to_s, new_project_guidelines: brand_new_project_guidelines, push_to_create_project_command: push_to_create_project_command, working_with_projects_help_path: help_page_path("user/project/working_with_projects"), parent_group_url: @project.parent && group_url(@project.parent), parent_group_name: @project.parent&.name } }
.js-new-project-creation{ data: {
is_ci_cd_available: remote_mirror_setting_enabled?.to_s,
has_errors: @project.errors.any?.to_s,
new_project_guidelines: brand_new_project_guidelines,
push_to_create_project_command: push_to_create_project_command,
working_with_projects_help_path: help_page_path("user/project/working_with_projects"),
parent_group_url: @project.parent && group_url(@project.parent),
parent_group_name: @project.parent&.name,
projects_url: dashboard_projects_url } }
.row{ 'v-cloak': true }
#blank-project-pane.tab-pane.active

View File

@ -2450,6 +2450,26 @@ Input type: `DesignManagementMoveInput`
| <a id="mutationdesignmanagementmovedesigncollection"></a>`designCollection` | [`DesignCollection`](#designcollection) | Current state of the collection. |
| <a id="mutationdesignmanagementmoveerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.designManagementUpdate`
Input type: `DesignManagementUpdateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdesignmanagementupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdesignmanagementupdatedescription"></a>`description` | [`String`](#string) | Description of the design. |
| <a id="mutationdesignmanagementupdateid"></a>`id` | [`DesignManagementDesignID!`](#designmanagementdesignid) | ID of the design to update. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdesignmanagementupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdesignmanagementupdatedesign"></a>`design` | [`Design!`](#design) | Updated design. |
| <a id="mutationdesignmanagementupdateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.designManagementUpload`
Input type: `DesignManagementUploadInput`
@ -12755,6 +12775,8 @@ A single design.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="designcommenters"></a>`commenters` | [`UserCoreConnection!`](#usercoreconnection) | All commenters on this noteable. (see [Connections](#connections)) |
| <a id="designdescription"></a>`description` | [`String`](#string) | Description of the design. |
| <a id="designdescriptionhtml"></a>`descriptionHtml` | [`String`](#string) | GitLab Flavored Markdown rendering of `description`. |
| <a id="designdiffrefs"></a>`diffRefs` | [`DiffRefs!`](#diffrefs) | Diff refs for this design. |
| <a id="designdiscussions"></a>`discussions` | [`DiscussionConnection!`](#discussionconnection) | All discussions on this noteable. (see [Connections](#connections)) |
| <a id="designevent"></a>`event` | [`DesignVersionEvent!`](#designversionevent) | How this design was changed in the current version. |

View File

@ -58,7 +58,7 @@ To run a DAST authenticated scan:
| CI/CD variable | Type | Description |
|:-----------------------------------------------|:------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `DAST_AUTH_COOKIES` | string | Set to a comma-separated list of cookie names to specify which cookies are used for authentication. |
| `DAST_AUTH_REPORT` | boolean | Used in combination with exporting the `gl-dast-debug-auth-report.html` artifact to aid in debugging authentication issues. |
| `DAST_AUTH_REPORT` | boolean | Set to `true` to generate a report detailing steps taken during the authentication process. You must also define `gl-dast-debug-auth-report.html` as a CI job artifact to be able to access the generated report. Useful for debugging when authentication fails. |
| `DAST_AUTH_URL` <sup>1</sup> | URL | The URL of the page containing the sign-in HTML form on the target website. `DAST_USERNAME` and `DAST_PASSWORD` are submitted with the login form to create an authenticated scan. Example: `https://login.example.com`. |
| `DAST_AUTH_VERIFICATION_LOGIN_FORM` | boolean | Verifies successful authentication by checking for the absence of a login form once the login form has been submitted. |
| `DAST_AUTH_VERIFICATION_SELECTOR` | [selector](#finding-an-elements-selector) | Verifies successful authentication by checking for presence of a selector once the login form has been submitted. Example: `css:.user-photo`. |

View File

@ -170,13 +170,13 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_ADVERTISE_SCAN` | boolean | `true` | Set to `true` to add a `Via` header to every request sent, advertising that the request was sent as part of a GitLab DAST scan. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/334947) in GitLab 14.1. |
| `DAST_BROWSER_ACTION_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `800ms` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after completing an action. |
| `DAST_BROWSER_ACTION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to complete an action. |
| `DAST_BROWSER_ALLOWED_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered in scope when crawled. By default the `DAST_WEBSITE` hostname is included in the allowed hosts list. Headers set using `DAST_REQUEST_HEADERS` are added to every request made to these hostnames. |
| `DAST_BROWSER_ALLOWED_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered in scope when crawled. By default the `DAST_WEBSITE` hostname is included in the allowed hosts list. Headers set using `DAST_REQUEST_HEADERS` are added to every request made to these hostnames. |
| `DAST_BROWSER_COOKIES` | dictionary | `abtesting_group:3,region:locked` | A cookie name and value to be added to every request. |
| `DAST_BROWSER_CRAWL_GRAPH` | boolean | `true` | Set to `true` to generate an SVG graph of navigation paths visited during crawl phase of the scan. |
| `DAST_BROWSER_CRAWL_GRAPH` | boolean | `true` | Set to `true` to generate an SVG graph of navigation paths visited during crawl phase of the scan. You must also define `gl-dast-crawl-graph.svg` as a CI job artifact to be able to access the generated graph. |
| `DAST_BROWSER_DEVTOOLS_LOG` | string | `Default:messageAndBody,truncate:2000` | Set to log protocol messages between DAST and the Chromium browser. | |
| `DAST_BROWSER_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `600ms` | The maximum amount of time to wait for an element before determining it is ready for analysis. |
| `DAST_BROWSER_EXCLUDED_ELEMENTS` | selector | `a[href='2.html'],css:.no-follow` | Comma-separated list of selectors that are ignored when scanning. |
| `DAST_BROWSER_EXCLUDED_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered excluded and connections are forcibly dropped. |
| `DAST_BROWSER_EXCLUDED_HOSTS` | List of strings | `site.com,another.com` | Hostnames included in this variable are considered excluded and connections are forcibly dropped. |
| `DAST_BROWSER_EXTRACT_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `5s` | The maximum amount of time to allow the browser to extract newly found elements or navigations. |
| `DAST_BROWSER_FILE_LOG` | List of strings | `brows:debug,auth:debug` | A list of modules and their intended logging level for use in the file log. |
| `DAST_BROWSER_FILE_LOG_PATH` | string | `/output/browserker.log` | Set to the path of the file log. |
@ -190,8 +190,8 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_BROWSER_NAVIGATION_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis after a navigation completes. |
| `DAST_BROWSER_NAVIGATION_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `15s` | The maximum amount of time to wait for a browser to navigate from one page to another. |
| `DAST_BROWSER_NUMBER_OF_BROWSERS` | number | `3` | The maximum number of concurrent browser instances to use. For shared runners on GitLab.com, we recommended a maximum of three. Private runners with more resources may benefit from a higher number, but are likely to produce little benefit after five to seven instances. |
| `DAST_BROWSER_PAGE_LOADING_SELECTOR` | selector | `css:#page-is-loading` | Selector that when is no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_BROWSER_PAGE_READY_SELECTOR` |
| `DAST_BROWSER_PAGE_READY_SELECTOR` | selector | `css:#page-is-ready` | Selector that when detected as visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_BROWSER_PAGE_LOADING_SELECTOR` |
| `DAST_BROWSER_PAGE_LOADING_SELECTOR` | selector | `css:#page-is-loading` | Selector that when is no longer visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_BROWSER_PAGE_READY_SELECTOR`. |
| `DAST_BROWSER_PAGE_READY_SELECTOR` | selector | `css:#page-is-ready` | Selector that when detected as visible on the page, indicates to the analyzer that the page has finished loading and the scan can continue. Cannot be used with `DAST_BROWSER_PAGE_LOADING_SELECTOR`. |
| `DAST_BROWSER_SCAN` | boolean | `true` | Required to be `true` to run a browser-based scan. |
| `DAST_BROWSER_SEARCH_ELEMENT_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `3s` | The maximum amount of time to allow the browser to search for new elements or user actions. |
| `DAST_BROWSER_STABILITY_TIMEOUT` | [Duration string](https://pkg.go.dev/time#ParseDuration) | `7s` | The maximum amount of time to wait for a browser to consider a page loaded and ready for analysis. |
@ -201,8 +201,8 @@ For authentication CI/CD variables, see [Authentication](authentication.md).
| `DAST_PATHS` | string | `/page1.html,/category1/page3.html` | Set to a comma-separated list of URL paths relative to `DAST_WEBSITE` for DAST to scan. |
| `DAST_PATHS_FILE` | string | `/builds/project/urls.txt` | Set to a file path containing a list of URL paths relative to `DAST_WEBSITE` for DAST to scan. The file must be plain text with one path per line. |
| `DAST_PKCS12_CERTIFICATE_BASE64` | string | `ZGZkZ2p5NGd...` | The PKCS12 certificate used for sites that require Mutual TLS. Must be encoded as base64 text. |
| `DAST_PKCS12_PASSWORD` | string | `password` | The password of the certificate used in `DAST_PKCS12_CERTIFICATE_BASE64`. Create sensitive [custom CI/CI variables](../../../ci/variables/index.md#define-a-cicd-variable-in-the-ui) using the GitLab UI. |
| `DAST_REQUEST_HEADERS` | string | `Cache-control:no-cache` | Set to a comma-separated list of request header names and values. |
| `DAST_PKCS12_PASSWORD` | string | `password` | The password of the certificate used in `DAST_PKCS12_CERTIFICATE_BASE64`. Create sensitive [custom CI/CI variables](../../../ci/variables/index.md#define-a-cicd-variable-in-the-ui) using the GitLab UI. |
| `DAST_REQUEST_HEADERS` | string | `Cache-control:no-cache` | Set to a comma-separated list of request header names and values. |
| `DAST_SKIP_TARGET_CHECK` | boolean | `true` | Set to `true` to prevent DAST from checking that the target is available before scanning. Default: `false`. |
| `DAST_TARGET_AVAILABILITY_TIMEOUT` | number | `60` | Time limit in seconds to wait for target availability. |
| `DAST_WEBSITE` | URL | `https://example.com` | The URL of the website to scan. |
@ -275,16 +275,6 @@ dast:
NOTE:
Adjusting these values may impact scan time because they adjust how long each browser waits for various activities to complete.
## Artifacts
Using the latest version of the DAST [template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/DAST.latest.gitlab-ci.yml) these artifacts are exposed for download by default.
The list of artifacts includes the following files:
- `gl-dast-debug-auth-report.html`
- `gl-dast-debug-crawl-report.html`
- `gl-dast-crawl-graph.svg`
## Troubleshooting
See [troubleshooting](browser_based_troubleshooting.md) for more information.

View File

@ -20,8 +20,8 @@ This method of scanning is also capable of parsing and identifying over 500 diff
Licenses not in the SPDX list are reported as "Unknown". License information can also be extracted from packages that are dual-licensed, or have multiple different licenses that apply.
To enable license detection using Dependency Scanning in a project,
include the `Jobs/Dependency-Scanning.yml` template in its CI configuration,
but do not include the `Jobs/License-Scanning.yml` template.
include the `Jobs/Dependency-Scanning.gitlab-ci.yml` template in its CI configuration,
but do not include the `Jobs/License-Scanning.gitlab-ci.yml` template.
## Requirements

View File

@ -85,7 +85,7 @@ or API. However, administrators can use a workaround:
group = Group.find(109)
# Create the group bot user. For further group access tokens, the username should be `group_{group_id}_bot_{random_string}` and email address `group_{group_id}_bot_{random_string}@noreply.{Gitlab.config.gitlab.host}`.
random_string = SecureRandom.hex(8)
random_string = SecureRandom.hex(16)
bot = Users::CreateService.new(admin, { name: 'group_token', username: "group_#{group.id}_bot_#{random_string}", email: "group_#{group.id}_bot_#{random_string}@noreply.#{Gitlab.config.gitlab.host}", user_type: :project_bot }).execute
# Confirm the group bot.

View File

@ -20,6 +20,8 @@ module API
end
def authorize_push_to_branch!(branch)
authenticate!
unless user_access.can_push_to_branch?(branch)
forbidden!("You are not allowed to push into this branch")
end
@ -127,6 +129,8 @@ module API
tags %w[commits]
failure [
{ code: 400, message: 'Bad request' },
{ code: 401, message: 'Unauthorized' },
{ code: 403, message: 'Forbidden' },
{ code: 404, message: 'Not found' }
]
detail 'This feature was introduced in GitLab 8.13'

View File

@ -9,3 +9,4 @@ Example:
spec/migrations/20230213215230_queue_my_batched_migration_spec.rb
lib/gitlab/background_migration/my_batched_migration.rb
spec/lib/gitlab/background_migration/my_batched_migration_spec.rb
db/docs/batched_background_migrations/my_batched_migration.yml

View File

@ -40,6 +40,13 @@ module BatchedBackgroundMigration
)
end
def create_dictionary_file
template(
"batched_background_migration_dictionary.template",
File.join("db/docs/batched_background_migrations/#{file_name}.yml")
)
end
def db_migrate_path
super.sub("migrate", "post_migrate")
end

View File

@ -0,0 +1,6 @@
---
migration_job_name: <%= class_name %>
description: # Please capture what <%= class_name %> does
feature_category: <%= feature_category %>
introduced_by_url: # URL of the MR (or issue/commit) that introduced the migration
milestone:

View File

@ -239,7 +239,7 @@ module Gitlab
sort_by = 'name' if sort_by == 'name_asc'
enum_value = Gitaly::FindLocalBranchesRequest::SortBy.resolve(sort_by.upcase.to_sym)
raise ArgumentError, "Invalid sort_by key `#{sort_by}`" unless enum_value
return Gitaly::FindLocalBranchesRequest::SortBy::NAME unless enum_value
enum_value
end

View File

@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'securerandom'
module Gitlab
module Utils
class UsernameAndEmailGenerator
include Gitlab::Utils::StrongMemoize
def initialize(username_prefix:, email_domain: Gitlab.config.gitlab.host)
@username_prefix = username_prefix
@email_domain = email_domain
end
def username
uniquify.string(->(counter) { Kernel.sprintf(username_pattern, counter) }) do |suggested_username|
::Namespace.by_path(suggested_username) || ::User.find_by_any_email(email_for(suggested_username))
end
end
strong_memoize_attr :username
def email
email_for(username)
end
strong_memoize_attr :email
private
def username_pattern
"#{@username_prefix}_#{SecureRandom.hex(16)}%s"
end
def email_for(name)
"#{name}@#{@email_domain}"
end
def uniquify
Gitlab::Utils::Uniquify.new
end
end
end
end

View File

@ -8581,7 +8581,7 @@ msgstr ""
msgid "Checkout|Coupon code (optional)"
msgstr ""
msgid "Checkout|Coupon has been applied to your purchase"
msgid "Checkout|Coupon has been applied and by continuing with your purchase, you accept and agree to the %{linkStart}Coupon Terms%{linkEnd}."
msgstr ""
msgid "Checkout|Create a new group"
@ -20538,6 +20538,9 @@ msgstr ""
msgid "GroupsNew|GitLab source instance URL"
msgstr ""
msgid "GroupsNew|Groups"
msgstr ""
msgid "GroupsNew|Groups can also be nested by creating %{linkStart}subgroups%{linkEnd}."
msgstr ""
@ -25288,9 +25291,6 @@ msgstr ""
msgid "LearnGitlab|Contact your administrator to start a free Ultimate trial."
msgstr ""
msgid "LearnGitlab|Creating your onboarding experience..."
msgstr ""
msgid "LearnGitlab|Ok, let's go"
msgstr ""
@ -34532,6 +34532,9 @@ msgstr ""
msgid "ProjectsNew|Project description %{tag_start}(optional)%{tag_end}"
msgstr ""
msgid "ProjectsNew|Projects"
msgstr ""
msgid "ProjectsNew|Recommended if you're new to GitLab"
msgstr ""

View File

@ -4,8 +4,18 @@ module QA
module Page
module Component
module Dropdown
def select_item(item_text)
find('li.gl-new-dropdown-item', text: item_text, match: :prefer_exact).click
# Find and click item using css selector and matching text
# If item_text is not provided, select the first item that matches the given css selector
#
# @param [String] item_text
# @param [String] css - css selector of the item
# @return [void]
def select_item(item_text, css: 'li.gl-new-dropdown-item')
if item_text
find(css, text: item_text, match: :prefer_exact).click
else
find(css, match: :first).click
end
end
def has_item?(item_text)
@ -65,8 +75,8 @@ module QA
find('li.gl-new-dropdown-item span:nth-child(2)', text: item_text, exact_text: true).click
end
def expand_select_list
find('.gl-new-dropdown-toggle').click
def expand_select_list(css: '.gl-new-dropdown-toggle')
find(css).click
end
def wait_for_search_to_complete

View File

@ -13,6 +13,10 @@ module QA
def has_alert_with_title?(title)
has_link?(title, wait: 5)
end
def go_to_alert(title)
click_link_with_text(title)
end
end
end
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
module QA
module Page
module Project
module Monitor
module Alerts
class Show < Page::Base
view 'app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue' do
element :alert_system_note_container
end
def go_to_activity_feed_tab
click_link_with_text('Activity feed')
end
def has_system_note?(text)
has_element?(:alert_system_note_container, text: text)
end
end
end
end
end
end
end

View File

@ -31,6 +31,22 @@ module QA
end
end
def go_to_monitor_on_call_schedules
hover_monitor do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'On-call Schedules')
end
end
end
def go_to_monitor_escalation_policies
hover_monitor do
within_submenu do
click_element(:sidebar_menu_item_link, menu_item: 'Escalation Policies')
end
end
end
private
def hover_monitor

View File

@ -25,6 +25,7 @@ module QA
tag
label
variable
system_note
].each do |predicate|
RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs|
match do |page_object|

View File

@ -170,8 +170,6 @@ class CreatePipelineFailureIncident
Additionally, a message can be posted in `#backend_maintainers` or `#frontend_maintainers` to get a maintainer take a look at the fix ASAP.
- Cherry picking a change that was used to fix a similar master-broken issue.
In both cases, make sure to add the ~"pipeline:expedite" label to speed up the `stable`-fixing pipelines.
### Resolution
Add a comment to this issue describing how this incident could have been prevented earlier in the Merge Request pipeline (rather than the merge commit pipeline).

View File

@ -1231,6 +1231,19 @@ RSpec.describe ProjectsController, feature_category: :projects do
expect(response).to have_gitlab_http_status(:success)
end
end
context 'when sort param is invalid' do
let(:request) { get :refs, params: { namespace_id: project.namespace, id: project, sort: 'invalid' } }
it 'uses default sort by name' do
request
expect(response).to have_gitlab_http_status(:success)
expect(json_response['Branches']).to include('master')
expect(json_response['Tags']).to include('v1.0.0')
expect(json_response['Commits']).to be_nil
end
end
end
describe 'POST #preview_markdown' do

View File

@ -44,6 +44,14 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
"#{get_cell_level_selector(contributions)}[title='#{contribution_text}<br /><span class=\"gl-text-gray-300\">#{date}</span>']"
end
def get_days_of_week
page.all('[data-testid="user-contrib-cell-group"]')[1]
.all('[data-testid="user-contrib-cell"]')
.map do |node|
node[:title].match(/(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)/)[0]
end
end
def push_code_contribution
event = create(:push_event, project: contributed_project, author: user)
@ -237,6 +245,32 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
it_behaves_like 'hidden activity calendar'
end
end
describe 'first_day_of_week setting' do
context 'when first day of the week is set to Monday' do
before do
stub_application_setting(first_day_of_week: 1)
end
include_context 'when user page is visited'
it 'shows calendar with Monday as the first day of the week' do
expect(get_days_of_week).to eq(%w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday])
end
end
context 'when first day of the week is set to Sunday' do
before do
stub_application_setting(first_day_of_week: 0)
end
include_context 'when user page is visited'
it 'shows calendar with Sunday as the first day of the week' do
expect(get_days_of_week).to eq(%w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday])
end
end
end
end
context 'with `profile_tabs_vue` feature flag enabled' do
@ -346,5 +380,31 @@ RSpec.describe 'Contributions Calendar', :js, feature_category: :user_profile do
it_behaves_like 'hidden activity calendar'
end
end
describe 'first_day_of_week setting' do
context 'when first day of the week is set to Monday' do
before do
stub_application_setting(first_day_of_week: 1)
end
include_context 'when user page is visited'
it 'shows calendar with Monday as the first day of the week' do
expect(get_days_of_week).to eq(%w[Monday Tuesday Wednesday Thursday Friday Saturday Sunday])
end
end
context 'when first day of the week is set to Sunday' do
before do
stub_application_setting(first_day_of_week: 0)
end
include_context 'when user page is visited'
it 'shows calendar with Sunday as the first day of the week' do
expect(get_days_of_week).to eq(%w[Sunday Monday Tuesday Wednesday Thursday Friday Saturday])
end
end
end
end
end

View File

@ -253,6 +253,6 @@ RSpec.describe 'Dashboard Projects', feature_category: :projects do
# - ProjectsHelper#load_pipeline_status / Ci::CommitWithPipeline#last_pipeline
# - Ci::Pipeline#detailed_status
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(0)
expect { visit dashboard_projects_path }.not_to exceed_query_limit(control).with_threshold(4)
end
end

View File

@ -17,12 +17,15 @@ import {
STATE_SUCCESS,
STATE_UNSUPPORTED,
STATE_WAITING,
WEBAUTHN_REGISTER,
} from '~/authentication/webauthn/constants';
import * as WebAuthnUtils from '~/authentication/webauthn/util';
import WebAuthnError from '~/authentication/webauthn/error';
const csrfToken = 'mock-csrf-token';
jest.mock('~/lib/utils/csrf', () => ({ token: csrfToken }));
jest.mock('~/authentication/webauthn/util');
jest.mock('~/authentication/webauthn/error');
describe('Registration', () => {
const initialError = null;
@ -221,10 +224,12 @@ describe('Registration', () => {
it('shows an error message and a retry button', async () => {
createComponent();
mockCreate.mockRejectedValueOnce(new Error());
const error = new Error();
mockCreate.mockRejectedValueOnce(error);
await setupDevice();
expect(WebAuthnError).toHaveBeenCalledWith(error, WEBAUTHN_REGISTER);
expect(wrapper.findComponent(GlAlert).props()).toMatchObject({
variant: 'danger',
secondaryButtonText: I18N_BUTTON_TRY_AGAIN,

View File

@ -1,16 +1,17 @@
import setWindowLocation from 'helpers/set_window_location_helper';
import WebAuthnError from '~/authentication/webauthn/error';
import { WEBAUTHN_AUTHENTICATE, WEBAUTHN_REGISTER } from '~/authentication/webauthn/constants';
describe('WebAuthnError', () => {
it.each([
[
'NotSupportedError',
'Your device is not compatible with GitLab. Please try another device',
'authenticate',
WEBAUTHN_AUTHENTICATE,
],
['InvalidStateError', 'This device has not been registered with us.', 'authenticate'],
['InvalidStateError', 'This device has already been registered with us.', 'register'],
['UnknownError', 'There was a problem communicating with your device.', 'register'],
['InvalidStateError', 'This device has not been registered with us.', WEBAUTHN_AUTHENTICATE],
['InvalidStateError', 'This device has already been registered with us.', WEBAUTHN_REGISTER],
['UnknownError', 'There was a problem communicating with your device.', WEBAUTHN_REGISTER],
])('exception %s will have message %s, flow type: %s', (exception, expectedMessage, flowType) => {
expect(new WebAuthnError(new DOMException('', exception), flowType).message()).toEqual(
expectedMessage,
@ -24,7 +25,7 @@ describe('WebAuthnError', () => {
const expectedMessage =
'WebAuthn only works with HTTPS-enabled websites. Contact your administrator for more details.';
expect(
new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
@ -33,7 +34,7 @@ describe('WebAuthnError', () => {
const expectedMessage = 'There was a problem communicating with your device.';
expect(
new WebAuthnError(new DOMException('', 'SecurityError'), 'authenticate').message(),
new WebAuthnError(new DOMException('', 'SecurityError'), WEBAUTHN_AUTHENTICATE).message(),
).toEqual(expectedMessage);
});
});

View File

@ -5,10 +5,10 @@ import waitForPromises from 'helpers/wait_for_promises';
import blobBundle from '~/blob_edit/blob_bundle';
import SourceEditor from '~/blob_edit/edit_blob';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
jest.mock('~/blob_edit/edit_blob');
jest.mock('~/flash');
jest.mock('~/alert');
describe('BlobBundle', () => {
it('does not load SourceEditor by default', () => {

View File

@ -5,7 +5,7 @@ import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_alert_to_local_storage';
@ -21,7 +21,7 @@ const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
Vue.use(VueApollo);
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
redirectTo: jest.fn(),

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');

View File

@ -5,14 +5,14 @@ import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registration/registration_token_reset_dropdown_item.vue';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
import runnersRegistrationTokenResetMutation from '~/ci/runner/graphql/list/runners_registration_token_reset.mutation.graphql';
import { captureException } from '~/ci/runner/sentry_utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
Vue.use(VueApollo);

View File

@ -2,7 +2,7 @@ import Vue from 'vue';
import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { s__ } from '~/locale';
@ -15,7 +15,7 @@ import { allRunnersData } from '../mock_data';
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
describe('RunnerBulkDelete', () => {
let wrapper;

View File

@ -8,7 +8,7 @@ import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutat
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
@ -21,7 +21,7 @@ const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
import RunnerJobsTable from '~/ci/runner/components/runner_jobs_table.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
@ -15,7 +15,7 @@ import runnerJobsQuery from '~/ci/runner/graphql/show/runner_jobs.query.graphql'
import { runnerData, runnerJobsData } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;

View File

@ -7,7 +7,7 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import runnerToggleActiveMutation from '~/ci/runner/graphql/shared/runner_toggle_active.mutation.graphql';
import waitForPromises from 'helpers/wait_for_promises';
import { captureException } from '~/ci/runner/sentry_utils';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@ -22,7 +22,7 @@ const mockRunner = allRunnersData.data.runners.nodes[0];
Vue.use(VueApollo);
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerPauseButton', () => {

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { sprintf } from '~/locale';
import {
I18N_ASSIGNED_PROJECTS,
@ -22,7 +22,7 @@ import runnerProjectsQuery from '~/ci/runner/graphql/show/runner_projects.query.
import { runnerData, runnerProjectsData } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
const mockRunner = runnerData.data.runner;

View File

@ -5,7 +5,7 @@ import { __ } from '~/locale';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import RunnerUpdateForm from '~/ci/runner/components/runner_update_form.vue';
import {
@ -21,7 +21,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerFormData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');

View File

@ -3,14 +3,14 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/vue_shared/components/filtered_search_bar/filtered_search_utils', () => ({
...jest.requireActual('~/vue_shared/components/filtered_search_bar/filtered_search_utils'),

View File

@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@ -24,7 +24,7 @@ import { saveAlertToLocalStorage } from '~/ci/runner/local_storage_alert/save_al
import { runnerData } from '../mock_data';
jest.mock('~/ci/runner/local_storage_alert/save_alert_to_local_storage');
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
jest.mock('~/lib/utils/url_utility');

View File

@ -2,9 +2,9 @@ import AccessorUtilities from '~/lib/utils/accessor';
import { showAlertFromLocalStorage } from '~/ci/runner/local_storage_alert/show_alert_from_local_storage';
import { LOCAL_STORAGE_ALERT_KEY } from '~/ci/runner/local_storage_alert/constants';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
jest.mock('~/flash');
jest.mock('~/alert');
describe('showAlertFromLocalStorage', () => {
useLocalStorageSpy();

View File

@ -8,7 +8,7 @@ import Audio from '~/content_editor/extensions/audio';
import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import { VARIANT_DANGER } from '~/flash';
import { VARIANT_DANGER } from '~/alert';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';

View File

@ -3,7 +3,7 @@ import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
import Bold from '~/content_editor/extensions/bold';
import { VARIANT_DANGER } from '~/flash';
import { VARIANT_DANGER } from '~/alert';
import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';

View File

@ -2,12 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
let mock;

View File

@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import {
HTTP_STATUS_BAD_REQUEST,
@ -14,7 +14,7 @@ import Poll from '~/lib/utils/poll';
let mockedAdapter;
let mockedRestart;
jest.mock('~/flash');
jest.mock('~/alert');
jest.mock('~/lib/utils/url_utility');
describe('Sentry error details store actions', () => {
@ -48,7 +48,7 @@ describe('Sentry error details store actions', () => {
);
});
it('should show flash on API error', async () => {
it('should show alert on API error', async () => {
mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(

View File

@ -2,11 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash');
jest.mock('~/alert');
describe('error tracking actions', () => {
let mock;
@ -38,7 +38,7 @@ describe('error tracking actions', () => {
);
});
it('should show flash on API error', async () => {
it('should show alert on API error', async () => {
mock.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(

View File

@ -6,10 +6,10 @@ import {
TOAST_MESSAGE_LOCALSTORAGE_KEY,
TOAST_MESSAGE_SUCCESSFUL,
} from '~/invite_members/constants';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
jest.mock('~/flash');
jest.mock('~/alert');
useLocalStorageSpy();
describe('Display Successful Invitation Alert', () => {

View File

@ -6,7 +6,7 @@ describe('App component', () => {
let wrapper;
const createComponent = (propsData = {}) => {
wrapper = shallowMount(App, { propsData });
wrapper = shallowMount(App, { propsData: { groupsUrl: '/dashboard/groups', ...propsData } });
};
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
@ -24,20 +24,27 @@ describe('App component', () => {
createComponent();
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
{ href: '/dashboard/groups', text: 'Groups' },
{ href: '#', text: 'New group' },
]);
expect(findCreateGroupPanel().title).toBe('Create group');
});
it('creates correct component for subgroup creation', () => {
const props = { parentGroupName: 'parent', importExistingGroupPath: '/path' };
const detailProps = {
parentGroupName: 'parent',
importExistingGroupPath: '/path',
};
const props = { ...detailProps, parentGroupUrl: '/parent' };
createComponent(props);
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
{ href: '#', text: 'New group' },
{ href: '/parent', text: 'parent' },
{ href: '#', text: 'New subgroup' },
]);
expect(findCreateGroupPanel().title).toBe('Create subgroup');
expect(findCreateGroupPanel().detailProps).toEqual(props);
expect(findCreateGroupPanel().detailProps).toEqual(detailProps);
});
});

View File

@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PersistentUserCallout from '~/persistent_user_callout';
jest.mock('~/flash');
jest.mock('~/alert');
describe('PersistentUserCallout', () => {
const dismissEndpoint = '/dismiss';

View File

@ -6,9 +6,13 @@ describe('Experimental new project creation app', () => {
let wrapper;
const createComponent = (propsData) => {
wrapper = shallowMount(App, { propsData });
wrapper = shallowMount(App, {
propsData: { projectsUrl: '/dashboard/projects', ...propsData },
});
};
const findNewNamespacePage = () => wrapper.findComponent(NewNamespacePage);
afterEach(() => {
wrapper.destroy();
});
@ -34,11 +38,28 @@ describe('Experimental new project creation app', () => {
expect(
Boolean(
wrapper
.findComponent(NewNamespacePage)
findNewNamespacePage()
.props()
.panels.find((p) => p.name === 'cicd_for_external_repo'),
),
).toBe(isCiCdAvailable);
});
it('creates correct breadcrumbs for top-level projects', () => {
createComponent();
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
{ href: '/dashboard/projects', text: 'Projects' },
{ href: '#', text: 'New project' },
]);
});
it('creates correct breadcrumbs for projects within groups', () => {
createComponent({ parentGroupUrl: '/parent-group', parentGroupName: 'Parent Group' });
expect(findNewNamespacePage().props('initialBreadcrumbs')).toEqual([
{ href: '/parent-group', text: 'Parent Group' },
{ href: '#', text: 'New project' },
]);
});
});

View File

@ -2,7 +2,7 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['DesignAtVersion'] do
RSpec.describe GitlabSchema.types['DesignAtVersion'], feature_category: :portfolio_management do
it_behaves_like 'a GraphQL type with design fields' do
let(:extra_design_fields) { %i[version design] }
let_it_be(:design) { create(:design, :with_versions) }

View File

@ -2,13 +2,16 @@
require 'spec_helper'
RSpec.describe GitlabSchema.types['Design'] do
RSpec.describe GitlabSchema.types['Design'], feature_category: :portfolio_management do
specify { expect(described_class.interfaces).to include(Types::CurrentUserTodos) }
specify { expect(described_class.interfaces).to include(Types::TodoableInterface) }
it_behaves_like 'a GraphQL type with design fields' do
let(:extra_design_fields) { %i[notes current_user_todos discussions versions web_url commenters] }
let(:extra_design_fields) do
%i[notes current_user_todos discussions versions web_url commenters description descriptionHtml]
end
let_it_be(:design) { create(:design, :with_versions) }
let(:object_id) { GitlabSchema.id_from_object(design) }
let_it_be(:object_id_b) { GitlabSchema.id_from_object(create(:design, :with_versions)) }

View File

@ -25,6 +25,7 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
let(:expected_migration_spec_file) { load_expected_file('queue_my_batched_migration_spec.txt') }
let(:expected_migration_job_file) { load_expected_file('my_batched_migration.txt') }
let(:expected_migration_job_spec_file) { load_expected_file('my_batched_migration_spec_matcher.txt') }
let(:expected_migration_dictionary) { load_expected_file('my_batched_migration_dictionary.txt') }
it 'generates expected files' do
run_generator %w[my_batched_migration --table_name=projects --column_name=id --feature_category=database]
@ -45,6 +46,10 @@ RSpec.describe BatchedBackgroundMigration::BatchedBackgroundMigrationGenerator,
# Regex is used to match the dynamic schema: <version> in the specs
expect(migration_job_spec_file).to match(/#{expected_migration_job_spec_file}/)
end
assert_file('db/docs/batched_background_migrations/my_batched_migration.yml') do |migration_dictionary|
expect(migration_dictionary).to eq(expected_migration_dictionary)
end
end
end

View File

@ -0,0 +1,6 @@
---
migration_job_name: MyBatchedMigration
description: # Please capture what MyBatchedMigration does
feature_category: database
introduced_by_url: # URL of the MR (or issue/commit) that introduced the migration
milestone:

View File

@ -213,8 +213,13 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
client.local_branches(sort_by: 'name_asc')
end
it 'raises an argument error if an invalid sort_by parameter is passed' do
expect { client.local_branches(sort_by: 'invalid_sort') }.to raise_error(ArgumentError)
it 'uses default sort by name' do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:find_local_branches)
.with(gitaly_request_with_params(sort_by: :NAME), kind_of(Hash))
.and_return([])
client.local_branches(sort_by: 'invalid')
end
end
@ -270,6 +275,17 @@ RSpec.describe Gitlab::GitalyClient::RefService, feature_category: :gitaly do
client.tags(sort_by: 'version_asc')
end
end
context 'when sorting option is invalid' do
it 'uses default sort by name' do
expect_any_instance_of(Gitaly::RefService::Stub)
.to receive(:find_all_tags)
.with(gitaly_request_with_params(sort_by: nil), kind_of(Hash))
.and_return([])
client.tags(sort_by: 'invalid')
end
end
end
context 'with pagination option' do

View File

@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Gitlab::Utils::UsernameAndEmailGenerator, feature_category: :system_access do
let(:username_prefix) { 'username_prefix' }
let(:email_domain) { 'example.com' }
subject { described_class.new(username_prefix: username_prefix, email_domain: email_domain) }
describe 'email domain' do
it 'defaults to `Gitlab.config.gitlab.host`' do
expect(described_class.new(username_prefix: username_prefix).email).to end_with("@#{Gitlab.config.gitlab.host}")
end
context 'when specified' do
it 'uses the specified email domain' do
expect(subject.email).to end_with("@#{email_domain}")
end
end
end
include_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator'
end

View File

@ -1,11 +1,11 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe DesignManagement::DesignPolicy do
RSpec.describe DesignManagement::DesignPolicy, feature_category: :portfolio_management do
include DesignManagementTestHelpers
let(:guest_design_abilities) { %i[read_design] }
let(:developer_design_abilities) { %i[create_design destroy_design move_design] }
let(:developer_design_abilities) { %i[create_design destroy_design move_design update_design] }
let(:design_abilities) { guest_design_abilities + developer_design_abilities }
let_it_be(:guest) { create(:user) }

View File

@ -425,6 +425,27 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
describe "POST /projects/:id/repository/commits" do
let!(:url) { "/projects/#{project_id}/repository/commits" }
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
let(:params) do
{
branch: 'master',
commit_message: 'message',
actions: [
{
action: 'create',
file_path: '/test.rb',
content: 'puts 8'
}
]
}
end
it_behaves_like '401 response' do
let(:request) { post api(url), params: params }
end
end
it 'returns a 403 unauthorized for user without permissions' do
post api(url, guest)
@ -1775,7 +1796,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: 'master' } }
end
end
@ -1955,7 +1976,7 @@ RSpec.describe API::Commits, feature_category: :source_code_management do
context 'when unauthenticated', 'and project is public' do
let_it_be(:project) { create(:project, :public, :repository) }
it_behaves_like '403 response' do
it_behaves_like '401 response' do
let(:request) { post api(route), params: { branch: branch } }
end
end

View File

@ -0,0 +1,77 @@
# frozen_string_literal: true
require "spec_helper"
RSpec.describe "updating designs", feature_category: :design_management do
include GraphqlHelpers
include DesignManagementTestHelpers
let_it_be(:issue) { create(:issue) }
let_it_be_with_reload(:design) { create(:design, description: 'old description', issue: issue) }
let_it_be(:developer) { create(:user, developer_projects: [issue.project]) }
let(:user) { developer }
let(:description) { 'new description' }
let(:mutation) do
input = {
id: design.to_global_id.to_s,
description: description
}.compact
graphql_mutation(:design_management_update, input, <<~FIELDS)
errors
design {
description
descriptionHtml
}
FIELDS
end
let(:update_design) { post_graphql_mutation(mutation, current_user: user) }
let(:mutation_response) { graphql_mutation_response(:design_management_update) }
before do
enable_design_management
end
it 'updates design' do
update_design
expect(graphql_errors).not_to be_present
expect(mutation_response).to eq(
'errors' => [],
'design' => {
'description' => description,
'descriptionHtml' => "<p data-sourcepos=\"1:1-1:15\" dir=\"auto\">#{description}</p>"
}
)
end
context 'when the user is not allowed to update designs' do
let(:user) { create(:user) }
it 'returns an error' do
update_design
expect(graphql_errors).to be_present
end
end
context 'when update fails' do
let(:description) { 'x' * 1_000_001 }
it 'returns an error' do
update_design
expect(graphql_errors).not_to be_present
expect(mutation_response).to eq(
'errors' => ["Description is too long (maximum is 1000000 characters)"],
'design' => {
'description' => 'old description',
'descriptionHtml' => '<p data-sourcepos="1:1-1:15" dir="auto">old description</p>'
}
)
end
end
end

View File

@ -99,27 +99,20 @@ RSpec.describe ResourceAccessTokens::CreateService, feature_category: :system_ac
end
end
context 'bot email' do
it 'check email domain' do
response = subject
access_token = response.payload[:access_token]
context 'bot username and email' do
include_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do
subject do
response = described_class.new(user, resource, params).execute
response.payload[:access_token].user
end
expect(access_token.user.email).to end_with("@noreply.#{Gitlab.config.gitlab.host}")
end
let(:username_prefix) do
"#{resource.class.name.downcase}_#{resource.id}_bot"
end
it 'contains SecureRandom part' do
expect(SecureRandom).to receive(:hex).at_least(:once).and_return('randomhex')
response = subject
access_token = response.payload[:access_token]
expect(access_token.user.email).to include('_randomhex@noreply')
end
it 'email is the same as username' do
response = subject
access_token = response.payload[:access_token]
expect(access_token.user.email).to include(access_token.user.username)
let(:email_domain) do
"noreply.#{Gitlab.config.gitlab.host}"
end
end
end

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
RSpec.shared_examples 'username and email pair is generated by Gitlab::Utils::UsernameAndEmailGenerator' do
let(:randomhex) { 'randomhex' }
it 'check email domain' do
expect(subject.email).to end_with("@#{email_domain}")
end
it 'contains SecureRandom part' do
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
expect(subject.username).to include("_#{randomhex}")
expect(subject.email).to include("_#{randomhex}@")
end
it 'email name is the same as username' do
expect(subject.email).to include("#{subject.username}@")
end
context 'when conflicts' do
let(:reserved_username) { "#{username_prefix}_#{randomhex}" }
let(:reserved_email) { "#{reserved_username}@#{email_domain}" }
shared_examples 'uniquifies username and email' do
it 'uniquifies username and email' do
expect(subject.username).to eq("#{reserved_username}1")
expect(subject.email).to include("#{subject.username}@")
end
end
context 'when username is reserved' do
context 'when username is reserved by user' do
before do
create(:user, username: reserved_username)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
context 'when it conflicts with top-level group namespace' do
before do
create(:group, path: reserved_username)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
context 'when it conflicts with top-level group namespace that includes upcased characters' do
before do
create(:group, path: reserved_username.upcase)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
end
context 'when email is reserved' do
context 'when it conflicts with confirmed primary email' do
before do
create(:user, email: reserved_email)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
context 'when it conflicts with unconfirmed primary email' do
before do
create(:user, :unconfirmed, email: reserved_email)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
context 'when it conflicts with confirmed secondary email' do
before do
create(:email, :confirmed, email: reserved_email)
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
include_examples 'uniquifies username and email'
end
end
context 'when email and username is reserved' do
before do
create(:user, email: reserved_email)
create(:user, username: "#{reserved_username}1")
allow(SecureRandom).to receive(:hex).at_least(:once).and_return(randomhex)
end
it 'uniquifies username and email' do
expect(subject.username).to eq("#{reserved_username}2")
expect(subject.email).to include("#{subject.username}@")
end
end
end
end

View File

@ -21,6 +21,23 @@ RSpec.shared_examples '400 response' do
end
end
RSpec.shared_examples '401 response' do
let(:message) { nil }
before do
# Fires the request
request
end
it 'returns 401' do
expect(response).to have_gitlab_http_status(:unauthorized)
if message.present?
expect(json_response['message']).to eq(message)
end
end
end
RSpec.shared_examples '403 response' do
before do
# Fires the request

Some files were not shown because too many files have changed in this diff Show More