Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
f675c7d41d
commit
444f662b8d
33 changed files with 569 additions and 305 deletions
|
@ -105,8 +105,8 @@ export default {
|
||||||
atVersion: this.designsVersion,
|
atVersion: this.designsVersion,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
isDiscussionHighlighted() {
|
isDiscussionActive() {
|
||||||
return this.discussion.notes[0].id === this.activeDiscussion.id;
|
return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
|
||||||
},
|
},
|
||||||
resolveCheckboxText() {
|
resolveCheckboxText() {
|
||||||
return this.discussion.resolved
|
return this.discussion.resolved
|
||||||
|
@ -134,18 +134,6 @@ export default {
|
||||||
isFormVisible() {
|
isFormVisible() {
|
||||||
return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
|
return this.isFormRendered && this.discussionWithOpenForm === this.discussion.id;
|
||||||
},
|
},
|
||||||
shouldScrollToDiscussion(activeDiscussion) {
|
|
||||||
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
|
|
||||||
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
|
|
||||||
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
|
|
||||||
];
|
|
||||||
const { id: activeDiscussionId, source: activeDiscussionSource } = activeDiscussion;
|
|
||||||
|
|
||||||
return (
|
|
||||||
ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(activeDiscussionSource) &&
|
|
||||||
activeDiscussionId === this.discussion.notes[0].id
|
|
||||||
);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
addDiscussionComment(
|
addDiscussionComment(
|
||||||
|
@ -199,6 +187,14 @@ export default {
|
||||||
this.isResolving = false;
|
this.isResolving = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
shouldScrollToDiscussion(activeDiscussion) {
|
||||||
|
const ALLOWED_ACTIVE_DISCUSSION_SOURCES = [
|
||||||
|
ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
|
||||||
|
ACTIVE_DISCUSSION_SOURCE_TYPES.url,
|
||||||
|
];
|
||||||
|
const { source } = activeDiscussion;
|
||||||
|
return ALLOWED_ACTIVE_DISCUSSION_SOURCES.includes(source) && this.isDiscussionActive;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
createNoteMutation,
|
createNoteMutation,
|
||||||
};
|
};
|
||||||
|
@ -221,7 +217,7 @@ export default {
|
||||||
:note="firstNote"
|
:note="firstNote"
|
||||||
:markdown-preview-path="markdownPreviewPath"
|
:markdown-preview-path="markdownPreviewPath"
|
||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
|
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
||||||
@error="$emit('updateNoteError', $event)"
|
@error="$emit('updateNoteError', $event)"
|
||||||
>
|
>
|
||||||
<template v-if="discussion.resolvable" #resolveDiscussion>
|
<template v-if="discussion.resolvable" #resolveDiscussion>
|
||||||
|
@ -265,7 +261,7 @@ export default {
|
||||||
:note="note"
|
:note="note"
|
||||||
:markdown-preview-path="markdownPreviewPath"
|
:markdown-preview-path="markdownPreviewPath"
|
||||||
:is-resolving="isResolving"
|
:is-resolving="isResolving"
|
||||||
:class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
|
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
|
||||||
@error="$emit('updateNoteError', $event)"
|
@error="$emit('updateNoteError', $event)"
|
||||||
/>
|
/>
|
||||||
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
|
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
|
||||||
|
|
|
@ -7,7 +7,7 @@ import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link
|
||||||
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
|
||||||
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
|
||||||
import DesignReplyForm from './design_reply_form.vue';
|
import DesignReplyForm from './design_reply_form.vue';
|
||||||
import { findNoteId } from '../../utils/design_management_utils';
|
import { findNoteId, extractDesignNoteId } from '../../utils/design_management_utils';
|
||||||
import { hasErrors } from '../../utils/cache_update';
|
import { hasErrors } from '../../utils/cache_update';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -47,7 +47,7 @@ export default {
|
||||||
return findNoteId(this.note.id);
|
return findNoteId(this.note.id);
|
||||||
},
|
},
|
||||||
isNoteLinked() {
|
isNoteLinked() {
|
||||||
return this.$route.hash === `#note_${this.noteAnchorId}`;
|
return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
|
||||||
},
|
},
|
||||||
mutationPayload() {
|
mutationPayload() {
|
||||||
return {
|
return {
|
||||||
|
@ -59,13 +59,6 @@ export default {
|
||||||
return !this.isEditing && this.note.userPermissions.adminNote;
|
return !this.isEditing && this.note.userPermissions.adminNote;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
if (this.isNoteLinked) {
|
|
||||||
this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
hideForm() {
|
hideForm() {
|
||||||
this.isEditing = false;
|
this.isEditing = false;
|
||||||
|
|
|
@ -237,7 +237,12 @@ export default {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isNoteInactive(note) {
|
isNoteInactive(note) {
|
||||||
return this.activeDiscussion.id && this.activeDiscussion.id !== note.id;
|
const discussionNotes = note.discussion.notes.nodes || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
this.activeDiscussion.id &&
|
||||||
|
!discussionNotes.some(({ id }) => id === this.activeDiscussion.id)
|
||||||
|
);
|
||||||
},
|
},
|
||||||
designPinClass(note) {
|
designPinClass(note) {
|
||||||
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
|
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
|
||||||
|
|
|
@ -25,5 +25,10 @@ fragment DesignNote on Note {
|
||||||
}
|
}
|
||||||
discussion {
|
discussion {
|
||||||
id
|
id
|
||||||
|
notes {
|
||||||
|
nodes {
|
||||||
|
id
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,11 +11,11 @@
|
||||||
- if auth_active?(provider)
|
- if auth_active?(provider)
|
||||||
- if unlink_allowed
|
- if unlink_allowed
|
||||||
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
|
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'provider-btn' do
|
||||||
= s_('Profiles|Disconnect')
|
= s_('Profiles|Disconnect %{provider}') % { provider: label_for_provider(provider) }
|
||||||
- else
|
- else
|
||||||
%a.provider-btn
|
%a.provider-btn
|
||||||
= s_('Profiles|Active')
|
= s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
|
||||||
- elsif link_allowed
|
- elsif link_allowed
|
||||||
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn gl-text-blue-500' do
|
= link_to omniauth_authorize_path(:user, provider), method: :post, class: 'provider-btn gl-text-blue-500' do
|
||||||
= s_('Profiles|Connect')
|
= s_('Profiles|Connect %{provider}') % { provider: label_for_provider(provider) }
|
||||||
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
|
= render_if_exists 'profiles/accounts/group_saml_unlink_buttons', group_saml_identities: group_saml_identities
|
||||||
|
|
5
changelogs/unreleased/202012-pypi-job-tokens.yml
Normal file
5
changelogs/unreleased/202012-pypi-job-tokens.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Add job token authentication for the GitLab PyPI package repository
|
||||||
|
merge_request: 40888
|
||||||
|
author:
|
||||||
|
type: added
|
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Drop code_owner column from approval_merge_request_rules
|
||||||
|
merge_request: 40322
|
||||||
|
author:
|
||||||
|
type: other
|
5
changelogs/unreleased/dblessing-social-connect-text.yml
Normal file
5
changelogs/unreleased/dblessing-social-connect-text.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Display provider name for profile social sign-in connectors
|
||||||
|
merge_request: 41198
|
||||||
|
author:
|
||||||
|
type: changed
|
5
changelogs/unreleased/fix-scroll-to-note-designs.yml
Normal file
5
changelogs/unreleased/fix-scroll-to-note-designs.yml
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
---
|
||||||
|
title: Highlight design discussion if any comment in discussion is linked
|
||||||
|
merge_request: 41062
|
||||||
|
author:
|
||||||
|
type: fixed
|
|
@ -0,0 +1,36 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
|
class DropCodeOwnerColumnFromApprovalMergeRequestRule < ActiveRecord::Migration[6.0]
|
||||||
|
include Gitlab::Database::MigrationHelpers
|
||||||
|
|
||||||
|
DOWNTIME = false
|
||||||
|
disable_ddl_transaction!
|
||||||
|
|
||||||
|
def up
|
||||||
|
with_lock_retries do
|
||||||
|
remove_column :approval_merge_request_rules, :code_owner
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def down
|
||||||
|
unless column_exists?(:approval_merge_request_rules, :code_owner)
|
||||||
|
with_lock_retries do
|
||||||
|
add_column :approval_merge_request_rules, :code_owner, :boolean, default: false, null: false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
add_concurrent_index(
|
||||||
|
:approval_merge_request_rules,
|
||||||
|
[:merge_request_id, :code_owner, :name],
|
||||||
|
unique: true,
|
||||||
|
where: "code_owner = true AND section IS NULL",
|
||||||
|
name: "approval_rule_name_index_for_code_owners"
|
||||||
|
)
|
||||||
|
|
||||||
|
add_concurrent_index(
|
||||||
|
:approval_merge_request_rules,
|
||||||
|
[:merge_request_id, :code_owner],
|
||||||
|
name: "index_approval_merge_request_rules_1"
|
||||||
|
)
|
||||||
|
end
|
||||||
|
end
|
1
db/schema_migrations/20200901212304
Normal file
1
db/schema_migrations/20200901212304
Normal file
|
@ -0,0 +1 @@
|
||||||
|
6fb93002ffd5c1d1bfff5bea8a99cbbfc7cefefbc450a9d067ee0cfab8d11e9e
|
|
@ -9304,7 +9304,6 @@ CREATE TABLE public.approval_merge_request_rules (
|
||||||
updated_at timestamp with time zone NOT NULL,
|
updated_at timestamp with time zone NOT NULL,
|
||||||
merge_request_id integer NOT NULL,
|
merge_request_id integer NOT NULL,
|
||||||
approvals_required smallint DEFAULT 0 NOT NULL,
|
approvals_required smallint DEFAULT 0 NOT NULL,
|
||||||
code_owner boolean DEFAULT false NOT NULL,
|
|
||||||
name character varying NOT NULL,
|
name character varying NOT NULL,
|
||||||
rule_type smallint DEFAULT 1 NOT NULL,
|
rule_type smallint DEFAULT 1 NOT NULL,
|
||||||
report_type smallint,
|
report_type smallint,
|
||||||
|
@ -19048,8 +19047,6 @@ CREATE UNIQUE INDEX any_approver_project_rule_type_unique_index ON public.approv
|
||||||
|
|
||||||
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
|
CREATE INDEX approval_mr_rule_index_merge_request_id ON public.approval_merge_request_rules USING btree (merge_request_id);
|
||||||
|
|
||||||
CREATE UNIQUE INDEX approval_rule_name_index_for_code_owners ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner, name) WHERE ((code_owner = true) AND (section IS NULL));
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
|
CREATE UNIQUE INDEX backup_labels_group_id_project_id_title_idx ON public.backup_labels USING btree (group_id, project_id, title);
|
||||||
|
|
||||||
CREATE INDEX backup_labels_group_id_title_idx ON public.backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
|
CREATE INDEX backup_labels_group_id_title_idx ON public.backup_labels USING btree (group_id, title) WHERE (project_id = NULL::integer);
|
||||||
|
@ -19222,8 +19219,6 @@ CREATE UNIQUE INDEX index_approval_merge_request_rule_sources_1 ON public.approv
|
||||||
|
|
||||||
CREATE INDEX index_approval_merge_request_rule_sources_2 ON public.approval_merge_request_rule_sources USING btree (approval_project_rule_id);
|
CREATE INDEX index_approval_merge_request_rule_sources_2 ON public.approval_merge_request_rule_sources USING btree (approval_project_rule_id);
|
||||||
|
|
||||||
CREATE INDEX index_approval_merge_request_rules_1 ON public.approval_merge_request_rules USING btree (merge_request_id, code_owner);
|
|
||||||
|
|
||||||
CREATE UNIQUE INDEX index_approval_merge_request_rules_approved_approvers_1 ON public.approval_merge_request_rules_approved_approvers USING btree (approval_merge_request_rule_id, user_id);
|
CREATE UNIQUE INDEX index_approval_merge_request_rules_approved_approvers_1 ON public.approval_merge_request_rules_approved_approvers USING btree (approval_merge_request_rule_id, user_id);
|
||||||
|
|
||||||
CREATE INDEX index_approval_merge_request_rules_approved_approvers_2 ON public.approval_merge_request_rules_approved_approvers USING btree (user_id);
|
CREATE INDEX index_approval_merge_request_rules_approved_approvers_2 ON public.approval_merge_request_rules_approved_approvers USING btree (user_id);
|
||||||
|
|
|
@ -51,12 +51,17 @@ Using the consolidated object storage configuration has a number of advantages:
|
||||||
- It enables the use of [encrypted S3 buckets](#encrypted-s3-buckets).
|
- It enables the use of [encrypted S3 buckets](#encrypted-s3-buckets).
|
||||||
- It [uploads files to S3 with proper `Content-MD5` headers](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222).
|
- It [uploads files to S3 with proper `Content-MD5` headers](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222).
|
||||||
|
|
||||||
NOTE: **Note:**
|
Because [direct upload mode](../development/uploads.md#direct-upload)
|
||||||
Only AWS S3-compatible providers and Google are
|
must be enabled, only the following providers can be used:
|
||||||
supported at the moment since [direct upload
|
|
||||||
mode](../development/uploads.md#direct-upload) must be used. Background
|
- [Amazon S3-compatible providers](#s3-compatible-connection-settings)
|
||||||
upload is not supported in this mode. We recommend direct upload mode because
|
- [Google Cloud Storage](#google-cloud-storage-gcs)
|
||||||
it does not require a shared folder, and [this setting may become the default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331).
|
- [Azure Blob storage](#azure-blob-storage)
|
||||||
|
|
||||||
|
Background upload is not supported with the consolidated object storage
|
||||||
|
configuration. We recommend enabling direct upload mode because it does
|
||||||
|
not require a shared folder, and [this setting may become the
|
||||||
|
default](https://gitlab.com/gitlab-org/gitlab/-/issues/27331).
|
||||||
|
|
||||||
NOTE: **Note:**
|
NOTE: **Note:**
|
||||||
Consolidated object storage configuration cannot be used for
|
Consolidated object storage configuration cannot be used for
|
||||||
|
|
|
@ -266,7 +266,7 @@ POST /groups/:id/epics
|
||||||
| `title` | string | yes | The title of the epic |
|
| `title` | string | yes | The title of the epic |
|
||||||
| `labels` | string | no | The comma separated list of labels |
|
| `labels` | string | no | The comma separated list of labels |
|
||||||
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
|
| `description` | string | no | The description of the epic. Limited to 1,048,576 characters. |
|
||||||
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
| `confidential` | boolean | no | Whether the epic should be confidential |
|
||||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||||
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
|
| `due_date_is_fixed` | boolean | no | Whether due date should be sourced from `due_date_fixed` or from milestones (since 11.3) |
|
||||||
|
@ -347,7 +347,7 @@ PUT /groups/:id/epics/:epic_iid
|
||||||
| `epic_iid` | integer/string | yes | The internal ID of the epic |
|
| `epic_iid` | integer/string | yes | The internal ID of the epic |
|
||||||
| `title` | string | no | The title of an epic |
|
| `title` | string | no | The title of an epic |
|
||||||
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
|
| `description` | string | no | The description of an epic. Limited to 1,048,576 characters. |
|
||||||
| `confidential` | boolean | no | Whether the epic should be confidential. Will be ignored if `confidential_epics` feature flag is disabled. |
|
| `confidential` | boolean | no | Whether the epic should be confidential |
|
||||||
| `labels` | string | no | The comma separated list of labels |
|
| `labels` | string | no | The comma separated list of labels |
|
||||||
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
| `start_date_is_fixed` | boolean | no | Whether start date should be sourced from `start_date_fixed` or from milestones (since 11.3) |
|
||||||
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
| `start_date_fixed` | string | no | The fixed start date of an epic (since 11.3) |
|
||||||
|
|
|
@ -2614,7 +2614,7 @@ input CreateEpicInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled
|
Indicates if the epic is confidential
|
||||||
"""
|
"""
|
||||||
confidential: Boolean
|
confidential: Boolean
|
||||||
|
|
||||||
|
@ -16809,7 +16809,7 @@ input UpdateEpicInput {
|
||||||
clientMutationId: String
|
clientMutationId: String
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled
|
Indicates if the epic is confidential
|
||||||
"""
|
"""
|
||||||
confidential: Boolean
|
confidential: Boolean
|
||||||
|
|
||||||
|
|
|
@ -7089,7 +7089,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "confidential",
|
"name": "confidential",
|
||||||
"description": "Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled",
|
"description": "Indicates if the epic is confidential",
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "Boolean",
|
"name": "Boolean",
|
||||||
|
@ -49522,7 +49522,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "confidential",
|
"name": "confidential",
|
||||||
"description": "Indicates if the epic is confidential. Will be ignored if `confidential_epics` feature flag is disabled",
|
"description": "Indicates if the epic is confidential",
|
||||||
"type": {
|
"type": {
|
||||||
"kind": "SCALAR",
|
"kind": "SCALAR",
|
||||||
"name": "Boolean",
|
"name": "Boolean",
|
||||||
|
|
|
@ -164,18 +164,6 @@ To make an epic confidential:
|
||||||
- **In an existing epic:** in the epic's sidebar, select **Edit** next to **Confidentiality** then
|
- **In an existing epic:** in the epic's sidebar, select **Edit** next to **Confidentiality** then
|
||||||
select **Turn on**.
|
select **Turn on**.
|
||||||
|
|
||||||
### Disable confidential epics **(PREMIUM ONLY)**
|
|
||||||
|
|
||||||
The confidential epics feature is deployed behind a feature flag that is **enabled by default**.
|
|
||||||
[GitLab administrators with access to the GitLab Rails console](../../../administration/feature_flags.md)
|
|
||||||
can disable it for your self-managed instance.
|
|
||||||
|
|
||||||
To disable it:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
Feature.disable(:confidential_epics)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Manage issues assigned to an epic
|
## Manage issues assigned to an epic
|
||||||
|
|
||||||
### Add a new issue to an epic
|
### Add a new issue to an epic
|
||||||
|
|
|
@ -26,19 +26,19 @@ For information on how to create and upload a package, view the GitLab documenta
|
||||||
## Use GitLab CI/CD to build packages
|
## Use GitLab CI/CD to build packages
|
||||||
|
|
||||||
You can use [GitLab CI/CD](../../../ci/README.md) to build packages.
|
You can use [GitLab CI/CD](../../../ci/README.md) to build packages.
|
||||||
For Maven, NuGet and NPM packages, and Composer dependencies, you can
|
For Maven, NuGet, NPM, Conan, and PyPI packages, and Composer dependencies, you can
|
||||||
authenticate with GitLab by using the `CI_JOB_TOKEN`.
|
authenticate with GitLab by using the `CI_JOB_TOKEN`.
|
||||||
|
|
||||||
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
|
CI/CD templates, which you can use to get started, are in [this repo](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/ci/templates).
|
||||||
|
|
||||||
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publishing-the-package-with-cicd), and [NuGet packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd).
|
Learn more about [using CI/CD to build Maven packages](../maven_repository/index.md#creating-maven-packages-with-gitlab-cicd), [NPM packages](../npm_registry/index.md#publishing-a-package-with-cicd), [Composer packages](../composer_repository/index.md#publishing-the-package-with-cicd), [NuGet Packages](../nuget_repository/index.md#publishing-a-nuget-package-with-cicd), [Conan Packages](../conan_repository/index.md#using-gitlab-ci-with-conan-packages), and [PyPI packages](../pypi_repository/index.md#using-gitlab-ci-with-pypi-packages).
|
||||||
|
|
||||||
If you use CI/CD to build a package, extended activity
|
If you use CI/CD to build a package, extended activity
|
||||||
information is displayed when you view the package details:
|
information is displayed when you view the package details:
|
||||||
|
|
||||||
![Package CI/CD activity](img/package_activity_v12_10.png)
|
![Package CI/CD activity](img/package_activity_v12_10.png)
|
||||||
|
|
||||||
You can view which pipeline published the package, as well as the commit and
|
When using Maven and NPM, you can view which pipeline published the package, as well as the commit and
|
||||||
user who triggered it.
|
user who triggered it.
|
||||||
|
|
||||||
## Download a package
|
## Download a package
|
||||||
|
|
|
@ -302,20 +302,10 @@ Successfully installed mypypipackage-0.0.1
|
||||||
|
|
||||||
## Using GitLab CI with PyPI packages
|
## Using GitLab CI with PyPI packages
|
||||||
|
|
||||||
NOTE: **Note:**
|
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11678) in GitLab 13.4.
|
||||||
`CI_JOB_TOKEN`s are not yet supported for use with PyPI.
|
|
||||||
|
|
||||||
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
|
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
|
||||||
[environment variables](./../../../ci/variables/README.md#custom-environment-variables)
|
`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
|
||||||
to access your authentication tokens in your commands.
|
|
||||||
|
|
||||||
Set up environment variables for `TWINE_PASSWORD` and `TWINE_USERNAME` using either:
|
|
||||||
|
|
||||||
- A [personal access token](../../../user/profile/personal_access_tokens.md) and your GitLab username.
|
|
||||||
- A [deploy token](./../../project/deploy_tokens/index.md) and its associated deploy token username.
|
|
||||||
|
|
||||||
You can now access your `TWINE_USERNAME` and `TWINE_PASSWORD` using any `twine` command in your
|
|
||||||
`.gitlab-ci.yml` file.
|
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
|
@ -326,5 +316,18 @@ run:
|
||||||
script:
|
script:
|
||||||
- pip install twine
|
- pip install twine
|
||||||
- python setup.py sdist bdist_wheel
|
- python setup.py sdist bdist_wheel
|
||||||
- TWINE_PASSWORD=${TWINE_PASSWORD} TWINE_USERNAME=${TWINE_USERNAME} python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
- TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url https://gitlab.com/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also use `CI_JOB_TOKEN` in a `~/.pypirc` file that you check into GitLab:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[distutils]
|
||||||
|
index-servers =
|
||||||
|
gitlab
|
||||||
|
|
||||||
|
[gitlab]
|
||||||
|
repository = https://gitlab.com/api/v4/projects/${env.CI_PROJECT_ID}/packages/pypi
|
||||||
|
username = gitlab-ci-token
|
||||||
|
password = ${env.CI_JOB_TOKEN}
|
||||||
```
|
```
|
||||||
|
|
|
@ -64,7 +64,7 @@ module API
|
||||||
requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
|
requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
|
||||||
end
|
end
|
||||||
|
|
||||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||||
get 'files/:sha256/*file_identifier' do
|
get 'files/:sha256/*file_identifier' do
|
||||||
project = unauthorized_user_project!
|
project = unauthorized_user_project!
|
||||||
|
|
||||||
|
@ -87,7 +87,7 @@ module API
|
||||||
|
|
||||||
# An Api entry point but returns an HTML file instead of JSON.
|
# An Api entry point but returns an HTML file instead of JSON.
|
||||||
# PyPi simple API returns the package descriptor as a simple HTML file.
|
# PyPi simple API returns the package descriptor as a simple HTML file.
|
||||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||||
get 'simple/*package_name', format: :txt do
|
get 'simple/*package_name', format: :txt do
|
||||||
authorize_read_package!(authorized_user_project)
|
authorize_read_package!(authorized_user_project)
|
||||||
|
|
||||||
|
@ -117,7 +117,7 @@ module API
|
||||||
optional :sha256_digest, type: String
|
optional :sha256_digest, type: String
|
||||||
end
|
end
|
||||||
|
|
||||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||||
post do
|
post do
|
||||||
authorize_upload!(authorized_user_project)
|
authorize_upload!(authorized_user_project)
|
||||||
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size)
|
bad_request!('File is too large') if authorized_user_project.actual_limits.exceeded?(:pypi_max_file_size, params[:content].size)
|
||||||
|
@ -135,7 +135,7 @@ module API
|
||||||
forbidden!
|
forbidden!
|
||||||
end
|
end
|
||||||
|
|
||||||
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true
|
route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth
|
||||||
post 'authorize' do
|
post 'authorize' do
|
||||||
authorize_workhorse!(
|
authorize_workhorse!(
|
||||||
subject: authorized_user_project,
|
subject: authorized_user_project,
|
||||||
|
|
|
@ -18783,6 +18783,9 @@ msgstr ""
|
||||||
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
|
msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|%{provider} Active"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|@username"
|
msgid "Profiles|@username"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
@ -18834,7 +18837,7 @@ msgstr ""
|
||||||
msgid "Profiles|Commit email"
|
msgid "Profiles|Commit email"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Connect"
|
msgid "Profiles|Connect %{provider}"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Connected Accounts"
|
msgid "Profiles|Connected Accounts"
|
||||||
|
@ -18858,6 +18861,9 @@ msgstr ""
|
||||||
msgid "Profiles|Disconnect"
|
msgid "Profiles|Disconnect"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Profiles|Disconnect %{provider}"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
msgid "Profiles|Do not show on profile"
|
msgid "Profiles|Do not show on profile"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,12 @@ RSpec.describe 'Merge request > User assigns themselves' do
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'updates updated_by', :js do
|
it 'updates updated_by', :js do
|
||||||
expect { click_button 'assign yourself' }.to change { merge_request.reload.updated_at }
|
expect do
|
||||||
|
click_button 'assign yourself'
|
||||||
|
|
||||||
|
expect(find('.assignee')).to have_content(user.name)
|
||||||
|
wait_for_all_requests
|
||||||
|
end.to change { merge_request.reload.updated_at }
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'returns user to the merge request', :js do
|
it 'returns user to the merge request', :js do
|
||||||
|
|
|
@ -9,6 +9,39 @@ RSpec.describe 'Profile > Account', :js do
|
||||||
sign_in(user)
|
sign_in(user)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'Social sign-in' do
|
||||||
|
context 'when an identity does not exist' do
|
||||||
|
before do
|
||||||
|
allow(Devise).to receive_messages(omniauth_configs: { google_oauth2: {} })
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the user to connect' do
|
||||||
|
visit profile_account_path
|
||||||
|
|
||||||
|
expect(page).to have_link('Connect Google', href: '/users/auth/google_oauth2')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when an identity already exists' do
|
||||||
|
before do
|
||||||
|
allow(Devise).to receive_messages(omniauth_configs: { twitter: {}, saml: {} })
|
||||||
|
|
||||||
|
create(:identity, user: user, provider: :twitter)
|
||||||
|
create(:identity, user: user, provider: :saml)
|
||||||
|
|
||||||
|
visit profile_account_path
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'allows the user to disconnect when there is an existing identity' do
|
||||||
|
expect(page).to have_link('Disconnect Twitter', href: '/profile/account/unlink?provider=twitter')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'shows active for a provider that is not allowed to unlink' do
|
||||||
|
expect(page).to have_content('Saml Active')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'Change username' do
|
describe 'Change username' do
|
||||||
let(:new_username) { 'bar' }
|
let(:new_username) { 'bar' }
|
||||||
let(:new_user_path) { "/#{new_username}" }
|
let(:new_user_path) { "/#{new_username}" }
|
||||||
|
|
|
@ -8,8 +8,9 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not
|
||||||
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
|
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
|
||||||
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
|
||||||
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
|
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
|
||||||
|
import mockDiscussion from '../../mock_data/discussion';
|
||||||
|
|
||||||
const discussion = {
|
const defaultMockDiscussion = {
|
||||||
id: '0',
|
id: '0',
|
||||||
resolved: false,
|
resolved: false,
|
||||||
resolvable: true,
|
resolvable: true,
|
||||||
|
@ -49,7 +50,7 @@ describe('Design discussions component', () => {
|
||||||
wrapper = mount(DesignDiscussion, {
|
wrapper = mount(DesignDiscussion, {
|
||||||
propsData: {
|
propsData: {
|
||||||
resolvedDiscussionsExpanded: true,
|
resolvedDiscussionsExpanded: true,
|
||||||
discussion,
|
discussion: defaultMockDiscussion,
|
||||||
noteableId: 'noteable-id',
|
noteableId: 'noteable-id',
|
||||||
designId: 'design-id',
|
designId: 'design-id',
|
||||||
discussionIndex: 1,
|
discussionIndex: 1,
|
||||||
|
@ -82,7 +83,7 @@ describe('Design discussions component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
discussion: {
|
discussion: {
|
||||||
...discussion,
|
...defaultMockDiscussion,
|
||||||
resolvable: false,
|
resolvable: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -125,7 +126,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
it('renders a checkbox with Resolve thread text in reply form', () => {
|
it('renders a checkbox with Resolve thread text in reply form', () => {
|
||||||
findReplyPlaceholder().vm.$emit('onClick');
|
findReplyPlaceholder().vm.$emit('onClick');
|
||||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(findResolveCheckbox().text()).toBe('Resolve thread');
|
expect(findResolveCheckbox().text()).toBe('Resolve thread');
|
||||||
|
@ -141,7 +142,7 @@ describe('Design discussions component', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createComponent({
|
createComponent({
|
||||||
discussion: {
|
discussion: {
|
||||||
...discussion,
|
...defaultMockDiscussion,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
resolvedBy: notes[0].author,
|
resolvedBy: notes[0].author,
|
||||||
resolvedAt: '2020-05-08T07:10:45Z',
|
resolvedAt: '2020-05-08T07:10:45Z',
|
||||||
|
@ -206,7 +207,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
it('renders a checkbox with Unresolve thread text in reply form', () => {
|
it('renders a checkbox with Unresolve thread text in reply form', () => {
|
||||||
findReplyPlaceholder().vm.$emit('onClick');
|
findReplyPlaceholder().vm.$emit('onClick');
|
||||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
|
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
|
||||||
|
@ -218,7 +219,7 @@ describe('Design discussions component', () => {
|
||||||
it('hides reply placeholder and opens form on placeholder click', () => {
|
it('hides reply placeholder and opens form on placeholder click', () => {
|
||||||
createComponent();
|
createComponent();
|
||||||
findReplyPlaceholder().vm.$emit('onClick');
|
findReplyPlaceholder().vm.$emit('onClick');
|
||||||
wrapper.setProps({ discussionWithOpenForm: discussion.id });
|
wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(findReplyPlaceholder().exists()).toBe(false);
|
expect(findReplyPlaceholder().exists()).toBe(false);
|
||||||
|
@ -228,7 +229,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
it('calls mutation on submitting form and closes the form', () => {
|
it('calls mutation on submitting form and closes the form', () => {
|
||||||
createComponent(
|
createComponent(
|
||||||
{ discussionWithOpenForm: discussion.id },
|
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||||
{ discussionComment: 'test', isFormRendered: true },
|
{ discussionComment: 'test', isFormRendered: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -246,7 +247,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
it('clears the discussion comment on closing comment form', () => {
|
it('clears the discussion comment on closing comment form', () => {
|
||||||
createComponent(
|
createComponent(
|
||||||
{ discussionWithOpenForm: discussion.id },
|
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||||
{ discussionComment: 'test', isFormRendered: true },
|
{ discussionComment: 'test', isFormRendered: true },
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -263,19 +264,26 @@ describe('Design discussions component', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('applies correct class to design notes when discussion is highlighted', () => {
|
describe('when any note from a discussion is active', () => {
|
||||||
|
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
|
||||||
|
'applies correct class to all notes in the active discussion',
|
||||||
|
note => {
|
||||||
createComponent(
|
createComponent(
|
||||||
{},
|
{ discussion: mockDiscussion },
|
||||||
{
|
{
|
||||||
activeDiscussion: {
|
activeDiscussion: {
|
||||||
id: notes[0].id,
|
id: note.id,
|
||||||
source: 'pin',
|
source: 'pin',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
|
expect(
|
||||||
true,
|
wrapper
|
||||||
|
.findAll(DesignNote)
|
||||||
|
.wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
|
||||||
|
).toBe(true);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -285,7 +293,7 @@ describe('Design discussions component', () => {
|
||||||
expect(mutate).toHaveBeenCalledWith({
|
expect(mutate).toHaveBeenCalledWith({
|
||||||
mutation: toggleResolveDiscussionMutation,
|
mutation: toggleResolveDiscussionMutation,
|
||||||
variables: {
|
variables: {
|
||||||
id: discussion.id,
|
id: defaultMockDiscussion.id,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -296,7 +304,7 @@ describe('Design discussions component', () => {
|
||||||
|
|
||||||
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
|
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
|
||||||
createComponent(
|
createComponent(
|
||||||
{ discussionWithOpenForm: discussion.id },
|
{ discussionWithOpenForm: defaultMockDiscussion.id },
|
||||||
{ discussionComment: 'test', isFormRendered: true },
|
{ discussionComment: 'test', isFormRendered: true },
|
||||||
);
|
);
|
||||||
findResolveButton().trigger('click');
|
findResolveButton().trigger('click');
|
||||||
|
@ -306,7 +314,7 @@ describe('Design discussions component', () => {
|
||||||
expect(mutate).toHaveBeenCalledWith({
|
expect(mutate).toHaveBeenCalledWith({
|
||||||
mutation: toggleResolveDiscussionMutation,
|
mutation: toggleResolveDiscussionMutation,
|
||||||
variables: {
|
variables: {
|
||||||
id: discussion.id,
|
id: defaultMockDiscussion.id,
|
||||||
resolve: true,
|
resolve: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -86,16 +86,6 @@ describe('Design note component', () => {
|
||||||
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
|
expect(wrapper.find(TimeAgoTooltip).exists()).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should trigger a scrollIntoView method', () => {
|
|
||||||
createComponent({
|
|
||||||
note,
|
|
||||||
});
|
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
|
||||||
expect(scrollIntoViewMock).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not render edit icon when user does not have a permission', () => {
|
it('should not render edit icon when user does not have a permission', () => {
|
||||||
createComponent({
|
createComponent({
|
||||||
note,
|
note,
|
||||||
|
|
|
@ -13,8 +13,9 @@ describe('Design overlay component', () => {
|
||||||
|
|
||||||
const findAllNotes = () => wrapper.findAll('.js-image-badge');
|
const findAllNotes = () => wrapper.findAll('.js-image-badge');
|
||||||
const findCommentBadge = () => wrapper.find('.comment-indicator');
|
const findCommentBadge = () => wrapper.find('.comment-indicator');
|
||||||
const findFirstBadge = () => findAllNotes().at(0);
|
const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex);
|
||||||
const findSecondBadge = () => findAllNotes().at(1);
|
const findFirstBadge = () => findBadgeAtIndex(0);
|
||||||
|
const findSecondBadge = () => findBadgeAtIndex(1);
|
||||||
|
|
||||||
const clickAndDragBadge = (elem, fromPoint, toPoint) => {
|
const clickAndDragBadge = (elem, fromPoint, toPoint) => {
|
||||||
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
|
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
|
||||||
|
@ -104,7 +105,32 @@ describe('Design overlay component', () => {
|
||||||
expect(findSecondBadge().classes()).toContain('resolved');
|
expect(findSecondBadge().classes()).toContain('resolved');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
|
describe('when no discussion is active', () => {
|
||||||
|
it('should not apply inactive class to any pins', () => {
|
||||||
|
expect(
|
||||||
|
findAllNotes(0).wrappers.every(designNote => designNote.classes('gl-bg-blue-50')),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when a discussion is active', () => {
|
||||||
|
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
|
||||||
|
'should not apply inactive class to the pin for the active discussion',
|
||||||
|
note => {
|
||||||
|
wrapper.setData({
|
||||||
|
activeDiscussion: {
|
||||||
|
id: note.id,
|
||||||
|
source: 'discussion',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
|
expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it('should apply inactive class to all pins besides the active one', () => {
|
||||||
wrapper.setData({
|
wrapper.setData({
|
||||||
activeDiscussion: {
|
activeDiscussion: {
|
||||||
id: notes[0].id,
|
id: notes[0].id,
|
||||||
|
@ -114,6 +140,8 @@ describe('Design overlay component', () => {
|
||||||
|
|
||||||
return wrapper.vm.$nextTick().then(() => {
|
return wrapper.vm.$nextTick().then(() => {
|
||||||
expect(findSecondBadge().classes()).toContain('inactive');
|
expect(findSecondBadge().classes()).toContain('inactive');
|
||||||
|
expect(findFirstBadge().classes()).not.toContain('inactive');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
45
spec/frontend/design_management/mock_data/discussion.js
Normal file
45
spec/frontend/design_management/mock_data/discussion.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
export default {
|
||||||
|
id: 'discussion-id-1',
|
||||||
|
resolved: false,
|
||||||
|
resolvable: true,
|
||||||
|
notes: [
|
||||||
|
{
|
||||||
|
id: 'note-id-1',
|
||||||
|
index: 1,
|
||||||
|
position: {
|
||||||
|
height: 100,
|
||||||
|
width: 100,
|
||||||
|
x: 10,
|
||||||
|
y: 15,
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
name: 'John',
|
||||||
|
webUrl: 'link-to-john-profile',
|
||||||
|
},
|
||||||
|
createdAt: '2020-05-08T07:10:45Z',
|
||||||
|
userPermissions: {
|
||||||
|
adminNote: true,
|
||||||
|
},
|
||||||
|
resolved: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'note-id-3',
|
||||||
|
index: 3,
|
||||||
|
position: {
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
x: 25,
|
||||||
|
y: 25,
|
||||||
|
},
|
||||||
|
author: {
|
||||||
|
name: 'Mary',
|
||||||
|
webUrl: 'link-to-mary-profile',
|
||||||
|
},
|
||||||
|
createdAt: '2020-05-08T07:10:45Z',
|
||||||
|
userPermissions: {
|
||||||
|
adminNote: true,
|
||||||
|
},
|
||||||
|
resolved: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
|
@ -1,26 +1,9 @@
|
||||||
export default [
|
import DISCUSSION_1 from './discussion';
|
||||||
{
|
|
||||||
id: 'note-id-1',
|
const DISCUSSION_2 = {
|
||||||
index: 1,
|
id: 'discussion-id-2',
|
||||||
position: {
|
notes: {
|
||||||
height: 100,
|
nodes: [
|
||||||
width: 100,
|
|
||||||
x: 10,
|
|
||||||
y: 15,
|
|
||||||
},
|
|
||||||
author: {
|
|
||||||
name: 'John',
|
|
||||||
webUrl: 'link-to-john-profile',
|
|
||||||
},
|
|
||||||
createdAt: '2020-05-08T07:10:45Z',
|
|
||||||
userPermissions: {
|
|
||||||
adminNote: true,
|
|
||||||
},
|
|
||||||
discussion: {
|
|
||||||
id: 'discussion-id-1',
|
|
||||||
},
|
|
||||||
resolved: false,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'note-id-2',
|
id: 'note-id-2',
|
||||||
index: 2,
|
index: 2,
|
||||||
|
@ -38,9 +21,24 @@ export default [
|
||||||
userPermissions: {
|
userPermissions: {
|
||||||
adminNote: true,
|
adminNote: true,
|
||||||
},
|
},
|
||||||
discussion: {
|
|
||||||
id: 'discussion-id-2',
|
|
||||||
},
|
|
||||||
resolved: true,
|
resolved: true,
|
||||||
},
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
...DISCUSSION_1.notes[0],
|
||||||
|
discussion: {
|
||||||
|
id: DISCUSSION_1.id,
|
||||||
|
notes: {
|
||||||
|
nodes: DISCUSSION_1.notes,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
...DISCUSSION_2.notes.nodes[0],
|
||||||
|
discussion: DISCUSSION_2,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
|
@ -1,141 +0,0 @@
|
||||||
import { shallowMount, createLocalVue } from '@vue/test-utils';
|
|
||||||
import VueApollo from 'vue-apollo';
|
|
||||||
import VueRouter from 'vue-router';
|
|
||||||
import VueDraggable from 'vuedraggable';
|
|
||||||
import Design from '~/design_management/components/list/item.vue';
|
|
||||||
import createRouter from '~/design_management/router';
|
|
||||||
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
|
||||||
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
|
|
||||||
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
|
|
||||||
import { deprecatedCreateFlash as createFlash } from '~/flash';
|
|
||||||
import createMockApollo from '../../helpers/mock_apollo_helper';
|
|
||||||
import Index from '~/design_management/pages/index.vue';
|
|
||||||
import {
|
|
||||||
designListQueryResponse,
|
|
||||||
permissionsQueryResponse,
|
|
||||||
moveDesignMutationResponse,
|
|
||||||
reorderedDesigns,
|
|
||||||
moveDesignMutationResponseWithErrors,
|
|
||||||
} from '../mock_data/apollo_mock';
|
|
||||||
|
|
||||||
jest.mock('~/flash');
|
|
||||||
|
|
||||||
const localVue = createLocalVue();
|
|
||||||
localVue.use(VueApollo);
|
|
||||||
|
|
||||||
const router = createRouter();
|
|
||||||
localVue.use(VueRouter);
|
|
||||||
|
|
||||||
const designToMove = {
|
|
||||||
__typename: 'Design',
|
|
||||||
id: '2',
|
|
||||||
event: 'NONE',
|
|
||||||
filename: 'fox_2.jpg',
|
|
||||||
notesCount: 2,
|
|
||||||
image: 'image-2',
|
|
||||||
imageV432x230: 'image-2',
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('Design management index page with Apollo mock', () => {
|
|
||||||
let wrapper;
|
|
||||||
let fakeApollo;
|
|
||||||
let moveDesignHandler;
|
|
||||||
|
|
||||||
async function moveDesigns(localWrapper) {
|
|
||||||
await jest.runOnlyPendingTimers();
|
|
||||||
await localWrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
|
|
||||||
localWrapper.find(VueDraggable).vm.$emit('change', {
|
|
||||||
moved: {
|
|
||||||
newIndex: 0,
|
|
||||||
element: designToMove,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const findDesigns = () => wrapper.findAll(Design);
|
|
||||||
|
|
||||||
function createComponent({
|
|
||||||
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
|
||||||
}) {
|
|
||||||
moveDesignHandler = moveHandler;
|
|
||||||
|
|
||||||
const requestHandlers = [
|
|
||||||
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
|
||||||
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
|
||||||
[moveDesignMutation, moveDesignHandler],
|
|
||||||
];
|
|
||||||
|
|
||||||
fakeApollo = createMockApollo(requestHandlers);
|
|
||||||
wrapper = shallowMount(Index, {
|
|
||||||
localVue,
|
|
||||||
apolloProvider: fakeApollo,
|
|
||||||
router,
|
|
||||||
stubs: { VueDraggable },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
wrapper.destroy();
|
|
||||||
wrapper = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('has a design with id 1 as a first one', async () => {
|
|
||||||
createComponent({});
|
|
||||||
|
|
||||||
await jest.runOnlyPendingTimers();
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
expect(findDesigns()).toHaveLength(3);
|
|
||||||
expect(
|
|
||||||
findDesigns()
|
|
||||||
.at(0)
|
|
||||||
.props('id'),
|
|
||||||
).toBe('1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('calls a mutation with correct parameters and reorders designs', async () => {
|
|
||||||
createComponent({});
|
|
||||||
|
|
||||||
await moveDesigns(wrapper);
|
|
||||||
|
|
||||||
expect(moveDesignHandler).toHaveBeenCalled();
|
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
findDesigns()
|
|
||||||
.at(0)
|
|
||||||
.props('id'),
|
|
||||||
).toBe('2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays flash if mutation had a recoverable error', async () => {
|
|
||||||
createComponent({
|
|
||||||
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
|
|
||||||
});
|
|
||||||
|
|
||||||
await moveDesigns(wrapper);
|
|
||||||
|
|
||||||
await wrapper.vm.$nextTick();
|
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('displays flash if mutation had a non-recoverable error', async () => {
|
|
||||||
createComponent({
|
|
||||||
moveHandler: jest.fn().mockRejectedValue('Error'),
|
|
||||||
});
|
|
||||||
|
|
||||||
await moveDesigns(wrapper);
|
|
||||||
|
|
||||||
await wrapper.vm.$nextTick(); // kick off the DOM update
|
|
||||||
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
|
||||||
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
|
|
||||||
|
|
||||||
expect(createFlash).toHaveBeenCalledWith(
|
|
||||||
'Something went wrong when reordering designs. Please try again',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
import { createLocalVue, shallowMount } from '@vue/test-utils';
|
||||||
import { ApolloMutation } from 'vue-apollo';
|
import VueApollo, { ApolloMutation } from 'vue-apollo';
|
||||||
import VueDraggable from 'vuedraggable';
|
import VueDraggable from 'vuedraggable';
|
||||||
import VueRouter from 'vue-router';
|
import VueRouter from 'vue-router';
|
||||||
import { GlEmptyState } from '@gitlab/ui';
|
import { GlEmptyState } from '@gitlab/ui';
|
||||||
|
import createMockApollo from 'jest/helpers/mock_apollo_helper';
|
||||||
import Index from '~/design_management/pages/index.vue';
|
import Index from '~/design_management/pages/index.vue';
|
||||||
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
|
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
|
||||||
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
|
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
|
||||||
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
|
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
|
||||||
import DeleteButton from '~/design_management/components/delete_button.vue';
|
import DeleteButton from '~/design_management/components/delete_button.vue';
|
||||||
|
import Design from '~/design_management/components/list/item.vue';
|
||||||
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
|
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
|
||||||
import {
|
import {
|
||||||
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
|
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
|
||||||
|
@ -17,6 +19,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
|
||||||
import createRouter from '~/design_management/router';
|
import createRouter from '~/design_management/router';
|
||||||
import * as utils from '~/design_management/utils/design_management_utils';
|
import * as utils from '~/design_management/utils/design_management_utils';
|
||||||
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
|
||||||
|
import {
|
||||||
|
designListQueryResponse,
|
||||||
|
permissionsQueryResponse,
|
||||||
|
moveDesignMutationResponse,
|
||||||
|
reorderedDesigns,
|
||||||
|
moveDesignMutationResponseWithErrors,
|
||||||
|
} from '../mock_data/apollo_mock';
|
||||||
|
import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
|
||||||
|
import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
|
||||||
|
import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
|
||||||
|
|
||||||
jest.mock('~/flash.js');
|
jest.mock('~/flash.js');
|
||||||
const mockPageEl = {
|
const mockPageEl = {
|
||||||
|
@ -61,9 +73,21 @@ const mockVersion = {
|
||||||
id: 'gid://gitlab/DesignManagement::Version/1',
|
id: 'gid://gitlab/DesignManagement::Version/1',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const designToMove = {
|
||||||
|
__typename: 'Design',
|
||||||
|
id: '2',
|
||||||
|
event: 'NONE',
|
||||||
|
filename: 'fox_2.jpg',
|
||||||
|
notesCount: 2,
|
||||||
|
image: 'image-2',
|
||||||
|
imageV432x230: 'image-2',
|
||||||
|
};
|
||||||
|
|
||||||
describe('Design management index page', () => {
|
describe('Design management index page', () => {
|
||||||
let mutate;
|
let mutate;
|
||||||
let wrapper;
|
let wrapper;
|
||||||
|
let fakeApollo;
|
||||||
|
let moveDesignHandler;
|
||||||
|
|
||||||
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
|
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
|
||||||
const findSelectAllButton = () => wrapper.find('.js-select-all');
|
const findSelectAllButton = () => wrapper.find('.js-select-all');
|
||||||
|
@ -74,6 +98,20 @@ describe('Design management index page', () => {
|
||||||
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
|
const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
|
||||||
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
|
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
|
||||||
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
|
const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
|
||||||
|
const findDesigns = () => wrapper.findAll(Design);
|
||||||
|
|
||||||
|
async function moveDesigns(localWrapper) {
|
||||||
|
await jest.runOnlyPendingTimers();
|
||||||
|
await localWrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
|
||||||
|
localWrapper.find(VueDraggable).vm.$emit('change', {
|
||||||
|
moved: {
|
||||||
|
newIndex: 0,
|
||||||
|
element: designToMove,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function createComponent({
|
function createComponent({
|
||||||
loading = false,
|
loading = false,
|
||||||
|
@ -118,8 +156,30 @@ describe('Design management index page', () => {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createComponentWithApollo({
|
||||||
|
moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
|
||||||
|
}) {
|
||||||
|
localVue.use(VueApollo);
|
||||||
|
moveDesignHandler = moveHandler;
|
||||||
|
|
||||||
|
const requestHandlers = [
|
||||||
|
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
|
||||||
|
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
|
||||||
|
[moveDesignMutation, moveDesignHandler],
|
||||||
|
];
|
||||||
|
|
||||||
|
fakeApollo = createMockApollo(requestHandlers);
|
||||||
|
wrapper = shallowMount(Index, {
|
||||||
|
localVue,
|
||||||
|
apolloProvider: fakeApollo,
|
||||||
|
router,
|
||||||
|
stubs: { VueDraggable },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
wrapper.destroy();
|
wrapper.destroy();
|
||||||
|
wrapper = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('designs', () => {
|
describe('designs', () => {
|
||||||
|
@ -584,4 +644,64 @@ describe('Design management index page', () => {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('with mocked Apollo client', () => {
|
||||||
|
it('has a design with id 1 as a first one', async () => {
|
||||||
|
createComponentWithApollo({});
|
||||||
|
|
||||||
|
await jest.runOnlyPendingTimers();
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(findDesigns()).toHaveLength(3);
|
||||||
|
expect(
|
||||||
|
findDesigns()
|
||||||
|
.at(0)
|
||||||
|
.props('id'),
|
||||||
|
).toBe('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('calls a mutation with correct parameters and reorders designs', async () => {
|
||||||
|
createComponentWithApollo({});
|
||||||
|
|
||||||
|
await moveDesigns(wrapper);
|
||||||
|
|
||||||
|
expect(moveDesignHandler).toHaveBeenCalled();
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
findDesigns()
|
||||||
|
.at(0)
|
||||||
|
.props('id'),
|
||||||
|
).toBe('2');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays flash if mutation had a recoverable error', async () => {
|
||||||
|
createComponentWithApollo({
|
||||||
|
moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
|
||||||
|
});
|
||||||
|
|
||||||
|
await moveDesigns(wrapper);
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick();
|
||||||
|
|
||||||
|
expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays flash if mutation had a non-recoverable error', async () => {
|
||||||
|
createComponentWithApollo({
|
||||||
|
moveHandler: jest.fn().mockRejectedValue('Error'),
|
||||||
|
});
|
||||||
|
|
||||||
|
await moveDesigns(wrapper);
|
||||||
|
|
||||||
|
await wrapper.vm.$nextTick(); // kick off the DOM update
|
||||||
|
await jest.runOnlyPendingTimers(); // kick off the mocked GQL stuff (promises)
|
||||||
|
await wrapper.vm.$nextTick(); // kick off the DOM update for flash
|
||||||
|
|
||||||
|
expect(createFlash).toHaveBeenCalledWith(
|
||||||
|
'Something went wrong when reordering designs. Please try again',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -45,7 +45,7 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#config_for' do
|
describe '.config_for' do
|
||||||
context 'for an LDAP provider' do
|
context 'for an LDAP provider' do
|
||||||
context 'when the provider exists' do
|
context 'when the provider exists' do
|
||||||
it 'returns the config' do
|
it 'returns the config' do
|
||||||
|
@ -91,4 +91,46 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '.label_for' do
|
||||||
|
subject { described_class.label_for(name) }
|
||||||
|
|
||||||
|
context 'when configuration specifies a custom label' do
|
||||||
|
let(:name) { 'google_oauth2' }
|
||||||
|
let(:label) { 'Custom Google Provider' }
|
||||||
|
let(:provider) { OpenStruct.new({ 'name' => name, 'label' => label }) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_omniauth_setting(providers: [provider])
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the custom label name' do
|
||||||
|
expect(subject).to eq(label)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when configuration does not specify a custom label' do
|
||||||
|
let(:provider) { OpenStruct.new({ 'name' => name } ) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
stub_omniauth_setting(providers: [provider])
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the name does not correspond to a label mapping' do
|
||||||
|
let(:name) { 'twitter' }
|
||||||
|
|
||||||
|
it 'returns the titleized name' do
|
||||||
|
expect(subject).to eq(name.titleize)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when the name corresponds to a label mapping' do
|
||||||
|
let(:name) { 'gitlab' }
|
||||||
|
|
||||||
|
it 'returns the mapped name' do
|
||||||
|
expect(subject).to eq('GitLab.com')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -11,6 +11,7 @@ RSpec.describe API::PypiPackages do
|
||||||
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
let_it_be(:personal_access_token) { create(:personal_access_token, user: user) }
|
||||||
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
let_it_be(:deploy_token) { create(:deploy_token, read_package_registry: true, write_package_registry: true) }
|
||||||
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
let_it_be(:project_deploy_token) { create(:project_deploy_token, deploy_token: deploy_token, project: project) }
|
||||||
|
let_it_be(:job) { create(:ci_build, :running, user: user) }
|
||||||
|
|
||||||
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
|
describe 'GET /api/v4/projects/:id/packages/pypi/simple/:package_name' do
|
||||||
let_it_be(:package) { create(:pypi_package, project: project) }
|
let_it_be(:package) { create(:pypi_package, project: project) }
|
||||||
|
@ -58,6 +59,8 @@ RSpec.describe API::PypiPackages do
|
||||||
|
|
||||||
it_behaves_like 'deploy token for package GET requests'
|
it_behaves_like 'deploy token for package GET requests'
|
||||||
|
|
||||||
|
it_behaves_like 'job token for package GET requests'
|
||||||
|
|
||||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -108,6 +111,8 @@ RSpec.describe API::PypiPackages do
|
||||||
|
|
||||||
it_behaves_like 'deploy token for package uploads'
|
it_behaves_like 'deploy token for package uploads'
|
||||||
|
|
||||||
|
it_behaves_like 'job token for package uploads'
|
||||||
|
|
||||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -198,6 +203,8 @@ RSpec.describe API::PypiPackages do
|
||||||
|
|
||||||
it_behaves_like 'deploy token for package uploads'
|
it_behaves_like 'deploy token for package uploads'
|
||||||
|
|
||||||
|
it_behaves_like 'job token for package uploads'
|
||||||
|
|
||||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||||
|
|
||||||
context 'file size above maximum limit' do
|
context 'file size above maximum limit' do
|
||||||
|
@ -273,6 +280,26 @@ RSpec.describe API::PypiPackages do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'with job token headers' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
|
||||||
|
|
||||||
|
context 'valid token' do
|
||||||
|
it_behaves_like 'returning response status', :success
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid token' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :success
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid user' do
|
||||||
|
let(:headers) { basic_auth_header('foo', job.token) }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :success
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
it_behaves_like 'rejects PyPI access with unknown project id'
|
it_behaves_like 'rejects PyPI access with unknown project id'
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -70,3 +70,59 @@ RSpec.shared_examples 'does not cause n^2 queries' do
|
||||||
end.not_to exceed_query_limit(control)
|
end.not_to exceed_query_limit(control)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'job token for package GET requests' do
|
||||||
|
context 'with job token headers' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token) }
|
||||||
|
|
||||||
|
subject { get api(url), headers: headers }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'valid token' do
|
||||||
|
it_behaves_like 'returning response status', :success
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid token' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar') }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid user' do
|
||||||
|
let(:headers) { basic_auth_header('foo', job.token) }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
RSpec.shared_examples 'job token for package uploads' do
|
||||||
|
context 'with job token headers' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, job.token).merge(workhorse_header) }
|
||||||
|
|
||||||
|
before do
|
||||||
|
project.update!(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
|
||||||
|
project.add_developer(user)
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'valid token' do
|
||||||
|
it_behaves_like 'returning response status', :success
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid token' do
|
||||||
|
let(:headers) { basic_auth_header(::Gitlab::Auth::CI_JOB_USER, 'bar').merge(workhorse_header) }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :unauthorized
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'invalid user' do
|
||||||
|
let(:headers) { basic_auth_header('foo', job.token).merge(workhorse_header) }
|
||||||
|
|
||||||
|
it_behaves_like 'returning response status', :unauthorized
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
Loading…
Reference in a new issue