Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-09-07 12:08:17 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-09-07 12:08:17 +0300
commit444f662b8d8cbe47a8f3fa1db6ed926d64f3def3 (patch)
treea6529bfe443562d7a1762be4ef6749fb6a95631a
parentf675c7d41d6b934d5b34998160b0ea95cc30598b (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue11
-rw-r--r--app/assets/javascripts/design_management/components/design_overlay.vue7
-rw-r--r--app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql5
-rw-r--r--app/views/profiles/accounts/_providers.html.haml6
-rw-r--r--changelogs/unreleased/202012-pypi-job-tokens.yml5
-rw-r--r--changelogs/unreleased/230835-drop-code_owner-column-from-approvalmergerequestrule.yml5
-rw-r--r--changelogs/unreleased/dblessing-social-connect-text.yml5
-rw-r--r--changelogs/unreleased/fix-scroll-to-note-designs.yml5
-rw-r--r--db/post_migrate/20200901212304_drop_code_owner_column_from_approval_merge_request_rule.rb36
-rw-r--r--db/schema_migrations/202009012123041
-rw-r--r--db/structure.sql5
-rw-r--r--doc/administration/object_storage.md17
-rw-r--r--doc/api/epics.md4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql4
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json4
-rw-r--r--doc/user/group/epics/manage_epics.md12
-rw-r--r--doc/user/packages/package_registry/index.md6
-rw-r--r--doc/user/packages/pypi_repository/index.md29
-rw-r--r--lib/api/pypi_packages.rb8
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/features/merge_request/user_assigns_themselves_spec.rb7
-rw-r--r--spec/features/profiles/account_spec.rb33
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js56
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js10
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js48
-rw-r--r--spec/frontend/design_management/mock_data/discussion.js45
-rw-r--r--spec/frontend/design_management/mock_data/notes.js74
-rw-r--r--spec/frontend/design_management/pages/index_apollo_spec.js141
-rw-r--r--spec/frontend/design_management/pages/index_spec.js122
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb44
-rw-r--r--spec/requests/api/pypi_packages_spec.rb27
-rw-r--r--spec/support/shared_examples/requests/api/packages_shared_examples.rb56
33 files changed, 569 insertions, 305 deletions
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index cd2545b48de..d28635db601 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -105,8 +105,8 @@ export default {
atVersion: this.designsVersion,
};
},
- isDiscussionHighlighted() {
- return this.discussion.notes[0].id === this.activeDiscussion.id;
+ isDiscussionActive() {
+ return this.discussion.notes.some(({ id }) => id === this.activeDiscussion.id);
},
resolveCheckboxText() {
return this.discussion.resolved
@@ -134,18 +134,6 @@ export default {
isFormVisible() {
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: {
addDiscussionComment(
@@ -199,6 +187,14 @@ export default {
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,
};
@@ -221,7 +217,7 @@ export default {
:note="firstNote"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('updateNoteError', $event)"
>
<template v-if="discussion.resolvable" #resolveDiscussion>
@@ -265,7 +261,7 @@ export default {
:note="note"
:markdown-preview-path="markdownPreviewPath"
:is-resolving="isResolving"
- :class="{ 'gl-bg-blue-50': isDiscussionHighlighted }"
+ :class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('updateNoteError', $event)"
/>
<li v-show="isReplyPlaceholderVisible" class="reply-wrapper">
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index eab19e38a45..6c380153a3f 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -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 TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.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';
export default {
@@ -47,7 +47,7 @@ export default {
return findNoteId(this.note.id);
},
isNoteLinked() {
- return this.$route.hash === `#note_${this.noteAnchorId}`;
+ return extractDesignNoteId(this.$route.hash) === this.noteAnchorId;
},
mutationPayload() {
return {
@@ -59,13 +59,6 @@ export default {
return !this.isEditing && this.note.userPermissions.adminNote;
},
},
- mounted() {
- this.$nextTick(() => {
- if (this.isNoteLinked) {
- this.$el.scrollIntoView({ behavior: 'smooth', inline: 'start' });
- }
- });
- },
methods: {
hideForm() {
this.isEditing = false;
diff --git a/app/assets/javascripts/design_management/components/design_overlay.vue b/app/assets/javascripts/design_management/components/design_overlay.vue
index 3f214ff54b4..5c4a3ab5f94 100644
--- a/app/assets/javascripts/design_management/components/design_overlay.vue
+++ b/app/assets/javascripts/design_management/components/design_overlay.vue
@@ -237,7 +237,12 @@ export default {
});
},
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) {
return { inactive: this.isNoteInactive(note), resolved: note.resolved };
diff --git a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
index 26edd2c0be1..28224671326 100644
--- a/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
+++ b/app/assets/javascripts/design_management/graphql/fragments/design_note.fragment.graphql
@@ -25,5 +25,10 @@ fragment DesignNote on Note {
}
discussion {
id
+ notes {
+ nodes {
+ id
+ }
+ }
}
}
diff --git a/app/views/profiles/accounts/_providers.html.haml b/app/views/profiles/accounts/_providers.html.haml
index a87191d0fa4..f7368c5e921 100644
--- a/app/views/profiles/accounts/_providers.html.haml
+++ b/app/views/profiles/accounts/_providers.html.haml
@@ -11,11 +11,11 @@
- if auth_active?(provider)
- if unlink_allowed
= 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
%a.provider-btn
- = s_('Profiles|Active')
+ = s_('Profiles|%{provider} Active') % { provider: label_for_provider(provider) }
- elsif link_allowed
= 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
diff --git a/changelogs/unreleased/202012-pypi-job-tokens.yml b/changelogs/unreleased/202012-pypi-job-tokens.yml
new file mode 100644
index 00000000000..9b0138faaa1
--- /dev/null
+++ b/changelogs/unreleased/202012-pypi-job-tokens.yml
@@ -0,0 +1,5 @@
+---
+title: Add job token authentication for the GitLab PyPI package repository
+merge_request: 40888
+author:
+type: added
diff --git a/changelogs/unreleased/230835-drop-code_owner-column-from-approvalmergerequestrule.yml b/changelogs/unreleased/230835-drop-code_owner-column-from-approvalmergerequestrule.yml
new file mode 100644
index 00000000000..81a96054c54
--- /dev/null
+++ b/changelogs/unreleased/230835-drop-code_owner-column-from-approvalmergerequestrule.yml
@@ -0,0 +1,5 @@
+---
+title: Drop code_owner column from approval_merge_request_rules
+merge_request: 40322
+author:
+type: other
diff --git a/changelogs/unreleased/dblessing-social-connect-text.yml b/changelogs/unreleased/dblessing-social-connect-text.yml
new file mode 100644
index 00000000000..05da9761cf6
--- /dev/null
+++ b/changelogs/unreleased/dblessing-social-connect-text.yml
@@ -0,0 +1,5 @@
+---
+title: Display provider name for profile social sign-in connectors
+merge_request: 41198
+author:
+type: changed
diff --git a/changelogs/unreleased/fix-scroll-to-note-designs.yml b/changelogs/unreleased/fix-scroll-to-note-designs.yml
new file mode 100644
index 00000000000..611c9654cce
--- /dev/null
+++ b/changelogs/unreleased/fix-scroll-to-note-designs.yml
@@ -0,0 +1,5 @@
+---
+title: Highlight design discussion if any comment in discussion is linked
+merge_request: 41062
+author:
+type: fixed
diff --git a/db/post_migrate/20200901212304_drop_code_owner_column_from_approval_merge_request_rule.rb b/db/post_migrate/20200901212304_drop_code_owner_column_from_approval_merge_request_rule.rb
new file mode 100644
index 00000000000..7524ae8e15b
--- /dev/null
+++ b/db/post_migrate/20200901212304_drop_code_owner_column_from_approval_merge_request_rule.rb
@@ -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
diff --git a/db/schema_migrations/20200901212304 b/db/schema_migrations/20200901212304
new file mode 100644
index 00000000000..3dcc9cdd8f0
--- /dev/null
+++ b/db/schema_migrations/20200901212304
@@ -0,0 +1 @@
+6fb93002ffd5c1d1bfff5bea8a99cbbfc7cefefbc450a9d067ee0cfab8d11e9e \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 826ffe6e9f5..0539f45bb2b 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9304,7 +9304,6 @@ CREATE TABLE public.approval_merge_request_rules (
updated_at timestamp with time zone NOT NULL,
merge_request_id integer NOT NULL,
approvals_required smallint DEFAULT 0 NOT NULL,
- code_owner boolean DEFAULT false NOT NULL,
name character varying NOT NULL,
rule_type smallint DEFAULT 1 NOT NULL,
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 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 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_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 INDEX index_approval_merge_request_rules_approved_approvers_2 ON public.approval_merge_request_rules_approved_approvers USING btree (user_id);
diff --git a/doc/administration/object_storage.md b/doc/administration/object_storage.md
index e0ae532dfb2..024e71a89ae 100644
--- a/doc/administration/object_storage.md
+++ b/doc/administration/object_storage.md
@@ -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 [uploads files to S3 with proper `Content-MD5` headers](https://gitlab.com/gitlab-org/gitlab-workhorse/-/issues/222).
-NOTE: **Note:**
-Only AWS S3-compatible providers and Google are
-supported at the moment since [direct upload
-mode](../development/uploads.md#direct-upload) must be used. Background
-upload is not supported in this mode. We recommend 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).
+Because [direct upload mode](../development/uploads.md#direct-upload)
+must be enabled, only the following providers can be used:
+
+- [Amazon S3-compatible providers](#s3-compatible-connection-settings)
+- [Google Cloud Storage](#google-cloud-storage-gcs)
+- [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:**
Consolidated object storage configuration cannot be used for
diff --git a/doc/api/epics.md b/doc/api/epics.md
index 45bf406dec2..c3ba42c6efd 100644
--- a/doc/api/epics.md
+++ b/doc/api/epics.md
@@ -266,7 +266,7 @@ POST /groups/:id/epics
| `title` | string | yes | The title of the epic |
| `labels` | string | no | The comma separated list of labels |
| `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_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) |
@@ -347,7 +347,7 @@ PUT /groups/:id/epics/:epic_iid
| `epic_iid` | integer/string | yes | The internal ID of the epic |
| `title` | string | no | The title of an epic |
| `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 |
| `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) |
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index ca3c71761a2..0fb210cf112 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -2614,7 +2614,7 @@ input CreateEpicInput {
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
@@ -16809,7 +16809,7 @@ input UpdateEpicInput {
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
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index fd545d4cbaa..01a6b0307a2 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -7089,7 +7089,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "Boolean",
@@ -49522,7 +49522,7 @@
},
{
"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": {
"kind": "SCALAR",
"name": "Boolean",
diff --git a/doc/user/group/epics/manage_epics.md b/doc/user/group/epics/manage_epics.md
index 3dfa6a33255..c09032bffb2 100644
--- a/doc/user/group/epics/manage_epics.md
+++ b/doc/user/group/epics/manage_epics.md
@@ -164,18 +164,6 @@ To make an epic confidential:
- **In an existing epic:** in the epic's sidebar, select **Edit** next to **Confidentiality** then
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
### Add a new issue to an epic
diff --git a/doc/user/packages/package_registry/index.md b/doc/user/packages/package_registry/index.md
index f7ee1a4808e..4f205013418 100644
--- a/doc/user/packages/package_registry/index.md
+++ b/doc/user/packages/package_registry/index.md
@@ -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
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`.
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
information is displayed when you view the package details:
![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.
## Download a package
diff --git a/doc/user/packages/pypi_repository/index.md b/doc/user/packages/pypi_repository/index.md
index 8e3a5f6eb5f..2b30fd9209a 100644
--- a/doc/user/packages/pypi_repository/index.md
+++ b/doc/user/packages/pypi_repository/index.md
@@ -302,20 +302,10 @@ Successfully installed mypypipackage-0.0.1
## Using GitLab CI with PyPI packages
-NOTE: **Note:**
-`CI_JOB_TOKEN`s are not yet supported for use with PyPI.
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/11678) in GitLab 13.4.
To work with PyPI commands within [GitLab CI/CD](./../../../ci/README.md), you can use
-[environment variables](./../../../ci/variables/README.md#custom-environment-variables)
-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.
+`CI_JOB_TOKEN` in place of the personal access token or deploy token in your commands.
For example:
@@ -326,5 +316,18 @@ run:
script:
- pip install twine
- 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}
```
diff --git a/lib/api/pypi_packages.rb b/lib/api/pypi_packages.rb
index b3668a88204..b2528ceae94 100644
--- a/lib/api/pypi_packages.rb
+++ b/lib/api/pypi_packages.rb
@@ -64,7 +64,7 @@ module API
requires :sha256, type: String, desc: 'The PyPi package sha256 check sum'
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
project = unauthorized_user_project!
@@ -87,7 +87,7 @@ module API
# An Api entry point but returns an HTML file instead of JSON.
# 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
authorize_read_package!(authorized_user_project)
@@ -117,7 +117,7 @@ module API
optional :sha256_digest, type: String
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
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)
@@ -135,7 +135,7 @@ module API
forbidden!
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
authorize_workhorse!(
subject: authorized_user_project,
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6910cf8f1a2..895b41a4ca7 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -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."
msgstr ""
+msgid "Profiles|%{provider} Active"
+msgstr ""
+
msgid "Profiles|@username"
msgstr ""
@@ -18834,7 +18837,7 @@ msgstr ""
msgid "Profiles|Commit email"
msgstr ""
-msgid "Profiles|Connect"
+msgid "Profiles|Connect %{provider}"
msgstr ""
msgid "Profiles|Connected Accounts"
@@ -18858,6 +18861,9 @@ msgstr ""
msgid "Profiles|Disconnect"
msgstr ""
+msgid "Profiles|Disconnect %{provider}"
+msgstr ""
+
msgid "Profiles|Do not show on profile"
msgstr ""
diff --git a/spec/features/merge_request/user_assigns_themselves_spec.rb b/spec/features/merge_request/user_assigns_themselves_spec.rb
index 35446b125ce..04d401683bf 100644
--- a/spec/features/merge_request/user_assigns_themselves_spec.rb
+++ b/spec/features/merge_request/user_assigns_themselves_spec.rb
@@ -22,7 +22,12 @@ RSpec.describe 'Merge request > User assigns themselves' do
end
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
it 'returns user to the merge request', :js do
diff --git a/spec/features/profiles/account_spec.rb b/spec/features/profiles/account_spec.rb
index 620c2f60ba3..e8caa2159a4 100644
--- a/spec/features/profiles/account_spec.rb
+++ b/spec/features/profiles/account_spec.rb
@@ -9,6 +9,39 @@ RSpec.describe 'Profile > Account', :js do
sign_in(user)
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
let(:new_username) { 'bar' }
let(:new_user_path) { "/#{new_username}" }
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index d7f136ddbac..b04bfa65e37 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -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 ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.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',
resolved: false,
resolvable: true,
@@ -49,7 +50,7 @@ describe('Design discussions component', () => {
wrapper = mount(DesignDiscussion, {
propsData: {
resolvedDiscussionsExpanded: true,
- discussion,
+ discussion: defaultMockDiscussion,
noteableId: 'noteable-id',
designId: 'design-id',
discussionIndex: 1,
@@ -82,7 +83,7 @@ describe('Design discussions component', () => {
beforeEach(() => {
createComponent({
discussion: {
- ...discussion,
+ ...defaultMockDiscussion,
resolvable: false,
},
});
@@ -125,7 +126,7 @@ describe('Design discussions component', () => {
it('renders a checkbox with Resolve thread text in reply form', () => {
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Resolve thread');
@@ -141,7 +142,7 @@ describe('Design discussions component', () => {
beforeEach(() => {
createComponent({
discussion: {
- ...discussion,
+ ...defaultMockDiscussion,
resolved: true,
resolvedBy: notes[0].author,
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', () => {
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findResolveCheckbox().text()).toBe('Unresolve thread');
@@ -218,7 +219,7 @@ describe('Design discussions component', () => {
it('hides reply placeholder and opens form on placeholder click', () => {
createComponent();
findReplyPlaceholder().vm.$emit('onClick');
- wrapper.setProps({ discussionWithOpenForm: discussion.id });
+ wrapper.setProps({ discussionWithOpenForm: defaultMockDiscussion.id });
return wrapper.vm.$nextTick().then(() => {
expect(findReplyPlaceholder().exists()).toBe(false);
@@ -228,7 +229,7 @@ describe('Design discussions component', () => {
it('calls mutation on submitting form and closes the form', () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
@@ -246,7 +247,7 @@ describe('Design discussions component', () => {
it('clears the discussion comment on closing comment form', () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
@@ -263,19 +264,26 @@ describe('Design discussions component', () => {
});
});
- it('applies correct class to design notes when discussion is highlighted', () => {
- createComponent(
- {},
- {
- activeDiscussion: {
- id: notes[0].id,
- source: 'pin',
- },
- },
- );
+ 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(
+ { discussion: mockDiscussion },
+ {
+ activeDiscussion: {
+ id: note.id,
+ source: 'pin',
+ },
+ },
+ );
- expect(wrapper.findAll(DesignNote).wrappers.every(note => note.classes('gl-bg-blue-50'))).toBe(
- true,
+ expect(
+ 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({
mutation: toggleResolveDiscussionMutation,
variables: {
- id: discussion.id,
+ id: defaultMockDiscussion.id,
resolve: true,
},
});
@@ -296,7 +304,7 @@ describe('Design discussions component', () => {
it('calls toggleResolveDiscussion mutation after adding a note if checkbox was checked', () => {
createComponent(
- { discussionWithOpenForm: discussion.id },
+ { discussionWithOpenForm: defaultMockDiscussion.id },
{ discussionComment: 'test', isFormRendered: true },
);
findResolveButton().trigger('click');
@@ -306,7 +314,7 @@ describe('Design discussions component', () => {
expect(mutate).toHaveBeenCalledWith({
mutation: toggleResolveDiscussionMutation,
variables: {
- id: discussion.id,
+ id: defaultMockDiscussion.id,
resolve: true,
},
});
diff --git a/spec/frontend/design_management/components/design_notes/design_note_spec.js b/spec/frontend/design_management/components/design_notes/design_note_spec.js
index a59a2223cf3..c35bb503c96 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -86,16 +86,6 @@ describe('Design note component', () => {
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', () => {
createComponent({
note,
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index bbd0fbee81f..673a09320e5 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -13,8 +13,9 @@ describe('Design overlay component', () => {
const findAllNotes = () => wrapper.findAll('.js-image-badge');
const findCommentBadge = () => wrapper.find('.comment-indicator');
- const findFirstBadge = () => findAllNotes().at(0);
- const findSecondBadge = () => findAllNotes().at(1);
+ const findBadgeAtIndex = noteIndex => findAllNotes().at(noteIndex);
+ const findFirstBadge = () => findBadgeAtIndex(0);
+ const findSecondBadge = () => findBadgeAtIndex(1);
const clickAndDragBadge = (elem, fromPoint, toPoint) => {
elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
@@ -104,16 +105,43 @@ describe('Design overlay component', () => {
expect(findSecondBadge().classes()).toContain('resolved');
});
- it('when there is an active discussion, should apply inactive class to all pins besides the active one', () => {
- wrapper.setData({
- activeDiscussion: {
- id: notes[0].id,
- source: 'discussion',
- },
+ 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(findSecondBadge().classes()).toContain('inactive');
+ 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({
+ activeDiscussion: {
+ id: notes[0].id,
+ source: 'discussion',
+ },
+ });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findSecondBadge().classes()).toContain('inactive');
+ expect(findFirstBadge().classes()).not.toContain('inactive');
+ });
});
});
});
diff --git a/spec/frontend/design_management/mock_data/discussion.js b/spec/frontend/design_management/mock_data/discussion.js
new file mode 100644
index 00000000000..fbf9a2fdcc1
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/discussion.js
@@ -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,
+ },
+ ],
+};
diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js
index 80cb3944786..41cefaca05b 100644
--- a/spec/frontend/design_management/mock_data/notes.js
+++ b/spec/frontend/design_management/mock_data/notes.js
@@ -1,46 +1,44 @@
+import DISCUSSION_1 from './discussion';
+
+const DISCUSSION_2 = {
+ id: 'discussion-id-2',
+ notes: {
+ nodes: [
+ {
+ id: 'note-id-2',
+ index: 2,
+ 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: true,
+ },
+ ],
+ },
+};
+
export default [
{
- 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,
- },
+ ...DISCUSSION_1.notes[0],
discussion: {
- id: 'discussion-id-1',
+ id: DISCUSSION_1.id,
+ notes: {
+ nodes: DISCUSSION_1.notes,
+ },
},
- resolved: false,
},
{
- id: 'note-id-2',
- index: 2,
- 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,
- },
- discussion: {
- id: 'discussion-id-2',
- },
- resolved: true,
+ ...DISCUSSION_2.notes.nodes[0],
+ discussion: DISCUSSION_2,
},
];
diff --git a/spec/frontend/design_management/pages/index_apollo_spec.js b/spec/frontend/design_management/pages/index_apollo_spec.js
deleted file mode 100644
index c1a3f8643e7..00000000000
--- a/spec/frontend/design_management/pages/index_apollo_spec.js
+++ /dev/null
@@ -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',
- );
- });
-});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 2da02732b1e..661717d29a3 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,13 +1,15 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { ApolloMutation } from 'vue-apollo';
+import VueApollo, { ApolloMutation } from 'vue-apollo';
import VueDraggable from 'vuedraggable';
import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
+import createMockApollo from 'jest/helpers/mock_apollo_helper';
import Index from '~/design_management/pages/index.vue';
import uploadDesignQuery from '~/design_management/graphql/mutations/upload_design.mutation.graphql';
import DesignDestroyer from '~/design_management/components/design_destroyer.vue';
import DesignDropzone from '~/design_management/components/upload/design_dropzone.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 {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
@@ -17,6 +19,16 @@ import { deprecatedCreateFlash as createFlash } from '~/flash';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
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');
const mockPageEl = {
@@ -61,9 +73,21 @@ const mockVersion = {
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', () => {
let mutate;
let wrapper;
+ let fakeApollo;
+ let moveDesignHandler;
const findDesignCheckboxes = () => wrapper.findAll('.design-checkbox');
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 findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
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({
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(() => {
wrapper.destroy();
+ wrapper = null;
});
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',
+ );
+ });
+ });
});
diff --git a/spec/lib/gitlab/auth/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
index 658a9976cc2..57f17365190 100644
--- a/spec/lib/gitlab/auth/o_auth/provider_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -45,7 +45,7 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
end
end
- describe '#config_for' do
+ describe '.config_for' do
context 'for an LDAP provider' do
context 'when the provider exists' do
it 'returns the config' do
@@ -91,4 +91,46 @@ RSpec.describe Gitlab::Auth::OAuth::Provider do
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
diff --git a/spec/requests/api/pypi_packages_spec.rb b/spec/requests/api/pypi_packages_spec.rb
index 0ac1d96bb3e..85a107ee804 100644
--- a/spec/requests/api/pypi_packages_spec.rb
+++ b/spec/requests/api/pypi_packages_spec.rb
@@ -11,6 +11,7 @@ RSpec.describe API::PypiPackages do
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(: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
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 'job token for package GET requests'
+
it_behaves_like 'rejects PyPI access with unknown project id'
end
@@ -108,6 +111,8 @@ RSpec.describe API::PypiPackages do
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'
end
@@ -198,6 +203,8 @@ RSpec.describe API::PypiPackages do
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'
context 'file size above maximum limit' do
@@ -273,6 +280,26 @@ RSpec.describe API::PypiPackages do
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'
end
end
diff --git a/spec/support/shared_examples/requests/api/packages_shared_examples.rb b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
index 4bcff505008..c9a33701161 100644
--- a/spec/support/shared_examples/requests/api/packages_shared_examples.rb
+++ b/spec/support/shared_examples/requests/api/packages_shared_examples.rb
@@ -70,3 +70,59 @@ RSpec.shared_examples 'does not cause n^2 queries' do
end.not_to exceed_query_limit(control)
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