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:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue126
-rw-r--r--app/channels/graphql_channel.rb56
-rw-r--r--app/controllers/graphql_controller.rb2
-rw-r--r--app/graphql/gitlab_schema.rb2
-rw-r--r--app/graphql/graphql_triggers.rb7
-rw-r--r--app/graphql/subscriptions/base_subscription.rb31
-rw-r--r--app/graphql/subscriptions/issuable_updated.rb29
-rw-r--r--app/graphql/types/issuable_type.rb23
-rw-r--r--app/graphql/types/subscription_type.rb10
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/models/concerns/routable.rb4
-rw-r--r--app/models/project_services/youtrack_service.rb9
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/issues/update_service.rb20
-rw-r--r--app/services/packages/debian/generate_distribution_key_service.rb106
-rw-r--r--app/services/system_notes/base_service.rb6
-rw-r--r--app/views/search/results/_user.html.haml6
-rw-r--r--changelogs/unreleased/21067-reduce-SQL-calls-when-creating-SystemNotes.yml5
-rw-r--r--changelogs/unreleased/300626-msj-youtrack.yml5
-rw-r--r--changelogs/unreleased/fix-search-users-avatar-size.yml5
-rw-r--r--config/feature_flags/development/optimize_linkable_attributes.yml8
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md1
-rw-r--r--doc/administration/pages/index.md2
-rw-r--r--doc/administration/pages/source.md2
-rw-r--r--doc/api/graphql/reference/index.md32
-rw-r--r--doc/development/distributed_tracing.md2
-rw-r--r--doc/development/img/distributed_tracing_performance_bar.pngbin34370 -> 0 bytes
-rw-r--r--doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.pngbin36268 -> 31905 bytes
-rw-r--r--lib/api/concerns/packages/debian_endpoints.rb133
-rw-r--r--lib/api/debian_group_packages.rb4
-rw-r--r--lib/api/debian_package_endpoints.rb127
-rw-r--r--lib/api/debian_project_packages.rb6
-rw-r--r--lib/banzai/filter/base_relative_link_filter.rb33
-rw-r--r--lib/banzai/filter/upload_link_filter.rb12
-rw-r--r--lib/file_size_validator.rb2
-rw-r--r--lib/gitlab/api_authentication/token_locator.rb11
-rw-r--r--lib/gitlab/api_authentication/token_resolver.rb48
-rw-r--r--lib/gitlab/ci/pipeline/metrics.rb7
-rw-r--r--lib/gitlab/jwt_token.rb70
-rw-r--r--lib/gitlab/terraform_registry_token.rb13
-rw-r--r--locale/gitlab.pot12
-rw-r--r--rubocop/cop/graphql/authorize_types.rb2
-rw-r--r--spec/factories/gitlab/jwt_token.rb21
-rw-r--r--spec/features/projects/fork_spec.rb2
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js152
-rw-r--r--spec/graphql/graphql_triggers_spec.rb20
-rw-r--r--spec/graphql/subscriptions/issuable_updated_spec.rb68
-rw-r--r--spec/graphql/types/issuable_type_spec.rb23
-rw-r--r--spec/graphql/types/subscription_type_spec.rb13
-rw-r--r--spec/lib/banzai/pipeline/post_process_pipeline_spec.rb54
-rw-r--r--spec/lib/gitlab/api_authentication/token_locator_spec.rb25
-rw-r--r--spec/lib/gitlab/api_authentication/token_resolver_spec.rb49
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb2
-rw-r--r--spec/lib/gitlab/jwt_token_spec.rb6
-rw-r--r--spec/lib/gitlab/terraform_registry_token_spec.rb41
-rw-r--r--spec/models/concerns/chronic_duration_attribute_spec.rb3
-rw-r--r--spec/models/custom_emoji_spec.rb2
-rw-r--r--spec/models/group_spec.rb4
-rw-r--r--spec/models/merge_request_diff_spec.rb2
-rw-r--r--spec/models/project_spec.rb4
-rw-r--r--spec/models/user_spec.rb3
-rw-r--r--spec/services/issues/update_service_spec.rb4
-rw-r--r--spec/services/notes/build_service_spec.rb2
-rw-r--r--spec/services/packages/debian/generate_distribution_key_service_spec.rb35
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb49
-rw-r--r--spec/validators/addressable_url_validator_spec.rb12
-rw-r--r--spec/validators/array_members_validator_spec.rb4
-rw-r--r--spec/validators/devise_email_validator_spec.rb4
-rw-r--r--spec/validators/gitlab/utils/zoom_url_validator_spec.rb2
-rw-r--r--spec/validators/qualified_domain_array_validator_spec.rb8
72 files changed, 1300 insertions, 305 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 1ca0d658ecd..31a8768968f 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-ffbce774bce90b5a65f5b235afe492a7266aa82f
+16479e5771b69a2b4c22aade7c0a7fc2a6f897ce
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 288d6711682..07cc0ce46bc 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -20,6 +20,7 @@ import axios from '~/lib/utils/axios_utils';
import csrf from '~/lib/utils/csrf';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import validation from '~/vue_shared/directives/validation';
const PRIVATE_VISIBILITY = 'private';
const INTERNAL_VISIBILITY = 'internal';
@@ -31,6 +32,13 @@ const ALLOWED_VISIBILITY = {
public: [INTERNAL_VISIBILITY, PRIVATE_VISIBILITY, PUBLIC_VISIBILITY],
};
+const initFormField = ({ value, required = true, skipValidation = false }) => ({
+ value,
+ required,
+ state: skipValidation ? true : null,
+ feedback: null,
+});
+
export default {
components: {
GlForm,
@@ -46,6 +54,9 @@ export default {
GlFormRadioGroup,
GlFormSelect,
},
+ directives: {
+ validation: validation(),
+ },
inject: {
newGroupPath: {
default: '',
@@ -77,7 +88,8 @@ export default {
},
projectDescription: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
projectVisibility: {
type: String,
@@ -85,16 +97,30 @@ export default {
},
},
data() {
+ const form = {
+ state: false,
+ showValidation: false,
+ fields: {
+ namespace: initFormField({
+ value: null,
+ }),
+ name: initFormField({ value: this.projectName }),
+ slug: initFormField({ value: this.projectPath }),
+ description: initFormField({
+ value: this.projectDescription,
+ required: false,
+ skipValidation: true,
+ }),
+ visibility: initFormField({
+ value: this.projectVisibility,
+ skipValidation: true,
+ }),
+ },
+ };
return {
isSaving: false,
namespaces: [],
- selectedNamespace: {},
- fork: {
- name: this.projectName,
- slug: this.projectPath,
- description: this.projectDescription,
- visibility: this.projectVisibility,
- },
+ form,
};
},
computed: {
@@ -106,7 +132,7 @@ export default {
},
namespaceAllowedVisibility() {
return (
- ALLOWED_VISIBILITY[this.selectedNamespace.visibility] ||
+ ALLOWED_VISIBILITY[this.form.fields.namespace.value?.visibility] ||
ALLOWED_VISIBILITY[PUBLIC_VISIBILITY]
);
},
@@ -139,16 +165,17 @@ export default {
},
},
watch: {
- selectedNamespace(newVal) {
+ // eslint-disable-next-line func-names
+ 'form.fields.namespace.value': function (newVal) {
const { visibility } = newVal;
if (this.projectAllowedVisibility.includes(visibility)) {
- this.fork.visibility = visibility;
+ this.form.fields.visibility.value = visibility;
}
},
// eslint-disable-next-line func-names
- 'fork.name': function (newVal) {
- this.fork.slug = kebabCase(newVal);
+ 'form.fields.name.value': function (newVal) {
+ this.form.fields.slug.value = kebabCase(newVal);
},
},
mounted() {
@@ -166,19 +193,25 @@ export default {
);
},
async onSubmit() {
+ this.form.showValidation = true;
+
+ if (!this.form.state) {
+ return;
+ }
+
this.isSaving = true;
+ this.form.showValidation = false;
const { projectId } = this;
- const { name, slug, description, visibility } = this.fork;
- const { id: namespaceId } = this.selectedNamespace;
+ const { name, slug, description, visibility, namespace } = this.form.fields;
const postParams = {
id: projectId,
- name,
- namespace_id: namespaceId,
- path: slug,
- description,
- visibility,
+ name: name.value,
+ namespace_id: namespace.value.id,
+ path: slug.value,
+ description: description.value,
+ visibility: visibility.value,
};
const forkProjectPath = `/api/:version/projects/:id/fork`;
@@ -198,16 +231,34 @@ export default {
</script>
<template>
- <gl-form method="POST" @submit.prevent="onSubmit">
+ <gl-form novalidate method="POST" @submit.prevent="onSubmit">
<input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
- <gl-form-group label="Project name" label-for="fork-name">
- <gl-form-input id="fork-name" v-model="fork.name" data-testid="fork-name-input" required />
+ <gl-form-group
+ :label="__('Project name')"
+ label-for="fork-name"
+ :invalid-feedback="form.fields.name.feedback"
+ >
+ <gl-form-input
+ id="fork-name"
+ v-model="form.fields.name.value"
+ v-validation:[form.showValidation]
+ name="name"
+ data-testid="fork-name-input"
+ :state="form.fields.name.state"
+ required
+ />
</gl-form-group>
<div class="gl-md-display-flex">
<div class="gl-flex-basis-half">
- <gl-form-group label="Project URL" label-for="fork-url" class="gl-md-mr-3">
+ <gl-form-group
+ :label="__('Project URL')"
+ label-for="fork-url"
+ class="gl-md-mr-3"
+ :state="form.fields.namespace.state"
+ :invalid-feedback="s__('ForkProject|Please select a namespace')"
+ >
<gl-form-input-group>
<template #prepend>
<gl-input-group-text>
@@ -216,9 +267,12 @@ export default {
</template>
<gl-form-select
id="fork-url"
- v-model="selectedNamespace"
+ v-model="form.fields.namespace.value"
+ v-validation:[form.showValidation]
+ name="namespace"
data-testid="fork-url-input"
data-qa-selector="fork_namespace_dropdown"
+ :state="form.fields.namespace.state"
required
>
<template slot="first">
@@ -232,11 +286,19 @@ export default {
</gl-form-group>
</div>
<div class="gl-flex-basis-half">
- <gl-form-group label="Project slug" label-for="fork-slug" class="gl-md-ml-3">
+ <gl-form-group
+ :label="__('Project slug')"
+ label-for="fork-slug"
+ class="gl-md-ml-3"
+ :invalid-feedback="form.fields.slug.feedback"
+ >
<gl-form-input
id="fork-slug"
- v-model="fork.slug"
+ v-model="form.fields.slug.value"
+ v-validation:[form.showValidation]
data-testid="fork-slug-input"
+ name="slug"
+ :state="form.fields.slug.state"
required
/>
</gl-form-group>
@@ -250,11 +312,13 @@ export default {
</gl-link>
</p>
- <gl-form-group label="Project description (optional)" label-for="fork-description">
+ <gl-form-group :label="__('Project description (optional)')" label-for="fork-description">
<gl-form-textarea
id="fork-description"
- v-model="fork.description"
+ v-model="form.fields.description.value"
data-testid="fork-description-textarea"
+ name="description"
+ :state="form.fields.description.state"
/>
</gl-form-group>
@@ -266,8 +330,9 @@ export default {
</gl-link>
</label>
<gl-form-radio-group
- v-model="fork.visibility"
+ v-model="form.fields.visibility.value"
data-testid="fork-visibility-radio-group"
+ name="visibility"
required
>
<gl-form-radio
@@ -291,6 +356,7 @@ export default {
type="submit"
category="primary"
variant="confirm"
+ class="js-no-auto-disable"
data-testid="submit-button"
data-qa-selector="fork_project_button"
:loading="isSaving"
diff --git a/app/channels/graphql_channel.rb b/app/channels/graphql_channel.rb
new file mode 100644
index 00000000000..d364cc2b64b
--- /dev/null
+++ b/app/channels/graphql_channel.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+# This is based on https://github.com/rmosolgo/graphql-ruby/blob/v1.11.8/lib/graphql/subscriptions/action_cable_subscriptions.rb#L19-L82
+# modified to work with our own ActionCableLink client
+
+class GraphqlChannel < ApplicationCable::Channel # rubocop:disable Gitlab/NamespacedClass
+ def subscribed
+ @subscription_ids = []
+
+ query = params['query']
+ variables = Gitlab::Graphql::Variables.new(params['variables']).to_h
+ operation_name = params['operationName']
+
+ result = GitlabSchema.execute(
+ query,
+ context: context,
+ variables: variables,
+ operation_name: operation_name
+ )
+
+ payload = {
+ result: result.to_h,
+ more: result.subscription?
+ }
+
+ # Track the subscription here so we can remove it
+ # on unsubscribe.
+ if result.context[:subscription_id]
+ @subscription_ids << result.context[:subscription_id]
+ end
+
+ transmit(payload)
+ end
+
+ def unsubscribed
+ @subscription_ids.each do |sid|
+ GitlabSchema.subscriptions.delete_subscription(sid)
+ end
+ end
+
+ rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
+ transmit({ errors: [{ message: exception.message }] })
+ end
+
+ private
+
+ # When modifying the context, also update GraphqlController#context if needed
+ # so that we have similar context when executing queries, mutations, and subscriptions
+ #
+ # Objects added to the context may also need to be reloaded in
+ # `Subscriptions::BaseSubscription` so that they are not stale
+ def context
+ # is_sessionless_user is always false because we only support cookie auth in ActionCable
+ { channel: self, current_user: current_user, is_sessionless_user: false }
+ end
+end
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index a13ec1daddb..b7daff04373 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -109,6 +109,8 @@ class GraphqlController < ApplicationController
end
end
+ # When modifying the context, also update GraphqlChannel#context if needed
+ # so that we have similar context when executing queries, mutations, and subscriptions
def context
@context ||= { current_user: current_user, is_sessionless_user: !!sessionless_user?, request: request }
end
diff --git a/app/graphql/gitlab_schema.rb b/app/graphql/gitlab_schema.rb
index 8369d0e120f..3778c928ec5 100644
--- a/app/graphql/gitlab_schema.rb
+++ b/app/graphql/gitlab_schema.rb
@@ -10,6 +10,7 @@ class GitlabSchema < GraphQL::Schema
DEFAULT_MAX_DEPTH = 15
AUTHENTICATED_MAX_DEPTH = 20
+ use GraphQL::Subscriptions::ActionCableSubscriptions
use GraphQL::Pagination::Connections
use BatchLoader::GraphQL
use Gitlab::Graphql::Pagination::Connections
@@ -24,6 +25,7 @@ class GitlabSchema < GraphQL::Schema
query Types::QueryType
mutation Types::MutationType
+ subscription Types::SubscriptionType
default_max_page_size 100
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb
new file mode 100644
index 00000000000..671c7c2cd25
--- /dev/null
+++ b/app/graphql/graphql_triggers.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module GraphqlTriggers
+ def self.issuable_assignees_updated(issuable)
+ GitlabSchema.subscriptions.trigger('issuableAssigneesUpdated', { issuable_id: issuable.to_gid }, issuable)
+ end
+end
diff --git a/app/graphql/subscriptions/base_subscription.rb b/app/graphql/subscriptions/base_subscription.rb
new file mode 100644
index 00000000000..5f7931787df
--- /dev/null
+++ b/app/graphql/subscriptions/base_subscription.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Subscriptions
+ class BaseSubscription < GraphQL::Schema::Subscription
+ object_class Types::BaseObject
+ field_class Types::BaseField
+
+ def initialize(object:, context:, field:)
+ super
+
+ # Reset user so that we don't use a stale user for authorization
+ current_user.reset if current_user
+ end
+
+ def authorized?(*)
+ raise NotImplementedError
+ end
+
+ private
+
+ def unauthorized!
+ unsubscribe if context.query.subscription_update?
+
+ raise GraphQL::ExecutionError, 'Unauthorized subscription'
+ end
+
+ def current_user
+ context[:current_user]
+ end
+ end
+end
diff --git a/app/graphql/subscriptions/issuable_updated.rb b/app/graphql/subscriptions/issuable_updated.rb
new file mode 100644
index 00000000000..c1d82bfcf9c
--- /dev/null
+++ b/app/graphql/subscriptions/issuable_updated.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Subscriptions
+ class IssuableUpdated < BaseSubscription
+ include Gitlab::Graphql::Laziness
+
+ payload_type Types::IssuableType
+
+ argument :issuable_id, Types::GlobalIDType[Issuable],
+ required: true,
+ description: 'ID of the issuable.'
+
+ def subscribe(issuable_id:)
+ nil
+ end
+
+ def authorized?(issuable_id:)
+ # TODO: remove this check when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ raise Gitlab::Graphql::Errors::ArgumentError, 'Invalid IssuableID' unless issuable_id.is_a?(GlobalID)
+
+ issuable = force(GitlabSchema.find_by_gid(issuable_id))
+
+ unauthorized! unless issuable && Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
+
+ true
+ end
+ end
+end
diff --git a/app/graphql/types/issuable_type.rb b/app/graphql/types/issuable_type.rb
new file mode 100644
index 00000000000..fc57491bb59
--- /dev/null
+++ b/app/graphql/types/issuable_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ class IssuableType < BaseUnion
+ graphql_name 'Issuable'
+ description 'Represents an issuable.'
+
+ possible_types Types::IssueType, Types::MergeRequestType
+
+ def self.resolve_type(object, context)
+ case object
+ when Issue
+ Types::IssueType
+ when MergeRequest
+ Types::MergeRequestType
+ else
+ raise 'Unsupported issuable type'
+ end
+ end
+ end
+end
+
+Types::IssuableType.prepend_if_ee('::EE::Types::IssuableType')
diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb
new file mode 100644
index 00000000000..5356a998f0d
--- /dev/null
+++ b/app/graphql/types/subscription_type.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Types
+ class SubscriptionType < ::Types::BaseObject
+ graphql_name 'Subscription'
+
+ field :issuable_assignees_updated, subscription: Subscriptions::IssuableUpdated, null: true,
+ description: 'Triggered when the assignees of an issuable are updated.'
+ end
+end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index cda87cbd212..4373f47877f 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -393,7 +393,7 @@ module ApplicationSettingsHelper
end
def integration_expanded?(substring)
- @application_setting.errors.any? { |k| k.to_s.start_with?(substring) }
+ @application_setting.errors.messages.any? { |k, _| k.to_s.start_with?(substring) }
end
def instance_clusters_enabled?
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 71d8e06de76..5881997ea56 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -124,7 +124,9 @@ module Routable
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
- self.errors[:path].concat(route_path_errors) if route_path_errors
+ route_path_errors&.each do |msg|
+ self.errors.add(:path, msg)
+ end
end
def full_name_changed?
diff --git a/app/models/project_services/youtrack_service.rb b/app/models/project_services/youtrack_service.rb
index 30abd0159b3..94cbd6c5959 100644
--- a/app/models/project_services/youtrack_service.rb
+++ b/app/models/project_services/youtrack_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class YoutrackService < IssueTrackerService
+ include ActionView::Helpers::UrlHelper
+
validates :project_url, :issues_url, presence: true, public_url: true, if: :activated?
# {PROJECT-KEY}-{NUMBER} Examples: YT-1, PRJ-1, gl-030
@@ -17,7 +19,12 @@ class YoutrackService < IssueTrackerService
end
def description
- s_('IssueTracker|YouTrack issue tracker')
+ s_("IssueTracker|Use YouTrack as this project's issue tracker.")
+ end
+
+ def help
+ docs_link = link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/youtrack'), target: '_blank', rel: 'noopener noreferrer'
+ s_("IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}").html_safe % { docs_link: docs_link.html_safe }
end
def self.to_param
diff --git a/app/models/user.rb b/app/models/user.rb
index 92d11d231ec..a5ef8d64133 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -1414,7 +1414,9 @@ class User < ApplicationRecord
if namespace_path_errors.include?('has already been taken') && !User.exists?(username: username)
self.errors.add(:base, :username_exists_as_a_different_namespace)
else
- self.errors[:username].concat(namespace_path_errors)
+ namespace_path_errors.each do |msg|
+ self.errors.add(:username, msg)
+ end
end
end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 702527d80a7..e05df029ae3 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -61,12 +61,7 @@ module Issues
todo_service.update_issue(issue, current_user, old_mentioned_users)
end
- if issue.assignees != old_assignees
- create_assignee_note(issue, old_assignees)
- notification_service.async.reassigned_issue(issue, current_user, old_assignees)
- todo_service.reassigned_assignable(issue, current_user, old_assignees)
- track_incident_action(current_user, issue, :incident_assigned)
- end
+ handle_assignee_changes(issue, old_assignees)
if issue.previous_changes.include?('confidential')
# don't enqueue immediately to prevent todos removal in case of a mistake
@@ -90,6 +85,19 @@ module Issues
end
end
+ def handle_assignee_changes(issue, old_assignees)
+ return if issue.assignees == old_assignees
+
+ create_assignee_note(issue, old_assignees)
+ notification_service.async.reassigned_issue(issue, current_user, old_assignees)
+ todo_service.reassigned_assignable(issue, current_user, old_assignees)
+ track_incident_action(current_user, issue, :incident_assigned)
+
+ if Gitlab::ActionCable::Config.in_app? || Feature.enabled?(:broadcast_issue_updates, issue.project)
+ GraphqlTriggers.issuable_assignees_updated(issue)
+ end
+ end
+
def handle_task_changes(issuable)
todo_service.resolve_todos_for_target(issuable, current_user)
todo_service.update_issue(issuable, current_user)
diff --git a/app/services/packages/debian/generate_distribution_key_service.rb b/app/services/packages/debian/generate_distribution_key_service.rb
new file mode 100644
index 00000000000..28c97c7681e
--- /dev/null
+++ b/app/services/packages/debian/generate_distribution_key_service.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class GenerateDistributionKeyService
+ include Gitlab::Utils::StrongMemoize
+
+ def initialize(current_user:, params: {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ raise ArgumentError, 'Please provide a user' unless current_user.is_a?(User)
+
+ generate_key
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def passphrase
+ strong_memoize(:passphrase) do
+ params[:passphrase] || ::User.random_password
+ end
+ end
+
+ def pinentry_script_content
+ escaped_passphrase = Shellwords.escape(passphrase)
+
+ <<~EOF
+ #!/bin/sh
+
+ echo OK Pleased to meet you
+
+ while read -r cmd; do
+ case "$cmd" in
+ GETPIN) echo D #{escaped_passphrase}; echo OK;;
+ *) echo OK;;
+ esac
+ done
+ EOF
+ end
+
+ def using_pinentry
+ Gitlab::Gpg.using_tmp_keychain do
+ home_dir = Gitlab::Gpg.current_home_dir
+
+ File.write("#{home_dir}/pinentry.sh", pinentry_script_content, mode: 'w', perm: 0755)
+
+ File.write("#{home_dir}/gpg-agent.conf", "pinentry-program #{home_dir}/pinentry.sh\n", mode: 'w')
+
+ GPGME::Ctx.new(armor: true, offline: true) do |ctx|
+ yield ctx
+ end
+ end
+ end
+
+ def generate_key_params
+ # https://www.gnupg.org/documentation/manuals/gnupg/Unattended-GPG-key-generation.html
+ '<GnupgKeyParms format="internal">' + "\n" +
+ {
+ 'Key-Type': params[:key_type] || 'RSA',
+ 'Key-Length': params[:key_length] || 4096,
+ 'Key-Usage': params[:key_usage] || 'sign',
+ 'Name-Real': params[:name_real] || 'GitLab Debian repository',
+ 'Name-Email': params[:name_email] || Gitlab.config.gitlab.email_reply_to,
+ 'Name-Comment': params[:name_comment] || 'GitLab Debian repository automatic signing key',
+ 'Expire-Date': params[:expire_date] || 0,
+ 'Passphrase': passphrase
+ }.map { |k, v| "#{k}: #{v}\n" }.join +
+ '</GnupgKeyParms>'
+ end
+
+ def generate_key
+ using_pinentry do |ctx|
+ # Generate key
+ ctx.generate_key generate_key_params
+
+ key = ctx.keys.first # rubocop:disable Gitlab/KeysFirstAndValuesFirst
+ fingerprint = key.fingerprint
+
+ # Export private key
+ data = GPGME::Data.new
+ ctx.export_keys fingerprint, data, GPGME::EXPORT_MODE_SECRET
+ data.seek 0
+ private_key = data.read
+
+ # Export public key
+ data = GPGME::Data.new
+ ctx.export_keys fingerprint, data
+ data.seek 0
+ public_key = data.read
+
+ {
+ private_key: private_key,
+ public_key: public_key,
+ passphrase: passphrase,
+ fingerprint: fingerprint
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/system_notes/base_service.rb b/app/services/system_notes/base_service.rb
index 7341a25b133..ee7784c127b 100644
--- a/app/services/system_notes/base_service.rb
+++ b/app/services/system_notes/base_service.rb
@@ -13,10 +13,10 @@ module SystemNotes
protected
def create_note(note_summary)
- note = Note.create(note_summary.note.merge(system: true))
- note.system_note_metadata = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
+ note_params = note_summary.note.merge(system: true)
+ note_params[:system_note_metadata] = SystemNoteMetadata.new(note_summary.metadata) if note_summary.metadata?
- note
+ Note.create(note_params)
end
def content_tag(*args)
diff --git a/app/views/search/results/_user.html.haml b/app/views/search/results/_user.html.haml
index 8060a1577e4..9e70d9c9baa 100644
--- a/app/views/search/results/_user.html.haml
+++ b/app/views/search/results/_user.html.haml
@@ -1,9 +1,9 @@
%ul.content-list
%li
- .avatar-cell.d-none.d-sm-block
- = user_avatar(user: user, user_name: user.name, css_class: 'd-none d-sm-inline avatar s40')
+ .avatar-cell
+ = user_avatar(user: user, size: 40, user_name: user.name)
.user-info
- = link_to user_path(user), class: 'd-none d-sm-inline' do
+ = link_to user_path(user) do
.item-title
= user.name
= user_status(user)
diff --git a/changelogs/unreleased/21067-reduce-SQL-calls-when-creating-SystemNotes.yml b/changelogs/unreleased/21067-reduce-SQL-calls-when-creating-SystemNotes.yml
new file mode 100644
index 00000000000..c3d881c9ded
--- /dev/null
+++ b/changelogs/unreleased/21067-reduce-SQL-calls-when-creating-SystemNotes.yml
@@ -0,0 +1,5 @@
+---
+title: Reduce number of SQL queries when creating SystemNotes
+merge_request: 59102
+author:
+type: performance
diff --git a/changelogs/unreleased/300626-msj-youtrack.yml b/changelogs/unreleased/300626-msj-youtrack.yml
new file mode 100644
index 00000000000..5efb8c8bab0
--- /dev/null
+++ b/changelogs/unreleased/300626-msj-youtrack.yml
@@ -0,0 +1,5 @@
+---
+title: Review and revise YouTrack integration UI text
+merge_request: 59998
+author:
+type: other
diff --git a/changelogs/unreleased/fix-search-users-avatar-size.yml b/changelogs/unreleased/fix-search-users-avatar-size.yml
new file mode 100644
index 00000000000..2bf438146c8
--- /dev/null
+++ b/changelogs/unreleased/fix-search-users-avatar-size.yml
@@ -0,0 +1,5 @@
+---
+title: 'Users search: fix avatar size'
+merge_request: 59883
+author:
+type: fixed
diff --git a/config/feature_flags/development/optimize_linkable_attributes.yml b/config/feature_flags/development/optimize_linkable_attributes.yml
new file mode 100644
index 00000000000..8f35306b4ac
--- /dev/null
+++ b/config/feature_flags/development/optimize_linkable_attributes.yml
@@ -0,0 +1,8 @@
+---
+name: optimize_linkable_attributes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59983
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/328696
+milestone: '13.12'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index ebb29b5c70e..6019c606c5a 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -129,6 +129,7 @@ The following metrics are available:
| `pipeline_graph_link_calculation_duration_seconds` | Histogram | 13.9 | Total time spent calculating links, in seconds | |
| `pipeline_graph_links_total` | Histogram | 13.9 | Number of links per graph | |
| `pipeline_graph_links_per_job_ratio` | Histogram | 13.9 | Ratio of links to job per graph | |
+| `gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds` | Histogram | 13.12 | Time in seconds it takes to process Security Policies in CI/CD pipeline | |
## Metrics controlled by a feature flag
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index ae4fa086e3f..13d91c83041 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
description: 'Learn how to administer GitLab Pages.'
---
-# GitLab Pages administration
+# GitLab Pages administration **(FREE SELF)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/80) in GitLab EE 8.3.
> - Custom CNAMEs with TLS support were [introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/173) in GitLab EE 8.5.
diff --git a/doc/administration/pages/source.md b/doc/administration/pages/source.md
index e1b54e11a1f..f1c3b515f68 100644
--- a/doc/administration/pages/source.md
+++ b/doc/administration/pages/source.md
@@ -4,7 +4,7 @@ group: Release
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# GitLab Pages administration for source installations
+# GitLab Pages administration for source installations **(FREE SELF)**
NOTE:
Before attempting to enable GitLab Pages, first make sure you have
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 6156526772a..9773ff441c5 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -11817,6 +11817,22 @@ Represents the Geo sync and verification state of a snippet repository.
| <a id="submoduletype"></a>`type` | [`EntryType!`](#entrytype) | Type of tree entry. |
| <a id="submoduleweburl"></a>`webUrl` | [`String`](#string) | Web URL for the sub-module. |
+### `Subscription`
+
+#### Fields with arguments
+
+##### `Subscription.issuableAssigneesUpdated`
+
+Triggered when the assignees of an issuable are updated.
+
+Returns [`Issuable`](#issuable).
+
+###### Arguments
+
+| Name | Type | Description |
+| ---- | ---- | ----------- |
+| <a id="subscriptionissuableassigneesupdatedissuableid"></a>`issuableId` | [`IssuableID!`](#issuableid) | ID of the issuable. |
+
### `TaskCompletionStatus`
Completion status of tasks.
@@ -14181,6 +14197,12 @@ An example `IncidentManagementOncallRotationID` is: `"gid://gitlab/IncidentManag
Represents non-fractional signed whole numeric values. Int can represent values between -(2^31) and 2^31 - 1.
+### `IssuableID`
+
+A `IssuableID` is a global ID. It is encoded as a string.
+
+An example `IssuableID` is: `"gid://gitlab/Issuable/1"`.
+
### `IssueID`
A `IssueID` is a global ID. It is encoded as a string.
@@ -14390,6 +14412,16 @@ abstract types.
### Unions
+#### `Issuable`
+
+Represents an issuable.
+
+One of:
+
+- [`Epic`](#epic)
+- [`Issue`](#issue)
+- [`MergeRequest`](#mergerequest)
+
#### `PackageMetadata`
Represents metadata associated with a Package.
diff --git a/doc/development/distributed_tracing.md b/doc/development/distributed_tracing.md
index e5293c0804c..8bd8599a0b0 100644
--- a/doc/development/distributed_tracing.md
+++ b/doc/development/distributed_tracing.md
@@ -89,8 +89,6 @@ The easiest way to access tracing from a GDK environment is through the
[performance-bar](../administration/monitoring/performance/performance_bar.md). This can be shown
by typing `p` `b` in the browser window.
-![Jaeger Search UI](img/distributed_tracing_performance_bar.png)
-
Once the performance bar is enabled, click on the **Trace** link in the performance bar to go to
the Jaeger UI.
diff --git a/doc/development/img/distributed_tracing_performance_bar.png b/doc/development/img/distributed_tracing_performance_bar.png
deleted file mode 100644
index 8c819045104..00000000000
--- a/doc/development/img/distributed_tracing_performance_bar.png
+++ /dev/null
Binary files differ
diff --git a/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png b/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png
index bd8401a1747..cad075ca421 100644
--- a/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png
+++ b/doc/operations/metrics/dashboards/img/metrics_dashboard_template_selection_v13_3.png
Binary files differ
diff --git a/lib/api/concerns/packages/debian_endpoints.rb b/lib/api/concerns/packages/debian_endpoints.rb
new file mode 100644
index 00000000000..6fc7c439464
--- /dev/null
+++ b/lib/api/concerns/packages/debian_endpoints.rb
@@ -0,0 +1,133 @@
+# frozen_string_literal: true
+
+module API
+ module Concerns
+ module Packages
+ module DebianEndpoints
+ extend ActiveSupport::Concern
+
+ DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
+ COMPONENT_REGEX = %r{[a-z-]+}.freeze
+ ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
+ LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
+ PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
+ DISTRIBUTION_REQUIREMENTS = {
+ distribution: DISTRIBUTION_REGEX
+ }.freeze
+ COMPONENT_ARCHITECTURE_REQUIREMENTS = {
+ component: COMPONENT_REGEX,
+ architecture: ARCHITECTURE_REGEX
+ }.freeze
+ COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
+ component: COMPONENT_REGEX,
+ letter: LETTER_REGEX,
+ source_package: PACKAGE_REGEX
+ }.freeze
+ FILE_NAME_REQUIREMENTS = {
+ file_name: API::NO_SLASH_URL_PART_REGEX
+ }.freeze
+
+ included do
+ feature_category :package_registry
+
+ helpers ::API::Helpers::PackagesHelpers
+ helpers ::API::Helpers::Packages::BasicAuthHelpers
+
+ format :txt
+ content_type :txt, 'text/plain'
+
+ rescue_from ArgumentError do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ rescue_from ActiveRecord::RecordInvalid do |e|
+ render_api_error!(e.message, 400)
+ end
+
+ before do
+ require_packages_enabled!
+ end
+
+ namespace 'packages/debian' do
+ params do
+ requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex
+ end
+
+ namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
+ desc 'The Release file signature' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
+ get 'Release.gpg' do
+ not_found!
+ end
+
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
+ desc 'The unsigned Release file' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
+ get 'Release' do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO Release'
+ end
+
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
+ desc 'The signed Release file' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
+ get 'InRelease' do
+ not_found!
+ end
+
+ params do
+ requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
+ requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
+ end
+
+ namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
+ # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
+ desc 'The binary files index' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
+ get 'Packages' do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO Packages'
+ end
+ end
+ end
+
+ params do
+ requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
+ requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
+ requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex
+ end
+
+ namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
+ # GET {projects|groups}/:id/packages/debian/pool/:component/:letter/:source_package/:file_name
+ params do
+ requires :file_name, type: String, desc: 'The Debian File Name'
+ end
+ desc 'The package' do
+ detail 'This feature was introduced in GitLab 13.5'
+ end
+
+ route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
+ get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
+ 'TODO File'
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/debian_group_packages.rb b/lib/api/debian_group_packages.rb
index cf9fa388e9e..06edab662bf 100644
--- a/lib/api/debian_group_packages.rb
+++ b/lib/api/debian_group_packages.rb
@@ -15,8 +15,8 @@ module API
authorize_read_package!(user_group)
end
- namespace ':id/-/packages/debian' do
- include DebianPackageEndpoints
+ namespace ':id/-' do
+ include ::API::Concerns::Packages::DebianEndpoints
end
end
end
diff --git a/lib/api/debian_package_endpoints.rb b/lib/api/debian_package_endpoints.rb
deleted file mode 100644
index e7689b3feff..00000000000
--- a/lib/api/debian_package_endpoints.rb
+++ /dev/null
@@ -1,127 +0,0 @@
-# frozen_string_literal: true
-
-module API
- module DebianPackageEndpoints
- extend ActiveSupport::Concern
-
- DISTRIBUTION_REGEX = %r{[a-zA-Z0-9][a-zA-Z0-9.-]*}.freeze
- COMPONENT_REGEX = %r{[a-z-]+}.freeze
- ARCHITECTURE_REGEX = %r{[a-z][a-z0-9]*}.freeze
- LETTER_REGEX = %r{(lib)?[a-z0-9]}.freeze
- PACKAGE_REGEX = API::NO_SLASH_URL_PART_REGEX
- DISTRIBUTION_REQUIREMENTS = {
- distribution: DISTRIBUTION_REGEX
- }.freeze
- COMPONENT_ARCHITECTURE_REQUIREMENTS = {
- component: COMPONENT_REGEX,
- architecture: ARCHITECTURE_REGEX
- }.freeze
- COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS = {
- component: COMPONENT_REGEX,
- letter: LETTER_REGEX,
- source_package: PACKAGE_REGEX
- }.freeze
- FILE_NAME_REQUIREMENTS = {
- file_name: API::NO_SLASH_URL_PART_REGEX
- }.freeze
-
- included do
- feature_category :package_registry
-
- helpers ::API::Helpers::PackagesHelpers
- helpers ::API::Helpers::Packages::BasicAuthHelpers
-
- format :txt
- content_type :txt, 'text/plain'
-
- rescue_from ArgumentError do |e|
- render_api_error!(e.message, 400)
- end
-
- rescue_from ActiveRecord::RecordInvalid do |e|
- render_api_error!(e.message, 400)
- end
-
- before do
- require_packages_enabled!
- end
-
- params do
- requires :distribution, type: String, desc: 'The Debian Codename', regexp: Gitlab::Regex.debian_distribution_regex
- end
-
- namespace 'dists/*distribution', requirements: DISTRIBUTION_REQUIREMENTS do
- # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release.gpg
- desc 'The Release file signature' do
- detail 'This feature was introduced in GitLab 13.5'
- end
-
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
- get 'Release.gpg' do
- not_found!
- end
-
- # GET {projects|groups}/:id/packages/debian/dists/*distribution/Release
- desc 'The unsigned Release file' do
- detail 'This feature was introduced in GitLab 13.5'
- end
-
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
- get 'Release' do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
- 'TODO Release'
- end
-
- # GET {projects|groups}/:id/packages/debian/dists/*distribution/InRelease
- desc 'The signed Release file' do
- detail 'This feature was introduced in GitLab 13.5'
- end
-
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
- get 'InRelease' do
- not_found!
- end
-
- params do
- requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
- requires :architecture, type: String, desc: 'The Debian Architecture', regexp: Gitlab::Regex.debian_architecture_regex
- end
-
- namespace ':component/binary-:architecture', requirements: COMPONENT_ARCHITECTURE_REQUIREMENTS do
- # GET {projects|groups}/:id/packages/debian/dists/*distribution/:component/binary-:architecture/Packages
- desc 'The binary files index' do
- detail 'This feature was introduced in GitLab 13.5'
- end
-
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
- get 'Packages' do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
- 'TODO Packages'
- end
- end
- end
-
- params do
- requires :component, type: String, desc: 'The Debian Component', regexp: Gitlab::Regex.debian_component_regex
- requires :letter, type: String, desc: 'The Debian Classification (first-letter or lib-first-letter)'
- requires :source_package, type: String, desc: 'The Debian Source Package Name', regexp: Gitlab::Regex.debian_package_name_regex
- end
-
- namespace 'pool/:component/:letter/:source_package', requirements: COMPONENT_LETTER_SOURCE_PACKAGE_REQUIREMENTS do
- # GET {projects|groups}/:id/packages/debian/pool/:component/:letter/:source_package/:file_name
- params do
- requires :file_name, type: String, desc: 'The Debian File Name'
- end
- desc 'The package' do
- detail 'This feature was introduced in GitLab 13.5'
- end
-
- route_setting :authentication, deploy_token_allowed: true, basic_auth_personal_access_token: true, job_token_allowed: :basic_auth, authenticate_non_public: true
- get ':file_name', requirements: FILE_NAME_REQUIREMENTS do
- # https://gitlab.com/gitlab-org/gitlab/-/issues/5835#note_414103286
- 'TODO File'
- end
- end
- end
- end
-end
diff --git a/lib/api/debian_project_packages.rb b/lib/api/debian_project_packages.rb
index 8c0db42a448..0ed828fd639 100644
--- a/lib/api/debian_project_packages.rb
+++ b/lib/api/debian_project_packages.rb
@@ -15,14 +15,14 @@ module API
authorize_read_package!
end
- namespace ':id/packages/debian' do
- include DebianPackageEndpoints
+ namespace ':id' do
+ include ::API::Concerns::Packages::DebianEndpoints
params do
requires :file_name, type: String, desc: 'The file name'
end
- namespace ':file_name', requirements: FILE_NAME_REQUIREMENTS do
+ namespace 'packages/debian/:file_name', requirements: FILE_NAME_REQUIREMENTS do
content_type :json, Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
# PUT {projects|groups}/:id/packages/debian/:file_name
diff --git a/lib/banzai/filter/base_relative_link_filter.rb b/lib/banzai/filter/base_relative_link_filter.rb
index fd526df4c48..84a6e18e77b 100644
--- a/lib/banzai/filter/base_relative_link_filter.rb
+++ b/lib/banzai/filter/base_relative_link_filter.rb
@@ -10,19 +10,16 @@ module Banzai
protected
def linkable_attributes
- strong_memoize(:linkable_attributes) do
- attrs = []
-
- attrs += doc.search('a:not(.gfm)').map do |el|
- el.attribute('href')
- end
-
- attrs += doc.search('img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
- [el.attribute('src'), el.attribute('data-src')]
- end
-
- attrs.reject do |attr|
- attr.blank? || attr.value.start_with?('//')
+ if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
+ # Nokorigi Nodeset#search performs badly for documents with many nodes
+ #
+ # Here we store fetched attributes in the shared variable "result"
+ # This variable is passed through the chain of filters and can be
+ # accessed by them
+ result[:linkable_attributes] ||= fetch_linkable_attributes
+ else
+ strong_memoize(:linkable_attributes) do
+ fetch_linkable_attributes
end
end
end
@@ -40,6 +37,16 @@ module Banzai
def unescape_and_scrub_uri(uri)
Addressable::URI.unescape(uri).scrub.delete("\0")
end
+
+ def fetch_linkable_attributes
+ attrs = []
+
+ attrs += doc.search('a:not(.gfm), img:not(.gfm), video:not(.gfm), audio:not(.gfm)').flat_map do |el|
+ [el.attribute('href'), el.attribute('src'), el.attribute('data-src')]
+ end
+
+ attrs.reject { |attr| attr.blank? || attr.value.start_with?('//') }
+ end
end
end
end
diff --git a/lib/banzai/filter/upload_link_filter.rb b/lib/banzai/filter/upload_link_filter.rb
index 762371e1418..ceb7547a85d 100644
--- a/lib/banzai/filter/upload_link_filter.rb
+++ b/lib/banzai/filter/upload_link_filter.rb
@@ -15,8 +15,16 @@ module Banzai
def call
return doc if context[:system_note]
- linkable_attributes.each do |attr|
- process_link_to_upload_attr(attr)
+ if Feature.enabled?(:optimize_linkable_attributes, project, default_enabled: :yaml)
+ # We exclude processed upload links from the linkable attributes to
+ # prevent further modifications by RepositoryLinkFilter
+ linkable_attributes.reject! do |attr|
+ process_link_to_upload_attr(attr)
+ end
+ else
+ linkable_attributes.each do |attr|
+ process_link_to_upload_attr(attr)
+ end
end
doc
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index e9868732172..ac3210b4e98 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -62,7 +62,7 @@ class FileSizeValidator < ActiveModel::EachValidator
default_message = options[MESSAGES[key]]
errors_options[:message] ||= default_message if default_message
- record.errors.add(attribute, MESSAGES[key], errors_options)
+ record.errors.add(attribute, MESSAGES[key], **errors_options)
end
end
diff --git a/lib/gitlab/api_authentication/token_locator.rb b/lib/gitlab/api_authentication/token_locator.rb
index 09039f3fc43..6ab37487822 100644
--- a/lib/gitlab/api_authentication/token_locator.rb
+++ b/lib/gitlab/api_authentication/token_locator.rb
@@ -10,7 +10,7 @@ module Gitlab
attr_reader :location
- validates :location, inclusion: { in: %i[http_basic_auth http_token] }
+ validates :location, inclusion: { in: %i[http_basic_auth http_token token_param] }
def initialize(location)
@location = location
@@ -23,6 +23,8 @@ module Gitlab
extract_from_http_basic_auth request
when :http_token
extract_from_http_token request
+ when :token_param
+ extract_from_token_param request
end
end
@@ -41,6 +43,13 @@ module Gitlab
UsernameAndPassword.new(nil, password)
end
+
+ def extract_from_token_param(request)
+ password = request.query_parameters['token']
+ return unless password.present?
+
+ UsernameAndPassword.new(nil, password)
+ end
end
end
end
diff --git a/lib/gitlab/api_authentication/token_resolver.rb b/lib/gitlab/api_authentication/token_resolver.rb
index 9234837cdf7..dd9039e37f6 100644
--- a/lib/gitlab/api_authentication/token_resolver.rb
+++ b/lib/gitlab/api_authentication/token_resolver.rb
@@ -15,9 +15,14 @@ module Gitlab
personal_access_token
job_token
deploy_token
+ personal_access_token_from_jwt
+ deploy_token_from_jwt
+ job_token_from_jwt
]
}
+ UsernameAndPassword = ::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword
+
def initialize(token_type)
@token_type = token_type
validate!
@@ -56,6 +61,15 @@ module Gitlab
when :deploy_token_with_username
resolve_deploy_token_with_username raw
+
+ when :personal_access_token_from_jwt
+ resolve_personal_access_token_from_jwt raw
+
+ when :deploy_token_from_jwt
+ resolve_deploy_token_from_jwt raw
+
+ when :job_token_from_jwt
+ resolve_job_token_from_jwt raw
end
end
@@ -116,6 +130,33 @@ module Gitlab
end
end
+ def resolve_personal_access_token_from_jwt(raw)
+ with_jwt_token(raw) do |jwt_token|
+ break unless jwt_token['token'].is_a?(Integer)
+
+ pat = ::PersonalAccessToken.find(jwt_token['token'])
+ break unless pat
+
+ pat
+ end
+ end
+
+ def resolve_deploy_token_from_jwt(raw)
+ with_jwt_token(raw) do |jwt_token|
+ break unless jwt_token['token'].is_a?(String)
+
+ resolve_deploy_token(UsernameAndPassword.new(nil, jwt_token['token']))
+ end
+ end
+
+ def resolve_job_token_from_jwt(raw)
+ with_jwt_token(raw) do |jwt_token|
+ break unless jwt_token['token'].is_a?(String)
+
+ resolve_job_token(UsernameAndPassword.new(nil, jwt_token['token']))
+ end
+ end
+
def with_personal_access_token(raw, &block)
pat = ::PersonalAccessToken.find_by_token(raw.password)
return unless pat
@@ -136,6 +177,13 @@ module Gitlab
yield(job)
end
+
+ def with_jwt_token(raw, &block)
+ jwt_token = ::Gitlab::JWTToken.decode(raw.password)
+ raise ::Gitlab::Auth::UnauthorizedError unless jwt_token
+
+ yield(jwt_token)
+ end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/metrics.rb b/lib/gitlab/ci/pipeline/metrics.rb
index 6cb6fd3920d..35e88ded416 100644
--- a/lib/gitlab/ci/pipeline/metrics.rb
+++ b/lib/gitlab/ci/pipeline/metrics.rb
@@ -13,6 +13,13 @@ module Gitlab
::Gitlab::Metrics.histogram(name, comment, labels, buckets)
end
+ def self.pipeline_security_orchestration_policy_processing_duration_histogram
+ name = :gitlab_ci_pipeline_security_orchestration_policy_processing_duration_seconds
+ comment = 'Pipeline security orchestration policy processing duration'
+
+ ::Gitlab::Metrics.histogram(name, comment)
+ end
+
def self.pipeline_size_histogram
name = :gitlab_ci_pipeline_size_builds
comment = 'Pipeline size'
diff --git a/lib/gitlab/jwt_token.rb b/lib/gitlab/jwt_token.rb
new file mode 100644
index 00000000000..11bc5479b6e
--- /dev/null
+++ b/lib/gitlab/jwt_token.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class JWTToken < JSONWebToken::HMACToken
+ HMAC_ALGORITHM = 'SHA256'
+ HMAC_KEY = 'gitlab-jwt'
+ HMAC_EXPIRES_IN = 5.minutes.freeze
+
+ class << self
+ def decode(jwt)
+ payload = super(jwt, secret).first
+
+ new.tap do |jwt_token|
+ jwt_token.id = payload.delete('jti')
+ jwt_token.issued_at = payload.delete('iat')
+ jwt_token.not_before = payload.delete('nbf')
+ jwt_token.expire_time = payload.delete('exp')
+
+ payload.each do |key, value|
+ jwt_token[key] = value
+ end
+ end
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature => ex
+ # we want to log and return on expired and errored tokens
+ Gitlab::ErrorTracking.track_exception(ex)
+ nil
+ end
+
+ def secret
+ OpenSSL::HMAC.hexdigest(
+ HMAC_ALGORITHM,
+ ::Settings.attr_encrypted_db_key_base,
+ HMAC_KEY
+ )
+ end
+ end
+
+ def initialize
+ super(self.class.secret)
+ self.expire_time = self.issued_at + HMAC_EXPIRES_IN.to_i
+ end
+
+ def ==(other)
+ self.id == other.id &&
+ self.payload == other.payload
+ end
+
+ def issued_at=(value)
+ super(convert_time(value))
+ end
+
+ def not_before=(value)
+ super(convert_time(value))
+ end
+
+ def expire_time=(value)
+ super(convert_time(value))
+ end
+
+ private
+
+ def convert_time(value)
+ # JSONWebToken::Token truncates subsecond precision causing comparisons to
+ # fail unless we truncate it here first
+ value = value.to_i if value.is_a?(Float)
+ value = Time.zone.at(value) if value.is_a?(Integer)
+ value
+ end
+ end
+end
diff --git a/lib/gitlab/terraform_registry_token.rb b/lib/gitlab/terraform_registry_token.rb
new file mode 100644
index 00000000000..ae7df49835f
--- /dev/null
+++ b/lib/gitlab/terraform_registry_token.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ class TerraformRegistryToken < JWTToken
+ class << self
+ def from_token(token)
+ new.tap do |terraform_registry_token|
+ terraform_registry_token['token'] = token.try(:token).presence || token.try(:id).presence
+ end
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1f787192137..702bdf3bf50 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14071,6 +14071,9 @@ msgstr ""
msgid "ForkProject|Internal"
msgstr ""
+msgid "ForkProject|Please select a namespace"
+msgstr ""
+
msgid "ForkProject|Private"
msgstr ""
@@ -17971,13 +17974,16 @@ msgstr ""
msgid "IssueTracker|Use Redmine as the issue tracker. %{docs_link}"
msgstr ""
-msgid "IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}"
+msgid "IssueTracker|Use YouTrack as this project's issue tracker."
msgstr ""
-msgid "IssueTracker|Use a custom issue tracker."
+msgid "IssueTracker|Use YouTrack as this project's issue tracker. %{docs_link}"
msgstr ""
-msgid "IssueTracker|YouTrack issue tracker"
+msgid "IssueTracker|Use a custom issue tracker that is not in the integration list. %{docs_link}"
+msgstr ""
+
+msgid "IssueTracker|Use a custom issue tracker."
msgstr ""
msgid "Issues"
diff --git a/rubocop/cop/graphql/authorize_types.rb b/rubocop/cop/graphql/authorize_types.rb
index 9bfd93aa3db..180a1a27a85 100644
--- a/rubocop/cop/graphql/authorize_types.rb
+++ b/rubocop/cop/graphql/authorize_types.rb
@@ -8,7 +8,7 @@ module RuboCop
'https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#type-authorization'
# We want to exclude our own basetypes and scalars
- ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType
+ ALLOWED_TYPES = %w[BaseEnum BaseScalar BasePermissionType MutationType SubscriptionType
QueryType GraphQL::Schema BaseUnion BaseInputObject].freeze
def_node_search :authorize?, <<~PATTERN
diff --git a/spec/factories/gitlab/jwt_token.rb b/spec/factories/gitlab/jwt_token.rb
new file mode 100644
index 00000000000..bc00c6a5ff4
--- /dev/null
+++ b/spec/factories/gitlab/jwt_token.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :jwt_token, class: 'Gitlab::JWTToken' do
+ skip_create
+
+ initialize_with { new }
+
+ trait :with_custom_payload do
+ transient do
+ custom_payload { {} }
+ end
+
+ after(:build) do |jwt, evaluator|
+ evaluator.custom_payload.each do |key, value|
+ jwt[key] = value
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/fork_spec.rb b/spec/features/projects/fork_spec.rb
index 2b7ea70fe5a..f60542bd695 100644
--- a/spec/features/projects/fork_spec.rb
+++ b/spec/features/projects/fork_spec.rb
@@ -6,7 +6,7 @@ RSpec.describe 'Project fork' do
include ProjectForksHelper
let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
+ let(:project) { create(:project, :public, :repository, description: 'some description') }
before do
sign_in(user)
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 2992c7f0624..6d853120232 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -1,5 +1,5 @@
-import { GlForm, GlFormInputGroup, GlFormInput } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormInputGroup, GlFormInput, GlForm } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import axios from 'axios';
import AxiosMockAdapter from 'axios-mock-adapter';
import { kebabCase } from 'lodash';
@@ -43,8 +43,8 @@ describe('ForkForm component', () => {
axiosMock.onGet(DEFAULT_PROPS.endpoint).replyOnce(statusCode, data);
};
- const createComponent = (props = {}, data = {}) => {
- wrapper = shallowMount(ForkForm, {
+ const createComponentFactory = (mountFn) => (props = {}, data = {}) => {
+ wrapper = mountFn(ForkForm, {
provide: {
newGroupPath: 'some/groups/path',
visibilityHelpPath: 'some/visibility/help/path',
@@ -65,6 +65,9 @@ describe('ForkForm component', () => {
});
};
+ const createComponent = createComponentFactory(shallowMount);
+ const createFullComponent = createComponentFactory(mount);
+
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
window.gon = {
@@ -99,44 +102,6 @@ describe('ForkForm component', () => {
expect(cancelButton.attributes('href')).toBe(projectFullPath);
});
- it('make POST request with project param', async () => {
- jest.spyOn(axios, 'post');
-
- const namespaceId = 20;
-
- mockGetRequest();
- createComponent(
- {},
- {
- selectedNamespace: {
- id: namespaceId,
- },
- },
- );
-
- wrapper.find(GlForm).vm.$emit('submit', { preventDefault: () => {} });
-
- const {
- projectId,
- projectDescription,
- projectName,
- projectPath,
- projectVisibility,
- } = DEFAULT_PROPS;
-
- const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
- const project = {
- description: projectDescription,
- id: projectId,
- name: projectName,
- namespace_id: namespaceId,
- path: projectPath,
- visibility: projectVisibility,
- };
-
- expect(axios.post).toHaveBeenCalledWith(url, project);
- });
-
it('has input with csrf token', () => {
mockGetRequest();
createComponent();
@@ -258,9 +223,7 @@ describe('ForkForm component', () => {
projectVisibility: project,
},
{
- selectedNamespace: {
- visibility: namespace,
- },
+ form: { fields: { namespace: { value: { visibility: namespace } } } },
},
);
@@ -274,34 +237,101 @@ describe('ForkForm component', () => {
describe('onSubmit', () => {
beforeEach(() => {
jest.spyOn(urlUtility, 'redirectTo').mockImplementation();
+
+ mockGetRequest();
+ createFullComponent(
+ {},
+ {
+ namespaces: MOCK_NAMESPACES_RESPONSE,
+ form: {
+ state: true,
+ },
+ },
+ );
});
- it('redirect to POST web_url response', async () => {
- const webUrl = `new/fork-project`;
+ const selectedMockNamespaceIndex = 1;
+ const namespaceId = MOCK_NAMESPACES_RESPONSE[selectedMockNamespaceIndex].id;
- jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } });
+ const fillForm = async () => {
+ const namespaceOptions = findForkUrlInput().findAll('option');
- mockGetRequest();
- createComponent();
+ await namespaceOptions.at(selectedMockNamespaceIndex + 1).setSelected();
+ };
- await wrapper.vm.onSubmit();
+ const submitForm = async () => {
+ await fillForm();
+ const form = wrapper.find(GlForm);
- expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
+ await form.trigger('submit');
+ await wrapper.vm.$nextTick();
+ };
+
+ describe('with invalid form', () => {
+ it('does not make POST request', async () => {
+ jest.spyOn(axios, 'post');
+
+ expect(axios.post).not.toHaveBeenCalled();
+ });
+
+ it('does not redirect the current page', async () => {
+ await submitForm();
+
+ expect(urlUtility.redirectTo).not.toHaveBeenCalled();
+ });
});
- it('display flash when POST is unsuccessful', async () => {
- const dummyError = 'Fork project failed';
+ describe('with valid form', () => {
+ beforeEach(() => {
+ fillForm();
+ });
- jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
+ it('make POST request with project param', async () => {
+ jest.spyOn(axios, 'post');
+
+ await submitForm();
+
+ const {
+ projectId,
+ projectDescription,
+ projectName,
+ projectPath,
+ projectVisibility,
+ } = DEFAULT_PROPS;
+
+ const url = `/api/${GON_API_VERSION}/projects/${projectId}/fork`;
+ const project = {
+ description: projectDescription,
+ id: projectId,
+ name: projectName,
+ namespace_id: namespaceId,
+ path: projectPath,
+ visibility: projectVisibility,
+ };
- mockGetRequest();
- createComponent();
+ expect(axios.post).toHaveBeenCalledWith(url, project);
+ });
+
+ it('redirect to POST web_url response', async () => {
+ const webUrl = `new/fork-project`;
+ jest.spyOn(axios, 'post').mockResolvedValue({ data: { web_url: webUrl } });
+
+ await submitForm();
+
+ expect(urlUtility.redirectTo).toHaveBeenCalledWith(webUrl);
+ });
+
+ it('display flash when POST is unsuccessful', async () => {
+ const dummyError = 'Fork project failed';
+
+ jest.spyOn(axios, 'post').mockRejectedValue(dummyError);
- await wrapper.vm.onSubmit();
+ await submitForm();
- expect(urlUtility.redirectTo).not.toHaveBeenCalled();
- expect(createFlash).toHaveBeenCalledWith({
- message: dummyError,
+ expect(urlUtility.redirectTo).not.toHaveBeenCalled();
+ expect(createFlash).toHaveBeenCalledWith({
+ message: dummyError,
+ });
});
});
});
diff --git a/spec/graphql/graphql_triggers_spec.rb b/spec/graphql/graphql_triggers_spec.rb
new file mode 100644
index 00000000000..0b53c633077
--- /dev/null
+++ b/spec/graphql/graphql_triggers_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GraphqlTriggers do
+ describe '.issuable_assignees_updated' do
+ it 'triggers the issuableAssigneesUpdated subscription' do
+ assignees = create_list(:user, 2)
+ issue = create(:issue, assignees: assignees)
+
+ expect(GitlabSchema.subscriptions).to receive(:trigger).with(
+ 'issuableAssigneesUpdated',
+ { issuable_id: issue.to_gid },
+ issue
+ )
+
+ GraphqlTriggers.issuable_assignees_updated(issue)
+ end
+ end
+end
diff --git a/spec/graphql/subscriptions/issuable_updated_spec.rb b/spec/graphql/subscriptions/issuable_updated_spec.rb
new file mode 100644
index 00000000000..cc88b37627d
--- /dev/null
+++ b/spec/graphql/subscriptions/issuable_updated_spec.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Subscriptions::IssuableUpdated do
+ include GraphqlHelpers
+
+ it { expect(described_class).to have_graphql_arguments(:issuable_id) }
+ it { expect(described_class.payload_type).to eq(Types::IssuableType) }
+
+ describe '#resolve' do
+ let_it_be(:unauthorized_user) { create(:user) }
+ let_it_be(:issue) { create(:issue) }
+
+ let(:current_user) { issue.author }
+ let(:issuable_id) { issue.to_gid }
+
+ subject { resolver.resolve_with_support(issuable_id: issuable_id) }
+
+ context 'initial subscription' do
+ let(:resolver) { resolver_instance(described_class, ctx: { current_user: current_user }, subscription_update: false) }
+
+ it 'returns nil' do
+ expect(subject).to eq(nil)
+ end
+
+ context 'when user is unauthorized' do
+ let(:current_user) { unauthorized_user }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(GraphQL::ExecutionError)
+ end
+ end
+
+ context 'when issue does not exist' do
+ let(:issuable_id) { GlobalID.parse("gid://gitlab/Issue/#{non_existing_record_id}") }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(GraphQL::ExecutionError)
+ end
+ end
+
+ context 'when a GraphQL::ID_TYPE is provided' do
+ let(:issuable_id) { issue.to_gid.to_s }
+
+ it 'raises an exception' do
+ expect { subject }.to raise_error(Gitlab::Graphql::Errors::ArgumentError)
+ end
+ end
+ end
+
+ context 'subscription updates' do
+ let(:resolver) { resolver_instance(described_class, obj: issue, ctx: { current_user: current_user }, subscription_update: true) }
+
+ it 'returns the resolved object' do
+ expect(subject).to eq(issue)
+ end
+
+ context 'when user is unauthorized' do
+ let(:current_user) { unauthorized_user }
+
+ it 'unsubscribes the user' do
+ expect { subject }.to throw_symbol(:graphql_subscription_unsubscribed)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/graphql/types/issuable_type_spec.rb b/spec/graphql/types/issuable_type_spec.rb
new file mode 100644
index 00000000000..992a58f524b
--- /dev/null
+++ b/spec/graphql/types/issuable_type_spec.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Issuable'] do
+ it 'returns possible types' do
+ expect(described_class.possible_types).to include(Types::IssueType, Types::MergeRequestType)
+ end
+
+ describe '.resolve_type' do
+ it 'resolves issues' do
+ expect(described_class.resolve_type(build(:issue), {})).to eq(Types::IssueType)
+ end
+
+ it 'resolves merge requests' do
+ expect(described_class.resolve_type(build(:merge_request), {})).to eq(Types::MergeRequestType)
+ end
+
+ it 'raises an error for invalid types' do
+ expect { described_class.resolve_type(build(:user), {}) }.to raise_error 'Unsupported issuable type'
+ end
+ end
+end
diff --git a/spec/graphql/types/subscription_type_spec.rb b/spec/graphql/types/subscription_type_spec.rb
new file mode 100644
index 00000000000..b99df374bb3
--- /dev/null
+++ b/spec/graphql/types/subscription_type_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GitlabSchema.types['Subscription'] do
+ it 'has the expected fields' do
+ expected_fields = %i[
+ issuable_assignees_updated
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields).only
+ end
+end
diff --git a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
index d9f45769550..ebe1ca4d403 100644
--- a/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/post_process_pipeline_spec.rb
@@ -3,24 +3,56 @@
require 'spec_helper'
RSpec.describe Banzai::Pipeline::PostProcessPipeline do
- context 'when a document only has upload links' do
- it 'does not make any Gitaly calls', :request_store do
- markdown = <<-MARKDOWN.strip_heredoc
- [Relative Upload Link](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
+ subject { described_class.call(doc, context) }
+
+ let_it_be(:project) { create(:project, :public, :repository) }
- ![Relative Upload Image](/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg)
- MARKDOWN
+ let(:context) { { project: project, ref: 'master' } }
- context = {
- project: create(:project, :public, :repository),
- ref: 'master'
- }
+ context 'when a document only has upload links' do
+ let(:doc) do
+ <<-HTML.strip_heredoc
+ <a href="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">Relative Upload Link</a>
+ <img src="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">
+ HTML
+ end
+ it 'does not make any Gitaly calls', :request_store do
Gitlab::GitalyClient.reset_counts
- described_class.call(markdown, context)
+ subject
expect(Gitlab::GitalyClient.get_request_count).to eq(0)
end
end
+
+ context 'when both upload and repository links are present' do
+ let(:html) do
+ <<-HTML.strip_heredoc
+ <a href="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">Relative Upload Link</a>
+ <img src="/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg">
+ <a href="/test.jpg">Just a link</a>
+ HTML
+ end
+
+ let(:doc) { HTML::Pipeline.parse(html) }
+
+ it 'searches for attributes only once' do
+ expect(doc).to receive(:search).once.and_call_original
+
+ subject
+ end
+
+ context 'when "optimize_linkable_attributes" is disabled' do
+ before do
+ stub_feature_flags(optimize_linkable_attributes: false)
+ end
+
+ it 'searches for attributes twice' do
+ expect(doc).to receive(:search).twice.and_call_original
+
+ subject
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/api_authentication/token_locator_spec.rb b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
index e933fd8352e..58e0b9fbc89 100644
--- a/spec/lib/gitlab/api_authentication/token_locator_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_locator_spec.rb
@@ -36,7 +36,7 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
let(:request) { double(authorization: nil) }
it 'returns nil' do
- expect(subject).to be(nil)
+ expect(subject).to be_nil
end
end
@@ -59,7 +59,7 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
let(:request) { double(headers: {}) }
it 'returns nil' do
- expect(subject).to be(nil)
+ expect(subject).to be_nil
end
end
@@ -72,5 +72,26 @@ RSpec.describe Gitlab::APIAuthentication::TokenLocator do
end
end
end
+
+ context 'with :token_param' do
+ let(:type) { :token_param }
+
+ context 'without credentials' do
+ let(:request) { double(query_parameters: {}) }
+
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
+
+ context 'with credentials' do
+ let(:password) { 'bar' }
+ let(:request) { double(query_parameters: { 'token' => password }) }
+
+ it 'returns the credentials' do
+ expect(subject.password).to eq(password)
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
index 97a7c8ba7cf..bbc6bf0d481 100644
--- a/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
+++ b/spec/lib/gitlab/api_authentication/token_resolver_spec.rb
@@ -160,9 +160,58 @@ RSpec.describe Gitlab::APIAuthentication::TokenResolver do
it_behaves_like 'an authorized request'
end
end
+
+ context 'with :personal_access_token_from_jwt' do
+ let(:type) { :personal_access_token_from_jwt }
+ let(:token) { personal_access_token }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password_from_jwt(token.id) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
+
+ context 'with :deploy_token_from_jwt' do
+ let(:type) { :deploy_token_from_jwt }
+ let(:token) { deploy_token }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password_from_jwt(token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+ end
+
+ context 'with :job_token_from_jwt' do
+ let(:type) { :job_token_from_jwt }
+ let(:token) { ci_job }
+
+ context 'with valid credentials' do
+ let(:raw) { username_and_password_from_jwt(token.token) }
+
+ it_behaves_like 'an authorized request'
+ end
+
+ context 'when the job is not running' do
+ let(:raw) { username_and_password_from_jwt(ci_job_done.token) }
+
+ it_behaves_like 'an unauthorized request'
+ end
+
+ context 'with an invalid job token' do
+ let(:raw) { username_and_password_from_jwt('not a valid CI job token') }
+
+ it_behaves_like 'an unauthorized request'
+ end
+ end
end
def username_and_password(username, password)
::Gitlab::APIAuthentication::TokenLocator::UsernameAndPassword.new(username, password)
end
+
+ def username_and_password_from_jwt(token)
+ username_and_password(nil, ::Gitlab::JWTToken.new.tap { |jwt| jwt['token'] = token }.encoded)
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
index 23cdec61bb3..499dc3554a3 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/limit/deployments_spec.rb
@@ -74,7 +74,7 @@ RSpec.describe ::Gitlab::Ci::Pipeline::Chain::Limit::Deployments do
it 'adds an informative error to the pipeline' do
perform
- expect(pipeline.errors.messages).to include(base: ['Pipeline has too many deployments! Requested 2, but the limit is 1.'])
+ expect(pipeline.errors.added?(:base, 'Pipeline has too many deployments! Requested 2, but the limit is 1.')).to be true
end
it 'increments the error metric' do
diff --git a/spec/lib/gitlab/jwt_token_spec.rb b/spec/lib/gitlab/jwt_token_spec.rb
new file mode 100644
index 00000000000..d89ca127393
--- /dev/null
+++ b/spec/lib/gitlab/jwt_token_spec.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::JWTToken do
+ it_behaves_like 'a gitlab jwt token'
+end
diff --git a/spec/lib/gitlab/terraform_registry_token_spec.rb b/spec/lib/gitlab/terraform_registry_token_spec.rb
new file mode 100644
index 00000000000..49c1c07e942
--- /dev/null
+++ b/spec/lib/gitlab/terraform_registry_token_spec.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+RSpec.describe Gitlab::TerraformRegistryToken do
+ let_it_be(:user) { create(:user) }
+
+ describe '.from_token' do
+ let(:jwt_token) { described_class.from_token(token) }
+
+ subject { described_class.decode(jwt_token.encoded) }
+
+ context 'with a deploy token' do
+ let(:deploy_token) { create(:deploy_token, username: 'deployer') }
+ let(:token) { deploy_token }
+
+ it 'returns the correct token' do
+ expect(subject['token']).to eq jwt_token['token']
+ end
+ end
+
+ context 'with a job' do
+ let_it_be(:job) { create(:ci_build) }
+
+ let(:token) { job }
+
+ it 'returns the correct token' do
+ expect(subject['token']).to eq jwt_token['token']
+ end
+ end
+
+ context 'with a personal access token' do
+ let(:token) { create(:personal_access_token) }
+
+ it 'returns the correct token' do
+ expect(subject['token']).to eq jwt_token['token']
+ end
+ end
+ end
+
+ it_behaves_like 'a gitlab jwt token'
+end
diff --git a/spec/models/concerns/chronic_duration_attribute_spec.rb b/spec/models/concerns/chronic_duration_attribute_spec.rb
index e6dbf403b63..00e28e19bd5 100644
--- a/spec/models/concerns/chronic_duration_attribute_spec.rb
+++ b/spec/models/concerns/chronic_duration_attribute_spec.rb
@@ -56,8 +56,7 @@ RSpec.shared_examples 'ChronicDurationAttribute writer' do
subject.send("#{virtual_field}=", '-10m')
expect(subject.valid?).to be_falsey
- expect(subject.errors&.messages)
- .to include(base: ['Maximum job timeout has a value which could not be accepted'])
+ expect(subject.errors.added?(:base, 'Maximum job timeout has a value which could not be accepted')).to be true
end
end
diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb
index e34934d393a..4a8b671bab7 100644
--- a/spec/models/custom_emoji_spec.rb
+++ b/spec/models/custom_emoji_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe CustomEmoji do
new_emoji = build(:custom_emoji, name: old_emoji.name, namespace: old_emoji.namespace, group: group)
expect(new_emoji).not_to be_valid
- expect(new_emoji.errors.messages).to include(name: ["has already been taken"])
+ expect(new_emoji.errors.messages).to eq(creator: ["can't be blank"], name: ["has already been taken"])
end
it 'disallows non http and https file value' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 05825a4f78c..f55599b3a79 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -579,7 +579,9 @@ RSpec.describe Group do
it "is false if avatar is html page" do
group.update_attribute(:avatar, 'uploads/avatar.html')
- expect(group.avatar_type).to eq(["file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp"])
+ group.avatar_type
+
+ expect(group.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 5b11a7bf079..021f8598975 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe MergeRequestDiff do
expect(subject.valid?).to be false
expect(subject.errors.count).to eq 3
- expect(subject.errors).to all(include('is not a valid SHA'))
+ expect(subject.errors.full_messages).to all(include('is not a valid SHA'))
end
it 'does not validate uniqueness by default' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 8c21b227ad3..dc1c418bea9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1276,7 +1276,9 @@ RSpec.describe Project, factory_default: :keep do
it 'is false if avatar is html page' do
project.update_attribute(:avatar, 'uploads/avatar.html')
- expect(project.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
+ project.avatar_type
+
+ expect(project.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 393fd53193b..ff2ee4adc1c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2526,8 +2526,9 @@ RSpec.describe User do
it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
+ user.avatar_type
- expect(user.avatar_type).to eq(['file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp'])
+ expect(user.errors.added?(:avatar, "file format is not supported. Please try one of the following supported formats: png, jpg, jpeg, gif, bmp, tiff, ico, webp")).to be true
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index fd42a84e405..88deeea49e5 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -1014,13 +1014,15 @@ RSpec.describe Issues::UpdateService, :mailer do
with_them do
it 'broadcasts to the issues channel based on ActionCable and feature flag values' do
- expect(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
+ allow(Gitlab::ActionCable::Config).to receive(:in_app?).and_return(action_cable_in_app_enabled)
stub_feature_flags(broadcast_issue_updates: feature_flag_enabled)
if should_broadcast
expect(IssuesChannel).to receive(:broadcast_to).with(issue, event: 'updated')
+ expect(GraphqlTriggers).to receive(:issuable_assignees_updated).with(issue)
else
expect(IssuesChannel).not_to receive(:broadcast_to)
+ expect(GraphqlTriggers).not_to receive(:issuable_assignees_updated).with(issue)
end
update_issue(update_params)
diff --git a/spec/services/notes/build_service_spec.rb b/spec/services/notes/build_service_spec.rb
index deeab66c4e9..b7b08390dcd 100644
--- a/spec/services/notes/build_service_spec.rb
+++ b/spec/services/notes/build_service_spec.rb
@@ -173,7 +173,7 @@ RSpec.describe Notes::BuildService do
let(:user) { create(:user) }
it 'returns `Discussion to reply to cannot be found` error' do
- expect(new_note.errors.first).to include("Discussion to reply to cannot be found")
+ expect(new_note.errors.added?(:base, "Discussion to reply to cannot be found")).to be true
end
end
end
diff --git a/spec/services/packages/debian/generate_distribution_key_service_spec.rb b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
new file mode 100644
index 00000000000..b31830c2d3b
--- /dev/null
+++ b/spec/services/packages/debian/generate_distribution_key_service_spec.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Packages::Debian::GenerateDistributionKeyService do
+ let_it_be(:user) { create(:user) }
+
+ let(:params) { {} }
+
+ subject { described_class.new(current_user: user, params: params) }
+
+ let(:response) { subject.execute }
+
+ context 'with a user' do
+ it 'returns an Hash', :aggregate_failures do
+ expect(GPGME::Ctx).to receive(:new).with(armor: true, offline: true).and_call_original
+ expect(User).to receive(:random_password).with(no_args).and_call_original
+
+ expect(response).to be_a Hash
+ expect(response.keys).to contain_exactly(:private_key, :public_key, :fingerprint, :passphrase)
+ expect(response[:private_key]).to start_with('-----BEGIN PGP PRIVATE KEY BLOCK-----')
+ expect(response[:public_key]).to start_with('-----BEGIN PGP PUBLIC KEY BLOCK-----')
+ expect(response[:fingerprint].length).to eq(40)
+ expect(response[:passphrase].length).to be > 10
+ end
+ end
+
+ context 'without a user' do
+ let(:user) { nil }
+
+ it 'raises an ArgumentError' do
+ expect { response }.to raise_error(ArgumentError, 'Please provide a user')
+ end
+ end
+end
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index d714f04fbba..ebf22987d50 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -142,9 +142,9 @@ module GraphqlHelpers
Class.new(::Types::BaseObject) { graphql_name name }
end
- def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema)
+ def resolver_instance(resolver_class, obj: nil, ctx: {}, field: nil, schema: GitlabSchema, subscription_update: false)
if ctx.is_a?(Hash)
- q = double('Query', schema: schema)
+ q = double('Query', schema: schema, subscription_update?: subscription_update)
ctx = GraphQL::Query::Context.new(query: q, object: obj, values: ctx)
end
diff --git a/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb
new file mode 100644
index 00000000000..5c92bb3b0d4
--- /dev/null
+++ b/spec/support/shared_examples/lib/gitlab/jwt_token_shared_examples.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples 'a gitlab jwt token' do
+ let_it_be(:base_secret) { SecureRandom.base64(64) }
+
+ let(:jwt_secret) do
+ OpenSSL::HMAC.hexdigest(
+ 'SHA256',
+ base_secret,
+ described_class::HMAC_KEY
+ )
+ end
+
+ before do
+ allow(Settings).to receive(:attr_encrypted_db_key_base).and_return(base_secret)
+ end
+
+ describe '#secret' do
+ subject { described_class.secret }
+
+ it { is_expected.to eq(jwt_secret) }
+ end
+
+ describe '#decode' do
+ let(:encoded_jwt_token) { jwt_token.encoded }
+
+ subject(:decoded_jwt_token) { described_class.decode(encoded_jwt_token) }
+
+ context 'with a custom payload' do
+ let(:personal_access_token) { create(:personal_access_token) }
+ let(:jwt_token) { described_class.new.tap { |jwt_token| jwt_token['token'] = personal_access_token.token } }
+
+ it 'returns the correct token' do
+ expect(decoded_jwt_token['token']).to eq jwt_token['token']
+ end
+
+ it 'returns nil and logs the exception after expiration' do
+ travel_to((described_class::HMAC_EXPIRES_IN + 1.minute).ago) do
+ encoded_jwt_token
+ end
+
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(instance_of(JWT::ExpiredSignature))
+
+ expect(decoded_jwt_token).to be_nil
+ end
+ end
+ end
+end
diff --git a/spec/validators/addressable_url_validator_spec.rb b/spec/validators/addressable_url_validator_spec.rb
index 394ffc7bbea..ec3ee9aa500 100644
--- a/spec/validators/addressable_url_validator_spec.rb
+++ b/spec/validators/addressable_url_validator_spec.rb
@@ -19,18 +19,20 @@ RSpec.describe AddressableUrlValidator do
it 'returns error when url is nil' do
expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
- expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
+ expect(badge.errors.added?(:link_url, validator.options.fetch(:message))).to be true
end
it 'returns error when url is empty' do
expect(validator.validate_each(badge, :link_url, '')).to be_falsey
- expect(badge.errors.first[1]).to eq validator.options.fetch(:message)
+ expect(badge.errors.added?(:link_url, validator.options.fetch(:message))).to be true
end
it 'does not allow urls with CR or LF characters' do
aggregate_failures do
urls_with_CRLF.each do |url|
- expect(validator.validate_each(badge, :link_url, url)[0]).to eq 'is blocked: URI is invalid'
+ validator.validate_each(badge, :link_url, url)
+
+ expect(badge.errors.added?(:link_url, 'is blocked: URI is invalid')).to be true
end
end
end
@@ -113,7 +115,7 @@ RSpec.describe AddressableUrlValidator do
it 'does block nil url with provided error message' do
expect(validator.validate_each(badge, :link_url, nil)).to be_falsey
- expect(badge.errors.first[1]).to eq message
+ expect(badge.errors.added?(:link_url, message)).to be true
end
end
@@ -126,7 +128,7 @@ RSpec.describe AddressableUrlValidator do
subject
- expect(badge.errors.first[1]).to eq 'is not allowed due to: Only allowed schemes are http, https'
+ expect(badge.errors.added?(:link_url, 'is not allowed due to: Only allowed schemes are http, https')).to be true
end
end
diff --git a/spec/validators/array_members_validator_spec.rb b/spec/validators/array_members_validator_spec.rb
index ff8f0da7651..c6960925487 100644
--- a/spec/validators/array_members_validator_spec.rb
+++ b/spec/validators/array_members_validator_spec.rb
@@ -49,7 +49,7 @@ RSpec.describe ArrayMembersValidator do
object = test_class.new(children: [])
expect(object.valid?).to be_falsey
- expect(object.errors.messages).to eql(children: ['should be an array of children objects'])
+ expect(object.errors.messages).to eq(children: ['should be an array of children objects'])
end
end
@@ -62,7 +62,7 @@ RSpec.describe ArrayMembersValidator do
object = test_class.new(children: [])
expect(object.valid?).to be_falsey
- expect(object.errors.messages).to eql(children: ['should be an array of test objects'])
+ expect(object.errors.messages).to eq(children: ['should be an array of test objects'])
end
end
end
diff --git a/spec/validators/devise_email_validator_spec.rb b/spec/validators/devise_email_validator_spec.rb
index 29a008f858a..64d11d4d963 100644
--- a/spec/validators/devise_email_validator_spec.rb
+++ b/spec/validators/devise_email_validator_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe DeviseEmailValidator do
subject
expect(user.errors).to be_present
- expect(user.errors.first[1]).to eq 'is invalid'
+ expect(user.errors.added?(:public_email)).to be true
end
it 'returns error when email is nil' do
@@ -40,7 +40,7 @@ RSpec.describe DeviseEmailValidator do
subject
expect(user.errors).to be_present
- expect(user.errors.first[1]).to eq 'is invalid'
+ expect(user.errors.added?(:public_email)).to be true
end
end
end
diff --git a/spec/validators/gitlab/utils/zoom_url_validator_spec.rb b/spec/validators/gitlab/utils/zoom_url_validator_spec.rb
index bc8236a2f5c..392d8b3a2fe 100644
--- a/spec/validators/gitlab/utils/zoom_url_validator_spec.rb
+++ b/spec/validators/gitlab/utils/zoom_url_validator_spec.rb
@@ -21,7 +21,7 @@ RSpec.describe Gitlab::Utils::ZoomUrlValidator do
expect(zoom_meeting.valid?).to eq(false)
expect(zoom_meeting.errors).to be_present
- expect(zoom_meeting.errors.first[1]).to eq 'must contain one valid Zoom URL'
+ expect(zoom_meeting.errors.added?(:url, 'must contain one valid Zoom URL')).to be true
end
end
diff --git a/spec/validators/qualified_domain_array_validator_spec.rb b/spec/validators/qualified_domain_array_validator_spec.rb
index 865ecffe05a..b2b13d358c4 100644
--- a/spec/validators/qualified_domain_array_validator_spec.rb
+++ b/spec/validators/qualified_domain_array_validator_spec.rb
@@ -52,7 +52,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
- expect(record.errors.first[1]).to eq('entries cannot be nil')
+ expect(record.errors.added?(:domain_array, "entries cannot be nil")).to be true
end
it 'allows when domain is valid' do
@@ -67,7 +67,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
- expect(record.errors.first[1]).to eq 'unicode domains should use IDNA encoding'
+ expect(record.errors.added?(:domain_array, 'unicode domains should use IDNA encoding')).to be true
end
it 'returns error when entry is larger than 255 chars' do
@@ -76,7 +76,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
- expect(record.errors.first[1]).to eq 'entries cannot be larger than 255 characters'
+ expect(record.errors.added?(:domain_array, 'entries cannot be larger than 255 characters')).to be true
end
it 'returns error when entry contains HTML tags' do
@@ -85,7 +85,7 @@ RSpec.describe QualifiedDomainArrayValidator do
subject
expect(record.errors).to be_present
- expect(record.errors.first[1]).to eq 'entries cannot contain HTML tags'
+ expect(record.errors.added?(:domain_array, 'entries cannot contain HTML tags')).to be true
end
end