Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2023-05-17 06:07:11 +00:00
parent 9bf8cb8d34
commit 75621c94b5
34 changed files with 653 additions and 113 deletions

View File

@ -20,6 +20,10 @@ reviewers and future readers. If you need help visually verifying the change,
please leave a comment and ping a GitLab reviewer, maintainer, or MR coach.
-->
| Before | After |
| ------ | ------ |
| | |
## How to set up and validate locally
_Numbered steps to set up and validate the change are strongly suggested._

View File

@ -1007,3 +1007,8 @@ SidekiqLoadBalancing/WorkerDataConsistency:
# This cop is disabled for Ruby 3.0+ anyway.
Lint/NonDeterministicRequireOrder:
Enabled: false
Graphql/ResourceNotAvailableError:
Exclude:
# Definition of `raise_resource_not_available_error!`
- 'lib/gitlab/graphql/authorize/authorize_resource.rb'

View File

@ -0,0 +1,42 @@
---
# Cop supports --autocorrect.
Graphql/ResourceNotAvailableError:
Details: grace period
Exclude:
- 'app/graphql/mutations/achievements/create.rb'
- 'app/graphql/mutations/admin/sidekiq_queues/delete_jobs.rb'
- 'app/graphql/mutations/ci/ci_cd_settings_update.rb'
- 'app/graphql/mutations/ci/job_artifact/bulk_destroy.rb'
- 'app/graphql/mutations/ci/runner/create.rb'
- 'app/graphql/mutations/custom_emoji/create.rb'
- 'app/graphql/mutations/custom_emoji/destroy.rb'
- 'app/graphql/mutations/design_management/move.rb'
- 'app/graphql/mutations/issues/bulk_update.rb'
- 'app/graphql/mutations/issues/set_crm_contacts.rb'
- 'app/graphql/mutations/issues/set_escalation_status.rb'
- 'app/graphql/mutations/notes/create/base.rb'
- 'app/graphql/mutations/notes/create/note.rb'
- 'app/graphql/mutations/notes/reposition_image_diff_note.rb'
- 'app/graphql/mutations/notes/update/image_diff_note.rb'
- 'app/graphql/mutations/saved_replies/create.rb'
- 'app/graphql/mutations/saved_replies/destroy.rb'
- 'app/graphql/mutations/saved_replies/update.rb'
- 'app/graphql/mutations/todos/mark_all_done.rb'
- 'app/graphql/mutations/work_items/export.rb'
- 'app/graphql/resolvers/ci/runner_setup_resolver.rb'
- 'app/graphql/resolvers/concerns/search_arguments.rb'
- 'app/graphql/resolvers/container_repository_tags_resolver.rb'
- 'app/graphql/resolvers/design_management/versions_resolver.rb'
- 'app/graphql/resolvers/kas/agent_configurations_resolver.rb'
- 'app/graphql/resolvers/kas/agent_connections_resolver.rb'
- 'app/graphql/resolvers/projects/snippets_resolver.rb'
- 'app/graphql/types/container_repository_details_type.rb'
- 'app/graphql/types/container_repository_type.rb'
- 'ee/app/graphql/ee/types/query_type.rb'
- 'ee/app/graphql/mutations/ai/action.rb'
- 'ee/app/graphql/mutations/audit_events/instance_external_audit_event_destinations/base.rb'
- 'ee/app/graphql/mutations/issues/set_escalation_policy.rb'
- 'ee/app/graphql/mutations/projects/set_locked.rb'
- 'ee/app/graphql/resolvers/incident_management/oncall_shifts_resolver.rb'
- 'ee/app/graphql/resolvers/product_analytics/visualization_resolver.rb'
- 'ee/app/graphql/resolvers/remote_development/workspaces_resolver.rb'

View File

@ -16,6 +16,21 @@ export function initScrollingTabs() {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
const el = $scrollingTabs.get(0);
const parentElement = el?.parentNode;
if (el && parentElement) {
parentElement
.querySelector('button.fade-left')
.addEventListener('click', function scrollLeft() {
el.scrollBy({ left: -200, behavior: 'smooth' });
});
parentElement
.querySelector('button.fade-right')
.addEventListener('click', function scrollRight() {
el.scrollBy({ left: 200, behavior: 'smooth' });
});
}
$(window)
.on('resize.nav', () => {
hideEndFade($scrollingTabs);

View File

@ -156,6 +156,12 @@
background: linear-gradient(to $gradient-direction,
$gradient-color 45%,
rgba($gradient-color, 0.4));
border: 0;
padding: 0;
&:hover {
@include gl-focus;
}
&.scrolling {
visibility: visible;
@ -164,8 +170,8 @@
}
svg {
position: relative;
top: 5px;
position: absolute;
top: 12px;
font-size: 18px;
}
}

View File

@ -29,6 +29,10 @@ class SentNotificationsController < ApplicationController
def unsubscribe_and_redirect
noteable.unsubscribe(@sent_notification.recipient, @sent_notification.project)
if noteable.is_a?(Issue) && @sent_notification.recipient_id == User.support_bot.id
noteable.unsubscribe_email_participant(noteable.external_author)
end
flash[:notice] = _("You have been unsubscribed from this thread.")
if current_user

View File

@ -758,6 +758,12 @@ class Issue < ApplicationRecord
end
end
def unsubscribe_email_participant(email)
return if email.blank?
issue_email_participants.find_by_email(email)&.destroy
end
private
def check_issue_type_in_sync!

View File

@ -26,7 +26,8 @@ module Ci
headers = JobArtifactUploader.workhorse_authorize(
has_length: false,
maximum_size: max_size(artifact_type),
use_final_store_path: Feature.enabled?(:ci_artifacts_upload_to_final_location, project)
use_final_store_path: Feature.enabled?(:ci_artifacts_upload_to_final_location, project),
final_store_path_root_id: project.id
)
if lsif?(artifact_type)

View File

@ -11,6 +11,7 @@ module ObjectStorage
RemoteStoreError = Class.new(StandardError)
UnknownStoreError = Class.new(StandardError)
ObjectStorageUnavailable = Class.new(StandardError)
MissingFinalStorePathRootId = Class.new(StandardError)
class ExclusiveLeaseTaken < StandardError
def initialize(lease_key)
@ -153,21 +154,30 @@ module ObjectStorage
[CarrierWave.generate_cache_id, SecureRandom.hex].join('-')
end
def generate_final_store_path
def generate_final_store_path(root_id:)
hash = Digest::SHA2.hexdigest(SecureRandom.uuid)
# We prefix '@final' to prevent clashes and make the files easily recognizable
# as having been created by this code.
File.join('@final', hash[0..1], hash[2..3], hash[4..])
sub_path = File.join('@final', hash[0..1], hash[2..3], hash[4..])
# We generate a hashed path of the root ID (e.g. Project ID) to distribute directories instead of
# filling up one root directory with a bunch of files.
Gitlab::HashedPath.new(sub_path, root_hash: root_id).to_s
end
def workhorse_authorize(has_length:, maximum_size: nil, use_final_store_path: false)
def workhorse_authorize(
has_length:,
maximum_size: nil,
use_final_store_path: false,
final_store_path_root_id: nil)
{}.tap do |hash|
if self.direct_upload_to_object_store?
hash[:RemoteObject] = workhorse_remote_upload_options(
has_length: has_length,
maximum_size: maximum_size,
use_final_store_path: use_final_store_path
use_final_store_path: use_final_store_path,
final_store_path_root_id: final_store_path_root_id
)
else
hash[:TempPath] = workhorse_local_upload_path
@ -190,11 +200,17 @@ module ObjectStorage
ObjectStorage::Config.new(object_store_options)
end
def workhorse_remote_upload_options(has_length:, maximum_size: nil, use_final_store_path: false)
def workhorse_remote_upload_options(
has_length:,
maximum_size: nil,
use_final_store_path: false,
final_store_path_root_id: nil)
return unless direct_upload_to_object_store?
if use_final_store_path
id = generate_final_store_path
raise MissingFinalStorePathRootId unless final_store_path_root_id.present?
id = generate_final_store_path(root_id: final_store_path_root_id)
upload_path = with_bucket_prefix(id)
prepare_pending_direct_upload(id)
else

View File

@ -10,8 +10,10 @@
- else
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope

View File

@ -4,8 +4,10 @@
.top-area.gl-flex-direction-column-reverse
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav nav gl-tabs-nav' }) do
= gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
= gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)

View File

@ -9,9 +9,9 @@
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
.fade-left
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
.fade-right
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full' }) do
= gl_tab_link_to admin_users_path, { item_active: active_when(params[:filter].nil?), class: 'gl-border-0!' } do

View File

@ -12,8 +12,10 @@
.top-area
.scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-flex-basis-0.gl-min-w-0
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
= render 'dashboard/projects_nav'
.nav-controls
= render 'shared/projects/search_form'

View File

@ -15,8 +15,10 @@
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
%ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do
@ -32,8 +34,10 @@
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
.merge-request-tabs-container.gl-display-flex.gl-justify-content-space-between
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
%ul.merge-request-tabs.nav.nav-tabs.nav-links.no-top.no-bottom.gl-display-flex.gl-flex-nowrap.gl-m-0.gl-p-0.js-tabs-affix
%li.commits-tab.new-tab
= link_to url_for(safe_params), data: {target: 'div#commits', action: 'new', toggle: 'tabvue'} do

View File

@ -1,8 +1,10 @@
- show_group_events = local_assigns.fetch(:show_group_events, false)
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller.flex-fill
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.event-filter.scrolling-tabs.nav.nav-tabs
= event_filter_link EventFilter::ALL, _('All'), s_('EventFilterBy|Filter by all')
- if event_filter_visible(:repository)

View File

@ -1,8 +1,10 @@
- show_project_name = local_assigns.fetch(:show_project_name, false)
.scrolling-tabs-container.inner-page-scroll-tabs.is-smaller
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
= gl_tabs_nav({ class: %w[scrolling-tabs js-milestone-tabs] }) do
= gl_tab_link_to '#tab-issues', item_active: true, data: { endpoint: milestone_tab_path(milestone, 'issues', show_project_name: show_project_name) } do
= _('Issues')

View File

@ -131,8 +131,10 @@
- if !profile_tabs.empty? && !Feature.enabled?(:profile_tabs_vue, current_user)
.scrolling-tabs-container{ class: [('gl-display-none' if show_super_sidebar?)] }
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
%button.fade-left{ type: 'button', title: _('Scroll left'), 'aria-label': _('Scroll left') }
= sprite_icon('chevron-lg-left', size: 12)
%button.fade-right{ type: 'button', title: _('Scroll right'), 'aria-label': _('Scroll right') }
= sprite_icon('chevron-lg-right', size: 12)
%ul.nav-links.user-profile-nav.scrolling-tabs.nav.nav-tabs.gl-border-b-0
- if profile_tab?(:overview)
%li.js-overview-tab

View File

@ -0,0 +1,34 @@
# frozen_string_literal: true
class AddUniqueNotesIdConvertToBigintForGitlabCom < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers::ConvertToBigint
disable_ddl_transaction!
TABLE_NAME = :notes
INDEX_NAME = :index_notes_on_id_convert_to_bigint
def up
return unless should_run?
# This was created async for GitLab.com with
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119913
# and will replace the existing PK index when we swap the integer and bigint columns in
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119705
add_concurrent_index TABLE_NAME, :id_convert_to_bigint,
unique: true,
name: INDEX_NAME
end
def down
return unless should_run?
remove_concurrent_index_by_name(TABLE_NAME, INDEX_NAME)
end
private
def should_run?
com_or_dev_or_test_but_not_jh?
end
end

View File

@ -0,0 +1,74 @@
# frozen_string_literal: true
class AddReferencingBigintFksForNotesOnGitlabCom < Gitlab::Database::Migration[2.1]
include Gitlab::Database::MigrationHelpers::ConvertToBigint
disable_ddl_transaction!
REFERENCING_FOREIGN_KEYS = [
[:todos, :fk_91d1f47b13, :note_id, :cascade],
[:incident_management_timeline_events, :fk_d606a2a890, :promoted_from_note_id, :nullify],
[:system_note_metadata, :fk_d83a918cb1, :note_id, :cascade],
[:diff_note_positions, :fk_rails_13c7212859, :note_id, :cascade],
[:epic_user_mentions, :fk_rails_1c65976a49, :note_id, :cascade],
[:suggestions, :fk_rails_33b03a535c, :note_id, :cascade],
[:issue_user_mentions, :fk_rails_3861d9fefa, :note_id, :cascade],
[:note_diff_files, :fk_rails_3d66047aeb, :diff_note_id, :cascade],
[:snippet_user_mentions, :fk_rails_4d3f96b2cb, :note_id, :cascade],
[:design_user_mentions, :fk_rails_8de8c6d632, :note_id, :cascade],
[:vulnerability_user_mentions, :fk_rails_a18600f210, :note_id, :cascade],
[:commit_user_mentions, :fk_rails_a6760813e0, :note_id, :cascade],
[:merge_request_user_mentions, :fk_rails_c440b9ea31, :note_id, :cascade],
[:note_metadata, :fk_rails_d853224d37, :note_id, :cascade],
[:alert_management_alert_user_mentions, :fk_rails_eb2de0cdef, :note_id, :cascade],
[:timelogs, :fk_timelogs_note_id, :note_id, :nullify]
]
def up
return unless should_run?
REFERENCING_FOREIGN_KEYS.each do |(from_table, name, column, on_delete)|
temporary_name = "#{name}_tmp"
# This will replace the existing FKs when
# we swap the integer and bigint columns in
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119705
add_concurrent_foreign_key(
from_table,
:notes,
column: column,
target_column: :id_convert_to_bigint,
name: temporary_name,
on_delete: on_delete,
reverse_lock_order: true,
validate: false)
prepare_async_foreign_key_validation from_table, column, name: temporary_name
end
end
def down
return unless should_run?
REFERENCING_FOREIGN_KEYS.each do |(from_table, name, column, _)|
temporary_name = "#{name}_tmp"
unprepare_async_foreign_key_validation from_table, column, name: temporary_name
with_lock_retries do
remove_foreign_key_if_exists(
from_table,
:notes,
name: temporary_name,
reverse_lock_order: true
)
end
end
end
private
def should_run?
com_or_dev_or_test_but_not_jh?
end
end

View File

@ -0,0 +1 @@
5f7e7d5b4af1a2e022e64ba2098c9e6be15853b2242334a41b4d53c5454201fe

View File

@ -0,0 +1 @@
ab3a2fa247fa1170aa38ec0471f136b479d715137138929a4d690f7b6022d022

View File

@ -31559,6 +31559,8 @@ CREATE INDEX index_notes_on_created_at ON notes USING btree (created_at);
CREATE INDEX index_notes_on_discussion_id ON notes USING btree (discussion_id);
CREATE UNIQUE INDEX index_notes_on_id_convert_to_bigint ON notes USING btree (id_convert_to_bigint);
CREATE INDEX index_notes_on_id_where_confidential ON notes USING btree (id) WHERE (confidential = true);
CREATE INDEX index_notes_on_id_where_internal ON notes USING btree (id) WHERE (internal = true);
@ -35156,6 +35158,9 @@ ALTER TABLE ONLY protected_tags
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_91d1f47b13_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY dast_site_profiles_builds
ADD CONSTRAINT fk_94e80df60e FOREIGN KEY (dast_site_profile_id) REFERENCES dast_site_profiles(id) ON DELETE CASCADE;
@ -35447,6 +35452,9 @@ ALTER TABLE ONLY ci_sources_pipelines
ALTER TABLE ONLY incident_management_timeline_events
ADD CONSTRAINT fk_d606a2a890 FOREIGN KEY (promoted_from_note_id) REFERENCES notes(id) ON DELETE SET NULL;
ALTER TABLE ONLY incident_management_timeline_events
ADD CONSTRAINT fk_d606a2a890_tmp FOREIGN KEY (promoted_from_note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE SET NULL NOT VALID;
ALTER TABLE ONLY lists
ADD CONSTRAINT fk_d6cf4279f7 FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;
@ -35468,6 +35476,9 @@ ALTER TABLE ONLY ci_pipelines
ALTER TABLE ONLY system_note_metadata
ADD CONSTRAINT fk_d83a918cb1 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY system_note_metadata
ADD CONSTRAINT fk_d83a918cb1_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY sbom_occurrences
ADD CONSTRAINT fk_d857c6edc1 FOREIGN KEY (component_id) REFERENCES sbom_components(id) ON DELETE CASCADE;
@ -35783,6 +35794,9 @@ ALTER TABLE ONLY bulk_imports
ALTER TABLE ONLY diff_note_positions
ADD CONSTRAINT fk_rails_13c7212859 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY diff_note_positions
ADD CONSTRAINT fk_rails_13c7212859_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY analytics_cycle_analytics_aggregations
ADD CONSTRAINT fk_rails_13c8374c7a FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -35846,6 +35860,9 @@ ALTER TABLE ONLY board_assignees
ALTER TABLE ONLY epic_user_mentions
ADD CONSTRAINT fk_rails_1c65976a49 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY epic_user_mentions
ADD CONSTRAINT fk_rails_1c65976a49_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY approver_groups
ADD CONSTRAINT fk_rails_1cdcbd7723 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -36005,6 +36022,9 @@ ALTER TABLE ONLY alert_management_alert_metric_images
ALTER TABLE ONLY suggestions
ADD CONSTRAINT fk_rails_33b03a535c FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY suggestions
ADD CONSTRAINT fk_rails_33b03a535c_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY requirements
ADD CONSTRAINT fk_rails_33fed8aa4e FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
@ -36035,6 +36055,9 @@ ALTER TABLE ONLY packages_debian_project_distribution_keys
ALTER TABLE ONLY issue_user_mentions
ADD CONSTRAINT fk_rails_3861d9fefa FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY issue_user_mentions
ADD CONSTRAINT fk_rails_3861d9fefa_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY namespace_settings
ADD CONSTRAINT fk_rails_3896d4fae5 FOREIGN KEY (namespace_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -36062,6 +36085,9 @@ ALTER TABLE ONLY cluster_groups
ALTER TABLE ONLY note_diff_files
ADD CONSTRAINT fk_rails_3d66047aeb FOREIGN KEY (diff_note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY note_diff_files
ADD CONSTRAINT fk_rails_3d66047aeb_tmp FOREIGN KEY (diff_note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY snippet_user_mentions
ADD CONSTRAINT fk_rails_3e00189191 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE;
@ -36167,6 +36193,9 @@ ALTER TABLE ONLY scim_identities
ALTER TABLE ONLY snippet_user_mentions
ADD CONSTRAINT fk_rails_4d3f96b2cb FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY snippet_user_mentions
ADD CONSTRAINT fk_rails_4d3f96b2cb_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY protected_environment_approval_rules
ADD CONSTRAINT fk_rails_4e554f96f5 FOREIGN KEY (protected_environment_id) REFERENCES protected_environments(id) ON DELETE CASCADE;
@ -36596,6 +36625,9 @@ ALTER TABLE ONLY approval_merge_request_rules_approved_approvers
ALTER TABLE ONLY design_user_mentions
ADD CONSTRAINT fk_rails_8de8c6d632 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY design_user_mentions
ADD CONSTRAINT fk_rails_8de8c6d632_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY clusters_kubernetes_namespaces
ADD CONSTRAINT fk_rails_8df789f3ab FOREIGN KEY (environment_id) REFERENCES environments(id) ON DELETE SET NULL;
@ -36725,6 +36757,9 @@ ALTER TABLE ONLY project_aliases
ALTER TABLE ONLY vulnerability_user_mentions
ADD CONSTRAINT fk_rails_a18600f210 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_user_mentions
ADD CONSTRAINT fk_rails_a18600f210_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY todos
ADD CONSTRAINT fk_rails_a27c483435 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
@ -36752,6 +36787,9 @@ ALTER TABLE ONLY cluster_projects
ALTER TABLE ONLY commit_user_mentions
ADD CONSTRAINT fk_rails_a6760813e0 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY commit_user_mentions
ADD CONSTRAINT fk_rails_a6760813e0_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY vulnerability_identifiers
ADD CONSTRAINT fk_rails_a67a16c885 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@ -36962,6 +37000,9 @@ ALTER TABLE ONLY project_wiki_repositories
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_request_user_mentions
ADD CONSTRAINT fk_rails_c440b9ea31_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY user_achievements
ADD CONSTRAINT fk_rails_c44f5b3b25 FOREIGN KEY (achievement_id) REFERENCES achievements(id) ON DELETE CASCADE;
@ -37079,6 +37120,9 @@ ALTER TABLE ONLY packages_rpm_metadata
ALTER TABLE ONLY note_metadata
ADD CONSTRAINT fk_rails_d853224d37 FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY note_metadata
ADD CONSTRAINT fk_rails_d853224d37_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY merge_request_reviewers
ADD CONSTRAINT fk_rails_d9fec24b9d FOREIGN KEY (merge_request_id) REFERENCES merge_requests(id) ON DELETE CASCADE;
@ -37220,6 +37264,9 @@ ALTER TABLE ONLY protected_branch_unprotect_access_levels
ALTER TABLE ONLY alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE CASCADE;
ALTER TABLE ONLY alert_management_alert_user_mentions
ADD CONSTRAINT fk_rails_eb2de0cdef_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE CASCADE NOT VALID;
ALTER TABLE ONLY snippet_statistics
ADD CONSTRAINT fk_rails_ebc283ccf1 FOREIGN KEY (snippet_id) REFERENCES snippets(id) ON DELETE CASCADE;
@ -37370,6 +37417,9 @@ ALTER TABLE ONLY timelogs
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_timelogs_note_id FOREIGN KEY (note_id) REFERENCES notes(id) ON DELETE SET NULL;
ALTER TABLE ONLY timelogs
ADD CONSTRAINT fk_timelogs_note_id_tmp FOREIGN KEY (note_id) REFERENCES notes(id_convert_to_bigint) ON DELETE SET NULL NOT VALID;
ALTER TABLE ONLY u2f_registrations
ADD CONSTRAINT fk_u2f_registrations_user_id FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE;

View File

@ -114,6 +114,17 @@ the assertion in the previous section.
After you configure the OIDC and role, the GitLab CI/CD job can retrieve a temporary credential from the
[Google Cloud Security Token Service (STS)](https://cloud.google.com/iam/docs/reference/sts/rest).
Add `id_tokens` to your CI/CD job:
```yaml
job:
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.example.com
```
Get temporary credentials using the ID token:
```shell
PAYLOAD="$(cat <<EOF
{
@ -122,7 +133,7 @@ PAYLOAD="$(cat <<EOF
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"subjectToken": "${CI_JOB_JWT_V2}"
"subjectToken": "${GITLAB_OIDC_TOKEN}"
}
EOF
)"
@ -142,8 +153,7 @@ Where:
- `PROJECT_NUMBER` is your Google Cloud project number (not name).
- `POOL_ID` is the ID of the Workload Identity Pool created in the first section.
- `PROVIDER_ID` is the ID of the Workload Identity Provider created in the second section.
- `CI_JOB_JWT_V2` is injected into the CI/CD job by GitLab. For more information about
this variable, read [Connect to cloud services](../index.md).
- `GITLAB_OIDC_TOKEN` is an OIDC [ID token](../../yaml/index.md#id_tokens).
You can then use the resulting federated token to impersonate the service account created
in the previous section:

View File

@ -46,12 +46,14 @@ You can use the following fuzzing engines to test the specified languages.
| Go | [go-fuzz (libFuzzer support)](https://github.com/dvyukov/go-fuzz) | [go-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/go-fuzzing-example) |
| Swift | [libFuzzer](https://github.com/apple/swift/blob/master/docs/libFuzzerIntegration.md) | [swift-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/swift-fuzzing-example) |
| Rust | [cargo-fuzz (libFuzzer support)](https://github.com/rust-fuzz/cargo-fuzz) | [rust-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/rust-fuzzing-example) |
| Java | [Javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) |
| Java (Maven only)<sup>1</sup> | [Javafuzz](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/javafuzz) (recommended) | [javafuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/javafuzz-fuzzing-example) |
| Java | [JQF](https://github.com/rohanpadhye/JQF) (not preferred) | [jqf-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/java-fuzzing-example) |
| JavaScript | [`jsfuzz`](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/jsfuzz) | [jsfuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/jsfuzz-fuzzing-example) |
| Python | [`pythonfuzz`](https://gitlab.com/gitlab-org/security-products/analyzers/fuzzers/pythonfuzz) | [pythonfuzz-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/pythonfuzz-fuzzing-example) |
| AFL (any language that works on top of AFL) | [AFL](https://lcamtuf.coredump.cx/afl/) | [afl-fuzzing-example](https://gitlab.com/gitlab-org/security-products/demos/coverage-fuzzing/afl-fuzzing-example) |
1. Support for Gradle is planned in [issue 409764](https://gitlab.com/gitlab-org/gitlab/-/issues/409764).
## Confirm status of coverage-guided fuzz testing
To confirm the status of coverage-guided fuzz testing:

View File

@ -11098,9 +11098,6 @@ msgstr ""
msgid "Committed-before"
msgstr ""
msgid "CommonJS module"
msgstr ""
msgid "Community forum"
msgstr ""
@ -16185,9 +16182,6 @@ msgstr ""
msgid "E-mail:"
msgstr ""
msgid "ESM module"
msgstr ""
msgid "Each project can also have an issue tracker and a wiki."
msgstr ""
@ -21779,9 +21773,6 @@ msgstr ""
msgid "HAR file path or URL"
msgstr ""
msgid "HTML script tag"
msgstr ""
msgid "HTTP Archive (HAR)"
msgstr ""
@ -34090,15 +34081,27 @@ msgstr ""
msgid "Product analytics"
msgstr ""
msgid "ProductAnalytics|1. Add the NPM package to your package.json using your preferred package manager"
msgstr ""
msgid "ProductAnalytics|2. Import the new package into your JS code"
msgstr ""
msgid "ProductAnalytics|3. Initiate the tracking"
msgstr ""
msgid "ProductAnalytics|Add another dimension"
msgstr ""
msgid "ProductAnalytics|Add the NPM package to your package.json using your preferred package manager:"
msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window"
msgstr ""
msgid "ProductAnalytics|Add the script to the page and assign the client SDK to window:"
msgstr ""
msgid "ProductAnalytics|After your application has been instrumented and data is being collected, you can visualize and monitor behaviors in your %{linkStart}analytics dashboards%{linkEnd}."
msgstr ""
msgid "ProductAnalytics|All Clicks Compared"
msgstr ""
@ -34198,12 +34201,6 @@ msgstr ""
msgid "ProductAnalytics|How often sessions are repeated"
msgstr ""
msgid "ProductAnalytics|Identifies the sender of tracking events"
msgstr ""
msgid "ProductAnalytics|Import the new package into your JS code:"
msgstr ""
msgid "ProductAnalytics|Instrument your application"
msgstr ""
@ -34234,10 +34231,13 @@ msgstr ""
msgid "ProductAnalytics|Repeat Visit Percentage"
msgstr ""
msgid "ProductAnalytics|SDK App ID"
msgid "ProductAnalytics|SDK application ID"
msgstr ""
msgid "ProductAnalytics|SDK Host"
msgid "ProductAnalytics|SDK clients"
msgstr ""
msgid "ProductAnalytics|SDK host"
msgstr ""
msgid "ProductAnalytics|Sessions"
@ -34252,18 +34252,12 @@ msgstr ""
msgid "ProductAnalytics|Set up to track how your product is performing and optimize your product and development processes."
msgstr ""
msgid "ProductAnalytics|Steps to add product analytics as a CommonJS module"
msgstr ""
msgid "ProductAnalytics|Steps to add product analytics as a HTML script tag"
msgstr ""
msgid "ProductAnalytics|Steps to add product analytics as an ESM module"
msgstr ""
msgid "ProductAnalytics|The host to send all tracking events to"
msgstr ""
msgid "ProductAnalytics|The sender of tracking events"
msgstr ""
msgid "ProductAnalytics|There is no data for this type of chart currently. Please see the Setup tab if you have not configured the product analytics tool already."
msgstr ""
@ -34288,12 +34282,18 @@ msgstr ""
msgid "ProductAnalytics|Users"
msgstr ""
msgid "ProductAnalytics|Using JS module"
msgstr ""
msgid "ProductAnalytics|Waiting for events"
msgstr ""
msgid "ProductAnalytics|What do you want to measure?"
msgstr ""
msgid "ProductAnalytics|You can instrument your application using a JS module or an HTML script. Follow the instructions below for the option you prefer."
msgstr ""
msgid "Productivity"
msgstr ""
@ -35314,10 +35314,10 @@ msgstr ""
msgid "ProjectSettings|Configure analytics features for this project"
msgstr ""
msgid "ProjectSettings|Configure product analytics to track events within your project applications."
msgid "ProjectSettings|Configure your infrastructure."
msgstr ""
msgid "ProjectSettings|Configure your infrastructure."
msgid "ProjectSettings|Connect to your instance"
msgstr ""
msgid "ProjectSettings|Contact an admin to change this setting."
@ -35419,6 +35419,9 @@ msgstr ""
msgid "ProjectSettings|Infrastructure"
msgstr ""
msgid "ProjectSettings|Instrument your application to track events and behaviors."
msgstr ""
msgid "ProjectSettings|Internal"
msgstr ""
@ -35503,6 +35506,9 @@ msgstr ""
msgid "ProjectSettings|Only signed commits can be pushed to this repository."
msgstr ""
msgid "ProjectSettings|Override instance analytics configuration for this project"
msgstr ""
msgid "ProjectSettings|Override user notification preferences for all project members."
msgstr ""
@ -35521,6 +35527,9 @@ msgstr ""
msgid "ProjectSettings|Private"
msgstr ""
msgid "ProjectSettings|Product analytics needs to be set up before your application can be instrumented. Follow the %{link_start}set up process%{link_end}."
msgstr ""
msgid "ProjectSettings|Project visibility"
msgstr ""
@ -49054,6 +49063,9 @@ msgstr ""
msgid "Using %{code_start}::%{code_end} denotes a %{link_start}scoped label set%{link_end}"
msgstr ""
msgid "Using HTML script"
msgstr ""
msgid "Using required encryption strategy when encrypted field is missing!"
msgstr ""

View File

@ -0,0 +1,47 @@
# frozen_string_literal: true
require_relative '../../node_pattern_helper'
module RuboCop
module Cop
module Graphql
# Encourages the use of `raise_resource_not_available_error!` method
# instead of `raise Gitlab::Graphql::Errors::ResourceNotAvailable`.
#
# @example
#
# # bad
# raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'message'
#
# # good
# raise_resource_not_available_error! 'message'
class ResourceNotAvailableError < Base
extend NodePatternHelper
extend AutoCorrector
MSG = 'Prefer using `raise_resource_not_available_error!` instead.'
EXCEPTION = 'Gitlab::Graphql::Errors::ResourceNotAvailable'
RESTRICT_ON_SEND = %i[raise].freeze
def_node_matcher :error, const_pattern(EXCEPTION)
def_node_matcher :raise_error, <<~PATTERN
(send nil? :raise #error $...)
PATTERN
def on_send(node)
raise_error(node) do |args|
add_offense(node) do |corrector|
replacement = +'raise_resource_not_available_error!'
replacement << " #{args.map(&:source).join(', ')}" if args.any?
corrector.replace(node, replacement)
end
end
end
end
end
end
end

View File

@ -0,0 +1,14 @@
# frozen_string_literal: true
module RuboCop
module NodePatternHelper
# Returns a nested `(const ...)` node pattern for a full qualified +name+.
#
# @examples
# const_pattern 'Foo::Bar' # => (const (const {nil? cbase} :Foo) :Bar)
# const_pattern 'Foo::Bar', parent: ':Baz' # => (const (const :Baz :Foo) :Bar)
def const_pattern(name, parent: '{nil? cbase}')
name.split('::').inject(parent) { |memo, name_part| "(const #{memo} :#{name_part})" }
end
end
end

View File

@ -7,10 +7,12 @@ RSpec.describe SentNotificationsController do
let(:project) { create(:project, :public) }
let(:private_project) { create(:project, :private) }
let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: user) }
let(:email) { 'email@example.com' }
let(:issue) do
create(:issue, project: target_project) do |issue|
create(:issue, project: target_project, external_author: email) do |issue|
issue.subscriptions.create!(user: user, project: target_project, subscribed: true)
issue.issue_email_participants.create!(email: email)
end
end
@ -29,6 +31,14 @@ RSpec.describe SentNotificationsController do
let(:noteable) { issue }
let(:target_project) { project }
def force_unsubscribe
get(:unsubscribe, params: { id: sent_notification.reply_key, force: true })
end
def unsubscribe
get(:unsubscribe, params: { id: sent_notification.reply_key })
end
describe 'GET unsubscribe' do
shared_examples 'returns 404' do
it 'does not set the flash message' do
@ -43,13 +53,17 @@ RSpec.describe SentNotificationsController do
context 'when the user is not logged in' do
context 'when the force param is passed' do
before do
get(:unsubscribe, params: { id: sent_notification.reply_key, force: true })
force_unsubscribe
end
it 'unsubscribes the user' do
expect(issue.subscribed?(user, project)).to be_falsey
end
it 'does not delete the issue email participant for non-service-desk issue' do
expect { force_unsubscribe }.not_to change { issue.issue_email_participants.count }
end
it 'sets the flash message' do
expect(controller).to set_flash[:notice].to(/unsubscribed/)
end
@ -63,7 +77,7 @@ RSpec.describe SentNotificationsController do
render_views
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
shared_examples 'unsubscribing as anonymous' do |project_visibility|
@ -101,6 +115,10 @@ RSpec.describe SentNotificationsController do
expect(response.body).to include(issue.title)
end
it 'does not delete the issue email participant' do
expect { unsubscribe }.not_to change { issue.issue_email_participants.count }
end
it_behaves_like 'unsubscribing as anonymous', :public
end
@ -171,7 +189,7 @@ RSpec.describe SentNotificationsController do
before do
sent_notification.noteable.destroy!
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
it_behaves_like 'returns 404'
@ -193,7 +211,7 @@ RSpec.describe SentNotificationsController do
context 'when the force param is passed' do
before do
get(:unsubscribe, params: { id: sent_notification.reply_key, force: true })
force_unsubscribe
end
it 'unsubscribes the user' do
@ -220,7 +238,7 @@ RSpec.describe SentNotificationsController do
let(:sent_notification) { create(:sent_notification, project: project, noteable: merge_request, recipient: user) }
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
it 'unsubscribes the user' do
@ -243,7 +261,7 @@ RSpec.describe SentNotificationsController do
let(:target_project) { private_project }
before do
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
it 'unsubscribes user and redirects to root path' do
@ -257,12 +275,16 @@ RSpec.describe SentNotificationsController do
before do
private_project.add_developer(user)
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
it 'unsubscribes user and redirects to issue path' do
expect(response).to redirect_to(project_issue_path(private_project, issue))
end
it 'does not delete the issue email participant for non-service-desk issue' do
expect { unsubscribe }.not_to change { issue.issue_email_participants.count }
end
end
end
@ -270,11 +292,27 @@ RSpec.describe SentNotificationsController do
before do
sent_notification.noteable.destroy!
get(:unsubscribe, params: { id: sent_notification.reply_key })
unsubscribe
end
it_behaves_like 'returns 404'
end
context 'when support bot is the notification recipient' do
let(:sent_notification) { create(:sent_notification, project: target_project, noteable: noteable, recipient: User.support_bot) }
it 'deletes the external author on the issue' do
expect { unsubscribe }.to change { issue.issue_email_participants.count }.by(-1)
end
context 'when noteable is not an issue' do
let(:noteable) { merge_request }
it 'does not delete the external author on the issue' do
expect { unsubscribe }.not_to change { issue.issue_email_participants.count }
end
end
end
end
end
end

View File

@ -0,0 +1,39 @@
import { initScrollingTabs } from '~/layout_nav';
import { setHTMLFixture } from './__helpers__/fixtures';
describe('initScrollingTabs', () => {
const htmlFixture = `
<button type='button' class='fade-left'></button>
<button type='button' class='fade-right'></button>
<div class='scrolling-tabs'></div>
`;
const findTabs = () => document.querySelector('.scrolling-tabs');
const findScrollLeftButton = () => document.querySelector('button.fade-left');
const findScrollRightButton = () => document.querySelector('button.fade-right');
beforeEach(() => {
setHTMLFixture(htmlFixture);
});
it('scrolls left when clicking on the left button', () => {
initScrollingTabs();
const tabs = findTabs();
tabs.scrollBy = jest.fn();
const fadeLeft = findScrollLeftButton();
fadeLeft.click();
expect(tabs.scrollBy).toHaveBeenCalledWith({ left: -200, behavior: 'smooth' });
});
it('scrolls right when clicking on the right button', () => {
initScrollingTabs();
const tabs = findTabs();
tabs.scrollBy = jest.fn();
const fadeRight = findScrollRightButton();
fadeRight.click();
expect(tabs.scrollBy).toHaveBeenCalledWith({ left: 200, behavior: 'smooth' });
});
});

View File

@ -2002,6 +2002,30 @@ RSpec.describe Issue, feature_category: :team_planning do
it { is_expected.to eq(WorkItems::Type.default_by_type(::Issue::DEFAULT_ISSUE_TYPE)) }
end
describe '#unsubscribe_email_participant' do
let_it_be(:email) { 'email@example.com' }
let_it_be(:issue1) do
create(:issue, project: reusable_project, external_author: email) do |issue|
issue.issue_email_participants.create!(email: email)
end
end
let_it_be(:issue2) do
create(:issue, project: reusable_project, external_author: email) do |issue|
issue.issue_email_participants.create!(email: email)
end
end
it 'deletes email for issue1' do
expect { issue1.unsubscribe_email_participant(email) }.to change { issue1.issue_email_participants.count }.by(-1)
end
it 'does not delete email for issue2 when issue1 is used' do
expect { issue1.unsubscribe_email_participant(email) }.not_to change { issue2.issue_email_participants.count }
end
end
describe 'issue_type enum generated methods' do
using RSpec::Parameterized::TableSyntax

View File

@ -0,0 +1,37 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
require_relative '../../../../rubocop/cop/graphql/resource_not_available_error'
RSpec.describe RuboCop::Cop::Graphql::ResourceNotAvailableError, feature_category: :shared do
shared_examples 'flagging and auto-correction' do |exception|
it "flags and auto-corrects `raise #{exception}`" do
expect_offense(<<~'RUBY', exception: exception)
raise %{exception}
^^^^^^^{exception} Prefer using `raise_resource_not_available_error!` instead.
raise %{exception}, 'message ' \
^^^^^^^{exception}^^^^^^^^^^^^^^ Prefer using `raise_resource_not_available_error!` instead.
'with new lines'
RUBY
expect_correction(<<~'RUBY')
raise_resource_not_available_error!
raise_resource_not_available_error! 'message ' \
'with new lines'
RUBY
end
end
it_behaves_like 'flagging and auto-correction', 'Gitlab::Graphql::Errors::ResourceNotAvailable'
it_behaves_like 'flagging and auto-correction', '::Gitlab::Graphql::Errors::ResourceNotAvailable'
it 'does not flag unrelated exceptions' do
expect_no_offenses(<<~RUBY)
raise Gitlab::Graphql::Errors::ResourceVeryAvailable
raise ::Gitlab::Graphql::Errors::ResourceVeryAvailable
RUBY
end
end

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
require 'rubocop_spec_helper'
require_relative '../../rubocop/node_pattern_helper'
RSpec.describe RuboCop::NodePatternHelper, feature_category: :tooling do
include described_class
describe '#const_pattern' do
it 'returns nested const node patterns' do
expect(const_pattern('Foo')).to eq('(const {nil? cbase} :Foo)')
expect(const_pattern('Foo::Bar')).to eq('(const (const {nil? cbase} :Foo) :Bar)')
end
it 'returns nested const node patterns with custom parent' do
expect(const_pattern('Foo::Bar', parent: 'nil?')).to eq('(const (const nil? :Foo) :Bar)')
end
end
end

View File

@ -82,7 +82,11 @@ RSpec.describe Ci::JobArtifacts::CreateService, :clean_gitlab_redis_shared_state
before do
stub_artifacts_object_storage(JobArtifactUploader, direct_upload: true)
allow(JobArtifactUploader).to receive(:generate_final_store_path).and_return(final_store_path)
allow(JobArtifactUploader)
.to receive(:generate_final_store_path)
.with(root_id: project.id)
.and_return(final_store_path)
end
it 'includes the authorize headers' do

View File

@ -525,12 +525,14 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
let(:has_length) { true }
let(:maximum_size) { nil }
let(:use_final_store_path) { false }
let(:final_store_path_root_id) { nil }
subject do
uploader_class.workhorse_authorize(
has_length: has_length,
maximum_size: maximum_size,
use_final_store_path: use_final_store_path
use_final_store_path: use_final_store_path,
final_store_path_root_id: final_store_path_root_id
)
end
@ -615,51 +617,30 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
shared_examples 'handling object storage final upload path' do |multipart|
context 'when use_final_store_path is true' do
let(:use_final_store_path) { true }
let(:final_store_path) { File.join('@final', 'abc', '123', 'somefilename') }
let(:final_store_path_root_id) { 12345 }
let(:final_store_path) { File.join('@final', 'myprefix', 'abc', '123', 'somefilename') }
let(:escaped_path) { escape_path(final_store_path) }
before do
stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{final_store_path}") if multipart
context 'and final_store_path_root_id was not given' do
let(:final_store_path_root_id) { nil }
allow(uploader_class).to receive(:generate_final_store_path).and_return(final_store_path)
end
it 'uses the full path instead of the temporary one' do
expect(subject[:RemoteObject][:ID]).to eq(final_store_path)
expect(subject[:RemoteObject][:GetURL]).to include(escaped_path)
expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path)
if multipart
expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path))
expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path)
expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path)
it 'raises an error' do
expect { subject }.to raise_error(ObjectStorage::MissingFinalStorePathRootId)
end
expect(subject[:RemoteObject][:SkipDelete]).to eq(true)
expect(
ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path)
).to eq(true)
end
context 'and bucket prefix is configured' do
let(:prefixed_final_store_path) { "my/prefix/#{final_store_path}" }
let(:escaped_path) { escape_path(prefixed_final_store_path) }
context 'and final_store_path_root_id was given' do
before do
allow(uploader_class.object_store_options).to receive(:bucket_prefix).and_return('my/prefix')
stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{final_store_path}") if multipart
if multipart
stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{prefixed_final_store_path}")
end
allow(uploader_class).to receive(:generate_final_store_path)
.with(root_id: final_store_path_root_id)
.and_return(final_store_path)
end
it 'sets the remote object ID to the final path without prefix' do
it 'uses the full path instead of the temporary one' do
expect(subject[:RemoteObject][:ID]).to eq(final_store_path)
end
it 'returns the final path with prefix' do
expect(subject[:RemoteObject][:GetURL]).to include(escaped_path)
expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path)
@ -668,15 +649,49 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path)
expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path)
end
end
it 'creates the pending upload entry without the prefix' do
is_expected.to have_key(:RemoteObject)
expect(subject[:RemoteObject][:SkipDelete]).to eq(true)
expect(
ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path)
).to eq(true)
end
context 'and bucket prefix is configured' do
let(:prefixed_final_store_path) { "my/prefix/#{final_store_path}" }
let(:escaped_path) { escape_path(prefixed_final_store_path) }
before do
allow(uploader_class.object_store_options).to receive(:bucket_prefix).and_return('my/prefix')
if multipart
stub_object_storage_multipart_init_with_final_store_path("#{storage_url}#{prefixed_final_store_path}")
end
end
it 'sets the remote object ID to the final path without prefix' do
expect(subject[:RemoteObject][:ID]).to eq(final_store_path)
end
it 'returns the final path with prefix' do
expect(subject[:RemoteObject][:GetURL]).to include(escaped_path)
expect(subject[:RemoteObject][:StoreURL]).to include(escaped_path)
if multipart
expect(subject[:RemoteObject][:MultipartUpload][:PartURLs]).to all(include(escaped_path))
expect(subject[:RemoteObject][:MultipartUpload][:CompleteURL]).to include(escaped_path)
expect(subject[:RemoteObject][:MultipartUpload][:AbortURL]).to include(escaped_path)
end
end
it 'creates the pending upload entry without the bucket prefix' do
is_expected.to have_key(:RemoteObject)
expect(
ObjectStorage::PendingDirectUpload.exists?(uploader_class.storage_location_identifier, final_store_path)
).to eq(true)
end
end
end
end
@ -716,7 +731,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
before do
expect_next_instance_of(ObjectStorage::Config) do |instance|
allow_next_instance_of(ObjectStorage::Config) do |instance|
allow(instance).to receive(:credentials).and_return(credentials)
end
end
@ -767,7 +782,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
before do
expect_next_instance_of(ObjectStorage::Config) do |instance|
allow_next_instance_of(ObjectStorage::Config) do |instance|
allow(instance).to receive(:credentials).and_return(credentials)
end
end
@ -812,7 +827,7 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
before do
expect_next_instance_of(ObjectStorage::Config) do |instance|
allow_next_instance_of(ObjectStorage::Config) do |instance|
allow(instance).to receive(:credentials).and_return(credentials)
end
end
@ -1184,14 +1199,17 @@ RSpec.describe ObjectStorage, :clean_gitlab_redis_shared_state, feature_category
end
describe '.generate_final_store_path' do
subject(:final_path) { uploader_class.generate_final_store_path }
let(:root_id) { 12345 }
let(:expected_root_hashed_path) { Gitlab::HashedPath.new(root_hash: root_id) }
subject(:final_path) { uploader_class.generate_final_store_path(root_id: root_id) }
before do
allow(Digest::SHA2).to receive(:hexdigest).and_return('somehash1234')
end
it 'returns the generated hashed path' do
expect(final_path).to eq('@final/so/me/hash1234')
it 'returns the generated hashed path nested under the hashed path of the root ID' do
expect(final_path).to eq(File.join(expected_root_hashed_path, '@final/so/me/hash1234'))
end
end