diff --git a/.gitlab/ci/qa-common/variables.gitlab-ci.yml b/.gitlab/ci/qa-common/variables.gitlab-ci.yml index 2fc9aaf3b62..024405aeb42 100644 --- a/.gitlab/ci/qa-common/variables.gitlab-ci.yml +++ b/.gitlab/ci/qa-common/variables.gitlab-ci.yml @@ -18,7 +18,7 @@ variables: # Helm chart ref used by test-on-cng pipeline GITLAB_HELM_CHART_REF: "6cdb0e1cd4ceb7c9fd01ffa2f62c4a7a4c77a23b" # Specific ref for cng-mirror project to trigger builds for - GITLAB_CNG_MIRROR_REF: "00736d96dbee30eaf6fa3701f0cfa99ad8621cf4" + GITLAB_CNG_MIRROR_REF: "272b2069fc386348d2474eac26060e781f2a0dd2" # Makes sure some of the common scripts from pipeline-common use bundler to execute commands RUN_WITH_BUNDLE: "true" # Makes sure reporting script defined in .gitlab-qa-report from pipeline-common is executed from correct folder diff --git a/.rubocop_todo/layout/line_length.yml b/.rubocop_todo/layout/line_length.yml index f412445b7b2..300f4ecda3b 100644 --- a/.rubocop_todo/layout/line_length.yml +++ b/.rubocop_todo/layout/line_length.yml @@ -1760,7 +1760,6 @@ Layout/LineLength: - 'ee/spec/support/shared_examples/models/protected_environments/authorizable_examples.rb' - 'ee/spec/support/shared_examples/policies/protected_environments_shared_examples.rb' - 'ee/spec/support/shared_examples/quick_actions/issuable/unassign_shared_examples.rb' - - 'ee/spec/support/shared_examples/quick_actions/issue/page_quick_action_shared_examples.rb' - 'ee/spec/support/shared_examples/quick_actions/merge_request/assign_reviewer_shared_examples.rb' - 'ee/spec/support/shared_examples/quick_actions/merge_request/unassign_reviewer_shared_examples.rb' - 'ee/spec/support/shared_examples/requests/api/graphql/geo/registries_shared_examples.rb' diff --git a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml index c4ea8ebb2d1..8d030a54e58 100644 --- a/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml +++ b/.rubocop_todo/lint/no_return_in_begin_end_blocks.yml @@ -2,12 +2,10 @@ Lint/NoReturnInBeginEndBlocks: Exclude: - 'app/models/concerns/metric_image_uploading.rb' - - 'app/models/merge_request.rb' - 'app/services/security/ci_configuration/sast_parser_service.rb' - 'ee/app/services/gitlab_subscriptions/preview_billable_user_change_service.rb' - 'ee/app/services/security/token_revocation_service.rb' - 'ee/lib/api/vulnerability_findings.rb' - 'ee/lib/ee/gitlab/scim/filter_parser.rb' - - 'lib/gitlab/git/repository.rb' - 'lib/object_storage/config.rb' - 'spec/support/database/prevent_cross_joins.rb' diff --git a/.rubocop_todo/rspec/before_all_role_assignment.yml b/.rubocop_todo/rspec/before_all_role_assignment.yml index 1d182a943eb..751ea80acd2 100644 --- a/.rubocop_todo/rspec/before_all_role_assignment.yml +++ b/.rubocop_todo/rspec/before_all_role_assignment.yml @@ -99,7 +99,6 @@ RSpec/BeforeAllRoleAssignment: - 'ee/spec/features/issues/list/user_sees_empty_state_spec.rb' - 'ee/spec/features/issues/new/user_creates_issue_spec.rb' - 'ee/spec/features/issues/related_issues_spec.rb' - - 'ee/spec/features/issues/user_uses_quick_actions_spec.rb' - 'ee/spec/features/merge_request/user_creates_merge_request_spec.rb' - 'ee/spec/features/merge_request/user_edits_approval_rules_mr_spec.rb' - 'ee/spec/features/merge_request/user_sees_mr_approvals_promo_spec.rb' @@ -706,7 +705,6 @@ RSpec/BeforeAllRoleAssignment: - 'spec/features/integrations_settings_spec.rb' - 'spec/features/invites_spec.rb' - 'spec/features/issues/email_participants_spec.rb' - - 'spec/features/issues/incident_issue_spec.rb' - 'spec/features/issues/issue_detail_spec.rb' - 'spec/features/issues/issue_header_spec.rb' - 'spec/features/issues/issue_sidebar_spec.rb' diff --git a/.rubocop_todo/rspec/context_wording.yml b/.rubocop_todo/rspec/context_wording.yml index cfeded17d92..9747f4c534f 100644 --- a/.rubocop_todo/rspec/context_wording.yml +++ b/.rubocop_todo/rspec/context_wording.yml @@ -742,7 +742,6 @@ RSpec/ContextWording: - 'ee/spec/support/shared_examples/models/member_shared_examples.rb' - 'ee/spec/support/shared_examples/models/mentionable_shared_examples.rb' - 'ee/spec/support/shared_examples/models/protected_environments/authorizable_examples.rb' - - 'ee/spec/support/shared_examples/quick_actions/issue/status_page_quick_actions_shared_examples.rb' - 'ee/spec/support/shared_examples/requests/credentials_inventory_shared_examples.rb' - 'ee/spec/support/shared_examples/services/boards/base_service_shared_examples.rb' - 'ee/spec/support/shared_examples/services/merge_merge_requests_shared_examples.rb' diff --git a/GITLAB_OPENBAO_VERSION b/GITLAB_OPENBAO_VERSION index 16078219779..aee76c7c5d5 100644 --- a/GITLAB_OPENBAO_VERSION +++ b/GITLAB_OPENBAO_VERSION @@ -1 +1 @@ -133afd922358b56c9c342703f33c3b01f5160737 \ No newline at end of file +133afd922358b56c9c342703f33c3b01f5160737 diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index 3da7a360a94..6504e7bb7b9 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -e7ca9463ccd7a3a3001d3d7d3ac21408b9632f6e +cdde52b3d3de97798d8043feb556476c6623ac9b diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone.vue new file mode 100644 index 00000000000..b974e6bb0eb --- /dev/null +++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone.vue @@ -0,0 +1,155 @@ + + + diff --git a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue index c5af25e7a7c..807c9e246f8 100644 --- a/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue +++ b/app/assets/javascripts/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue @@ -13,6 +13,8 @@ import WorkItemBulkEditAssignee from './work_item_bulk_edit_assignee.vue'; import WorkItemBulkEditDropdown from './work_item_bulk_edit_dropdown.vue'; import WorkItemBulkEditLabels from './work_item_bulk_edit_labels.vue'; +import WorkItemBulkEditMilestone from './work_item_bulk_edit_milestone.vue'; + const WorkItemBulkEditIteration = () => import('ee_component/work_items/components/list/work_item_bulk_edit_iteration.vue'); @@ -41,6 +43,7 @@ export default { WorkItemBulkEditDropdown, WorkItemBulkEditLabels, WorkItemBulkEditIteration, + WorkItemBulkEditMilestone, }, mixins: [glFeatureFlagsMixin()], inject: ['hasIssuableHealthStatusFeature', 'hasIterationsFeature'], @@ -75,6 +78,7 @@ export default { state: undefined, subscription: undefined, iteration: undefined, + milestone: undefined, }; }, apollo: { @@ -161,6 +165,9 @@ export default { } : undefined, iterationWidget: this.iteration ? { iterationId: this.iteration } : undefined, + milestoneWidget: this.milestone ? { milestoneId: this.milestone } : undefined, + stateEvent: this.state && this.state.toUpperCase(), + subscriptionEvent: this.subscription && this.subscription.toUpperCase(), }, }, }); @@ -193,7 +200,7 @@ export default { diff --git a/app/controllers/admin/slacks_controller.rb b/app/controllers/admin/slacks_controller.rb index edb9acbe8ad..ab4d9a05844 100644 --- a/app/controllers/admin/slacks_controller.rb +++ b/app/controllers/admin/slacks_controller.rb @@ -17,7 +17,12 @@ module Admin end def installation_service - Integrations::SlackInstallation::InstanceService.new(current_user: current_user, params: params.permit(:code)) + Integrations::SlackInstallation::InstanceService.new( + current_user: current_user, + params: params + .permit(:code) + .merge(organization_id: Current.organization.id) + ) end end end diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb index 64a4ae07934..0008a54abce 100644 --- a/app/controllers/projects/merge_requests_controller.rb +++ b/app/controllers/projects/merge_requests_controller.rb @@ -519,13 +519,6 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo end def merge! - # Disable the CI check if auto_merge_strategy is specified since we have - # to wait until CI completes to know - skipped_checks = @merge_request.skipped_mergeable_checks( - auto_merge_requested: auto_merge_requested?, - auto_merge_strategy: params[:auto_merge_strategy] - ) - return :failed unless @merge_request.mergeable?(**skipped_checks) squashing = params.fetch(:squash, false) @@ -549,7 +542,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo AutoMergeService.new(project, current_user, merge_params) .execute( merge_request, - params[:auto_merge_strategy] || merge_request.default_auto_merge_strategy + auto_merge_strategy ) end else @@ -715,6 +708,20 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ::Feature.enabled?(:rapid_diffs_on_mr_show, current_user, type: :wip) && params[:rapid_diffs] == 'true' end + + def skipped_checks + if auto_merge_requested? + @merge_request.skipped_mergeable_checks( + auto_merge_strategy: auto_merge_strategy + ) + else + {} + end + end + + def auto_merge_strategy + params[:auto_merge_strategy] || merge_request.default_auto_merge_strategy + end end Projects::MergeRequestsController.prepend_mod_with('Projects::MergeRequestsController') diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb index cacd4199767..13b2b1933f6 100644 --- a/app/controllers/projects/work_items_controller.rb +++ b/app/controllers/projects/work_items_controller.rb @@ -44,8 +44,7 @@ class Projects::WorkItemsController < Projects::ApplicationController def show return if show_params[:iid] == 'new' - @work_item = ::WorkItems::WorkItemsFinder.new(current_user, project_id: project.id) - .execute.with_work_item_type.find_by_iid(show_params[:iid]) + @work_item = issuable end private @@ -87,6 +86,12 @@ class Projects::WorkItemsController < Projects::ApplicationController def file_extension_allowlist EXTENSION_ALLOWLIST end + + def issuable + @issuable ||= ::WorkItems::WorkItemsFinder.new(current_user, project_id: project.id) + .execute.with_work_item_type + .find_by_iid(show_params[:iid]) + end end Projects::WorkItemsController.prepend_mod diff --git a/app/graphql/mutations/merge_requests/accept.rb b/app/graphql/mutations/merge_requests/accept.rb index 7e341b8b261..7eaacd15d76 100644 --- a/app/graphql/mutations/merge_requests/accept.rb +++ b/app/graphql/mutations/merge_requests/accept.rb @@ -83,7 +83,6 @@ module Mutations def validate(merge_request, merge_service, merge_params) skipped_checks = merge_request.skipped_mergeable_checks( - auto_merge_requested: merge_params.key?(:auto_merge_strategy), auto_merge_strategy: merge_params[:auto_merge_strategy] ) diff --git a/app/models/concerns/integrations/base/integration.rb b/app/models/concerns/integrations/base/integration.rb index 0279ebbaf59..0ee61c74b91 100644 --- a/app/models/concerns/integrations/base/integration.rb +++ b/app/models/concerns/integrations/base/integration.rb @@ -304,6 +304,7 @@ module Integrations new_integration = integration.dup new_integration.instance = false + new_integration.organization_id = nil new_integration.project_id = project_id new_integration.group_id = group_id new_integration.inherit_from_id = integration.id if integration.inheritable? @@ -513,12 +514,13 @@ module Integrations validates :project_id, presence: true, unless: -> { instance_level? || group_level? } validates :group_id, presence: true, unless: -> { instance_level? || project_level? } - validates :project_id, :group_id, absence: true, if: -> { instance_level? } + validates :project_id, :group, absence: true, if: -> { instance_level? } + validates :organization_id, presence: true, if: -> { instance_level? } validates :type, presence: true, exclusion: BASE_CLASSES validates :type, uniqueness: { scope: :instance }, if: :instance_level? validates :type, uniqueness: { scope: :project_id }, if: :project_level? validates :type, uniqueness: { scope: :group_id }, if: :group_level? - validate :validate_belongs_to_project_or_group + validate :validate_belongs_to_one_of_project_group_or_organization scope :external_issue_trackers, -> { where(category: 'issue_tracker').active } scope :third_party_wikis, -> { where(category: 'third_party_wiki').active } @@ -811,10 +813,10 @@ module Integrations private - def validate_belongs_to_project_or_group - return unless project_level? && group_level? + def validate_belongs_to_one_of_project_group_or_organization + return if [group_id, project_id, organization_id].compact.one? - errors.add(:project_id, 'The integration cannot belong to both a project and a group') + errors.add(:base, 'The integration must belong to one organization, group, or project.') end def validate_recipients? diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb index efbe65fd142..f9a26104b85 100644 --- a/app/models/merge_request.rb +++ b/app/models/merge_request.rb @@ -1415,7 +1415,7 @@ class MergeRequest < ApplicationRecord merge_when_checks_pass_strat = options[:auto_merge_strategy] == ::AutoMergeService::STRATEGY_MERGE_WHEN_CHECKS_PASS || options[:auto_merge_strategy] == ::AutoMergeService::STRATEGY_ADD_TO_MERGE_TRAIN_WHEN_CHECKS_PASS { - skip_ci_check: options.fetch(:auto_merge_requested, false), + skip_ci_check: merge_when_checks_pass_strat, skip_approved_check: merge_when_checks_pass_strat, skip_draft_check: merge_when_checks_pass_strat, skip_blocked_check: merge_when_checks_pass_strat, @@ -2169,12 +2169,11 @@ class MergeRequest < ApplicationRecord # Note that this could also return SHA from now dangling commits # def all_commit_shas - @all_commit_shas ||= begin - return commit_shas unless persisted? + return commit_shas unless persisted? - all_commits.pluck(:sha).uniq - end + all_commits.pluck(:sha).uniq end + strong_memoize_attr :all_commit_shas def merge_commit @merge_commit ||= project.commit(merge_commit_sha) if merge_commit_sha diff --git a/app/services/auto_merge/base_service.rb b/app/services/auto_merge/base_service.rb index e3532c0c3bb..f97bd4cc968 100644 --- a/app/services/auto_merge/base_service.rb +++ b/app/services/auto_merge/base_service.rb @@ -87,7 +87,6 @@ module AutoMerge def skippable_available_for_checks(merge_request) merge_request.skipped_mergeable_checks( - auto_merge_requested: true, auto_merge_strategy: strategy ) end diff --git a/app/services/integrations/slack_installation/instance_service.rb b/app/services/integrations/slack_installation/instance_service.rb index 6b6e2950ca2..2d53547cd3c 100644 --- a/app/services/integrations/slack_installation/instance_service.rb +++ b/app/services/integrations/slack_installation/instance_service.rb @@ -18,7 +18,9 @@ module Integrations end def find_or_create_integration! - GitlabSlackApplication.for_instance.first_or_create! + GitlabSlackApplication + .for_instance + .first_or_create!(organization_id: params[:organization_id]) # rubocop:disable CodeReuse/ActiveRecord: -- legacy use end end end diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index dee2d65874d..d8301da9bb4 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -78,7 +78,7 @@ :feature_category: :permissions :has_external_dependencies: false :urgency: :low - :resource_boundary: :unknown + :resource_boundary: :cpu :weight: 2 :idempotent: true :tags: [] @@ -2960,7 +2960,7 @@ :feature_category: :permissions :has_external_dependencies: false :urgency: :high - :resource_boundary: :unknown + :resource_boundary: :cpu :weight: 2 :idempotent: true :tags: [] diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb index 7f5f351f8da..e2021bfdfd8 100644 --- a/app/workers/authorized_projects_worker.rb +++ b/app/workers/authorized_projects_worker.rb @@ -5,6 +5,8 @@ class AuthorizedProjectsWorker data_consistency :sticky + worker_resource_boundary :cpu + sidekiq_options retry: 3 feature_category :permissions diff --git a/db/docs/scim_group_memberships.yml b/db/docs/scim_group_memberships.yml index 39841456ad3..f8b68913363 100644 --- a/db/docs/scim_group_memberships.yml +++ b/db/docs/scim_group_memberships.yml @@ -7,6 +7,7 @@ feature_categories: description: Represents a user membership to a particular SCIM group (defined by identity providers) introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/192157 milestone: '18.1' -gitlab_schema: gitlab_main_cell -exempt_from_sharding: true +gitlab_schema: gitlab_main_user +sharding_key: + user_id: users table_size: small diff --git a/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb b/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb new file mode 100644 index 00000000000..170706e1477 --- /dev/null +++ b/db/migrate/20250701165056_add_multi_not_null_constraint_to_integrations.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +class AddMultiNotNullConstraintToIntegrations < Gitlab::Database::Migration[2.3] + milestone '18.2' + + disable_ddl_transaction! + + def up + add_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id, validate: false) + end + + def down + remove_multi_column_not_null_constraint(:integrations, :group_id, :project_id, :organization_id) + end +end diff --git a/db/schema_migrations/20250701165056 b/db/schema_migrations/20250701165056 new file mode 100644 index 00000000000..f457e3d4c2b --- /dev/null +++ b/db/schema_migrations/20250701165056 @@ -0,0 +1 @@ +92b0d4c446beb14d8fcbad090c8465b1d5830659fa6ad5c8df150c67f1da9ba0 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 2fb5bdbf739..64a69a8f619 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -29777,6 +29777,9 @@ ALTER TABLE ONLY chat_teams ALTER TABLE workspaces ADD CONSTRAINT check_2a89035b04 CHECK ((personal_access_token_id IS NOT NULL)) NOT VALID; +ALTER TABLE integrations + ADD CONSTRAINT check_2aae034509 CHECK ((num_nonnulls(group_id, organization_id, project_id) = 1)) NOT VALID; + ALTER TABLE security_scans ADD CONSTRAINT check_2d56d882f6 CHECK ((project_id IS NOT NULL)) NOT VALID; diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 949656d5daf..c2ec1e824bd 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -9694,6 +9694,7 @@ Input type: `ProjectSecretUpdateInput` | `clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | `description` | [`String`](#string) | New description of the project secret. | | `environment` | [`String`](#string) | New environments that can access the secret. | +| `metadataCas` | [`Int`](#int) | This should match the current metadata version of the project secret being updated. | | `name` | [`String!`](#string) | Name of the project secret to update. | | `projectPath` | [`ID!`](#id) | Project of the secret. | | `secret` | [`String`](#string) | New value of the project secret. | @@ -39105,6 +39106,7 @@ Representation of a project secret. | `branch` | [`String!`](#string) | Branches that can access the secret. | | `description` | [`String`](#string) | Description of the project secret. | | `environment` | [`String!`](#string) | Environments that can access the secret. | +| `metadataVersion` | [`Int`](#int) | Current metadata version of the project secret. | | `name` | [`String!`](#string) | Name of the project secret. | | `project` | [`Project!`](#project) | Project the secret belong to. | diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb index fb2e9d0beaf..92a61bb3953 100644 --- a/lib/gitlab/git/repository.rb +++ b/lib/gitlab/git/repository.rb @@ -464,17 +464,14 @@ module Gitlab def raw_changes_between(old_rev, new_rev) @raw_changes_between ||= {} - @raw_changes_between[[old_rev, new_rev]] ||= - begin - return [] if new_rev.blank? || Gitlab::Git.blank_ref?(new_rev) + return [] if new_rev.blank? || Gitlab::Git.blank_ref?(new_rev) - wrapped_gitaly_errors do - gitaly_repository_client.raw_changes_between(old_rev, new_rev) - .each_with_object([]) do |msg, arr| - msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) } - end - end + @raw_changes_between[[old_rev, new_rev]] ||= wrapped_gitaly_errors do + gitaly_repository_client.raw_changes_between(old_rev, new_rev) + .each_with_object([]) do |msg, arr| + msg.raw_changes.each { |change| arr << ::Gitlab::Git::RawDiffChange.new(change) } end + end rescue ArgumentError => e raise Gitlab::Git::Repository::GitError, e end diff --git a/lib/gitlab/github_import/attachments_downloader.rb b/lib/gitlab/github_import/attachments_downloader.rb index 4162a000d6c..f03c0d1286b 100644 --- a/lib/gitlab/github_import/attachments_downloader.rb +++ b/lib/gitlab/github_import/attachments_downloader.rb @@ -8,7 +8,6 @@ module Gitlab include ::BulkImports::FileDownloads::Validations DownloadError = Class.new(StandardError) - UnsupportedAttachmentError = Class.new(StandardError) FILENAME_SIZE_LIMIT = 255 # chars before the extension DEFAULT_FILE_SIZE_LIMIT = Gitlab::CurrentSettings.max_attachment_size.megabytes @@ -55,19 +54,15 @@ module Gitlab options[:follow_redirects] = false response = ::Import::Clients::HTTP.get(file_url, options) - download_url = if response.redirection? - response.headers[:location] - else - file_url - end - - file_type_valid?(URI.parse(download_url).path) - - download_url + if response.redirection? + response.headers[:location] + else + file_url + end end def github_assets_url_regex - %r{#{Regexp.escape(::Gitlab::GithubImport::MarkdownText.github_url)}/.*/assets/} + %r{#{Regexp.escape(::Gitlab::GithubImport::MarkdownText.github_url)}/.*/(assets|files)/} end def download_from(url) @@ -95,12 +90,6 @@ module Gitlab File.join(dir, filename) end end - - def file_type_valid?(file_url) - return if Gitlab::GithubImport::Markdown::Attachment::MEDIA_TYPES.any? { |type| file_url.ends_with?(type) } - - raise UnsupportedAttachmentError - end end end end diff --git a/lib/gitlab/github_import/importer/note_attachments_importer.rb b/lib/gitlab/github_import/importer/note_attachments_importer.rb index 36a256bbef5..75fddcb80e6 100644 --- a/lib/gitlab/github_import/importer/note_attachments_importer.rb +++ b/lib/gitlab/github_import/importer/note_attachments_importer.rb @@ -6,6 +6,8 @@ module Gitlab class NoteAttachmentsImporter attr_reader :note_text, :project, :client + SUPPORTED_RECORD_TYPES = [::Release.name, ::Issue.name, ::MergeRequest.name, ::Note.name].freeze + # note_text - An instance of `Gitlab::GithubImport::Representation::NoteText`. # project - An instance of `Project`. # client - An instance of `Gitlab::GithubImport::Client`. @@ -33,7 +35,8 @@ module Gitlab if attachment.part_of_project_blob?(project_import_source) convert_project_content_link(attachment.url, project_import_source) - elsif attachment.media?(project_import_source) || attachment.doc_belongs_to_project?(project_import_source) + elsif attachment.media?(project_import_source) || attachment.doc_belongs_to_project?(project_import_source) || + attachment.user_attachment? download_attachment(attachment) else # url to other GitHub project attachment.url @@ -57,10 +60,6 @@ module Gitlab file = downloader.perform uploader = UploadService.new(project, file, FileUploader).execute uploader.to_h[:url] - rescue ::Gitlab::GithubImport::AttachmentsDownloader::UnsupportedAttachmentError - attachment.url - ensure - downloader&.delete end def options @@ -72,15 +71,30 @@ module Gitlab end def update_note_record(text) - case note_text.record_type - when ::Release.name - ::Release.find(note_text.record_db_id).update_column(:description, text) - when ::Issue.name - ::Issue.find(note_text.record_db_id).update_column(:description, text) - when ::MergeRequest.name - ::MergeRequest.find(note_text.record_db_id).update_column(:description, text) + return unless supported_record_type? + + record = find_record + column_name = record_column_for_type(note_text.record_type) + + record.update_column(column_name, text) + record.refresh_markdown_cache! + end + + def supported_record_type? + SUPPORTED_RECORD_TYPES.include?(note_text.record_type) + end + + def find_record + record_class = note_text.record_type.constantize + record_class.find(note_text.record_db_id) + end + + def record_column_for_type(record_type) + case record_type + when ::Release.name, ::Issue.name, ::MergeRequest.name + :description when ::Note.name - ::Note.find(note_text.record_db_id).update_column(:note, text) + :note end end end diff --git a/lib/gitlab/github_import/markdown/attachment.rb b/lib/gitlab/github_import/markdown/attachment.rb index 0d8f3196719..3b3699f46a3 100644 --- a/lib/gitlab/github_import/markdown/attachment.rb +++ b/lib/gitlab/github_import/markdown/attachment.rb @@ -102,6 +102,10 @@ module Gitlab ) end + def user_attachment? + url.start_with?("#{::Gitlab::GithubImport::MarkdownText.github_url}/user-attachments/") + end + def inspect "<#{self.class.name}: { name: #{name}, url: #{url} }>" end diff --git a/lib/gitlab/import/source_user_mapper.rb b/lib/gitlab/import/source_user_mapper.rb index 6b56905017d..2a396063a37 100644 --- a/lib/gitlab/import/source_user_mapper.rb +++ b/lib/gitlab/import/source_user_mapper.rb @@ -116,7 +116,7 @@ module Gitlab end def create_placeholder_user(import_source_user) - return namespace_import_user if placeholder_user_limit_exceeded? || namespace.user_namespace? + return namespace_import_user if namespace.user_namespace? || placeholder_user_limit_exceeded? Gitlab::Import::PlaceholderUserCreator.new(import_source_user).execute end diff --git a/package.json b/package.json index 43ed699f6a2..1bdda801fdb 100644 --- a/package.json +++ b/package.json @@ -60,7 +60,7 @@ "@gitlab/application-sdk-browser": "^0.3.4", "@gitlab/at.js": "1.5.7", "@gitlab/cluster-client": "^3.0.0", - "@gitlab/duo-ui": "^8.24.0", + "@gitlab/duo-ui": "^10.0.0", "@gitlab/favicon-overlay": "2.0.0", "@gitlab/fonts": "^1.3.0", "@gitlab/query-language-rust": "0.12.0", diff --git a/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/commands/subcommands/deployment.rb b/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/commands/subcommands/deployment.rb index 00a81648379..73bcacd60e4 100644 --- a/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/commands/subcommands/deployment.rb +++ b/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/commands/subcommands/deployment.rb @@ -57,10 +57,7 @@ module Gitlab desc: "Kubernetes resource definition preset", default: Gitlab::Orchestrator::Deployment::ResourcePresets::DEFAULT, type: :string, - enum: [ - Gitlab::Orchestrator::Deployment::ResourcePresets::DEFAULT, - Gitlab::Orchestrator::Deployment::ResourcePresets::HIGH - ] + enum: Gitlab::Orchestrator::Deployment::ResourcePresets::PRESETS super end diff --git a/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/lib/deployment/resource_presets.rb b/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/lib/deployment/resource_presets.rb index 6e0c95607f5..319f9f4f62e 100644 --- a/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/lib/deployment/resource_presets.rb +++ b/qa/gems/gitlab-orchestrator/lib/gitlab/orchestrator/lib/deployment/resource_presets.rb @@ -5,9 +5,18 @@ module Gitlab module Deployment # Kubernetes resource request/limit presets optimized for different usecases # + # Prefer vertical scaling over hpa for test stability + # as waiting for new pods to scale will lead to test flakiness + # Configure most pods with minReplicas: 1 to simplify debugging + # by having less logs files to review + # To scale webservice and sidekiq, concurrency parameters need to be adjusted together + # with cpu and memory values class ResourcePresets DEFAULT = "default" HIGH = "high" + PERFORMANCE = "performance" + + PRESETS = [DEFAULT, HIGH, PERFORMANCE].freeze class << self # Kubernetes resources values for given preset @@ -15,147 +24,158 @@ module Gitlab # @param [String] preset_name # @return [Hash] def resource_values(preset_name) - presets.fetch(preset_name) + raise ArgumentError, "'#{preset_name}' is not a valid preset name" unless PRESETS.include?(preset_name) + + send(preset_name) # rubocop:disable GitlabSecurity/PublicSend -- send with user input is prevented by validating PRESETS end private - # Different resources presets and replicas count + # Default resource preset for local deployments # - # Prefer vertical scaling over hpa for test stability - # as waiting for new pods to scale will lead to test flakiness - # Configure most pods with minReplicas: 1 to simplify debugging - # by having less logs files to review - # To scale webservice and sidekiq, concurrency parameters need to be adjusted together - # with cpu and memory values # @return [Hash] - def presets - @presets ||= { - # Default preset for local deployments - DEFAULT => { - gitlab: { - webservice: { - workerProcesses: 2, - minReplicas: 1, - resources: resources("1500m", "3Gi") - }, - sidekiq: { - concurrency: 20, - minReplicas: 1, - resources: resources("900m", "2Gi"), - hpa: { - cpu: { targetAverageValue: "800m" } - } - }, - kas: { - minReplicas: 1, - resources: resources("40m", "96Mi") - }, - # TODO: if limits are defined, git operations start failing in e2e tests, investigate potential cause - # https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/3699 - "gitlab-shell": { - minReplicas: 1, - resources: resources("30m", "16Mi", no_limits: true) - }, - gitaly: { - resources: resources("300m", "300Mi") - }, - toolbox: { - resources: resources("50m", "128Mi", no_limits: true) - } + def default + @default ||= { + gitlab: { + webservice: { + workerProcesses: 2, + minReplicas: 1, + resources: resources("1500m", "3Gi") }, - registry: { - resources: resources("40m", "96Mi"), + sidekiq: { + concurrency: 20, + minReplicas: 1, + resources: resources("900m", "2Gi"), hpa: { - minReplicas: 1, - **cpu_utilization + cpu: { targetAverageValue: "800m" } } }, - minio: { - resources: resources("30m", "32Mi") + kas: { + minReplicas: 1, + resources: resources("40m", "96Mi") }, - "nginx-ingress": { - controller: { - resources: resources("30m", "256Mi") - } + # TODO: if limits are defined, git operations start failing in e2e tests, investigate potential cause + # https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/3699 + "gitlab-shell": { + minReplicas: 1, + resources: resources("30m", "16Mi", no_limits: true) }, - postgresql: { - primary: { - resources: resources("400m", "1Gi") - } + gitaly: { + resources: resources("300m", "300Mi") }, - redis: { - master: { - resources: resources("50m", "16Mi") - } + toolbox: { + resources: resources("50m", "128Mi", no_limits: true) } }, - # This preset is optimized for running e2e tests in parallel - HIGH => { - gitlab: { - webservice: { - workerProcesses: 4, - minReplicas: 1, - # See https://docs.gitlab.com/charts/charts/gitlab/webservice/#memory-requestslimits - resources: resources(3, "5Gi", 3, "7Gi"), - hpa: cpu_utilization - }, - sidekiq: { - concurrency: 30, - minReplicas: 1, - resources: resources("1200m", "2Gi"), - hpa: cpu_utilization - }, - kas: { - minReplicas: 1, - resources: resources("60m", "96Mi"), - hpa: cpu_utilization - }, - # TODO: if limits are defined, git operations start failing in e2e tests, investigate potential cause - # https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/3699 - "gitlab-shell": { - minReplicas: 2, - resources: resources("60m", "32Mi", no_limits: true), - hpa: cpu_utilization - }, - gitaly: { - resources: resources("400m", "384Mi") - }, - # Toolbox create peak load during startup but then consumes very little - # Set high limit value but don't request full amount to avoid unnecessary lock - toolbox: { - resources: resources("50m", "128Mi", no_limits: true) - } - }, - registry: { - resources: resources("50m", "128Mi"), - hpa: { - minReplicas: 1, - **cpu_utilization - } - }, - minio: { - resources: resources("50m", "32Mi") - }, - "nginx-ingress": { - controller: { - resources: resources("30m", "256Mi") - } - }, - postgresql: { - primary: { - resources: resources("600m", "1536Mi") - } - }, - redis: { - master: { - resources: resources("100m", "16Mi") - } + registry: { + resources: resources("40m", "96Mi"), + hpa: { + minReplicas: 1, + **cpu_utilization + } + }, + minio: { + resources: resources("30m", "32Mi") + }, + "nginx-ingress": { + controller: { + resources: resources("30m", "256Mi") + } + }, + postgresql: { + primary: { + resources: resources("400m", "1Gi") + } + }, + redis: { + master: { + resources: resources("50m", "16Mi") } } } end + # High resource preset optimized for running e2e tests in parallel + # + # @return [Hash] + def high + @high ||= { + gitlab: { + webservice: { + workerProcesses: 4, + minReplicas: 1, + # See https://docs.gitlab.com/charts/charts/gitlab/webservice/#memory-requestslimits + resources: resources(3, "5Gi", 3, "7Gi"), + hpa: cpu_utilization + }, + sidekiq: { + concurrency: 30, + minReplicas: 1, + resources: resources("1200m", "2Gi"), + hpa: cpu_utilization + }, + kas: { + minReplicas: 1, + resources: resources("60m", "96Mi"), + hpa: cpu_utilization + }, + # TODO: if limits are defined, git operations start failing in e2e tests, investigate potential cause + # https://gitlab.com/gitlab-org/quality/quality-engineering/team-tasks/-/issues/3699 + "gitlab-shell": { + minReplicas: 2, + resources: resources("60m", "32Mi", no_limits: true), + hpa: cpu_utilization + }, + gitaly: { + resources: resources("400m", "384Mi") + }, + # Toolbox create peak load during startup but then consumes very little + # Set high limit value but don't request full amount to avoid unnecessary lock + toolbox: { + resources: resources("50m", "128Mi", no_limits: true) + } + }, + registry: { + resources: resources("50m", "128Mi"), + hpa: { + minReplicas: 1, + **cpu_utilization + } + }, + minio: { + resources: resources("50m", "32Mi") + }, + "nginx-ingress": { + controller: { + resources: resources("30m", "256Mi") + } + }, + postgresql: { + primary: { + resources: resources("600m", "1536Mi") + } + }, + redis: { + master: { + resources: resources("100m", "16Mi") + } + } + } + end + + # Resource preset optimized for performance tests + # + # @return [Hash] + def performance + high.deep_merge({ + redis: { + master: { + resources: resources("200m", "128Mi") + } + } + }) + end + # Kubernetes resources configuration # # Set limits equal to requests by default for simplicity diff --git a/qa/gems/gitlab-orchestrator/spec/unit/gitlab/orchestrator/deployment/resource_presets_spec.rb b/qa/gems/gitlab-orchestrator/spec/unit/gitlab/orchestrator/deployment/resource_presets_spec.rb index 7acbfe686f6..7dc1f7cc4b8 100644 --- a/qa/gems/gitlab-orchestrator/spec/unit/gitlab/orchestrator/deployment/resource_presets_spec.rb +++ b/qa/gems/gitlab-orchestrator/spec/unit/gitlab/orchestrator/deployment/resource_presets_spec.rb @@ -1,101 +1,8 @@ # frozen_string_literal: true RSpec.describe Gitlab::Orchestrator::Deployment::ResourcePresets do - it "returns default resources values preset" do - expect(described_class.resource_values(described_class::DEFAULT)).to eq({ - gitlab: { - webservice: { - workerProcesses: 2, - minReplicas: 1, - resources: { - requests: { cpu: "1500m", memory: "3Gi" }, - limits: { cpu: "1500m", memory: "3Gi" } - } - }, - sidekiq: { - concurrency: 20, - minReplicas: 1, - resources: { - requests: { cpu: "900m", memory: "2Gi" }, - limits: { cpu: "900m", memory: "2Gi" } - }, - hpa: { - cpu: { targetAverageValue: "800m" } - } - }, - kas: { - minReplicas: 1, - resources: { - requests: { cpu: "40m", memory: "96Mi" }, - limits: { cpu: "40m", memory: "96Mi" } - } - }, - "gitlab-shell": { - minReplicas: 1, - resources: { - requests: { cpu: "30m", memory: "16Mi" } - } - }, - gitaly: { - resources: { - requests: { cpu: "300m", memory: "300Mi" }, - limits: { cpu: "300m", memory: "300Mi" } - } - }, - toolbox: { - resources: { - requests: { cpu: "50m", memory: "128Mi" } - } - } - }, - registry: { - resources: { - requests: { cpu: "40m", memory: "96Mi" }, - limits: { cpu: "40m", memory: "96Mi" } - }, - hpa: { - minReplicas: 1, - cpu: { - targetType: "Utilization", - targetAverageUtilization: 90 - } - } - }, - minio: { - resources: { - requests: { cpu: "30m", memory: "32Mi" }, - limits: { cpu: "30m", memory: "32Mi" } - } - }, - "nginx-ingress": { - controller: { - resources: { - requests: { cpu: "30m", memory: "256Mi" }, - limits: { cpu: "30m", memory: "256Mi" } - } - } - }, - postgresql: { - primary: { - resources: { - requests: { cpu: "400m", memory: "1Gi" }, - limits: { cpu: "400m", memory: "1Gi" } - } - } - }, - redis: { - master: { - resources: { - requests: { cpu: "50m", memory: "16Mi" }, - limits: { cpu: "50m", memory: "16Mi" } - } - } - } - }) - end - - it "returns high resources values preset" do - expect(described_class.resource_values(described_class::HIGH)).to eq({ + let(:high_preset) do + { gitlab: { webservice: { workerProcesses: 4, @@ -205,6 +112,123 @@ RSpec.describe Gitlab::Orchestrator::Deployment::ResourcePresets do } } } + } + end + + it "returns default resources values preset" do + expect(described_class.resource_values(described_class::DEFAULT)).to eq({ + gitlab: { + webservice: { + workerProcesses: 2, + minReplicas: 1, + resources: { + requests: { cpu: "1500m", memory: "3Gi" }, + limits: { cpu: "1500m", memory: "3Gi" } + } + }, + sidekiq: { + concurrency: 20, + minReplicas: 1, + resources: { + requests: { cpu: "900m", memory: "2Gi" }, + limits: { cpu: "900m", memory: "2Gi" } + }, + hpa: { + cpu: { targetAverageValue: "800m" } + } + }, + kas: { + minReplicas: 1, + resources: { + requests: { cpu: "40m", memory: "96Mi" }, + limits: { cpu: "40m", memory: "96Mi" } + } + }, + "gitlab-shell": { + minReplicas: 1, + resources: { + requests: { cpu: "30m", memory: "16Mi" } + } + }, + gitaly: { + resources: { + requests: { cpu: "300m", memory: "300Mi" }, + limits: { cpu: "300m", memory: "300Mi" } + } + }, + toolbox: { + resources: { + requests: { cpu: "50m", memory: "128Mi" } + } + } + }, + registry: { + resources: { + requests: { cpu: "40m", memory: "96Mi" }, + limits: { cpu: "40m", memory: "96Mi" } + }, + hpa: { + minReplicas: 1, + cpu: { + targetType: "Utilization", + targetAverageUtilization: 90 + } + } + }, + minio: { + resources: { + requests: { cpu: "30m", memory: "32Mi" }, + limits: { cpu: "30m", memory: "32Mi" } + } + }, + "nginx-ingress": { + controller: { + resources: { + requests: { cpu: "30m", memory: "256Mi" }, + limits: { cpu: "30m", memory: "256Mi" } + } + } + }, + postgresql: { + primary: { + resources: { + requests: { cpu: "400m", memory: "1Gi" }, + limits: { cpu: "400m", memory: "1Gi" } + } + } + }, + redis: { + master: { + resources: { + requests: { cpu: "50m", memory: "16Mi" }, + limits: { cpu: "50m", memory: "16Mi" } + } + } + } }) end + + it "returns high resources values preset" do + expect(described_class.resource_values(described_class::HIGH)).to eq(high_preset) + end + + it "returns performance resources values preset" do + expect(described_class.resource_values(described_class::PERFORMANCE)).to eq(high_preset.deep_merge({ + redis: { + master: { + resources: { + requests: { cpu: "200m", memory: "128Mi" }, + limits: { cpu: "200m", memory: "128Mi" } + } + } + } + })) + end + + it "raises an error when an invalid preset name is provided" do + expect { described_class.resource_values("invalid_preset") }.to raise_error( + ArgumentError, + "'invalid_preset' is not a valid preset name" + ) + end end diff --git a/scripts/frontend/quarantined_vue3_specs.txt b/scripts/frontend/quarantined_vue3_specs.txt index bb2af626395..f052548ffd5 100644 --- a/scripts/frontend/quarantined_vue3_specs.txt +++ b/scripts/frontend/quarantined_vue3_specs.txt @@ -111,6 +111,5 @@ spec/frontend/vue_shared/components/metric_images/metric_image_details_modal_spe spec/frontend/vue_shared/components/registry/code_instruction_spec.js spec/frontend/vue_shared/components/tooltip_on_truncate_spec.js spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js -spec/frontend/vue_shared/directives/tooltip_on_truncate_spec.js spec/frontend/vue_shared/directives/track_event_spec.js spec/frontend/work_items/components/work_item_description_rendered_spec.js diff --git a/scripts/internal_events/monitor.rb b/scripts/internal_events/monitor.rb index a380a64c736..dc129cdd6e3 100755 --- a/scripts/internal_events/monitor.rb +++ b/scripts/internal_events/monitor.rb @@ -26,7 +26,7 @@ unless defined?(Rails) Ensure GDK is running, then run: - bin/rails runner scripts/internal_events/monitor.rb #{ARGV.any? ? ARGV.join(' ') : ''} + bin/rails runner scripts/internal_events/monitor.rb #{ARGV.any? ? ARGV.join(' ') : ''} TEXT @@ -36,12 +36,16 @@ end unless ARGV.any? puts <<~TEXT - Error! The Internal Events Tracking Monitor requires events to be specified. + Error! The Internal Events Tracking Monitor requires events or key path to be specified. For example, to monitor events g_edit_by_web_ide and g_edit_by_sfe, run: bin/rails runner scripts/internal_events/monitor.rb g_edit_by_web_ide g_edit_by_sfe + to monitor metrics where the key_path starts with counts.count_total_invocations_of_internal_events, run: + + bin/rails runner scripts/internal_events/monitor.rb counts.count_total_invocations_of_internal_events + TEXT exit! 1 @@ -58,7 +62,7 @@ Gitlab::Usage::TimeFrame.prepend(ServicePingHelpers::CurrentTimeFrame) def metric_definitions_from_args args = ARGV Gitlab::Usage::MetricDefinition.all.select do |metric| - metric.available? && args.any? { |arg| metric.events.key?(arg) } + metric.available? && args.any? { |arg| metric.events.key?(arg) || metric.key_path.start_with?(arg) } end end @@ -191,7 +195,7 @@ def render_screen(paused) print TTY::Cursor.move_to(0, 0) puts "Updated at #{Time.current} #{'[PAUSED]' if paused}" - puts "Monitored events: #{ARGV.join(', ')}" + puts "Monitored events or key path prefix: #{ARGV.join(', ')}" puts puts metrics_table diff --git a/spec/controllers/admin/integrations_controller_spec.rb b/spec/controllers/admin/integrations_controller_spec.rb index 5b8ee305acd..1faa61cc0cc 100644 --- a/spec/controllers/admin/integrations_controller_spec.rb +++ b/spec/controllers/admin/integrations_controller_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Admin::IntegrationsController, :with_current_organization, featur end it_behaves_like Integrations::Actions do - let(:integration_attributes) { { instance: true, project: nil } } + let(:integration_attributes) { { instance: true, project: nil, organization: organization } } let(:routing_params) do { id: integration.to_param } diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb index 363a5971bfe..9815759646b 100644 --- a/spec/controllers/projects/merge_requests_controller_spec.rb +++ b/spec/controllers/projects/merge_requests_controller_spec.rb @@ -613,6 +613,23 @@ RSpec.describe Projects::MergeRequestsController, feature_category: :code_review end end + context 'when the merge when checks strategy is passed' do + let!(:head_pipeline) do + create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) + end + + def set_auto_merge + post :merge, params: base_params.merge(sha: merge_request.diff_head_sha, auto_merge_strategy: + 'merge_when_checks_pass') + end + + it_behaves_like 'api merge with auto merge' do + let(:service_class) { AutoMerge::MergeWhenChecksPassService } + let(:status) { 'merge_when_checks_pass' } + let(:not_current_pipeline_status) { 'merge_when_checks_pass' } + end + end + context 'when merge when pipeline succeeds option is passed' do let!(:head_pipeline) do create(:ci_empty_pipeline, project: project, sha: merge_request.diff_head_sha, ref: merge_request.source_branch, head_pipeline_of: merge_request) diff --git a/spec/features/incidents/incident_details_spec.rb b/spec/features/incidents/incident_details_spec.rb index a6b66657140..1f03a2040a7 100644 --- a/spec/features/incidents/incident_details_spec.rb +++ b/spec/features/incidents/incident_details_spec.rb @@ -5,14 +5,34 @@ require 'spec_helper' RSpec.describe 'Incident details', :js, feature_category: :incident_management do include MergeRequestDiffHelpers + let_it_be(:payload) do + { + 'title' => 'Alert title', + 'start_time' => '2020-04-27T10:10:22.265949279Z', + 'custom' => { + 'alert' => { + 'fields' => %w[one two] + } + }, + 'yet' => { + 'another' => 73 + } + } + end + let_it_be(:project) { create(:project) } let_it_be(:developer) { create(:user, developer_of: project) } + + let_it_be(:alert) do + create(:alert_management_alert, project: project, payload: payload) + end + let_it_be(:confidential_incident) do create(:incident, confidential: true, project: project, author: developer, description: 'Confidential') end let_it_be_with_reload(:incident) do - create(:incident, project: project, author: developer, description: 'description') + create(:incident, project: project, author: developer, description: 'description', alert_management_alert: alert) end let_it_be(:escalation_status) { create(:incident_management_issuable_escalation_status, issue: incident) } @@ -50,9 +70,49 @@ RSpec.describe 'Incident details', :js, feature_category: :incident_management d page.within('.issuable-details') do incident_tabs = find_by_testid('incident-tabs') - expect(find('h1')).to have_content(incident.title) - expect(incident_tabs).to have_content('Summary') - expect(incident_tabs).to have_content(incident.description) + aggregate_failures 'shows title and Summary tab' do + expect(find('h1')).to have_content(incident.title) + expect(incident_tabs).to have_content('Summary') + expect(incident_tabs).to have_content(incident.description) + end + + aggregate_failures 'shows the incident highlight bar' do + expect(incident_tabs).to have_content('Alert events: 1') + expect(incident_tabs).to have_content('Original alert: #1') + end + + aggregate_failures 'when on summary tab (default tab)' do + hidden_items = find_all('.js-issue-widgets') + + # Linked Issues/MRs + comment box + emoji block + expect(hidden_items.count).to eq(3) + expect(hidden_items).to all(be_visible) + + edit_button = find_all('[aria-label="Edit title and description"]') + expect(edit_button).to all(be_visible) + end + + aggregate_failures 'shows the Alert details tab' do + click_link 'Alert details' + + expect(incident_tabs).to have_content('"title": "Alert title"') + expect(incident_tabs).to have_content('"yet.another": 73') + + # does not show the linked issues and notes/comment components' do + hidden_items = find_all('.js-issue-widgets', wait: false) + + # Linked Issues/MRs and comment box are hidden on page + expect(hidden_items.count).to eq(0) + end + + aggregate_failures 'does not show the linked issues and notes/comment components for the Timeline tab' do + click_link 'Timeline' + + hidden_items = find_all('.js-issue-widgets', wait: false) + + # Linked Issues/MRs and comment box are hidden on page + expect(hidden_items.count).to eq(0) + end end # shows the right sidebar mounted with type issue diff --git a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb index 212862654e0..de613bfefcb 100644 --- a/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb +++ b/spec/features/integrations/exclusions/adding_exclusions_for_projects_spec.rb @@ -23,7 +23,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", end context 'when the integration is active for the instance', :enable_admin_mode do - let(:instance_integration) { create :beyond_identity_integration } + let(:instance_integration) { create(:beyond_identity_integration, :instance) } before do ::Integrations::PropagateService.new(instance_integration).execute @@ -126,7 +126,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", end context 'and the integration is activated for the instance' do - let(:instance_integration) { create :beyond_identity_integration } + let(:instance_integration) { create(:beyond_identity_integration, :instance) } before do ::Integrations::PropagateService.new(instance_integration).execute @@ -144,7 +144,7 @@ RSpec.describe "Adding and removing exclusions to Beyond Identity integration", it { expect(project.reload.beyond_identity_integration).not_to be_activated } context 'and the integration is activated for the instance' do - let(:instance_integration) { create :beyond_identity_integration } + let(:instance_integration) { create(:beyond_identity_integration, :instance) } before do ::Integrations::PropagateService.new(instance_integration).execute diff --git a/spec/features/issues/incident_issue_spec.rb b/spec/features/issues/incident_issue_spec.rb deleted file mode 100644 index a150200bb3c..00000000000 --- a/spec/features/issues/incident_issue_spec.rb +++ /dev/null @@ -1,104 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Incident Detail', :js, feature_category: :team_planning do - let_it_be(:project) { create(:project, :public) } - let_it_be(:payload) do - { - 'title' => 'Alert title', - 'start_time' => '2020-04-27T10:10:22.265949279Z', - 'custom' => { - 'alert' => { - 'fields' => %w[one two] - } - }, - 'yet' => { - 'another' => 73 - } - } - end - - let_it_be(:user) { create(:user) } - let_it_be(:started_at) { Time.now.rfc3339 } - let_it_be(:alert) { create(:alert_management_alert, project: project, payload: payload, started_at: started_at) } - let_it_be(:incident) { create(:incident, project: project, description: 'hello', alert_management_alert: alert) } - - before do - # TODO: When removing the feature flag, - # we won't need the tests for the issues listing page, since we'll be using - # the work items listing page. - stub_feature_flags(work_item_planning_view: false) - stub_feature_flags(hide_incident_management_features: false) - end - - context 'when user displays the incident' do - before do - project.add_developer(user) - sign_in(user) - - visit incident_project_issues_path(project, incident) - wait_for_requests - end - - it 'shows incident and alert data' do - page.within('.issuable-details') do - incident_tabs = find_by_testid('incident-tabs') - - aggregate_failures 'shows title and Summary tab' do - expect(find('h1')).to have_content(incident.title) - expect(incident_tabs).to have_content('Summary') - expect(incident_tabs).to have_content(incident.description) - end - - aggregate_failures 'shows the incident highlight bar' do - expect(incident_tabs).to have_content('Alert events: 1') - expect(incident_tabs).to have_content('Original alert: #1') - end - - aggregate_failures 'when on summary tab (default tab)' do - hidden_items = find_all('.js-issue-widgets') - - # Linked Issues/MRs + comment box + emoji block - expect(hidden_items.count).to eq(3) - expect(hidden_items).to all(be_visible) - - edit_button = find_all('[aria-label="Edit title and description"]') - expect(edit_button).to all(be_visible) - end - - aggregate_failures 'shows the Alert details tab' do - click_link 'Alert details' - - expect(incident_tabs).to have_content('"title": "Alert title"') - expect(incident_tabs).to have_content('"yet.another": 73') - - # does not show the linked issues and notes/comment components' do - hidden_items = find_all('.js-issue-widgets', wait: false) - - # Linked Issues/MRs and comment box are hidden on page - expect(hidden_items.count).to eq(0) - end - end - end - - context 'when on timeline events tab from issue route' do - before do - visit project_issue_path(project, incident) - wait_for_requests - - click_link 'Timeline' - wait_for_requests - end - - it 'does not show the linked issues and notes/comment components' do - page.within('.issuable-details') do - hidden_items = find_all('.js-issue-widgets', wait: false) - - # Linked Issues/MRs and comment box are hidden on page - expect(hidden_items.count).to eq(0) - end - end - end - end -end diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb index 2dfe47cc2f9..6e808424988 100644 --- a/spec/features/issues/issue_detail_spec.rb +++ b/spec/features/issues/issue_detail_spec.rb @@ -29,17 +29,6 @@ RSpec.describe 'Issue Detail', :js, feature_category: :team_planning do end end - context 'when user displays the issue as an incident' do - before do - visit project_issue_path(project, incident) - wait_for_requests - end - - it 'does not show design management' do - expect(page).not_to have_selector('.js-design-management') - end - end - context 'when issue description has emojis' do let(:issue) { create(:issue, project: project, author: user, description: 'hello world :100:') } diff --git a/spec/features/profiles/gpg_keys_spec.rb b/spec/features/profiles/gpg_keys_spec.rb index a36400d7f7c..b217bcced12 100644 --- a/spec/features/profiles/gpg_keys_spec.rb +++ b/spec/features/profiles/gpg_keys_spec.rb @@ -80,7 +80,7 @@ RSpec.describe 'User Settings > GPG keys', feature_category: :user_profile do end context 'when external verification is required' do - let!(:beyond_identity_integration) { create(:beyond_identity_integration) } + let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) } let!(:gpg_key) do create :gpg_key, externally_verified: externally_verified, user: user, key: GpgHelpers::User2.public_key end diff --git a/spec/features/work_items/user_views_work_items_list_spec.rb b/spec/features/work_items/user_views_work_items_list_spec.rb index 127ffdf7ced..4a3d5c49b78 100644 --- a/spec/features/work_items/user_views_work_items_list_spec.rb +++ b/spec/features/work_items/user_views_work_items_list_spec.rb @@ -6,8 +6,9 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do let_it_be(:user) { create(:user) } let_it_be(:group) { create(:group, :public) } let_it_be(:project) { create(:project, :public, group: group) } + let_it_be(:project_internal) { create(:project_empty_repo, :internal, group: group) } - context 'when user is signed in as owner' do + context 'if user is signed in as owner' do let(:issuable_container) { '[data-testid="issuable-container"]' } before_all do @@ -22,9 +23,7 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do wait_for_all_requests end - it 'shows message when there are no items in the list' do - expect(page).to have_content("No results found") - end + it_behaves_like 'no work items in the list' context 'when the work items list page renders' do let_it_be(:issue) { create(:work_item, :issue, project: project, title: 'There is an issue in the list') } @@ -33,19 +32,13 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do create(:work_item, :incident, project: project, title: 'An incident happened while loading the list') end - let!(:closed_issue) { create(:work_item, :closed, project: project) } + let_it_be(:closed_issue) { create(:work_item, :closed, project: project) } it 'show actions based on user permissions' do expect(page).to have_link('New item') expect(page).to have_button('Bulk edit') end - it 'display the recent history widget when configured' do - within_testid('issuable-search-container') do - expect(page).to have_selector('[data-testid="history-icon"]') - end - end - it 'show default sort order' do within_testid('issuable-search-container') do expect(page).to have_button('Created date') @@ -53,23 +46,20 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do end end - it 'load the open items in the project' do - within('.issuable-list') do - expect(page).to have_link(issue.title) - .and have_link(task.title) - .and have_link(incident.title) - .and have_no_content(closed_issue.title) - end + it_behaves_like 'shows open items in the list' do + let(:open_item) { task } + let(:closed_item) { closed_issue } end - it 'load the closed items in the project' do - visit project_work_items_path(project, state: :closed) + context 'when viewing closed work items' do + before do + visit project_work_items_path(project, state: :closed) + wait_for_all_requests + end - wait_for_all_requests - - within('.issuable-list') do - expect(page).to have_link(closed_issue.title) - .and have_no_link(issue.title) + it_behaves_like 'shows closed items in the list' do + let(:open_item) { issue } + let(:closed_item) { closed_issue } end end @@ -92,13 +82,15 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do let_it_be(:award_emoji_upvote) { create(:award_emoji, :upvote, user: user, awardable: task) } let_it_be(:award_emoji_downvote) { create(:award_emoji, :downvote, user: user, awardable: task) } + it_behaves_like 'dates on the work items list' do + let(:date) { 'Dec 31, 2025' } + end + it 'display available metadata' do within(all(issuable_container)[0]) do expect(page).to have_link(milestone.title) .and have_link(label.name) - expect(find_by_testid('issuable-due-date-title').text).to have_text('Dec 31, 2025') - expect(page).to have_link(user.name, href: user_path(user)) expect(find_by_testid('time-estimate-title').text).to have_text('4h') expect(page).to have_text(%r{created .* by #{task.author.name}}) @@ -119,45 +111,7 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do visit project_work_items_path(project) end - it 'displays default page size of 20 items with correct dropdown text' do - expect(page).to have_selector(issuable_container, count: 20) - - expect(page).to have_button _('Show 20 items') - - expect(page).to have_button _('Next') - expect(page).to have_button _('Previous'), disabled: true - end - - it 'navigates through pages using Next and Previous buttons' do - expect(page).to have_button _('Previous'), disabled: true - expect(page).to have_button _('Next'), disabled: false - - click_button _('Next') - - expect(page).to have_button _('Previous'), disabled: false - expect(page).to have_button _('Next'), disabled: true - - expect(page).to have_selector(issuable_container, count: 5) - - click_button _('Previous') - - expect(page).to have_button _('Previous'), disabled: true - expect(page).to have_button _('Next'), disabled: false - expect(page).to have_selector(issuable_container, count: 20) - end - - it 'changes page size and updates display accordingly' do - click_button _('Show 20 items') - - within_testid('list-footer') do - find('[role="option"]', text: _('Show 50 items')).click - end - - expect(page).to have_selector(issuable_container, count: 25) - - expect(page).not_to have_button _('Next'), disabled: true - expect(page).not_to have_button _('Previous'), disabled: true - end + it_behaves_like 'pagination on the work items list page' it 'respects per_page parameter in URL' do visit project_work_items_path(project, first_page_size: 50) @@ -187,4 +141,29 @@ RSpec.describe 'Work Items List', :js, feature_category: :team_planning do expect(page).not_to have_content(confidential_issue.title) end end + + context 'with internal project visibility level' do + let_it_be(:open_work_item) { create(:work_item, :issue, project: project_internal, title: 'Open work item') } + + let_it_be(:closed_work_item) do + create(:work_item, :issue, :closed, project: project_internal, title: 'Closed work item') + end + + context 'when a member views all work items' do + before_all do + project_internal.add_developer(user) + end + + before do + sign_in(user) + visit project_work_items_path(project_internal, state: :all) + wait_for_all_requests + end + + it_behaves_like 'shows all items in the list' do + let(:open_item) { open_work_item } + let(:closed_item) { closed_work_item } + end + end + end end diff --git a/spec/frontend/vue_shared/directives/tooltip_on_truncate_spec.js b/spec/frontend/vue_shared/directives/tooltip_on_truncate_spec.js index c50d98afbfa..1d42666a0af 100644 --- a/spec/frontend/vue_shared/directives/tooltip_on_truncate_spec.js +++ b/spec/frontend/vue_shared/directives/tooltip_on_truncate_spec.js @@ -1,4 +1,5 @@ import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective as mockDirective, getBinding } from 'helpers/vue_mock_directive'; import { hasHorizontalOverflow } from '~/lib/utils/dom_utils'; @@ -102,6 +103,7 @@ describe('TooltipOnTruncate directive', () => { expect(getTooltip().value).toBe(''); await wrapper.setProps({ content: 'Some content' }); + await nextTick(); expect(getTooltip().value).toBe('Some content'); }); diff --git a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone_spec.js b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone_spec.js new file mode 100644 index 00000000000..b0e6231e4db --- /dev/null +++ b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone_spec.js @@ -0,0 +1,166 @@ +import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui'; +import { mount } from '@vue/test-utils'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { createAlert } from '~/alert'; +import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql'; +import WorkItemBulkEditMilestone from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone.vue'; +import { projectMilestonesResponse } from 'ee_else_ce_jest/work_items/mock_data'; + +jest.mock('~/alert'); + +Vue.use(VueApollo); + +const listResults = [ + { + expired: false, + text: 'v4.0', + value: 'gid://gitlab/Milestone/5', + }, + { + expired: false, + text: 'v3.0', + value: 'gid://gitlab/Milestone/4', + }, +]; + +describe('WorkItemBulkEditMilestone component', () => { + let wrapper; + + const milestoneSearchQueryHandler = jest.fn().mockResolvedValue(projectMilestonesResponse); + + const createComponent = ({ + props = {}, + searchQueryHandler = milestoneSearchQueryHandler, + } = {}) => { + wrapper = mount(WorkItemBulkEditMilestone, { + apolloProvider: createMockApollo([[projectMilestonesQuery, searchQueryHandler]]), + propsData: { + fullPath: 'group/project', + isGroup: false, + ...props, + }, + stubs: { + GlCollapsibleListbox, + GlFormGroup: true, + }, + }); + }; + + const findFormGroup = () => wrapper.findComponent(GlFormGroup); + const findListbox = () => wrapper.findComponent(GlCollapsibleListbox); + + const openListboxAndSelect = async (value) => { + findListbox().vm.$emit('shown'); + findListbox().vm.$emit('select', value); + await waitForPromises(); + }; + + it('renders the form group', () => { + createComponent(); + + expect(findFormGroup().attributes('label')).toBe('Milestone'); + }); + + it('renders a header and reset button', () => { + createComponent(); + + expect(findListbox().props()).toMatchObject({ + headerText: 'Select milestone', + resetButtonLabel: 'Reset', + }); + }); + + it('resets the selected milestone when the Reset button is clicked', async () => { + createComponent(); + + await openListboxAndSelect('gid://gitlab/Milestone/5'); + + expect(findListbox().props('selected')).toBe('gid://gitlab/Milestone/5'); + + findListbox().vm.$emit('reset'); + await nextTick(); + + expect(findListbox().props('selected')).toEqual([]); + }); + + describe('milestones query', () => { + it('is not called before dropdown is shown', () => { + createComponent(); + + expect(milestoneSearchQueryHandler).not.toHaveBeenCalled(); + }); + + it('is called when dropdown is shown', async () => { + createComponent(); + + findListbox().vm.$emit('shown'); + await nextTick(); + + expect(milestoneSearchQueryHandler).toHaveBeenCalled(); + }); + + it('emits an error when there is an error in the call', async () => { + createComponent({ searchQueryHandler: jest.fn().mockRejectedValue(new Error('error!')) }); + + findListbox().vm.$emit('shown'); + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith({ + captureError: true, + error: new Error('error!'), + message: 'Failed to load milestones. Please try again.', + }); + }); + }); + + describe('listbox items', () => { + it('renders all milestones', async () => { + createComponent(); + + findListbox().vm.$emit('shown'); + await waitForPromises(); + + expect(findListbox().props('items')).toEqual(listResults); + }); + + describe('with search', () => { + it('displays search results', async () => { + createComponent(); + + findListbox().vm.$emit('shown'); + findListbox().vm.$emit('search', 'search query'); + await waitForPromises(); + + expect(findListbox().props('items')).toEqual(listResults); + expect(milestoneSearchQueryHandler).toHaveBeenCalledWith( + expect.objectContaining({ + title: 'search query', + }), + ); + }); + }); + }); + + describe('listbox text', () => { + describe('with no selected milestone', () => { + it('renders "Select milestone"', () => { + createComponent(); + + expect(findListbox().props('toggleText')).toBe('Select milestone'); + }); + }); + + describe('with selected milestone', () => { + it('renders the milestone title', async () => { + createComponent(); + + await openListboxAndSelect('gid://gitlab/Milestone/5'); + + expect(findListbox().props('toggleText')).toBe('v4.0'); + }); + }); + }); +}); diff --git a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js index b6ab044ef33..9ed102b5f98 100644 --- a/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js +++ b/spec/frontend/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar_spec.js @@ -10,6 +10,7 @@ import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import WorkItemBulkEditAssignee from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_assignee.vue'; import WorkItemBulkEditLabels from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_labels.vue'; +import WorkItemBulkEditMilestone from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_milestone.vue'; import WorkItemBulkEditSidebar from '~/work_items/components/work_item_bulk_edit/work_item_bulk_edit_sidebar.vue'; import workItemBulkUpdateMutation from '~/work_items/graphql/list/work_item_bulk_update.mutation.graphql'; import workItemParentQuery from '~/work_items/graphql/list//work_item_parent.query.graphql'; @@ -66,6 +67,7 @@ describe('WorkItemBulkEditSidebar component', () => { const findSubscriptionComponent = () => wrapper.findComponentByTestId('bulk-edit-subscription'); const findConfidentialityComponent = () => wrapper.findComponentByTestId('bulk-edit-confidentiality'); + const findMilestoneComponent = () => wrapper.findComponent(WorkItemBulkEditMilestone); beforeEach(() => { axiosMock = new MockAdapter(axios); @@ -143,6 +145,9 @@ describe('WorkItemBulkEditSidebar component', () => { findRemoveLabelsComponent().vm.$emit('select', removeLabelIds); findHealthStatusComponent().vm.$emit('input', 'on_track'); findConfidentialityComponent().vm.$emit('input', 'true'); + findMilestoneComponent().vm.$emit('input', 'gid://gitlab/Milestone/30'); + findSubscriptionComponent().vm.$emit('input', 'unsubscribe'); + findStateComponent().vm.$emit('input', 'reopen'); findForm().vm.$emit('submit', { preventDefault: () => {} }); expect(workItemBulkUpdateHandler).toHaveBeenCalledWith({ @@ -160,6 +165,11 @@ describe('WorkItemBulkEditSidebar component', () => { healthStatusWidget: { healthStatus: 'onTrack', }, + milestoneWidget: { + milestoneId: 'gid://gitlab/Milestone/30', + }, + subscriptionEvent: 'UNSUBSCRIBE', + stateEvent: 'REOPEN', }, }); expect(findAddLabelsComponent().props('selectedLabelsIds')).toEqual([]); @@ -399,4 +409,35 @@ describe('WorkItemBulkEditSidebar component', () => { expect(findConfidentialityComponent().props('value')).toBe('false'); }); }); + + describe('"Milestone" component', () => { + it.each([true, false])('renders depending on isEpicsList prop', (isEpicsList) => { + createComponent({ + provide: { + glFeatures: { + workItemsBulkEdit: true, + }, + }, + props: { isEpicsList }, + }); + + expect(findMilestoneComponent().exists()).toBe(!isEpicsList); + }); + + it('updates milestone when "Milestone" component emits "input" event', async () => { + createComponent({ + provide: { + glFeatures: { + workItemsBulkEdit: true, + }, + }, + props: { isEpicsList: false }, + }); + + findMilestoneComponent().vm.$emit('input', 'gid://gitlab/Milestone/30'); + await nextTick(); + + expect(findMilestoneComponent().props('value')).toBe('gid://gitlab/Milestone/30'); + }); + }); }); diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb index 65167f2be5f..371e96bdb64 100644 --- a/spec/helpers/projects_helper_spec.rb +++ b/spec/helpers/projects_helper_spec.rb @@ -1917,7 +1917,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do it { is_expected.to be_falsey } context 'when beyond identity is disabled for a project' do - let_it_be(:integration) { create(:beyond_identity_integration, active: false) } + let_it_be(:integration) { create(:beyond_identity_integration, :instance, active: false) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1927,7 +1927,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when a GPG key failed external validation and one GPC key is externally validated' do - let_it_be(:integration) { create(:beyond_identity_integration) } + let_it_be(:integration) { create(:beyond_identity_integration, :instance) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1939,7 +1939,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when there are no GPG keys externally validated' do - let_it_be(:integration) { create(:beyond_identity_integration) } + let_it_be(:integration) { create(:beyond_identity_integration, :instance) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) @@ -1951,7 +1951,7 @@ RSpec.describe ProjectsHelper, feature_category: :source_code_management do end context 'when GPG keys are missing' do - let_it_be(:integration) { create(:beyond_identity_integration) } + let_it_be(:integration) { create(:beyond_identity_integration, :instance) } before do allow(project).to receive(:beyond_identity_integration).and_return(integration) diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb index e9e9e9189e6..47c73707042 100644 --- a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb +++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb @@ -3,10 +3,17 @@ require 'spec_helper' RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do + let(:organization) { table(:organizations).create!(name: 'organization', path: 'organization') } + + let(:namespaces_table) { table(:namespaces) } + let(:group1) { namespaces_table.create!(name: 'group1', path: 'group1', organization_id: organization.id) } + let(:group2) { namespaces_table.create!(name: 'group2', path: 'group2', organization_id: organization.id) } + let(:group3) { namespaces_table.create!(name: 'group3', path: 'group3', organization_id: organization.id) } + let(:integrations_table) { table(:integrations) } - let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService') } - let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService') } - let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService') } + let(:service_jira_cloud) { integrations_table.create!(id: 1, type_new: 'JiraService', group_id: group1.id) } + let(:service_jira_server) { integrations_table.create!(id: 2, type_new: 'JiraService', group_id: group2.id) } + let(:service_jira_unknown) { integrations_table.create!(id: 3, type_new: 'JiraService', group_id: group3.id) } let(:table_name) { :jira_tracker_data } let(:batch_column) { :id } diff --git a/spec/lib/gitlab/beyond_identity/client_spec.rb b/spec/lib/gitlab/beyond_identity/client_spec.rb index f0a3e11ed2a..e4d1b281657 100644 --- a/spec/lib/gitlab/beyond_identity/client_spec.rb +++ b/spec/lib/gitlab/beyond_identity/client_spec.rb @@ -3,7 +3,7 @@ require 'spec_helper' RSpec.describe ::Gitlab::BeyondIdentity::Client, feature_category: :source_code_management do - let_it_be_with_reload(:integration) { create(:beyond_identity_integration) } + let_it_be_with_reload(:integration) { create(:beyond_identity_integration, :instance) } let(:stubbed_response) do { 'authorized' => true }.to_json diff --git a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb index e2b72d87c56..3fd8795e966 100644 --- a/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb +++ b/spec/lib/gitlab/checks/integrations/beyond_identity_check_spec.rb @@ -5,7 +5,7 @@ require 'spec_helper' RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_category: :source_code_management do include_context 'changes access checks context' let(:integration_check) { Gitlab::Checks::IntegrationsCheck.new(changes_access) } - let!(:beyond_identity_integration) { create(:beyond_identity_integration) } + let!(:beyond_identity_integration) { create(:beyond_identity_integration, :instance) } subject(:check) { described_class.new(integration_check) } @@ -65,7 +65,7 @@ RSpec.describe Gitlab::Checks::Integrations::BeyondIdentityCheck, feature_catego context 'when service accounts are excluded' do let!(:beyond_identity_integration) do - create(:beyond_identity_integration, exclude_service_accounts: true) + create(:beyond_identity_integration, :instance, exclude_service_accounts: true) end it 'does not raise an error' do diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb index 3b9418e1679..bc74798ea33 100644 --- a/spec/lib/gitlab/git/repository_spec.rb +++ b/spec/lib/gitlab/git/repository_spec.rb @@ -1440,6 +1440,15 @@ RSpec.describe Gitlab::Git::Repository, feature_category: :source_code_managemen expect(changes.last.new_path).to eq('with space/README.md') end end + + context 'with an empty new rev' do + let(:old_rev) { 'foo' } + let(:new_rev) { Gitlab::Git::SHA1_BLANK_SHA } + + it 'returns an empty array' do + expect(changes).to eq([]) + end + end end describe '#merge_base' do diff --git a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb index d81d857d623..61ab720e1a6 100644 --- a/spec/lib/gitlab/github_import/attachments_downloader_spec.rb +++ b/spec/lib/gitlab/github_import/attachments_downloader_spec.rb @@ -144,17 +144,6 @@ RSpec.describe Gitlab::GithubImport::AttachmentsDownloader, feature_category: :i expect(File.exist?(file.path)).to eq(true) end end - - context 'when redirection url is not supported' do - let(:redirect_url) { "https://https://github-production-user-asset-6210df.s3.amazonaws.com/142635249/740edb05293e.idk" } - - it 'raises UnsupportedAttachmentError on unsupported extension' do - expect(::Import::Clients::HTTP).to receive(:get).with(file_url, { follow_redirects: false }) - .and_return sample_response - - expect { downloader.perform }.to raise_error(described_class::UnsupportedAttachmentError) - end - end end end diff --git a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb index b9829c09cfd..34d00a9a9ef 100644 --- a/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb +++ b/spec/lib/gitlab/github_import/importer/note_attachments_importer_spec.rb @@ -15,6 +15,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter, feature_ let(:image_tag_url) { 'https://user-images.githubusercontent.com/6833842/0cf366b61ea5.jpeg' } let(:project_blob_url) { 'https://github.com/nickname/public-test-repo/blob/main/example.md' } let(:other_project_blob_url) { 'https://github.com/nickname/other-repo/blob/main/README.md' } + let(:user_attachment_url) { 'https://github.com/user-attachments/assets/73433gh3' } let(:text) do <<-TEXT.split("\n").map(&:strip).join("\n") Some text... @@ -25,6 +26,8 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter, feature_ [link to project blob file](#{project_blob_url}) [link to other project blob file](#{other_project_blob_url}) + + \"user-attachment-image\" TEXT end @@ -35,7 +38,7 @@ RSpec.describe Gitlab::GithubImport::Importer::NoteAttachmentsImporter, feature_ record.reload expect(record.description).to start_with("Some text...\n\n[special-doc](/uploads/") expect(record.description).to include('![image.jpeg](/uploads/') - expect(record.description).to include('tag-image status) end - it 'sets the MR to merge when the pipeline succeeds' do + it 'sets the MR to merge when checks pass' do expect_next_instance_of(service_class) do |service| expect(service).to receive(:execute).with(merge_request) allow(service).to receive(:available_for?).and_return(true) @@ -16,6 +16,18 @@ RSpec.shared_examples 'api merge with auto merge' do set_auto_merge end + context 'when the merge request is not mergable' do + before do + merge_request.update!(title: "Draft: #{merge_request.title}") + end + + it 'returns the correct auto merge strategy' do + set_auto_merge + + expect(json_response).to eq('status' => status) + end + end + context 'for logging' do let(:expected_params) { { merge_action_status: status } } let(:subject_proc) { proc { subject } } diff --git a/spec/support/shared_examples/features/work_items/work_items_list_shared_examples.rb b/spec/support/shared_examples/features/work_items/work_items_list_shared_examples.rb new file mode 100644 index 00000000000..af6e7b04b49 --- /dev/null +++ b/spec/support/shared_examples/features/work_items/work_items_list_shared_examples.rb @@ -0,0 +1,91 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'no work items in the list' do + it 'shows message when there are no items in the list' do + expect(page).to have_content("No results found") + end +end + +RSpec.shared_examples 'shows open items in the list' do + it 'loads the open items' do + within('.issuable-list') do + expect(page).to have_link(open_item.title) + .and have_no_link(closed_item.title) + end + end +end + +RSpec.shared_examples 'shows closed items in the list' do + it 'load the closed items' do + within('.issuable-list') do + expect(page).to have_no_link(open_item.title) + .and have_link(closed_item.title) + end + end +end + +RSpec.shared_examples 'shows all items in the list' do + it 'load all the items' do + within('.issuable-list') do + expect(page).to have_link(open_item.title) + .and have_link(closed_item.title) + end + end +end + +RSpec.shared_examples 'do not shows items in the list' do + it 'load all the items' do + within('.issuable-list') do + expect(page).to have_no_link(open_item.title) + .and have_no_link(closed_item.title) + end + end +end + +RSpec.shared_examples 'dates on the work items list' do |date| + it 'renders the date' do + expect(find_by_testid('issuable-due-date-title').text).to have_text(date) + end +end + +RSpec.shared_examples 'pagination on the work items list page' do + it 'displays default page size of 20 items with correct dropdown text' do + expect(page).to have_selector(issuable_container, count: 20) + + expect(page).to have_button _('Show 20 items') + + expect(page).to have_button _('Next') + expect(page).to have_button _('Previous'), disabled: true + end + + it 'navigates through pages using Next and Previous buttons' do + expect(page).to have_button _('Previous'), disabled: true + expect(page).to have_button _('Next'), disabled: false + + click_button _('Next') + + expect(page).to have_button _('Previous'), disabled: false + expect(page).to have_button _('Next'), disabled: true + + expect(page).to have_selector(issuable_container, count: 5) + + click_button _('Previous') + + expect(page).to have_button _('Previous'), disabled: true + expect(page).to have_button _('Next'), disabled: false + expect(page).to have_selector(issuable_container, count: 20) + end + + it 'changes page size and updates display accordingly' do + click_button _('Show 20 items') + + within_testid('list-footer') do + find('[role="option"]', text: _('Show 50 items')).click + end + + expect(page).to have_selector(issuable_container, count: 25) + + expect(page).not_to have_button _('Next'), disabled: true + expect(page).not_to have_button _('Previous'), disabled: true + end +end diff --git a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb index 6176f2f2445..9a1db4bad5c 100644 --- a/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb +++ b/spec/workers/authorized_project_update/user_refresh_with_low_urgency_worker_spec.rb @@ -17,5 +17,9 @@ RSpec.describe AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker, feature ) end + it 'is cpu-bound' do + expect(described_class.get_worker_resource_boundary).to eq(:cpu) + end + it_behaves_like "refreshes user's project authorizations" end diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb index 32e4cfbde0e..1dd4dc7bcc3 100644 --- a/spec/workers/authorized_projects_worker_spec.rb +++ b/spec/workers/authorized_projects_worker_spec.rb @@ -13,5 +13,9 @@ RSpec.describe AuthorizedProjectsWorker, feature_category: :permissions do ) end + it 'is cpu-bound' do + expect(described_class.get_worker_resource_boundary).to eq(:cpu) + end + it_behaves_like "refreshes user's project authorizations" end diff --git a/spec/workers/projects/post_creation_worker_spec.rb b/spec/workers/projects/post_creation_worker_spec.rb index 1d02f768dbe..cf1edc28746 100644 --- a/spec/workers/projects/post_creation_worker_spec.rb +++ b/spec/workers/projects/post_creation_worker_spec.rb @@ -75,7 +75,13 @@ RSpec.describe Projects::PostCreationWorker, feature_category: :source_code_mana end it 'cleans invalid record and logs warning', :aggregate_failures do - invalid_integration_record = build(:prometheus_integration, properties: { api_url: nil, manual_configuration: true }) + invalid_integration_record = build( + :prometheus_integration, + properties: { api_url: nil, manual_configuration: true }, + project: build(:project), + group: build(:group) + ) + allow(::Integrations::Prometheus).to receive(:new).and_return(invalid_integration_record) expect(Gitlab::ErrorTracking).to receive(:track_exception).with(an_instance_of(ActiveRecord::RecordInvalid), include(extra: { project_id: a_kind_of(Integer) })).twice diff --git a/yarn.lock b/yarn.lock index ff0e1100830..d6be1438630 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1353,7 +1353,7 @@ "@floating-ui/core" "^1.7.1" "@floating-ui/utils" "^0.2.9" -"@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.7.2": +"@floating-ui/dom@1.7.2", "@floating-ui/dom@^1.0.0", "@floating-ui/dom@^1.7.2": version "1.7.2" resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.7.2.tgz#3540b051cf5ce0d4f4db5fb2507a76e8ea5b4a45" integrity sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA== @@ -1397,12 +1397,12 @@ core-js "^3.29.1" mitt "^3.0.1" -"@gitlab/duo-ui@^8.24.0": - version "8.24.0" - resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-8.24.0.tgz#096b410ea2483e0f632a23b69f97a7ae9ffc97c5" - integrity sha512-Vn0AEdy0+Bc4YBPwY8w07Em1n/E7vjQIsWP6AAztSQv4VO5JUVNRouZb/ZUSFJD0s8fGcv+tIH8uJNlFFim9Xw== +"@gitlab/duo-ui@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@gitlab/duo-ui/-/duo-ui-10.0.0.tgz#b4a60bb409bd8212031cf802fb5385aee7842a9f" + integrity sha512-Msr8H6j3muTK4l9laa8/9cf0mpn+7T755CcEOVPd01L7pnbJfYCUSK7TjdZfdJtX82OCRyfHGtlRG0B0pxbqoA== dependencies: - "@floating-ui/dom" "1.7.1" + "@floating-ui/dom" "1.7.2" echarts "^5.3.2" iframe-resizer "^4.3.2" lodash "^4.17.20" @@ -1440,7 +1440,7 @@ resolved "https://registry.yarnpkg.com/@gitlab/fonts/-/fonts-1.3.0.tgz#df89c1bb6714e4a8a5d3272568aa4de7fb337267" integrity sha512-DoMUIN3DqjEn7wvcxBg/b7Ite5fTdF5EmuOZoBRo2j0UBGweDXmNBi+9HrTZs4cBU660dOxcf1hATFcG3npbPg== -"@gitlab/noop@^1.0.1": +"@gitlab/noop@^1.0.1", jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== @@ -9509,11 +9509,6 @@ istanbul-reports@^3.1.3: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jackspeak@^3.1.2, "jackspeak@npm:@gitlab/noop@1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@gitlab/noop/-/noop-1.0.1.tgz#71a831146ee02732b4a61d2d3c11204564753454" - integrity sha512-s++4wjMYeDvBp9IO59DBrWjy8SE/gFkjTDO5ck2W0S6Vv7OlqgErwL7pHngAnrSmTJAzyUG8wHGqo0ViS4jn5Q== - jed@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/jed/-/jed-1.1.1.tgz#7a549bbd9ffe1585b0cd0a191e203055bee574b4"