Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
94a5041917
commit
ee772e0c77
|
@ -12,6 +12,21 @@
|
|||
|
||||
## Proposal
|
||||
|
||||
## Additional details
|
||||
<!--
|
||||
_NOTE: If the issue has addressed all of these questions, this separate section can be removed._
|
||||
-->
|
||||
|
||||
Some relevant technical details, if applicable, such as:
|
||||
|
||||
- Does this need a ~"feature flag"?
|
||||
- Is there an example response showing the data structure that should be returned (new endpoints only)?
|
||||
- What permissions should be used?
|
||||
- Is this EE or CE?
|
||||
- [ ] EE
|
||||
- [ ] CE
|
||||
- Additional comments:
|
||||
|
||||
## Implementation Table
|
||||
|
||||
<!--
|
||||
|
|
|
@ -258,16 +258,6 @@ Layout/HashAlignment:
|
|||
- 'lib/tasks/gitlab/import_export/export.rake'
|
||||
- 'lib/tasks/gitlab/import_export/import.rake'
|
||||
- 'lib/tasks/tanuki_emoji.rake'
|
||||
- 'qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/13_secure/change_vulnerability_status_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/13_secure/security_reports_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_enforced_sso_git_access_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/1_manage/group/group_saml_non_enforced_sso_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/project_templates_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/3_create/repository/push_rules_spec.rb'
|
||||
- 'qa/qa/specs/features/ee/browser_ui/5_package/dependency_proxy_sso_spec.rb'
|
||||
- 'qa/qa/support/loglinking.rb'
|
||||
- 'qa/spec/support/loglinking_spec.rb'
|
||||
- 'spec/controllers/concerns/product_analytics_tracking_spec.rb'
|
||||
- 'spec/controllers/concerns/redis_tracking_spec.rb'
|
||||
- 'spec/controllers/import/bitbucket_controller_spec.rb'
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<script>
|
||||
import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
|
||||
import {
|
||||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormCheckbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
} from '@gitlab/ui';
|
||||
import { get as getPropValueByPath, isEmpty } from 'lodash';
|
||||
import { produce } from 'immer';
|
||||
import { MountingPortal } from 'portal-vue';
|
||||
|
@ -26,6 +34,7 @@ export default {
|
|||
GlAlert,
|
||||
GlButton,
|
||||
GlDrawer,
|
||||
GlFormCheckbox,
|
||||
GlFormGroup,
|
||||
GlFormInput,
|
||||
GlFormSelect,
|
||||
|
@ -113,7 +122,9 @@ export default {
|
|||
const { fields, model } = this;
|
||||
|
||||
return fields.some((field) => {
|
||||
return field.required && isEmpty(model[field.name]);
|
||||
return (
|
||||
field.required && isEmpty(model[field.name]) && typeof model[field.name] !== 'boolean'
|
||||
);
|
||||
});
|
||||
},
|
||||
variables() {
|
||||
|
@ -216,6 +227,8 @@ export default {
|
|||
});
|
||||
},
|
||||
getFieldLabel(field) {
|
||||
if (field.bool) return null;
|
||||
|
||||
const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
|
||||
return field.label + optionalSuffix;
|
||||
},
|
||||
|
@ -273,6 +286,9 @@ export default {
|
|||
v-model="model[field.name]"
|
||||
:options="field.values"
|
||||
/>
|
||||
<gl-form-checkbox v-else-if="field.bool" :id="field.name" v-model="model[field.name]"
|
||||
><span class="gl-font-weight-bold">{{ field.label }}</span></gl-form-checkbox
|
||||
>
|
||||
<gl-form-input v-else :id="field.name" v-bind="field.input" v-model="model[field.name]" />
|
||||
</gl-form-group>
|
||||
<span class="gl-float-right">
|
||||
|
|
|
@ -74,7 +74,7 @@ export default {
|
|||
return { groupId: this.groupGraphQLId };
|
||||
},
|
||||
fields() {
|
||||
return [
|
||||
const fields = [
|
||||
{ name: 'firstName', label: __('First name'), required: true },
|
||||
{ name: 'lastName', label: __('Last name'), required: true },
|
||||
{ name: 'email', label: __('Email'), required: true },
|
||||
|
@ -86,6 +86,11 @@ export default {
|
|||
},
|
||||
{ name: 'description', label: __('Description') },
|
||||
];
|
||||
|
||||
if (this.isEditMode)
|
||||
fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true });
|
||||
|
||||
return fields;
|
||||
},
|
||||
organizationSelectValues() {
|
||||
const values = this.organizations.map((o) => {
|
||||
|
|
|
@ -6,6 +6,7 @@ fragment ContactFragment on CustomerRelationsContact {
|
|||
email
|
||||
phone
|
||||
description
|
||||
active
|
||||
organization {
|
||||
__typename
|
||||
id
|
||||
|
|
|
@ -4,4 +4,5 @@ fragment OrganizationFragment on CustomerRelationsOrganization {
|
|||
name
|
||||
defaultRate
|
||||
description
|
||||
active
|
||||
}
|
||||
|
|
|
@ -52,16 +52,23 @@ export default {
|
|||
additionalCreateParams() {
|
||||
return { groupId: this.groupGraphQLId };
|
||||
},
|
||||
},
|
||||
fields: [
|
||||
{ name: 'name', label: __('Name'), required: true },
|
||||
{
|
||||
name: 'defaultRate',
|
||||
label: s__('Crm|Default rate'),
|
||||
input: { type: 'number', step: '0.01' },
|
||||
fields() {
|
||||
const fields = [
|
||||
{ name: 'name', label: __('Name'), required: true },
|
||||
{
|
||||
name: 'defaultRate',
|
||||
label: s__('Crm|Default rate'),
|
||||
input: { type: 'number', step: '0.01' },
|
||||
},
|
||||
{ name: 'description', label: __('Description') },
|
||||
];
|
||||
|
||||
if (this.isEditMode)
|
||||
fields.push({ name: 'active', label: s__('Crm|Active'), required: true, bool: true });
|
||||
|
||||
return fields;
|
||||
},
|
||||
{ name: 'description', label: __('Description') },
|
||||
],
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
@ -73,7 +80,7 @@ export default {
|
|||
:mutation="mutation"
|
||||
:additional-create-params="additionalCreateParams"
|
||||
:existing-id="organizationGraphQLId"
|
||||
:fields="$options.fields"
|
||||
:fields="fields"
|
||||
:title="title"
|
||||
:success-message="successMessage"
|
||||
/>
|
||||
|
|
|
@ -104,12 +104,9 @@ export default {
|
|||
class="d-inline-flex mb-2"
|
||||
/>
|
||||
<gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
|
||||
<gl-button
|
||||
label
|
||||
class="gl-font-monospace"
|
||||
data-testid="commit-sha-short-id"
|
||||
v-text="commit.short_id"
|
||||
/>
|
||||
<gl-button label class="gl-font-monospace" data-testid="commit-sha-short-id">{{
|
||||
commit.short_id
|
||||
}}</gl-button>
|
||||
<modal-copy-button
|
||||
:text="commit.id"
|
||||
:title="__('Copy commit SHA')"
|
||||
|
|
|
@ -18,6 +18,7 @@ export default {
|
|||
<div class="context-header ide-context-header">
|
||||
<a :href="project.web_url" :title="s__('IDE|Go to project')" data-testid="go-to-project-link">
|
||||
<project-avatar
|
||||
:project-id="project.id"
|
||||
:project-name="project.name"
|
||||
:project-avatar-url="project.avatar_url"
|
||||
:size="48"
|
||||
|
|
|
@ -125,6 +125,7 @@ export default {
|
|||
>
|
||||
<project-avatar
|
||||
class="gl-mr-3"
|
||||
:project-id="item.id"
|
||||
:project-avatar-url="item.avatar_url"
|
||||
:project-name="item.name"
|
||||
aria-hidden="true"
|
||||
|
|
|
@ -189,25 +189,23 @@ export default {
|
|||
v-if="hasEnvironment"
|
||||
:href="environmentLink.link"
|
||||
data-testid="job-environment-link"
|
||||
v-text="environmentLink.name"
|
||||
/>
|
||||
>{{ environmentLink.name }}</gl-link
|
||||
>
|
||||
</template>
|
||||
<template #clusterNameOrLink>
|
||||
<gl-link
|
||||
v-if="clusterNameOrLink.path"
|
||||
:href="clusterNameOrLink.path"
|
||||
data-testid="job-cluster-link"
|
||||
v-text="clusterNameOrLink.name"
|
||||
/>
|
||||
>{{ clusterNameOrLink.name }}</gl-link
|
||||
>
|
||||
<template v-else>{{ clusterNameOrLink.name }}</template>
|
||||
</template>
|
||||
<template #kubernetesNamespace>{{ kubernetesNamespace }}</template>
|
||||
<template #deploymentLink>
|
||||
<gl-link
|
||||
:href="deploymentLink.path"
|
||||
data-testid="job-deployment-link"
|
||||
v-text="deploymentLink.name"
|
||||
/>
|
||||
<gl-link :href="deploymentLink.path" data-testid="job-deployment-link">{{
|
||||
deploymentLink.name
|
||||
}}</gl-link>
|
||||
</template>
|
||||
</gl-sprintf>
|
||||
</p>
|
||||
|
|
|
@ -350,7 +350,7 @@ function linesFromSelection(textArea) {
|
|||
* @param {Object} textArea - the targeted text area
|
||||
* @param {Number} selectionStart - start position of original selection
|
||||
* @param {Number} selectionEnd - end position of original selection
|
||||
* @param {Number} startPos - start pos of first line
|
||||
* @param {Number} lineStart - start pos of first line
|
||||
* @param {Number} firstLineChange - number of characters changed on first line
|
||||
* @param {Number} totalChanged - total number of characters changed
|
||||
*/
|
||||
|
@ -358,23 +358,20 @@ function setNewSelectionRange(
|
|||
textArea,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
startPos,
|
||||
lineStart,
|
||||
firstLineChange,
|
||||
totalChanged,
|
||||
) {
|
||||
let newStart = Math.max(lineStart, selectionStart + firstLineChange);
|
||||
let newEnd = Math.max(lineStart, selectionEnd + totalChanged);
|
||||
|
||||
if (selectionStart === selectionEnd) {
|
||||
textArea.setSelectionRange(
|
||||
Math.max(0, selectionStart + firstLineChange),
|
||||
Math.max(0, selectionEnd + firstLineChange),
|
||||
);
|
||||
} else if (selectionStart === startPos) {
|
||||
textArea.setSelectionRange(selectionStart, Math.max(0, selectionEnd + totalChanged));
|
||||
} else {
|
||||
textArea.setSelectionRange(
|
||||
Math.max(0, selectionStart + firstLineChange),
|
||||
Math.max(0, selectionEnd + totalChanged),
|
||||
);
|
||||
newEnd = newStart;
|
||||
} else if (selectionStart === lineStart) {
|
||||
newStart = lineStart;
|
||||
}
|
||||
|
||||
textArea.setSelectionRange(newStart, newEnd);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -390,10 +387,8 @@ function indentLines(textArea) {
|
|||
textArea.setSelectionRange(startPos, endPos);
|
||||
|
||||
lines.forEach((line) => {
|
||||
if (line.length > 0) {
|
||||
line = INDENT_CHAR.repeat(INDENT_LENGTH) + line;
|
||||
totalAdded += INDENT_LENGTH;
|
||||
}
|
||||
line = INDENT_CHAR.repeat(INDENT_LENGTH) + line;
|
||||
totalAdded += INDENT_LENGTH;
|
||||
|
||||
shiftedLines.push(line);
|
||||
});
|
||||
|
@ -439,7 +434,8 @@ function outdentLines(textArea) {
|
|||
|
||||
const textToInsert = shiftedLines.join('\n');
|
||||
|
||||
insertText(textArea, textToInsert);
|
||||
if (totalRemoved > 0) insertText(textArea, textToInsert);
|
||||
|
||||
setNewSelectionRange(
|
||||
textArea,
|
||||
selectionStart,
|
||||
|
|
|
@ -166,11 +166,9 @@ export default {
|
|||
<div class="card card-slim gl-overflow-hidden">
|
||||
<div
|
||||
:class="{ 'panel-empty-heading border-bottom-0': !hasBody, 'gl-border-b-0': !isOpen }"
|
||||
class="card-header gl-display-flex gl-justify-content-space-between gl-align-items-center gl-bg-gray-10"
|
||||
class="gl-display-flex gl-justify-content-space-between gl-line-height-24 gl-py-3 gl-px-5 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
|
||||
>
|
||||
<h3
|
||||
class="card-title h5 position-relative gl-my-0 gl-display-flex gl-align-items-center gl-h-7"
|
||||
>
|
||||
<h3 class="card-title h5 gl-my-0 gl-display-flex gl-align-items-center gl-flex-grow-1">
|
||||
<gl-link
|
||||
id="user-content-related-issues"
|
||||
class="anchor position-absolute gl-text-decoration-none"
|
||||
|
@ -189,28 +187,30 @@ export default {
|
|||
<gl-icon name="question" :size="12" />
|
||||
</gl-link>
|
||||
|
||||
<div class="gl-display-inline-flex">
|
||||
<div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-5">
|
||||
<span class="gl-display-inline-flex gl-align-items-center">
|
||||
<gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
|
||||
{{ badgeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
data-qa-selector="related_issues_plus_button"
|
||||
data-testid="add-button"
|
||||
icon="plus"
|
||||
:aria-label="addIssuableButtonText"
|
||||
:class="qaClass"
|
||||
@click="$emit('toggleAddRelatedIssuesForm', $event)"
|
||||
/>
|
||||
<div class="js-related-issues-header-issue-count gl-display-inline-flex gl-mx-3">
|
||||
<span class="gl-display-inline-flex gl-align-items-center">
|
||||
<gl-icon :name="issuableTypeIcon" class="gl-mr-2 gl-text-gray-500" />
|
||||
{{ badgeLabel }}
|
||||
</span>
|
||||
</div>
|
||||
</h3>
|
||||
<slot name="header-actions"></slot>
|
||||
<div class="gl-pl-4 gl-ml-3">
|
||||
<gl-button
|
||||
v-if="canAdmin"
|
||||
size="small"
|
||||
data-qa-selector="related_issues_plus_button"
|
||||
data-testid="related-issues-plus-button"
|
||||
:aria-label="addIssuableButtonText"
|
||||
:class="qaClass"
|
||||
class="gl-ml-3"
|
||||
@click="$emit('toggleAddRelatedIssuesForm', $event)"
|
||||
>
|
||||
<slot name="add-button-text">{{ __('Add') }}</slot>
|
||||
</gl-button>
|
||||
<div class="gl-pl-3 gl-ml-3 gl-border-l-1 gl-border-l-solid gl-border-l-gray-100">
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
size="small"
|
||||
:icon="toggleIcon"
|
||||
:aria-label="toggleLabel"
|
||||
data-testid="toggle-links"
|
||||
|
|
|
@ -196,12 +196,9 @@ export default {
|
|||
</gl-link>
|
||||
</div>
|
||||
<gl-button-group class="gl-ml-4 js-commit-sha-group">
|
||||
<gl-button
|
||||
label
|
||||
class="gl-font-monospace"
|
||||
data-testid="last-commit-id-label"
|
||||
v-text="showCommitId"
|
||||
/>
|
||||
<gl-button label class="gl-font-monospace" data-testid="last-commit-id-label">{{
|
||||
showCommitId
|
||||
}}</gl-button>
|
||||
<clipboard-button
|
||||
:text="commit.sha"
|
||||
:title="__('Copy commit SHA')"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<script>
|
||||
import { GlAvatar } from '@gitlab/ui';
|
||||
import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
|
||||
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
|
||||
|
||||
export default {
|
||||
|
@ -8,9 +9,12 @@ export default {
|
|||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: Number,
|
||||
type: [Number, String],
|
||||
default: 0,
|
||||
required: false,
|
||||
validator(value) {
|
||||
return typeof value === 'string' ? isGid(value) : true;
|
||||
},
|
||||
},
|
||||
projectName: {
|
||||
type: String,
|
||||
|
@ -36,6 +40,9 @@ export default {
|
|||
avatarAlt() {
|
||||
return this.alt ?? this.projectName;
|
||||
},
|
||||
entityId() {
|
||||
return isGid(this.projectId) ? getIdFromGraphQLId(this.projectId) : this.projectId;
|
||||
},
|
||||
},
|
||||
AVATAR_SHAPE_OPTION_RECT,
|
||||
};
|
||||
|
@ -44,7 +51,7 @@ export default {
|
|||
<template>
|
||||
<gl-avatar
|
||||
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
|
||||
:entity-id="projectId"
|
||||
:entity-id="entityId"
|
||||
:entity-name="projectName"
|
||||
:src="projectAvatarUrl"
|
||||
:alt="avatarAlt"
|
||||
|
|
|
@ -53,6 +53,7 @@ export default {
|
|||
>
|
||||
<gl-icon v-if="selected" class="js-selected-icon" name="mobile-issue-close" />
|
||||
<project-avatar
|
||||
:project-id="project.id"
|
||||
:project-avatar-url="projectAvatarUrl"
|
||||
:project-name="projectNameWithNamespace"
|
||||
class="gl-mr-3"
|
||||
|
|
|
@ -244,7 +244,7 @@ export default {
|
|||
>
|
||||
{{ $options.i18n.addChildButtonLabel }}
|
||||
</gl-button>
|
||||
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-50 gl-pl-3 gl-ml-3">
|
||||
<div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
|
||||
<gl-button
|
||||
category="tertiary"
|
||||
size="small"
|
||||
|
|
|
@ -11,7 +11,6 @@ class Admin::ApplicationsController < Admin::ApplicationController
|
|||
def index
|
||||
applications = ApplicationsFinder.new.execute
|
||||
@applications = Kaminari.paginate_array(applications).page(params[:page])
|
||||
@application_counts = OauthAccessToken.distinct_resource_owner_counts(@applications)
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -4,6 +4,7 @@ module Projects
|
|||
module Pipelines
|
||||
class StagesController < Projects::Pipelines::ApplicationController
|
||||
before_action :authorize_update_pipeline!
|
||||
before_action :stage, only: [:play_manual]
|
||||
|
||||
urgency :low, [
|
||||
:play_manual
|
||||
|
@ -26,7 +27,7 @@ module Projects
|
|||
private
|
||||
|
||||
def stage
|
||||
@pipeline_stage ||= pipeline.find_stage_by_name!(params[:stage_name])
|
||||
@stage ||= pipeline.stage(params[:stage_name]).presence || render_404
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1224,10 +1224,6 @@ module Ci
|
|||
stages.find_by(name: name)
|
||||
end
|
||||
|
||||
def find_stage_by_name!(name)
|
||||
stages.find_by!(name: name)
|
||||
end
|
||||
|
||||
def full_error_messages
|
||||
errors ? errors.full_messages.to_sentence : ""
|
||||
end
|
||||
|
|
|
@ -6,7 +6,6 @@ class OauthAccessToken < Doorkeeper::AccessToken
|
|||
|
||||
alias_attribute :user, :resource_owner
|
||||
|
||||
scope :distinct_resource_owner_counts, ->(applications) { where(application: applications).distinct.group(:application_id).count(:resource_owner_id) }
|
||||
scope :latest_per_application, -> { select('distinct on(application_id) *').order(application_id: :desc, created_at: :desc) }
|
||||
scope :preload_application, -> { preload(:application) }
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ module Integrations
|
|||
class ProjectEntity < Grape::Entity
|
||||
include RequestAwareEntity
|
||||
|
||||
expose :id
|
||||
expose :avatar_url
|
||||
expose :full_name
|
||||
expose :name
|
||||
|
|
|
@ -28,8 +28,6 @@
|
|||
= _('Name')
|
||||
%th
|
||||
= _('Callback URL')
|
||||
%th
|
||||
= _('Clients')
|
||||
%th
|
||||
= _('Trusted')
|
||||
%th
|
||||
|
@ -41,7 +39,6 @@
|
|||
%tr{ id: "application_#{application.id}" }
|
||||
%td= link_to application.name, admin_application_path(application)
|
||||
%td= application.redirect_uri
|
||||
%td= @application_counts[application.id].to_i
|
||||
%td= application.trusted? ? _('Yes'): _('No')
|
||||
%td= application.confidential? ? _('Yes'): _('No')
|
||||
%td= link_to 'Edit', edit_admin_application_path(application), class: 'gl-button btn btn-link'
|
||||
|
|
|
@ -230,6 +230,96 @@ In the upstream pipeline:
|
|||
artifacts: true
|
||||
```
|
||||
|
||||
#### Pass artifacts to a downstream pipeline
|
||||
|
||||
You can pass artifacts to a downstream pipeline by using [`needs:project`](../yaml/index.md#needsproject).
|
||||
|
||||
1. In a job in the upstream pipeline, save the artifacts using the [`artifacts`](../yaml/index.md#artifacts) keyword.
|
||||
1. Trigger the downstream pipeline with a trigger job:
|
||||
|
||||
```yaml
|
||||
build_artifacts:
|
||||
stage: build
|
||||
script:
|
||||
- echo "This is a test artifact!" >> artifact.txt
|
||||
artifacts:
|
||||
paths:
|
||||
- artifact.txt
|
||||
|
||||
deploy:
|
||||
stage: deploy
|
||||
trigger: my/downstream_project
|
||||
```
|
||||
|
||||
1. In a job in the downstream pipeline, fetch the artifacts from the upstream pipeline
|
||||
by using `needs:project`. Set `job` to the job in the upstream pipeline to fetch artifacts from,
|
||||
`ref` to the branch, and `artifacts: true`.
|
||||
|
||||
```yaml
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- cat artifact.txt
|
||||
needs:
|
||||
- project: my/upstream_project
|
||||
job: build_artifacts
|
||||
ref: main
|
||||
artifacts: true
|
||||
```
|
||||
|
||||
#### Pass artifacts to a downstream pipeline from a Merge Request pipeline
|
||||
|
||||
When you use `needs:project` to [pass artifacts to a downstream pipeline](#pass-artifacts-to-a-downstream-pipeline),
|
||||
the `ref` value is usually a branch name, like `main` or `development`.
|
||||
|
||||
For merge request pipelines, the `ref` value is in the form of `refs/merge-requests/<id>/head`,
|
||||
where `id` is the merge request ID. You can retrieve this ref with the [`CI_MERGE_REQUEST_REF_PATH`](../variables/predefined_variables.md#predefined-variables-for-merge-request-pipelines)
|
||||
CI/CD variable. Do not use a branch name as the `ref` with merge request pipelines,
|
||||
because the downstream pipeline attempts to fetch artifacts from the latest branch pipeline.
|
||||
|
||||
To fetch the artifacts from the upstream `merge request` pipeline instead of the `branch` pipeline,
|
||||
pass this variable to the downstream pipeline using variable inheritance:
|
||||
|
||||
1. In a job in the upstream pipeline, save the artifacts using the [`artifacts`](../yaml/index.md#artifacts) keyword.
|
||||
1. In the job that triggers the downstream pipeline, pass the `$CI_MERGE_REQUEST_REF_PATH` variable by using
|
||||
[variable inheritance](#pass-cicd-variables-to-a-downstream-pipeline-by-using-the-variables-keyword):
|
||||
|
||||
```yaml
|
||||
build_artifacts:
|
||||
stage: build
|
||||
script:
|
||||
- echo "This is a test artifact!" >> artifact.txt
|
||||
artifacts:
|
||||
paths:
|
||||
- artifact.txt
|
||||
|
||||
upstream_job:
|
||||
variables:
|
||||
UPSTREAM_REF: $CI_MERGE_REQUEST_REF_PATH
|
||||
trigger:
|
||||
project: my/downstream_project
|
||||
branch: my-branch
|
||||
```
|
||||
|
||||
1. In a job in the downstream pipeline, fetch the artifacts from the upstream pipeline
|
||||
by using `needs:project`. Set the `ref` to the `UPSTREAM_REF` variable, and `job`
|
||||
to the job in the upstream pipeline to fetch artifacts from:
|
||||
|
||||
```yaml
|
||||
test:
|
||||
stage: test
|
||||
script:
|
||||
- cat artifact.txt
|
||||
needs:
|
||||
- project: my/upstream_project
|
||||
job: build_artifacts
|
||||
ref: UPSTREAM_REF
|
||||
artifacts: true
|
||||
```
|
||||
|
||||
This method works for fetching artifacts from a regular merge request parent pipeline,
|
||||
but fetching artifacts from [merge results](merged_results_pipelines.md) pipelines is not supported.
|
||||
|
||||
#### Use `rules` or `only`/`except` with multi-project pipelines
|
||||
|
||||
You can use CI/CD variables or the [`rules`](../yaml/index.md#rulesif) keyword to
|
||||
|
|
|
@ -289,10 +289,10 @@ smoke-test-job:
|
|||
|
||||
In `include` sections in your `.gitlab-ci.yml` file, you can use:
|
||||
|
||||
- [Project variables](../variables/index.md#add-a-cicd-variable-to-a-project)
|
||||
- [Group variables](../variables/index.md#add-a-cicd-variable-to-a-group)
|
||||
- [Instance variables](../variables/index.md#add-a-cicd-variable-to-an-instance)
|
||||
- Project [predefined variables](../variables/predefined_variables.md)
|
||||
- [Project variables](../variables/index.md#add-a-cicd-variable-to-a-project).
|
||||
- [Group variables](../variables/index.md#add-a-cicd-variable-to-a-group).
|
||||
- [Instance variables](../variables/index.md#add-a-cicd-variable-to-an-instance).
|
||||
- Project [predefined variables](../variables/predefined_variables.md).
|
||||
- In GitLab 14.2 and later, the `$CI_COMMIT_REF_NAME` [predefined variable](../variables/predefined_variables.md).
|
||||
|
||||
When used in `include`, the `CI_COMMIT_REF_NAME` variable returns the full
|
||||
|
@ -322,7 +322,11 @@ include:
|
|||
file: '.compliance-gitlab-ci.yml'
|
||||
```
|
||||
|
||||
For an example of how you can include these predefined variables, and the variables' impact on CI/CD jobs,
|
||||
You cannot use variables defined in jobs, or in a global [`variables`](../yaml/index.md#variables)
|
||||
section which defines the default variables for all jobs. Includes are evaluated before jobs,
|
||||
so these variables cannot be used with `include`.
|
||||
|
||||
For an example of how you can include predefined variables, and the variables' impact on CI/CD jobs,
|
||||
see this [CI/CD variable demo](https://youtu.be/4XR8gw3Pkos).
|
||||
|
||||
## Use `rules` with `include`
|
||||
|
|
|
@ -171,11 +171,11 @@ Use `include:local` instead of symbolic links.
|
|||
|
||||
**Possible inputs**:
|
||||
|
||||
- A full path relative to the root directory (`/`).
|
||||
A full path relative to the root directory (`/`):
|
||||
|
||||
- The YAML file must have the extension `.yml` or `.yaml`.
|
||||
- You can [use `*` and `**` wildcards in the file path](includes.md#use-includelocal-with-wildcard-file-paths).
|
||||
|
||||
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
|
||||
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
|
||||
|
||||
**Example of `include:local`**:
|
||||
|
||||
|
@ -208,10 +208,10 @@ use `include:file`. You can use `include:file` in combination with `include:proj
|
|||
|
||||
**Possible inputs**:
|
||||
|
||||
- A full path, relative to the root directory (`/`). The YAML file must have the
|
||||
extension `.yml` or `.yaml`.
|
||||
A full path, relative to the root directory (`/`):
|
||||
|
||||
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
|
||||
- The YAML file must have the extension `.yml` or `.yaml`.
|
||||
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
|
||||
|
||||
**Example of `include:file`**:
|
||||
|
||||
|
@ -268,10 +268,11 @@ Use `include:remote` with a full URL to include a file from a different location
|
|||
|
||||
**Possible inputs**:
|
||||
|
||||
- A public URL accessible by an HTTP/HTTPS `GET` request. Authentication with the
|
||||
remote URL is not supported. The YAML file must have the extension `.yml` or `.yaml`.
|
||||
A public URL accessible by an HTTP/HTTPS `GET` request:
|
||||
|
||||
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
|
||||
- Authentication with the remote URL is not supported.
|
||||
- The YAML file must have the extension `.yml` or `.yaml`.
|
||||
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
|
||||
|
||||
**Example of `include:remote`**:
|
||||
|
||||
|
@ -296,9 +297,12 @@ Use `include:template` to include [`.gitlab-ci.yml` templates](https://gitlab.co
|
|||
|
||||
**Possible inputs**:
|
||||
|
||||
- [`.gitlab-ci.yml` templates](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
|
||||
A [CI/CD template](../examples/index.md#cicd-templates):
|
||||
|
||||
CI/CD variables [are supported](../variables/where_variables_can_be_used.md#gitlab-ciyml-file).
|
||||
- Templates are stored in [`lib/gitlab/ci/templates`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
|
||||
Not all templates are designed to be used with `include:template`, so check template
|
||||
comments before using one.
|
||||
- You can use [certain CI/CD variables](includes.md#use-variables-with-include).
|
||||
|
||||
**Example of `include:template`**:
|
||||
|
||||
|
|
|
@ -147,7 +147,7 @@ Best practices for [client-side logging](logging.md) for GitLab frontend develop
|
|||
|
||||
## [Internationalization (i18n) and Translations](../i18n/externalization.md)
|
||||
|
||||
Frontend internationalization support is described in [this document](../i18n/).
|
||||
Frontend internationalization support is described in [this document](../i18n/index.md).
|
||||
The [externalization part of the guide](../i18n/externalization.md) explains the helpers/methods available.
|
||||
|
||||
## [Troubleshooting](troubleshooting.md)
|
||||
|
|
|
@ -426,6 +426,21 @@ Feature.enabled?(:a_feature, project) && Feature.disabled?(:a_feature_override,
|
|||
/chatops run feature set --project=gitlab-org/gitlab a_feature_override true
|
||||
```
|
||||
|
||||
#### Percentage-based actor selection
|
||||
|
||||
When using the percentage rollout of actors on multiple feature flags, the actors for each feature flag are selected separately.
|
||||
|
||||
For example, the following feature flags are enabled for a certain percentage of actors:
|
||||
|
||||
```plaintext
|
||||
/chatops run chatops feature set feature-set-1 25 --actors
|
||||
/chatops run chatops feature set feature-set-2 25 --actors
|
||||
```
|
||||
|
||||
If a project A has `:feature-set-1` enabled, there is no guarantee that project A also has `:feature-set-2` enabled.
|
||||
|
||||
For more detail, see [This is how percentages work in Flipper](https://www.hackwithpassion.com/this-is-how-percentages-work-in-flipper).
|
||||
|
||||
#### Use actors for verifying in production
|
||||
|
||||
WARNING:
|
||||
|
|
|
@ -14,8 +14,8 @@ When implementing new features, please refer to these existing features to avoid
|
|||
- [Merge request Templates](../user/project/description_templates.md#create-a-merge-request-template): `.gitlab/merge_request_templates/`.
|
||||
- [GitLab agent](https://gitlab.com/gitlab-org/cluster-integration/gitlab-agent/-/blob/master/doc/configuration_repository.md#layout): `.gitlab/agents/`.
|
||||
- [CODEOWNERS](../user/project/code_owners.md#set-up-code-owners): `.gitlab/CODEOWNERS`.
|
||||
- [Route Maps](../ci/review_apps/#route-maps): `.gitlab/route-map.yml`.
|
||||
- [Route Maps](../ci/review_apps/index.md#route-maps): `.gitlab/route-map.yml`.
|
||||
- [Customize Auto DevOps Helm Values](../topics/autodevops/customize.md#customize-values-for-helm-chart): `.gitlab/auto-deploy-values.yaml`.
|
||||
- [Insights](../user/project/insights/index.md#configure-your-insights): `.gitlab/insights.yml`.
|
||||
- [Service Desk Templates](../user/project/service_desk.md#using-customized-email-templates): `.gitlab/service_desk_templates/`.
|
||||
- [Web IDE](../user/project/web_ide/#web-ide-configuration-file): `.gitlab/.gitlab-webide.yml`.
|
||||
- [Web IDE](../user/project/web_ide/index.md#web-ide-configuration-file): `.gitlab/.gitlab-webide.yml`.
|
||||
|
|
|
@ -314,7 +314,7 @@ This documentation gives an overview of the report JSON format,
|
|||
as well as recommendations and examples to help integrators set its fields.
|
||||
The format is extensively described in the documentation of
|
||||
[SAST](../../user/application_security/sast/index.md#reports-json-format),
|
||||
[DAST](../../user/application_security/dast/#reports),
|
||||
[DAST](../../user/application_security/dast/index.md#reports),
|
||||
[Dependency Scanning](../../user/application_security/dependency_scanning/index.md#reports-json-format),
|
||||
and [Container Scanning](../../user/application_security/container_scanning/index.md#reports-json-format)
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ MultiStore uses two feature flags to control the actual migration:
|
|||
- `use_primary_and_secondary_stores_for_[store_name]`
|
||||
- `use_primary_store_as_default_for_[store_name]`
|
||||
|
||||
For example, if our new Redis instance is called `Gitlab::Redis::Foo`, we can [create](../../../ee/development/feature_flags/#create-a-new-feature-flag) two feature flags by executing:
|
||||
For example, if our new Redis instance is called `Gitlab::Redis::Foo`, we can [create](../feature_flags/index.md#create-a-new-feature-flag) two feature flags by executing:
|
||||
|
||||
```shell
|
||||
bin/feature-flag use_primary_and_secondary_stores_for_foo
|
||||
|
|
|
@ -28,7 +28,7 @@ others - particularly [latency-sensitive jobs](worker_attributes.md#latency-sens
|
|||
this will result in a poor user experience.
|
||||
|
||||
This only applies to new worker classes when they are first introduced.
|
||||
As we recommend [using feature flags](../feature_flags/) as a general
|
||||
As we recommend [using feature flags](../feature_flags/index.md) as a general
|
||||
development process, it's best to control the entire change (including
|
||||
scheduling of the new Sidekiq worker) with a feature flag.
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
|
|||
|
||||
# Writing consumer tests
|
||||
|
||||
This tutorial guides you through writing a consumer test from scratch. To start, the consumer tests are written using [`jest-pact`](https://github.com/pact-foundation/jest-pact) that builds on top of [`pact-js`](https://github.com/pact-foundation/pact-js). This tutorial shows you how to write a consumer test for the `/discussions.json` endpoint, which is actually `/:namespace_name/:project_name/-/merge_requests/:id/discussions.json`.
|
||||
This tutorial guides you through writing a consumer test from scratch. To start, the consumer tests are written using [`jest-pact`](https://github.com/pact-foundation/jest-pact) that builds on top of [`pact-js`](https://github.com/pact-foundation/pact-js). This tutorial shows you how to write a consumer test for the `/discussions.json` REST API endpoint, which is actually `/:namespace_name/:project_name/-/merge_requests/:id/discussions.json`. For an example of a GraphQL consumer test, see [`spec/contracts/consumer/specs/project/pipeline/show.spec.js`](https://gitlab.com/gitlab-org/gitlab/-/tree/master/spec/contracts/consumer/specs/project/pipeline/show.spec.js).
|
||||
|
||||
## Create the skeleton
|
||||
|
||||
|
@ -24,7 +24,7 @@ To learn more about how the contract test directory is structured, see the contr
|
|||
The Pact consumer test is defined through the `pactWith` function that takes `PactOptions` and the `PactFn`.
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
|
||||
pactWith(PactOptions, PactFn);
|
||||
```
|
||||
|
@ -34,7 +34,7 @@ pactWith(PactOptions, PactFn);
|
|||
`PactOptions` with `jest-pact` introduces [additional options](https://github.com/pact-foundation/jest-pact/blob/dce370c1ab4b7cb5dff12c4b62246dc229c53d0e/README.md#defaults) that build on top of the ones [provided in `pact-js`](https://github.com/pact-foundation/pact-js#constructor). In most cases, you define the `consumer`, `provider`, `log`, and `dir` options for these tests.
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
|
||||
pactWith(
|
||||
{
|
||||
|
@ -54,7 +54,7 @@ To learn more about how to name the consumers and providers, see contract testin
|
|||
The `PactFn` is where your tests are defined. This is where you set up the mock provider and where you can use the standard Jest methods like [`Jest.describe`](https://jestjs.io/docs/api#describename-fn), [`Jest.beforeEach`](https://jestjs.io/docs/api#beforeeachfn-timeout), and [`Jest.it`](https://jestjs.io/docs/api#testname-fn-timeout). For more information, see [https://jestjs.io/docs/api](https://jestjs.io/docs/api).
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
|
||||
pactWith(
|
||||
{
|
||||
|
@ -70,7 +70,7 @@ pactWith(
|
|||
|
||||
});
|
||||
|
||||
it('return a successful body', () => {
|
||||
it('return a successful body', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -92,8 +92,8 @@ For this tutorial, define four attributes for the `Interaction`:
|
|||
After you define the `Interaction`, add that interaction to the mock provider by calling `addInteraction`.
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
const { Matchers } = require('@pact-foundation/pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
import { Matchers } from '@pact-foundation/pact';
|
||||
|
||||
pactWith(
|
||||
{
|
||||
|
@ -132,7 +132,7 @@ pactWith(
|
|||
provider.addInteraction(interaction);
|
||||
});
|
||||
|
||||
it('return a successful body', () => {
|
||||
it('return a successful body', async () => {
|
||||
|
||||
});
|
||||
});
|
||||
|
@ -142,38 +142,36 @@ pactWith(
|
|||
|
||||
### Response body `Matchers`
|
||||
|
||||
Notice how we use `Matchers` in the `body` of the expected response. This allows us to be flexible enough to accept different values but still be strict enough to distinguish between valid and invalid values. We must ensure that we have a tight definition that is neither too strict nor too lax. Read more about the [different types of `Matchers`](https://github.com/pact-foundation/pact-js#using-the-v3-matching-rules).
|
||||
Notice how we use `Matchers` in the `body` of the expected response. This allows us to be flexible enough to accept different values but still be strict enough to distinguish between valid and invalid values. We must ensure that we have a tight definition that is neither too strict nor too lax. Read more about the [different types of `Matchers`](https://github.com/pact-foundation/pact-js/blob/master/docs/matching.md). We are currently using the V2 matching rules.
|
||||
|
||||
## Write the test
|
||||
|
||||
After the mock provider is set up, you can write the test. For this test, you make a request and expect a particular response.
|
||||
|
||||
First, set up the client that makes the API request. To do that, create `spec/contracts/consumer/endpoints/project/merge_requests.js` and add the following API request.
|
||||
First, set up the client that makes the API request. To do that, create `spec/contracts/consumer/resources/api/project/merge_requests.js` and add the following API request. If the endpoint is a GraphQL, then we create it under `spec/contracts/consumer/resources/graphql` instead.
|
||||
|
||||
```javascript
|
||||
const axios = require('axios');
|
||||
import axios from 'axios';
|
||||
|
||||
exports.getDiscussions = (endpoint) => {
|
||||
const url = endpoint.url;
|
||||
export async function getDiscussions(endpoint) {
|
||||
const { url } = endpoint;
|
||||
|
||||
return axios
|
||||
.request({
|
||||
method: 'GET',
|
||||
baseURL: url,
|
||||
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
|
||||
headers: { Accept: '*/*' },
|
||||
})
|
||||
.then((response) => response.data);
|
||||
};
|
||||
return axios({
|
||||
method: 'GET',
|
||||
baseURL: url,
|
||||
url: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
|
||||
headers: { Accept: '*/*' },
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
After that's set up, import it to the test file and call it to make the request. Then, you can make the request and define your expectations.
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
const { Matchers } = require('@pact-foundation/pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
import { Matchers } from '@pact-foundation/pact';
|
||||
|
||||
const { getDiscussions } = require('../endpoints/project/merge_requests');
|
||||
import { getDiscussions } from '../../../resources/api/project/merge_requests';
|
||||
|
||||
pactWith(
|
||||
{
|
||||
|
@ -211,17 +209,17 @@ pactWith(
|
|||
};
|
||||
});
|
||||
|
||||
it('return a successful body', () => {
|
||||
return getDiscussions({
|
||||
it('return a successful body', async () => {
|
||||
const discussions = await getDiscussions({
|
||||
url: provider.mockService.baseUrl,
|
||||
}).then((discussions) => {
|
||||
expect(discussions).toEqual(Matchers.eachLike({
|
||||
id: 'fd73763cbcbf7b29eb8765d969a38f7d735e222a',
|
||||
project_id: 6954442,
|
||||
...
|
||||
resolved: true
|
||||
}));
|
||||
});
|
||||
|
||||
expect(discussions).toEqual(Matchers.eachLike({
|
||||
id: 'fd73763cbcbf7b29eb8765d969a38f7d735e222a',
|
||||
project_id: 6954442,
|
||||
...
|
||||
resolved: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
@ -237,7 +235,7 @@ As you may have noticed, the request and response definitions can get large. Thi
|
|||
Create a file under `spec/contracts/consumer/fixtures/project/merge_request` called `discussions.fixture.js` where you will place the `request` and `response` definitions.
|
||||
|
||||
```javascript
|
||||
const { Matchers } = require('@pact-foundation/pact');
|
||||
import { Matchers } from '@pact-foundation/pact';
|
||||
|
||||
const body = Matchers.eachLike({
|
||||
id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
|
||||
|
@ -254,11 +252,15 @@ const Discussions = {
|
|||
headers: {
|
||||
'Content-Type': 'application/json; charset=utf-8',
|
||||
},
|
||||
body: body,
|
||||
body,
|
||||
},
|
||||
|
||||
scenario: {
|
||||
state: 'a merge request with discussions exists',
|
||||
uponReceiving: 'a request for discussions',
|
||||
},
|
||||
|
||||
request: {
|
||||
uponReceiving: 'a request for discussions',
|
||||
withRequest: {
|
||||
method: 'GET',
|
||||
path: '/gitlab-org/gitlab-qa/-/merge_requests/1/discussions.json',
|
||||
|
@ -275,36 +277,41 @@ exports.Discussions = Discussions;
|
|||
With all of that moved to the `fixture`, you can simplify the test to the following:
|
||||
|
||||
```javascript
|
||||
const { pactWith } = require('jest-pact');
|
||||
import { pactWith } from 'jest-pact';
|
||||
|
||||
const { Discussions } = require('../fixtures/discussions.fixture');
|
||||
const { getDiscussions } = require('../endpoints/project/merge_requests');
|
||||
import { Discussions } from '../../../fixtures/project/merge_request/discussions.fixture';
|
||||
import { getDiscussions } from '../../../resources/api/project/merge_requests';
|
||||
|
||||
const CONSUMER_NAME = 'MergeRequest#show';
|
||||
const PROVIDER_NAME = 'Merge Request Discussions Endpoint';
|
||||
const CONSUMER_LOG = '../logs/consumer.log';
|
||||
const CONTRACT_DIR = '../contracts/project/merge_request/show';
|
||||
|
||||
pactWith(
|
||||
{
|
||||
consumer: 'MergeRequest#show',
|
||||
provider: 'Merge Request Discussions Endpoint',
|
||||
log: '../logs/consumer.log',
|
||||
dir: '../contracts/project/merge_request/show',
|
||||
consumer: CONSUMER_NAME,
|
||||
provider: PROVIDER_NAME,
|
||||
log: CONSUMER_LOG,
|
||||
dir: CONTRACT_DIR,
|
||||
},
|
||||
|
||||
(provider) => {
|
||||
describe('Merge Request Discussions Endpoint', () => {
|
||||
describe(PROVIDER_NAME, () => {
|
||||
beforeEach(() => {
|
||||
const interaction = {
|
||||
state: 'a merge request with discussions exists',
|
||||
...Discussions.scenario,
|
||||
...Discussions.request,
|
||||
willRespondWith: Discussions.success,
|
||||
};
|
||||
return provider.addInteraction(interaction);
|
||||
provider.addInteraction(interaction);
|
||||
});
|
||||
|
||||
it('return a successful body', () => {
|
||||
return getDiscussions({
|
||||
it('return a successful body', async () => {
|
||||
const discussions = await getDiscussions({
|
||||
url: provider.mockService.baseUrl,
|
||||
}).then((discussions) => {
|
||||
expect(discussions).toEqual(Discussions.body);
|
||||
});
|
||||
|
||||
expect(discussions).toEqual(Discussions.body);
|
||||
});
|
||||
});
|
||||
},
|
||||
|
|
|
@ -50,11 +50,11 @@ Having an organized and sensible folder structure for the test suite makes it ea
|
|||
|
||||
The consumer tests are grouped according to the different pages in the application. Each file contains various types of requests found in a page. As such, the consumer test files are named using the Rails standards of how pages are referenced. For example, the project pipelines page would be the `Project::Pipeline#index` page so the equivalent consumer test would be located in `consumer/specs/project/pipelines/index.spec.js`.
|
||||
|
||||
When defining the location to output the contract generated by the test, we want to follow the same file structure which would be `contracts/project/pipelines/` for this example. This is the structure in `consumer/endpoints` and `consumer/fixtures` as well.
|
||||
When defining the location to output the contract generated by the test, we want to follow the same file structure which would be `contracts/project/pipelines/` for this example. This is the structure in `consumer/resources` and `consumer/fixtures` as well.
|
||||
|
||||
#### Provider tests
|
||||
|
||||
The provider tests are grouped similarly to our controllers. Each of these tests contains various tests for an API endpoint. For example, the API endpoint to get a list of pipelines for a project would be located in `provider/pact_helpers/project/pipelines/get_list_project_pipelines_helper.rb`. The provider states are structured the same way.
|
||||
The provider tests are grouped similarly to our controllers. Each of these tests contains various tests for an API endpoint. For example, the API endpoint to get a list of pipelines for a project would be located in `provider/pact_helpers/project/pipelines/get_list_project_pipelines_helper.rb`. The provider states are grouped according to the different pages in the application similar to the consumer tests.
|
||||
|
||||
### Naming conventions
|
||||
|
||||
|
|
|
@ -391,7 +391,7 @@ git checkout <default-branch>
|
|||
git merge <feature-branch>
|
||||
```
|
||||
|
||||
In GitLab, you typically use a [merge request](../user/project/merge_requests/) to merge your changes, instead of using the command line.
|
||||
In GitLab, you typically use a [merge request](../user/project/merge_requests/index.md) to merge your changes, instead of using the command line.
|
||||
|
||||
To create a merge request from a fork to an upstream repository, see the
|
||||
[forking workflow](../user/project/repository/forking_workflow.md).
|
||||
|
|
|
@ -19,6 +19,6 @@ sudo gitlab-rake gitlab:spdx:import
|
|||
bundle exec rake gitlab:spdx:import RAILS_ENV=production
|
||||
```
|
||||
|
||||
To perform this task in the [offline environment](../user/application_security/offline_deployments/#defining-offline-environments),
|
||||
To perform this task in the [offline environment](../user/application_security/offline_deployments/index.md#defining-offline-environments),
|
||||
an outbound connection to [`licenses.json`](https://spdx.org/licenses/licenses.json) should be
|
||||
allowed.
|
||||
|
|
|
@ -30,6 +30,6 @@ type: index
|
|||
|
||||
## Securing your GitLab installation
|
||||
|
||||
Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/) to harden your GitLab instance and minimize the risk of unwanted user account creation.
|
||||
Consider access control features like [Sign up restrictions](../user/admin_area/settings/sign_up_restrictions.md) and [Authentication options](../topics/authentication/index.md) to harden your GitLab instance and minimize the risk of unwanted user account creation.
|
||||
|
||||
Self-hosting GitLab customers and administrators are responsible for the security of their underlying hosts, and for keeping GitLab itself up to date. It is important to [regularly patch GitLab](../policy/maintenance.md), patch your operating system and its software, and harden your hosts in accordance with vendor guidance.
|
||||
|
|
|
@ -2247,6 +2247,9 @@ msgstr ""
|
|||
msgid "Add existing confidential %{issuableType}"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add existing issue"
|
||||
msgstr ""
|
||||
|
||||
msgid "Add header and footer to emails. Please note that color settings will only be applied within the application interface"
|
||||
msgstr ""
|
||||
|
||||
|
@ -11220,6 +11223,9 @@ msgstr ""
|
|||
msgid "Critical vulnerabilities present"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Active"
|
||||
msgstr ""
|
||||
|
||||
msgid "Crm|Contact"
|
||||
msgstr ""
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@
|
|||
"devDependencies": {
|
||||
"@gitlab/eslint-plugin": "15.0.0",
|
||||
"@gitlab/stylelint-config": "4.1.0",
|
||||
"@graphql-eslint/eslint-plugin": "3.10.6",
|
||||
"@graphql-eslint/eslint-plugin": "3.10.7",
|
||||
"@testing-library/dom": "^7.16.2",
|
||||
"@vue/test-utils": "1.3.0",
|
||||
"@vue/vue2-jest": "^27.0.0",
|
||||
|
@ -211,7 +211,7 @@
|
|||
"cheerio": "^1.0.0-rc.9",
|
||||
"commander": "^2.20.3",
|
||||
"custom-jquery-matchers": "^2.1.0",
|
||||
"eslint": "8.19.0",
|
||||
"eslint": "8.21.0",
|
||||
"eslint-import-resolver-jest": "3.0.2",
|
||||
"eslint-import-resolver-webpack": "0.13.2",
|
||||
"eslint-plugin-no-jquery": "2.7.0",
|
||||
|
|
|
@ -33,7 +33,7 @@ module QA
|
|||
# wait_until required due to feature_caching. Remove along with feature flag removal.
|
||||
Page::File::Edit.perform do |file|
|
||||
Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page,
|
||||
retry_on_exception: true) do
|
||||
retry_on_exception: true) do
|
||||
expect(file).to have_element(:editor_toolbar_button)
|
||||
end
|
||||
file.remove_content
|
||||
|
|
|
@ -8,18 +8,18 @@ module QA
|
|||
PRODUCTION_ADDRESS = 'https://gitlab.com'
|
||||
PRE_PROD_ADDRESS = 'https://pre.gitlab.com'
|
||||
SENTRY_ENVIRONMENTS = {
|
||||
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
|
||||
staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
|
||||
staging_canary: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
|
||||
staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
|
||||
pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
|
||||
canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
|
||||
production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
|
||||
staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
|
||||
pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
|
||||
canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
|
||||
production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
|
||||
}.freeze
|
||||
KIBANA_ENVIRONMENTS = {
|
||||
staging: 'https://nonprod-log.gitlab.net/',
|
||||
staging: 'https://nonprod-log.gitlab.net/',
|
||||
staging_canary: 'https://nonprod-log.gitlab.net/',
|
||||
canary: 'https://log.gprd.gitlab.net/',
|
||||
production: 'https://log.gprd.gitlab.net/'
|
||||
canary: 'https://log.gprd.gitlab.net/',
|
||||
production: 'https://log.gprd.gitlab.net/'
|
||||
}.freeze
|
||||
|
||||
def self.failure_metadata(correlation_id)
|
||||
|
|
|
@ -37,14 +37,14 @@ RSpec.describe QA::Support::Loglinking do
|
|||
describe '.sentry_url' do
|
||||
let(:url_hash) do
|
||||
{
|
||||
:staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
|
||||
:staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
|
||||
:staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
|
||||
:pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
|
||||
:canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
|
||||
:production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
|
||||
:foo => nil,
|
||||
nil => nil
|
||||
:staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
|
||||
:staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
|
||||
:staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
|
||||
:pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
|
||||
:canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
|
||||
:production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
|
||||
:foo => nil,
|
||||
nil => nil
|
||||
}
|
||||
end
|
||||
|
||||
|
@ -60,14 +60,14 @@ RSpec.describe QA::Support::Loglinking do
|
|||
describe '.kibana_url' do
|
||||
let(:url_hash) do
|
||||
{
|
||||
:staging => 'https://nonprod-log.gitlab.net/',
|
||||
:staging_canary => 'https://nonprod-log.gitlab.net/',
|
||||
:staging_ref => nil,
|
||||
:pre => nil,
|
||||
:canary => 'https://log.gprd.gitlab.net/',
|
||||
:production => 'https://log.gprd.gitlab.net/',
|
||||
:foo => nil,
|
||||
nil => nil
|
||||
:staging => 'https://nonprod-log.gitlab.net/',
|
||||
:staging_canary => 'https://nonprod-log.gitlab.net/',
|
||||
:staging_ref => nil,
|
||||
:pre => nil,
|
||||
:canary => 'https://log.gprd.gitlab.net/',
|
||||
:production => 'https://log.gprd.gitlab.net/',
|
||||
:foo => nil,
|
||||
nil => nil
|
||||
}
|
||||
end
|
||||
|
||||
|
|
|
@ -232,7 +232,9 @@ RSpec.describe 'Related issues', :js do
|
|||
it 'add related issue' do
|
||||
click_button 'Add a related issue'
|
||||
fill_in 'Paste issue link', with: "#{issue_b.to_reference(project)} "
|
||||
click_button 'Add'
|
||||
page.within('.linked-issues-card-body') do
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -249,7 +251,9 @@ RSpec.describe 'Related issues', :js do
|
|||
it 'add cross-project related issue' do
|
||||
click_button 'Add a related issue'
|
||||
fill_in 'Paste issue link', with: "#{issue_project_b_a.to_reference(project)} "
|
||||
click_button 'Add'
|
||||
page.within('.linked-issues-card-body') do
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -359,7 +363,9 @@ RSpec.describe 'Related issues', :js do
|
|||
it 'add related issue' do
|
||||
click_button 'Add a related issue'
|
||||
fill_in 'Paste issue link', with: "##{issue_d.iid} "
|
||||
click_button 'Add'
|
||||
page.within('.linked-issues-card-body') do
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -375,7 +381,9 @@ RSpec.describe 'Related issues', :js do
|
|||
it 'add invalid related issue' do
|
||||
click_button 'Add a related issue'
|
||||
fill_in 'Paste issue link', with: '#9999999 '
|
||||
click_button 'Add'
|
||||
page.within('.linked-issues-card-body') do
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
@ -390,7 +398,9 @@ RSpec.describe 'Related issues', :js do
|
|||
it 'add unauthorized related issue' do
|
||||
click_button 'Add a related issue'
|
||||
fill_in 'Paste issue link', with: "#{issue_project_unauthorized_a.to_reference(project)} "
|
||||
click_button 'Add'
|
||||
page.within('.linked-issues-card-body') do
|
||||
click_button 'Add'
|
||||
end
|
||||
|
||||
wait_for_requests
|
||||
|
||||
|
|
|
@ -56,8 +56,9 @@ describe('Customer relations contact form wrapper', () => {
|
|||
${'edit'} | ${'Edit contact'} | ${'Contact has been updated.'} | ${updateContactMutation} | ${contacts[0].id}
|
||||
${'create'} | ${'New contact'} | ${'Contact has been added.'} | ${createContactMutation} | ${null}
|
||||
`('in $mode mode', ({ mode, title, successMessage, mutation, existingId }) => {
|
||||
const isEditMode = mode === 'edit';
|
||||
|
||||
beforeEach(() => {
|
||||
const isEditMode = mode === 'edit';
|
||||
mountComponent({ isEditMode });
|
||||
|
||||
return waitForPromises();
|
||||
|
@ -82,7 +83,7 @@ describe('Customer relations contact form wrapper', () => {
|
|||
});
|
||||
|
||||
it('renders correct fields prop', () => {
|
||||
expect(findContactForm().props('fields')).toEqual([
|
||||
const fields = [
|
||||
{ name: 'firstName', label: 'First name', required: true },
|
||||
{ name: 'lastName', label: 'Last name', required: true },
|
||||
{ name: 'email', label: 'Email', required: true },
|
||||
|
@ -98,7 +99,9 @@ describe('Customer relations contact form wrapper', () => {
|
|||
],
|
||||
},
|
||||
{ name: 'description', label: 'Description' },
|
||||
]);
|
||||
];
|
||||
if (isEditMode) fields.push({ name: 'active', label: 'Active', required: true, bool: true });
|
||||
expect(findContactForm().props('fields')).toEqual(fields);
|
||||
});
|
||||
|
||||
it('renders correct title prop', () => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { GlAlert, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
|
||||
import { GlAlert, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormGroup } from '@gitlab/ui';
|
||||
import Vue from 'vue';
|
||||
import VueApollo from 'vue-apollo';
|
||||
import VueRouter from 'vue-router';
|
||||
|
@ -78,6 +78,7 @@ describe('Reusable form component', () => {
|
|||
const findSaveButton = () => wrapper.findByTestId('save-button');
|
||||
const findForm = () => wrapper.find('form');
|
||||
const findError = () => wrapper.findComponent(GlAlert);
|
||||
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
|
||||
|
||||
const mountComponent = (propsData) => {
|
||||
wrapper = shallowMountExtended(Form, {
|
||||
|
@ -92,7 +93,7 @@ describe('Reusable form component', () => {
|
|||
});
|
||||
};
|
||||
|
||||
const mountContact = ({ propsData } = {}) => {
|
||||
const mountContact = ({ propsData, extraFields = [] } = {}) => {
|
||||
mountComponent({
|
||||
fields: [
|
||||
{ name: 'firstName', label: 'First name', required: true },
|
||||
|
@ -108,6 +109,7 @@ describe('Reusable form component', () => {
|
|||
{ key: 'gid://gitlab/CustomerRelations::Organization/2', value: 'ABC Corp' },
|
||||
],
|
||||
},
|
||||
...extraFields,
|
||||
],
|
||||
getQuery: {
|
||||
query: getGroupContactsQuery,
|
||||
|
@ -136,7 +138,8 @@ describe('Reusable form component', () => {
|
|||
mutation: updateContactMutation,
|
||||
existingId: 'gid://gitlab/CustomerRelations::Contact/12',
|
||||
};
|
||||
mountContact({ propsData });
|
||||
const extraFields = [{ name: 'active', label: 'Active', required: true, bool: true }];
|
||||
mountContact({ propsData, extraFields });
|
||||
};
|
||||
|
||||
const mountOrganization = ({ propsData } = {}) => {
|
||||
|
@ -285,18 +288,16 @@ describe('Reusable form component', () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
index | id | componentName | value
|
||||
${0} | ${'firstName'} | ${'GlFormInput'} | ${'Marty'}
|
||||
${1} | ${'lastName'} | ${'GlFormInput'} | ${'McFly'}
|
||||
${2} | ${'email'} | ${'GlFormInput'} | ${'example@gitlab.com'}
|
||||
${4} | ${'description'} | ${'GlFormInput'} | ${undefined}
|
||||
${3} | ${'phone'} | ${'GlFormInput'} | ${undefined}
|
||||
${5} | ${'organizationId'} | ${'GlFormSelect'} | ${'gid://gitlab/CustomerRelations::Organization/2'}
|
||||
index | id | component | value
|
||||
${0} | ${'firstName'} | ${GlFormInput} | ${'Marty'}
|
||||
${1} | ${'lastName'} | ${GlFormInput} | ${'McFly'}
|
||||
${2} | ${'email'} | ${GlFormInput} | ${'example@gitlab.com'}
|
||||
${4} | ${'description'} | ${GlFormInput} | ${undefined}
|
||||
${3} | ${'phone'} | ${GlFormInput} | ${undefined}
|
||||
${5} | ${'organizationId'} | ${GlFormSelect} | ${'gid://gitlab/CustomerRelations::Organization/2'}
|
||||
`(
|
||||
'should render a $componentName for #$id with the value "$value"',
|
||||
({ index, id, componentName, value }) => {
|
||||
const component = componentName === 'GlFormInput' ? GlFormInput : GlFormSelect;
|
||||
const findFormGroup = (at) => wrapper.findAllComponents(GlFormGroup).at(at);
|
||||
'should render the correct component for #$id with the value "$value"',
|
||||
({ index, id, component, value }) => {
|
||||
const findFormElement = () => findFormGroup(index).find(component);
|
||||
|
||||
expect(findFormElement().attributes('id')).toBe(id);
|
||||
|
@ -304,6 +305,14 @@ describe('Reusable form component', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('should render a checked GlFormCheckbox for #active', () => {
|
||||
const activeCheckboxIndex = 6;
|
||||
const findFormElement = () => findFormGroup(activeCheckboxIndex).find(GlFormCheckbox);
|
||||
|
||||
expect(findFormElement().attributes('id')).toBe('active');
|
||||
expect(findFormElement().attributes('checked')).toBe('true');
|
||||
});
|
||||
|
||||
it('should include updated values in update mutation', () => {
|
||||
wrapper.find('#firstName').vm.$emit('input', 'Michael');
|
||||
wrapper
|
||||
|
@ -314,6 +323,7 @@ describe('Reusable form component', () => {
|
|||
|
||||
expect(handler).toHaveBeenCalledWith('updateContact', {
|
||||
input: {
|
||||
active: true,
|
||||
description: null,
|
||||
email: 'example@gitlab.com',
|
||||
firstName: 'Michael',
|
||||
|
|
|
@ -13,6 +13,7 @@ export const getGroupContactsQueryResponse = {
|
|||
email: 'example@gitlab.com',
|
||||
phone: null,
|
||||
description: null,
|
||||
active: true,
|
||||
organization: {
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
id: 'gid://gitlab/CustomerRelations::Organization/2',
|
||||
|
@ -27,6 +28,7 @@ export const getGroupContactsQueryResponse = {
|
|||
email: null,
|
||||
phone: null,
|
||||
description: null,
|
||||
active: true,
|
||||
organization: null,
|
||||
},
|
||||
{
|
||||
|
@ -37,6 +39,7 @@ export const getGroupContactsQueryResponse = {
|
|||
email: 'jd@gitlab.com',
|
||||
phone: '+44 44 4444 4444',
|
||||
description: 'Vice President',
|
||||
active: true,
|
||||
organization: null,
|
||||
},
|
||||
],
|
||||
|
@ -58,6 +61,7 @@ export const getGroupOrganizationsQueryResponse = {
|
|||
name: 'Test Inc',
|
||||
defaultRate: 100,
|
||||
description: null,
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
|
@ -65,6 +69,7 @@ export const getGroupOrganizationsQueryResponse = {
|
|||
name: 'ABC Company',
|
||||
defaultRate: 110,
|
||||
description: 'VIP',
|
||||
active: true,
|
||||
},
|
||||
{
|
||||
__typename: 'CustomerRelationsOrganization',
|
||||
|
@ -72,6 +77,7 @@ export const getGroupOrganizationsQueryResponse = {
|
|||
name: 'GitLab',
|
||||
defaultRate: 120,
|
||||
description: null,
|
||||
active: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -91,6 +97,7 @@ export const createContactMutationResponse = {
|
|||
phone: null,
|
||||
description: null,
|
||||
organization: null,
|
||||
active: true,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
|
@ -119,6 +126,7 @@ export const updateContactMutationResponse = {
|
|||
phone: null,
|
||||
description: null,
|
||||
organization: null,
|
||||
active: true,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
|
@ -143,6 +151,7 @@ export const createOrganizationMutationResponse = {
|
|||
name: 'A',
|
||||
defaultRate: null,
|
||||
description: null,
|
||||
active: true,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
|
@ -168,6 +177,7 @@ export const updateOrganizationMutationResponse = {
|
|||
name: 'A',
|
||||
defaultRate: null,
|
||||
description: null,
|
||||
active: true,
|
||||
},
|
||||
errors: [],
|
||||
},
|
||||
|
|
|
@ -49,7 +49,7 @@ describe('Customer relations organization form wrapper', () => {
|
|||
mountComponent({ isEditMode: true });
|
||||
|
||||
const organizationForm = findOrganizationForm();
|
||||
expect(organizationForm.props('fields')).toHaveLength(3);
|
||||
expect(organizationForm.props('fields')).toHaveLength(4);
|
||||
expect(organizationForm.props('title')).toBe('Edit organization');
|
||||
expect(organizationForm.props('successMessage')).toBe('Organization has been updated.');
|
||||
expect(organizationForm.props('mutation')).toBe(updateOrganizationMutation);
|
||||
|
|
|
@ -3,6 +3,7 @@ import IDEProjectHeader from '~/ide/components/ide_project_header.vue';
|
|||
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
|
||||
|
||||
const mockProject = {
|
||||
id: 1,
|
||||
name: 'test proj',
|
||||
avatar_url: 'https://gitlab.com',
|
||||
path_with_namespace: 'path/with-namespace',
|
||||
|
@ -30,6 +31,7 @@ describe('IDE project header', () => {
|
|||
|
||||
it('renders ProjectAvatar with correct props', () => {
|
||||
expect(findProjectAvatar().props()).toMatchObject({
|
||||
projectId: mockProject.id,
|
||||
projectName: mockProject.name,
|
||||
projectAvatarUrl: mockProject.avatar_url,
|
||||
});
|
||||
|
|
|
@ -15,6 +15,7 @@ import UrlSync from '~/vue_shared/components/url_sync.vue';
|
|||
const mockOverrides = Array(DEFAULT_PER_PAGE * 3)
|
||||
.fill(1)
|
||||
.map((_, index) => ({
|
||||
id: index,
|
||||
name: `test-proj-${index}`,
|
||||
avatar_url: `avatar-${index}`,
|
||||
full_path: `test-proj-${index}`,
|
||||
|
@ -59,6 +60,7 @@ describe('IntegrationOverrides', () => {
|
|||
const avatar = link.findComponent(ProjectAvatar);
|
||||
|
||||
return {
|
||||
id: avatar.props('projectId'),
|
||||
href: link.attributes('href'),
|
||||
avatarUrl: avatar.props('projectAvatarUrl'),
|
||||
avatarName: avatar.props('projectName'),
|
||||
|
@ -109,6 +111,7 @@ describe('IntegrationOverrides', () => {
|
|||
it('renders overrides as rows in table', () => {
|
||||
expect(findRowsAsModel()).toEqual(
|
||||
mockOverrides.map((x) => ({
|
||||
id: x.id,
|
||||
href: x.full_path,
|
||||
avatarUrl: x.avatar_url,
|
||||
avatarName: x.name,
|
||||
|
|
|
@ -18,9 +18,10 @@ import {
|
|||
describe('RelatedIssuesBlock', () => {
|
||||
let wrapper;
|
||||
|
||||
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('add-button');
|
||||
const findToggleButton = () => wrapper.findByTestId('toggle-links');
|
||||
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
|
||||
const findIssueCountBadgeAddButton = () =>
|
||||
wrapper.find('[data-testid="related-issues-plus-button"]');
|
||||
|
||||
afterEach(() => {
|
||||
if (wrapper) {
|
||||
|
|
|
@ -346,6 +346,17 @@ describe('init markdown', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('indents a blank line two spaces to the right', () => {
|
||||
textArea.value = '012\n\n89';
|
||||
textArea.setSelectionRange(4, 4);
|
||||
|
||||
textArea.dispatchEvent(indentEvent);
|
||||
|
||||
expect(textArea.value).toEqual('012\n \n89');
|
||||
expect(textArea.selectionStart).toEqual(6);
|
||||
expect(textArea.selectionEnd).toEqual(6);
|
||||
});
|
||||
|
||||
it.each`
|
||||
selectionStart | selectionEnd | expected | expectedSelectionStart | expectedSelectionEnd
|
||||
${0} | ${0} | ${'234\n 789\n 34'} | ${0} | ${0}
|
||||
|
@ -356,6 +367,7 @@ describe('init markdown', () => {
|
|||
${14} | ${15} | ${' 234\n 789\n34'} | ${12} | ${13}
|
||||
${0} | ${15} | ${'234\n789\n34'} | ${0} | ${10}
|
||||
${3} | ${13} | ${'234\n789\n34'} | ${1} | ${8}
|
||||
${6} | ${6} | ${' 234\n789\n 34'} | ${6} | ${6}
|
||||
`(
|
||||
'outdents the selected lines two spaces to the left',
|
||||
({
|
||||
|
@ -377,6 +389,17 @@ describe('init markdown', () => {
|
|||
},
|
||||
);
|
||||
|
||||
it('outdent a blank line has no effect', () => {
|
||||
textArea.value = '012\n\n89';
|
||||
textArea.setSelectionRange(4, 4);
|
||||
|
||||
textArea.dispatchEvent(outdentEvent);
|
||||
|
||||
expect(textArea.value).toEqual('012\n\n89');
|
||||
expect(textArea.selectionStart).toEqual(4);
|
||||
expect(textArea.selectionEnd).toEqual(4);
|
||||
});
|
||||
|
||||
it('does not indent if meta is not set', () => {
|
||||
const indentNoMetaEvent = new KeyboardEvent('keydown', { key: ']' });
|
||||
const text = '012\n456\n89';
|
||||
|
|
|
@ -128,7 +128,7 @@ describe('packages_list_row', () => {
|
|||
findDeleteButton().vm.$emit('click');
|
||||
|
||||
await nextTick();
|
||||
expect(wrapper.emitted('packageToDelete')).toBeTruthy();
|
||||
expect(wrapper.emitted('packageToDelete')).toHaveLength(1);
|
||||
expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,13 +43,39 @@ describe('ProjectAvatar', () => {
|
|||
});
|
||||
|
||||
describe('with `projectId` prop', () => {
|
||||
it('renders GlAvatar with specified `entityId` prop', () => {
|
||||
const validatorFunc = ProjectAvatar.props.projectId.validator;
|
||||
|
||||
it('prop validators return true for valid types', () => {
|
||||
expect(validatorFunc(1)).toBe(true);
|
||||
expect(validatorFunc('gid://gitlab/Project/1')).toBe(true);
|
||||
});
|
||||
|
||||
it('prop validators return false for invalid types', () => {
|
||||
expect(validatorFunc('1')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders GlAvatar with `entityId` 0 when `projectId` is not informed', () => {
|
||||
createComponent({ props: { projectId: undefined } });
|
||||
|
||||
const avatar = findGlAvatar();
|
||||
expect(avatar.props('entityId')).toBe(0);
|
||||
});
|
||||
|
||||
it('renders GlAvatar with specified `entityId` when `projectId` is a Number', () => {
|
||||
const mockProjectId = 1;
|
||||
createComponent({ props: { projectId: mockProjectId } });
|
||||
|
||||
const avatar = findGlAvatar();
|
||||
expect(avatar.props('entityId')).toBe(mockProjectId);
|
||||
});
|
||||
|
||||
it('renders GlAvatar with specified `entityId` when `projectId` is a gid String', () => {
|
||||
const mockProjectId = 'gid://gitlab/Project/1';
|
||||
createComponent({ props: { projectId: mockProjectId } });
|
||||
|
||||
const avatar = findGlAvatar();
|
||||
expect(avatar.props('entityId')).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with `projectAvatarUrl` prop', () => {
|
||||
|
|
|
@ -56,6 +56,7 @@ describe('ProjectListItem component', () => {
|
|||
|
||||
expect(avatar.exists()).toBe(true);
|
||||
expect(avatar.props()).toMatchObject({
|
||||
projectId: project.id,
|
||||
projectAvatarUrl: '',
|
||||
projectName: project.name_with_namespace,
|
||||
});
|
||||
|
|
|
@ -4589,24 +4589,6 @@ RSpec.describe Ci::Pipeline, :mailer, factory_default: :keep do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#find_stage_by_name' do
|
||||
subject { pipeline.find_stage_by_name!(stage_name) }
|
||||
|
||||
context 'when stage exists' do
|
||||
it { is_expected.to eq(stage) }
|
||||
end
|
||||
|
||||
context 'when stage does not exist' do
|
||||
let(:stage_name) { 'build' }
|
||||
|
||||
it 'raises an ActiveRecord exception' do
|
||||
expect do
|
||||
subject
|
||||
end.to raise_exception(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#full_error_messages' do
|
||||
|
|
|
@ -10,27 +10,6 @@ RSpec.describe OauthAccessToken do
|
|||
let(:token) { create(:oauth_access_token, application_id: app_one.id) }
|
||||
|
||||
describe 'scopes' do
|
||||
describe '.distinct_resource_owner_counts' do
|
||||
let(:tokens) { described_class.all }
|
||||
|
||||
before do
|
||||
token
|
||||
create_list(:oauth_access_token, 2, resource_owner: user, application_id: app_two.id)
|
||||
end
|
||||
|
||||
it 'returns unique owners' do
|
||||
expect(tokens.count).to eq(3)
|
||||
expect(tokens.distinct_resource_owner_counts([app_one])).to eq({ app_one.id => 1 })
|
||||
expect(tokens.distinct_resource_owner_counts([app_two])).to eq({ app_two.id => 1 })
|
||||
expect(tokens.distinct_resource_owner_counts([app_three])).to eq({})
|
||||
expect(tokens.distinct_resource_owner_counts([app_one, app_two]))
|
||||
.to eq({
|
||||
app_one.id => 1,
|
||||
app_two.id => 1
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
describe '.latest_per_application' do
|
||||
let!(:app_two_token1) { create(:oauth_access_token, application: app_two) }
|
||||
let!(:app_two_token2) { create(:oauth_access_token, application: app_two) }
|
||||
|
|
|
@ -38,6 +38,7 @@ RSpec.describe Admin::IntegrationsController, :enable_admin_mode do
|
|||
expect(response).to include_pagination_headers
|
||||
expect(json_response).to contain_exactly(
|
||||
{
|
||||
'id' => project.id,
|
||||
'avatar_url' => project.avatar_url,
|
||||
'full_name' => project.full_name,
|
||||
'name' => project.name,
|
||||
|
|
|
@ -16,6 +16,7 @@ RSpec.describe Integrations::ProjectEntity do
|
|||
|
||||
it 'contains needed attributes' do
|
||||
expect(subject).to include(
|
||||
id: project.id,
|
||||
avatar_url: include('uploads'),
|
||||
name: project.name,
|
||||
full_path: project_path(project),
|
||||
|
|
76
yarn.lock
76
yarn.lock
|
@ -1075,10 +1075,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@gitlab/visual-review-tools/-/visual-review-tools-1.7.3.tgz#9ea641146436da388ffbad25d7f2abe0df52c235"
|
||||
integrity sha512-NMV++7Ew1FSBDN1xiZaauU9tfeSfgDHcOLpn+8bGpP+O5orUPm2Eu66R5eC5gkjBPaXosNAxNWtriee+aFk4+g==
|
||||
|
||||
"@graphql-eslint/eslint-plugin@3.10.6":
|
||||
version "3.10.6"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.6.tgz#4d5748fade6c11d74aeff9a99d6e38d2ed8f6310"
|
||||
integrity sha512-rxGSrKVsDHCuZRvP81ElgtCs0sikdhcHqQySiyhir4G+VhiNlPZ7SQJWrXm9JJEAeB0wQ50kabvse5NRk0hqog==
|
||||
"@graphql-eslint/eslint-plugin@3.10.7":
|
||||
version "3.10.7"
|
||||
resolved "https://registry.yarnpkg.com/@graphql-eslint/eslint-plugin/-/eslint-plugin-3.10.7.tgz#9a203e2084371eca933d88b73ce7a6bebbbb9872"
|
||||
integrity sha512-Vp32LMsHTgRNc2q+OrXRNR1i2nlAbVfN0tMTlFHgnzJfnEJDV332cpjiUF9F82IKjNFSde/pF3cuYccu+UUR/g==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.16.7"
|
||||
"@graphql-tools/code-file-loader" "^7.2.14"
|
||||
|
@ -1248,15 +1248,20 @@
|
|||
resolved "https://registry.yarnpkg.com/@graphql-typed-document-node/core/-/core-3.1.0.tgz#0eee6373e11418bfe0b5638f654df7a4ca6a3950"
|
||||
integrity sha512-wYn6r8zVZyQJ6rQaALBEln5B1pzxb9shV5Ef97kTvn6yVGrqyXVnDqnU24MXnFubR+rZjBY9NWuxX3FB2sTsjg==
|
||||
|
||||
"@humanwhocodes/config-array@^0.9.2":
|
||||
version "0.9.5"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.5.tgz#2cbaf9a89460da24b5ca6531b8bbfc23e1df50c7"
|
||||
integrity sha512-ObyMyWxZiCu/yTisA7uzx81s40xR2fD5Cg/2Kq7G02ajkNubJf6BopgDTmDyc3U7sXpNKM8cYOw7s7Tyr+DnCw==
|
||||
"@humanwhocodes/config-array@^0.10.4":
|
||||
version "0.10.4"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.10.4.tgz#01e7366e57d2ad104feea63e72248f22015c520c"
|
||||
integrity sha512-mXAIHxZT3Vcpg83opl1wGlVZ9xydbfZO3r5YfRSH6Gpp2J/PfdBP0wbDa2sO6/qRbcalpoevVyW6A/fI6LfeMw==
|
||||
dependencies:
|
||||
"@humanwhocodes/object-schema" "^1.2.1"
|
||||
debug "^4.1.1"
|
||||
minimatch "^3.0.4"
|
||||
|
||||
"@humanwhocodes/gitignore-to-minimatch@^1.0.2":
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/gitignore-to-minimatch/-/gitignore-to-minimatch-1.0.2.tgz#316b0a63b91c10e53f242efb4ace5c3b34e8728d"
|
||||
integrity sha512-rSqmMJDdLFUsyxR6FMtD00nfQKKLFb1kv+qBbOVKqErvloEIJLo5bDTJTQNTYgeyp78JsA7u/NPi5jT1GR/MuA==
|
||||
|
||||
"@humanwhocodes/object-schema@^1.2.1":
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45"
|
||||
|
@ -2485,7 +2490,7 @@ acorn@^7.1.1:
|
|||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa"
|
||||
integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==
|
||||
|
||||
acorn@^8.0.4, acorn@^8.2.4, acorn@^8.7.1:
|
||||
acorn@^8.0.4, acorn@^8.2.4, acorn@^8.8.0:
|
||||
version "8.8.0"
|
||||
resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.8.0.tgz#88c0187620435c7f6015803f5539dae05a9dbea8"
|
||||
integrity sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==
|
||||
|
@ -5262,13 +5267,14 @@ eslint-visitor-keys@^3.1.0, eslint-visitor-keys@^3.3.0:
|
|||
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826"
|
||||
integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==
|
||||
|
||||
eslint@8.19.0:
|
||||
version "8.19.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.19.0.tgz#7342a3cbc4fbc5c106a1eefe0fd0b50b6b1a7d28"
|
||||
integrity sha512-SXOPj3x9VKvPe81TjjUJCYlV4oJjQw68Uek+AM0X4p+33dj2HY5bpTZOgnQHcG2eAm1mtCU9uNMnJi7exU/kYw==
|
||||
eslint@8.21.0:
|
||||
version "8.21.0"
|
||||
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.21.0.tgz#1940a68d7e0573cef6f50037addee295ff9be9ef"
|
||||
integrity sha512-/XJ1+Qurf1T9G2M5IHrsjp+xrGT73RZf23xA1z5wB1ZzzEAWSZKvRwhWxTFp1rvkvCfwcvAUNAP31bhKTTGfDA==
|
||||
dependencies:
|
||||
"@eslint/eslintrc" "^1.3.0"
|
||||
"@humanwhocodes/config-array" "^0.9.2"
|
||||
"@humanwhocodes/config-array" "^0.10.4"
|
||||
"@humanwhocodes/gitignore-to-minimatch" "^1.0.2"
|
||||
ajv "^6.10.0"
|
||||
chalk "^4.0.0"
|
||||
cross-spawn "^7.0.2"
|
||||
|
@ -5278,14 +5284,17 @@ eslint@8.19.0:
|
|||
eslint-scope "^7.1.1"
|
||||
eslint-utils "^3.0.0"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
espree "^9.3.2"
|
||||
espree "^9.3.3"
|
||||
esquery "^1.4.0"
|
||||
esutils "^2.0.2"
|
||||
fast-deep-equal "^3.1.3"
|
||||
file-entry-cache "^6.0.1"
|
||||
find-up "^5.0.0"
|
||||
functional-red-black-tree "^1.0.1"
|
||||
glob-parent "^6.0.1"
|
||||
globals "^13.15.0"
|
||||
globby "^11.1.0"
|
||||
grapheme-splitter "^1.0.4"
|
||||
ignore "^5.2.0"
|
||||
import-fresh "^3.0.0"
|
||||
imurmurhash "^0.1.4"
|
||||
|
@ -5303,12 +5312,12 @@ eslint@8.19.0:
|
|||
text-table "^0.2.0"
|
||||
v8-compile-cache "^2.0.3"
|
||||
|
||||
espree@^9.0.0, espree@^9.3.2:
|
||||
version "9.3.2"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.2.tgz#f58f77bd334731182801ced3380a8cc859091596"
|
||||
integrity sha512-D211tC7ZwouTIuY5x9XnS0E9sWNChB7IYKX/Xp5eQj3nFXhqmiUDB9q27y76oFl8jTg3pXcQx/bpxMfs3CIZbA==
|
||||
espree@^9.0.0, espree@^9.3.2, espree@^9.3.3:
|
||||
version "9.3.3"
|
||||
resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.3.tgz#2dd37c4162bb05f433ad3c1a52ddf8a49dc08e9d"
|
||||
integrity sha512-ORs1Rt/uQTqUKjDdGCyrtYxbazf5umATSf/K4qxjmZHORR6HJk+2s/2Pqe+Kk49HHINC/xNIrGfgh8sZcll0ng==
|
||||
dependencies:
|
||||
acorn "^8.7.1"
|
||||
acorn "^8.8.0"
|
||||
acorn-jsx "^5.3.2"
|
||||
eslint-visitor-keys "^3.3.0"
|
||||
|
||||
|
@ -5678,6 +5687,14 @@ find-up@^4.0.0, find-up@^4.1.0:
|
|||
locate-path "^5.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
find-up@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-up/-/find-up-5.0.0.tgz#4c92819ecb7083561e4f4a240a86be5198f536fc"
|
||||
integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
|
||||
dependencies:
|
||||
locate-path "^6.0.0"
|
||||
path-exists "^4.0.0"
|
||||
|
||||
find-yarn-workspace-root@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-yarn-workspace-root/-/find-yarn-workspace-root-2.0.0.tgz#f47fb8d239c900eb78179aa81b66673eac88f7bd"
|
||||
|
@ -6007,6 +6024,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6
|
|||
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
|
||||
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
|
||||
|
||||
grapheme-splitter@^1.0.4:
|
||||
version "1.0.4"
|
||||
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
|
||||
integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==
|
||||
|
||||
graphlib@^2.1.8:
|
||||
version "2.1.8"
|
||||
resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da"
|
||||
|
@ -7711,6 +7733,13 @@ locate-path@^5.0.0:
|
|||
dependencies:
|
||||
p-locate "^4.1.0"
|
||||
|
||||
locate-path@^6.0.0:
|
||||
version "6.0.0"
|
||||
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-6.0.0.tgz#55321eb309febbc59c4801d931a72452a681d286"
|
||||
integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
|
||||
dependencies:
|
||||
p-locate "^5.0.0"
|
||||
|
||||
lodash.assign@^4.2.0:
|
||||
version "4.2.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7"
|
||||
|
@ -9241,6 +9270,13 @@ p-locate@^4.1.0:
|
|||
dependencies:
|
||||
p-limit "^2.2.0"
|
||||
|
||||
p-locate@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-5.0.0.tgz#83c8315c6785005e3bd021839411c9e110e6d834"
|
||||
integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
|
||||
dependencies:
|
||||
p-limit "^3.0.2"
|
||||
|
||||
p-map@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/p-map/-/p-map-4.0.0.tgz#bb2f95a5eda2ec168ec9274e06a747c3e2904d2b"
|
||||
|
|
Loading…
Reference in New Issue