+
+
+
+ {{ $options.i18n.stage }}
+ {{ stageName }}
+
+
+
+
{{ $options.i18n.loadingText }}
-
+
+
+
+ {{ $options.i18n.mergeTrainMessage }}
+
+
+
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index a904afa7337..83245e2659a 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -21,15 +21,7 @@
- mini graph in Commit widget pipeline
*/
@mixin pipeline-graph-dropdown-menu() {
- width: auto;
- max-width: 400px;
-
- // override dropdown.scss
- &.dropdown-menu li button,
- &.dropdown-menu li a.ci-action-icon-container {
- padding: 0;
- text-align: center;
- }
+ max-height: $gl-max-dropdown-max-height;
.ci-action-icon-container {
position: absolute;
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index d61e3f85995..a441b92da29 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -78,7 +78,7 @@
border-bottom: 2px solid $gray-200;
position: absolute;
right: -4px;
- top: 11px;
+ top: 12px;
width: 4px;
}
}
diff --git a/app/controllers/admin/slacks_controller.rb b/app/controllers/admin/slacks_controller.rb
new file mode 100644
index 00000000000..a03b87b4272
--- /dev/null
+++ b/app/controllers/admin/slacks_controller.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Admin
+ class SlacksController < Admin::ApplicationController
+ before_action do
+ render_404 if Feature.disabled?(:gitlab_for_slack_app_instance_and_group_level, type: :wip)
+ end
+
+ include ::Integrations::SlackControllerSettings
+
+ def slack_auth; end
+
+ private
+
+ def integration
+ @integration ||= Integrations::GitlabSlackApplication.for_instance.first
+ end
+
+ def redirect_to_integration_page
+ redirect_to edit_admin_application_settings_integration_path(
+ integration || Integrations::GitlabSlackApplication.for_instance.new
+ )
+ end
+ end
+end
diff --git a/app/controllers/concerns/integrations/slack_controller_settings.rb b/app/controllers/concerns/integrations/slack_controller_settings.rb
new file mode 100644
index 00000000000..8a0e310b6d4
--- /dev/null
+++ b/app/controllers/concerns/integrations/slack_controller_settings.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# Shared concern for controllers to handle editing the GitLab for Slack app
+# integration at project, group and instance-levels.
+#
+# Controllers should define these methods:
+# - `#integration` to return the Integrations::GitLabSlackApplication record.
+# - `#redirect_to_integration_page` to redirect to the integration edit page
+module Integrations
+ module SlackControllerSettings
+ extend ActiveSupport::Concern
+
+ included do
+ feature_category :integrations
+ end
+
+ def destroy
+ slack_integration.destroy
+
+ redirect_to_integration_page
+ end
+
+ private
+
+ def slack_integration
+ @slack_integration ||= integration.slack_integration
+ end
+ end
+end
diff --git a/app/controllers/groups/settings/slacks_controller.rb b/app/controllers/groups/settings/slacks_controller.rb
new file mode 100644
index 00000000000..771fc517016
--- /dev/null
+++ b/app/controllers/groups/settings/slacks_controller.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Groups
+ module Settings
+ class SlacksController < Groups::ApplicationController
+ before_action :authorize_admin_group!
+
+ before_action do
+ render_404 if Feature.disabled?(:gitlab_for_slack_app_instance_and_group_level, type: :wip)
+ end
+
+ include ::Integrations::SlackControllerSettings
+
+ layout 'group_settings'
+
+ def slack_auth; end
+
+ private
+
+ def integration
+ @integration ||= Integrations::GitlabSlackApplication.for_group(group).first
+ end
+
+ def redirect_to_integration_page
+ redirect_to edit_group_settings_integration_path(
+ group, integration || Integrations::GitlabSlackApplication.for_group(group).new
+ )
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/settings/slacks_controller.rb b/app/controllers/projects/settings/slacks_controller.rb
index 33c290c51c7..32dece1b40e 100644
--- a/app/controllers/projects/settings/slacks_controller.rb
+++ b/app/controllers/projects/settings/slacks_controller.rb
@@ -5,26 +5,22 @@ module Projects
class SlacksController < Projects::ApplicationController
before_action :handle_oauth_error, only: :slack_auth
before_action :check_oauth_state, only: :slack_auth
+
+ include ::Integrations::SlackControllerSettings
+
before_action :authorize_admin_project!
+ before_action :integration, only: [:edit, :update]
before_action :slack_integration, only: [:edit, :update]
layout 'project_settings'
- feature_category :integrations
-
def slack_auth
result = Projects::SlackApplicationInstallService.new(project, current_user, params).execute
flash[:alert] = result[:message] if result[:status] == :error
session[:slack_install_success] = true
- redirect_to_service_page
- end
-
- def destroy
- slack_integration.destroy
-
- redirect_to_service_page
+ redirect_to_integration_page
end
def edit; end
@@ -33,7 +29,7 @@ module Projects
if slack_integration.update(slack_integration_params)
flash[:notice] = 'The project alias was updated successfully'
- redirect_to_service_page
+ redirect_to_integration_page
else
render :edit
end
@@ -41,10 +37,13 @@ module Projects
private
- def redirect_to_service_page
+ def integration
+ @integration ||= project.gitlab_slack_application_integration
+ end
+
+ def redirect_to_integration_page
redirect_to edit_project_settings_integration_path(
- project,
- project.gitlab_slack_application_integration || project.build_gitlab_slack_application_integration
+ project, integration || project.build_gitlab_slack_application_integration
)
end
@@ -58,11 +57,7 @@ module Projects
return unless params[:error] == 'access_denied'
flash[:alert] = 'Access denied'
- redirect_to_service_page
- end
-
- def slack_integration
- @slack_integration ||= project.gitlab_slack_application_integration.slack_integration
+ redirect_to_integration_page
end
def slack_integration_params
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index 5b4b83652df..e84adc82f0c 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -75,12 +75,12 @@ class MembersFinder
end
def project_invited_groups
- invited_groups_ids_including_ancestors = project
- .invited_groups
- .self_and_ancestors
- .public_or_visible_to_user(current_user)
- .select(:id)
+ invited_groups_including_ancestors = project.invited_groups.self_and_ancestors
+ if Feature.disabled?(:webui_members_inherited_users, current_user) || !project.member?(current_user)
+ invited_groups_including_ancestors = invited_groups_including_ancestors.public_or_visible_to_user(current_user)
+ end
+ invited_groups_ids_including_ancestors = invited_groups_including_ancestors.select(:id)
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end
@@ -117,10 +117,16 @@ class MembersFinder
'member_union'
end
+ def project_authorization_table
+ ProjectAuthorization.table_name
+ end
+
def member_columns
Member.column_names.map do |column_name|
# fallback to members.access_level when project_authorizations.access_level is missing
- next "COALESCE(#{ProjectAuthorization.table_name}.access_level, #{member_union_table}.access_level) access_level" if column_name == 'access_level'
+ if column_name == 'access_level'
+ next "COALESCE(#{project_authorization_table}.access_level, #{member_union_table}.access_level) access_level"
+ end
"#{member_union_table}.#{column_name}"
end.join(',')
diff --git a/app/graphql/types/projects/service_type_enum.rb b/app/graphql/types/projects/service_type_enum.rb
index 7a8863c7d67..6c576b3ddcd 100644
--- a/app/graphql/types/projects/service_type_enum.rb
+++ b/app/graphql/types/projects/service_type_enum.rb
@@ -17,7 +17,9 @@ module Types
# This prepend must stay here because the dynamic block below depends on it.
prepend_mod # rubocop: disable Cop/InjectEnterpriseEditionModule
- ::Integration.available_integration_names(include_instance_specific: false, include_dev: false).each do |name|
+ ::Integration.available_integration_names(
+ include_instance_specific: false, include_dev: false, include_disabled: true
+ ).each do |name|
type = "#{name.camelize}Service"
domain_value = Integration.integration_name_to_type(name)
value type.underscore.upcase, value: domain_value, description: type_description(name, type)
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 510561ec614..89c1f6a64bf 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -137,7 +137,7 @@ module IntegrationsHelper
end
if integration.is_a?(::Integrations::GitlabSlackApplication)
- form_data[:upgrade_slack_url] = add_to_slack_link(project, slack_app_id)
+ form_data[:upgrade_slack_url] = add_to_slack_link(integration.parent, slack_app_id)
form_data[:should_upgrade_slack] = integration.upgrade_needed?.to_s
end
@@ -218,17 +218,28 @@ module IntegrationsHelper
event_i18n_map[event] || event.to_s.humanize
end
- def add_to_slack_link(project, slack_app_id)
+ def add_to_slack_link(parent, slack_app_id)
query = {
scope: SlackIntegration::SCOPES.join(','),
client_id: slack_app_id,
- redirect_uri: slack_auth_project_settings_slack_url(project),
+ redirect_uri: add_to_slack_link_redirect_url(parent),
state: form_authenticity_token
}
"#{::Projects::SlackApplicationInstallService::SLACK_AUTHORIZE_URL}?#{query.to_query}"
end
+ def slack_integration_destroy_path(parent)
+ case parent
+ when Project
+ project_settings_slack_path(parent)
+ when Group
+ group_settings_slack_path(parent)
+ when nil
+ admin_application_settings_slack_path
+ end
+ end
+
def gitlab_slack_application_data(projects)
{
projects: (projects || []).to_json(only: [:id, :name], methods: [:avatar_url, :name_with_namespace]),
@@ -244,6 +255,17 @@ module IntegrationsHelper
private
+ def add_to_slack_link_redirect_url(parent)
+ case parent
+ when Project
+ slack_auth_project_settings_slack_url(parent)
+ when Group
+ slack_auth_group_settings_slack_url(parent)
+ when nil
+ slack_auth_admin_application_settings_slack_url
+ end
+ end
+
def jira_integration_event_description(event)
case event
when "merge_request", "merge_request_events"
diff --git a/app/models/analytics/cycle_analytics/stage.rb b/app/models/analytics/cycle_analytics/stage.rb
index 4686dc3aedd..93aac0c732e 100644
--- a/app/models/analytics/cycle_analytics/stage.rb
+++ b/app/models/analytics/cycle_analytics/stage.rb
@@ -12,6 +12,7 @@ module Analytics
validates :name, uniqueness: { scope: [:group_id, :group_value_stream_id] }
validate :max_stages_count, on: :create
+ validate :validate_default_stage_name
belongs_to :value_stream, class_name: 'Analytics::CycleAnalytics::ValueStream',
foreign_key: :group_value_stream_id, inverse_of: :stages
@@ -45,6 +46,17 @@ module Analytics
errors.add(:value_stream, _('Maximum number of stages per value stream exceeded'))
end
+
+ def validate_default_stage_name
+ return if name.blank?
+ return if custom
+ return if Gitlab::Analytics::CycleAnalytics::DefaultStages.find_by_name(name.downcase)
+
+ names = Gitlab::Analytics::CycleAnalytics::DefaultStages.names.join(', ')
+ message = format(_('Invalid name %{input} was given for this default stage, allowed names: %{names}'),
+ input: name.downcase, names: names)
+ errors.add(:name, message)
+ end
end
end
end
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 2491e961bc3..b1725cdf87d 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -20,7 +20,8 @@ class Integration < ApplicationRecord
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire clickup confluence custom_issue_tracker
- datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
+ datadog diffblue_cover discord drone_ci emails_on_push ewm external_wiki
+ gitlab_slack_application hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands squash_tm teamcity telegram
unify_circuit webex_teams youtrack zentao
@@ -32,7 +33,7 @@ class Integration < ApplicationRecord
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/345677
PROJECT_SPECIFIC_INTEGRATION_NAMES = %w[
- apple_app_store gitlab_slack_application google_play jenkins
+ apple_app_store google_play jenkins
].freeze
# Fake integrations to help with local development.
@@ -311,18 +312,26 @@ class Integration < ApplicationRecord
# Returns a list of available integration names.
# Example: ["asana", ...]
def self.available_integration_names(
- include_project_specific: true, include_dev: true, include_instance_specific: true
+ include_project_specific: true, include_dev: true, include_instance_specific: true, include_disabled: false
)
names = integration_names
names += project_specific_integration_names if include_project_specific
names += dev_integration_names if include_dev
names += instance_specific_integration_names if include_instance_specific
+ names -= disabled_integration_names unless include_disabled
names.sort_by(&:downcase)
end
def self.integration_names
- INTEGRATION_NAMES
+ names = INTEGRATION_NAMES.dup
+
+ unless Feature.enabled?(:gitlab_for_slack_app_instance_and_group_level, type: :wip) &&
+ (Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
+ names.delete('gitlab_slack_application')
+ end
+
+ names
end
def self.instance_specific_integration_names
@@ -337,7 +346,12 @@ class Integration < ApplicationRecord
def self.project_specific_integration_names
names = PROJECT_SPECIFIC_INTEGRATION_NAMES.dup
- names.delete('gitlab_slack_application') unless Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?
+
+ if Feature.disabled?(:gitlab_for_slack_app_instance_and_group_level, type: :wip) &&
+ (Gitlab::CurrentSettings.slack_app_enabled || Gitlab.dev_or_test_env?)
+ names << 'gitlab_slack_application'
+ end
+
names
end
@@ -349,6 +363,15 @@ class Integration < ApplicationRecord
end
end
+ # Returns a list of disabled integration names.
+ # Example: ["gitlab_slack_application", ...]
+ def self.disabled_integration_names
+ # The GitLab for Slack app integration is only available when enabled through settings.
+ # The Slack Slash Commands integration is only available for customers who cannot use the GitLab for Slack app.
+ Gitlab::CurrentSettings.slack_app_enabled ? ['slack_slash_commands'] : ['gitlab_slack_application']
+ end
+ private_class_method :disabled_integration_names
+
# Returns the model for the given integration name.
# Example: :asana => Integrations::Asana
def self.integration_name_to_model(name)
@@ -360,7 +383,7 @@ class Integration < ApplicationRecord
# Example: "asana" => "Integrations::Asana"
def self.integration_name_to_type(name)
name = name.to_s
- if available_integration_names.exclude?(name)
+ if available_integration_names(include_disabled: true).exclude?(name)
Gitlab::ErrorTracking.track_and_raise_for_dev_exception(UnknownType.new(name.inspect))
else
"Integrations::#{name.camelize}"
diff --git a/app/models/project.rb b/app/models/project.rb
index c255ccfc7fa..8d462df3ab0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1749,14 +1749,12 @@ class Project < ApplicationRecord
.sort_by(&:title)
end
+ # Returns a list of integration names that should be disabled at the project-level.
+ # Globally disabled integrations should go in Integration.disabled_integration_names.
def disabled_integrations
return [] if Rails.env.development?
- names = %w[zentao]
-
- # The Slack Slash Commands integration is only available for customers who cannot use the GitLab for Slack app.
- # The GitLab for Slack app integration is only available when enabled through settings.
- names << (Gitlab::CurrentSettings.slack_app_enabled ? 'slack_slash_commands' : 'gitlab_slack_application')
+ %w[zentao]
end
def find_or_initialize_integration(name)
diff --git a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
index 2e7768e54f4..ae0e535efa0 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_help.html.haml
@@ -1,10 +1,10 @@
.info-well
.well-segment
%p
- = s_("SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack.")
+ = s_("SlackIntegration|This integration allows users to perform common operations on their projects by entering slash commands in Slack.")
= link_to _('Learn more'), help_page_path('user/project/integrations/gitlab_slack_application')
%p
= s_("SlackIntegration|See the list of available commands in Slack after setting up this integration by entering")
%kbd.inline /gitlab help
-- if integration.project_level?
+- if integration.project_level? || Feature.enabled?(:gitlab_for_slack_app_instance_and_group_level, type: :wip)
= render "shared/integrations/#{integration.to_param}/slack_integration_form", integration: integration
diff --git a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
index 7f708a1e730..ca9a9aef33f 100644
--- a/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
+++ b/app/views/shared/integrations/gitlab_slack_application/_slack_integration_form.html.haml
@@ -1,35 +1,33 @@
- slack_integration = integration.slack_integration
- if slack_integration
%table.gl-table.gl-w-full
- %colgroup
- %col{ width: "25%" }
- %col{ width: "35%" }
- %col{ width: "20%" }
- %col
%thead
%tr
- %th= s_('SlackIntegration|Team name')
- %th= s_('SlackIntegration|Project alias')
+ %th= s_('SlackIntegration|Workspace name')
+ - if integration.project_level?
+ %th
+ = s_('SlackIntegration|Project alias')
%th= _('Created')
%th
%tr
%td{ class: 'gl-py-3!' }
= slack_integration.team_name
- %td{ class: 'gl-py-3!' }
- = slack_integration.alias
+ - if integration.project_level?
+ %td{ class: 'gl-py-3!' }
+ = slack_integration.alias
%td{ class: 'gl-py-3!' }
= time_ago_with_tooltip(slack_integration.created_at)
%td{ class: 'gl-py-3!' }
- .controls.gl-display-flex.gl-gap-3
- - project = integration.project
- = render Pajamas::ButtonComponent.new(href: edit_project_settings_slack_path(project)) do
- = _('Edit')
- = render Pajamas::ButtonComponent.new(method: :delete, category: 'secondary', variant: "danger", href: project_settings_slack_path(project), icon: 'remove', button_options: { aria: { label: s_('SlackIntegration|Remove project') }, data: { confirm_btn_variant: "danger", confirm: s_('SlackIntegration|Are you sure you want to remove this project from the GitLab for Slack app?') }})
+ .controls.gl-display-flex.gl-justify-content-end.gl-gap-3
+ - if integration.project_level?
+ = render Pajamas::ButtonComponent.new(href: edit_project_settings_slack_path(integration.parent)) do
+ = _('Edit')
+ = render Pajamas::ButtonComponent.new(method: :delete, category: 'secondary', variant: "danger", href: slack_integration_destroy_path(integration.parent), icon: 'remove', button_options: { aria: { label: s_('Remove') }, data: { confirm_btn_variant: "danger", confirm: s_('SlackIntegration|Are you sure you want to unlink this Slack Workspace from this integration?') }})
.gl-my-5
- = render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
+ = render Pajamas::ButtonComponent.new(href: add_to_slack_link(integration.parent, slack_app_id)) do
= s_('SlackIntegration|Reinstall GitLab for Slack app…')
%p
= html_escape(s_('SlackIntegration|You may need to reinstall the GitLab for Slack app when we %{linkStart}make updates or change permissions%{linkEnd}.')) % { linkStart: %(
).html_safe, linkEnd: ''.html_safe}
- else
- = render Pajamas::ButtonComponent.new(href: add_to_slack_link(@project, slack_app_id)) do
+ = render Pajamas::ButtonComponent.new(href: add_to_slack_link(integration.parent, slack_app_id)) do
= s_('SlackIntegration|Install GitLab for Slack app…')
diff --git a/config/feature_flags/wip/gitlab_for_slack_app_instance_and_group_level.yml b/config/feature_flags/wip/gitlab_for_slack_app_instance_and_group_level.yml
new file mode 100644
index 00000000000..4d7ad171540
--- /dev/null
+++ b/config/feature_flags/wip/gitlab_for_slack_app_instance_and_group_level.yml
@@ -0,0 +1,9 @@
+---
+name: gitlab_for_slack_app_instance_and_group_level
+feature_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/391526
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/142868
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/production/-/issues/17410
+milestone: '16.10'
+group: group::import and integrate
+type: wip
+default_enabled: false
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 44e2a0f5d53..6de58cbada2 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -156,6 +156,10 @@ namespace :admin do
end
end
+ resource :slack, only: [:destroy] do
+ get :slack_auth
+ end
+
get :usage_data
put :reset_registration_token
put :reset_health_check_token
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 115c6abeb5f..2de5938a0e7 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -60,6 +60,10 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
end
end
+ resource :slack, only: [:destroy] do
+ get :slack_auth
+ end
+
resources :applications do
put 'renew', on: :member
end
diff --git a/db/docs/batched_background_migrations/backfill_finding_id_in_vulnerabilities.yml b/db/docs/batched_background_migrations/backfill_finding_id_in_vulnerabilities.yml
index aaa901d46ea..f2eca56c407 100644
--- a/db/docs/batched_background_migrations/backfill_finding_id_in_vulnerabilities.yml
+++ b/db/docs/batched_background_migrations/backfill_finding_id_in_vulnerabilities.yml
@@ -6,3 +6,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/130058
milestone: '16.7'
queued_migration_version: 20231129105945
finalize_after: '2024-01-15'
+finalized_by: 20240214204757
diff --git a/db/docs/batched_background_migrations/drop_vulnerabilities_without_finding_id.yml b/db/docs/batched_background_migrations/drop_vulnerabilities_without_finding_id.yml
index 37e7fae2ea6..b183369fa85 100644
--- a/db/docs/batched_background_migrations/drop_vulnerabilities_without_finding_id.yml
+++ b/db/docs/batched_background_migrations/drop_vulnerabilities_without_finding_id.yml
@@ -6,4 +6,4 @@ introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/140532
milestone: '16.8'
queued_migration_version: 20231221223259
finalize_after: '2024-01-22'
-finalized_by: # version of the migration that finalized this BBM
+finalized_by: 20240214204757
diff --git a/db/docs/project_relation_exports.yml b/db/docs/project_relation_exports.yml
index f25fe8280ae..5d3cf0d29e5 100644
--- a/db/docs/project_relation_exports.yml
+++ b/db/docs/project_relation_exports.yml
@@ -7,4 +7,19 @@ feature_categories:
description: Used to track the generation of relation export files for projects
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90624
milestone: '15.2'
-gitlab_schema: gitlab_main
+gitlab_schema: gitlab_main_cell
+allow_cross_joins:
+- gitlab_main_clusterwide
+allow_cross_transactions:
+- gitlab_main_clusterwide
+allow_cross_foreign_keys:
+- gitlab_main_clusterwide
+desired_sharding_key:
+ project_id:
+ references: projects
+ backfill_via:
+ parent:
+ foreign_key: project_export_job_id
+ table: project_export_jobs
+ sharding_key: project_id
+ belongs_to: project_export_job
diff --git a/db/post_migrate/20240214204757_finalize_finding_id_migrations.rb b/db/post_migrate/20240214204757_finalize_finding_id_migrations.rb
new file mode 100644
index 00000000000..7b925bcb6da
--- /dev/null
+++ b/db/post_migrate/20240214204757_finalize_finding_id_migrations.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class FinalizeFindingIdMigrations < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.9'
+ restrict_gitlab_migration gitlab_schema: :gitlab_main
+
+ def up
+ finalize_finding_id_backfill
+ finalize_empty_finding_id_removal
+ end
+
+ def down; end
+
+ private
+
+ def finalize_finding_id_backfill
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'BackfillFindingIdInVulnerabilities',
+ table_name: :vulnerabilities,
+ column_name: 'id',
+ job_arguments: []
+ )
+ end
+
+ def finalize_empty_finding_id_removal
+ ensure_batched_background_migration_is_finished(
+ job_class_name: 'DropVulnerabilitiesWithoutFindingId',
+ table_name: :vulnerabilities,
+ column_name: 'id',
+ job_arguments: []
+ )
+ end
+end
diff --git a/db/post_migrate/20240214204805_make_finding_id_not_null.rb b/db/post_migrate/20240214204805_make_finding_id_not_null.rb
new file mode 100644
index 00000000000..380e6fca5da
--- /dev/null
+++ b/db/post_migrate/20240214204805_make_finding_id_not_null.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class MakeFindingIdNotNull < Gitlab::Database::Migration[2.2]
+ disable_ddl_transaction!
+ milestone '16.9'
+
+ def up
+ add_not_null_constraint :vulnerabilities, :finding_id
+ end
+
+ def down
+ remove_not_null_constraint :vulnerabilities, :finding_id
+ end
+end
diff --git a/db/schema_migrations/20240214204757 b/db/schema_migrations/20240214204757
new file mode 100644
index 00000000000..96c471ced61
--- /dev/null
+++ b/db/schema_migrations/20240214204757
@@ -0,0 +1 @@
+35a60bc57ed21353aa5570831aac6f991a10bb4e3ec10559bc38000488f59799
\ No newline at end of file
diff --git a/db/schema_migrations/20240214204805 b/db/schema_migrations/20240214204805
new file mode 100644
index 00000000000..946011a1e6f
--- /dev/null
+++ b/db/schema_migrations/20240214204805
@@ -0,0 +1 @@
+078e43ac4de3b81bcc9cd73d37e7bd614f1ffd70df41693988a187caee6fdf6d
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 47f57e1d960..2d12315c9f8 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -16930,7 +16930,8 @@ CREATE TABLE vulnerabilities (
present_on_default_branch boolean DEFAULT true NOT NULL,
detected_at timestamp with time zone DEFAULT now(),
finding_id bigint,
- cvss jsonb DEFAULT '[]'::jsonb
+ cvss jsonb DEFAULT '[]'::jsonb,
+ CONSTRAINT check_4d8a873f1f CHECK ((finding_id IS NOT NULL))
);
CREATE SEQUENCE vulnerabilities_id_seq
diff --git a/doc/administration/reporting/spamcheck.md b/doc/administration/reporting/spamcheck.md
index c92d55299bd..fa62c02e264 100644
--- a/doc/administration/reporting/spamcheck.md
+++ b/doc/administration/reporting/spamcheck.md
@@ -15,7 +15,7 @@ DETAILS:
WARNING:
Spamcheck is available to all tiers, but only on instances using GitLab Enterprise Edition (EE). For [licensing reasons](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6259#note_726605397), it is not included in the GitLab Community Edition (CE) package. You can [migrate from CE to EE](../../update/package/convert_to_ee.md).
-[Spamcheck](https://gitlab.com/gitlab-org/spamcheck) is an anti-spam engine
+[Spamcheck](https://gitlab.com/gitlab-org/gl-security/security-engineering/security-automation/spam/spamcheck) is an anti-spam engine
developed by GitLab originally to combat rising amount of spam in GitLab.com,
and later made public to be used in self-managed GitLab instances.
diff --git a/doc/ci/variables/index.md b/doc/ci/variables/index.md
index 85e6ae5f53a..642257ca03b 100644
--- a/doc/ci/variables/index.md
+++ b/doc/ci/variables/index.md
@@ -957,7 +957,7 @@ so you should only use the variable in GitLab itself.
## Known issues and workarounds
-These are some know issues with CI/CD variables, and where applicable, known workarounds.
+These are some known issues with CI/CD variables, and where applicable, known workarounds.
### "argument list too long"
diff --git a/lib/gitlab/analytics/cycle_analytics/default_stages.rb b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
index 0f1380125fa..0d139b441d4 100644
--- a/lib/gitlab/analytics/cycle_analytics/default_stages.rb
+++ b/lib/gitlab/analytics/cycle_analytics/default_stages.rb
@@ -23,7 +23,11 @@ module Gitlab
end
def self.find_by_name!(name)
- all.find { |raw_stage| raw_stage[:name].to_s.eql?(name.to_s) } || raise("Default stage '#{name}' not found")
+ find_by_name(name) || raise("Default stage '#{name}' not found")
+ end
+
+ def self.find_by_name(name)
+ all.find { |raw_stage| raw_stage[:name].to_s.eql?(name.to_s) }
end
def self.names
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index e36bf9ff6ad..ae45269e08c 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -231,7 +231,7 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def integrations_usage
# rubocop: disable UsageData/LargeTable:
- Integration.available_integration_names(include_dev: false).each_with_object({}) do |name, response|
+ Integration.available_integration_names(include_dev: false, include_disabled: true).each_with_object({}) do |name, response|
type = Integration.integration_name_to_type(name)
response[:"projects_#{name}_active"] = count(Integration.active.where.not(project: nil).where(type: type))
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 92b0537814c..c19995ea963 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8491,6 +8491,9 @@ msgstr ""
msgid "Boards|New board"
msgstr ""
+msgid "Boards|No cadence matches current iteration filter"
+msgstr ""
+
msgid "Boards|Retrieving blocking %{issuableType}s"
msgstr ""
@@ -26654,6 +26657,9 @@ msgstr ""
msgid "Invalid login or password"
msgstr ""
+msgid "Invalid name %{input} was given for this default stage, allowed names: %{names}"
+msgstr ""
+
msgid "Invalid period"
msgstr ""
@@ -47018,7 +47024,7 @@ msgstr ""
msgid "SlackIntegration|- *Slash commands:* Quickly open, access, or close issues from Slack using the `%{slash_command}` command. Streamline your GitLab deployments with ChatOps."
msgstr ""
-msgid "SlackIntegration|Are you sure you want to remove this project from the GitLab for Slack app?"
+msgid "SlackIntegration|Are you sure you want to unlink this Slack Workspace from this integration?"
msgstr ""
msgid "SlackIntegration|Client ID"
@@ -47069,9 +47075,6 @@ msgstr ""
msgid "SlackIntegration|Reinstall GitLab for Slack app…"
msgstr ""
-msgid "SlackIntegration|Remove project"
-msgstr ""
-
msgid "SlackIntegration|Run ChatOps jobs."
msgstr ""
@@ -47093,10 +47096,7 @@ msgstr ""
msgid "SlackIntegration|Step 2: Configure the app settings"
msgstr ""
-msgid "SlackIntegration|Team name"
-msgstr ""
-
-msgid "SlackIntegration|This integration allows users to perform common operations on this project by entering slash commands in Slack."
+msgid "SlackIntegration|This integration allows users to perform common operations on their projects by entering slash commands in Slack."
msgstr ""
msgid "SlackIntegration|Update to the latest version"
@@ -47126,6 +47126,9 @@ msgstr ""
msgid "SlackIntegration|When GitLab releases new features for the GitLab for Slack app, you might have to manually update your copy to use the new features."
msgstr ""
+msgid "SlackIntegration|Workspace name"
+msgstr ""
+
msgid "SlackIntegration|You can now close this window and go to your Slack workspace."
msgstr ""
diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb
index 8c7d87e960a..a132a5fc52e 100644
--- a/qa/qa/support/wait_for_requests.rb
+++ b/qa/qa/support/wait_for_requests.rb
@@ -36,9 +36,11 @@ module QA
# migration to the new spinner is complete.
# https://gitlab.com/groups/gitlab-org/-/epics/956
# retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485
- Support::Retrier.retry_on_exception do
- Capybara.page.has_no_css?('.gl-spinner', wait: wait)
- end
+
+ Capybara.page.has_no_css?('.gl-spinner', wait: wait)
+ rescue Selenium::WebDriver::Error::StaleElementReferenceError => e
+ QA::Runtime::Logger.error(".gl-spinner reference has become stale: #{e}")
+ true
end
end
end
diff --git a/spec/features/admin/integrations/admin_manages_gitlab_for_slack_app_spec.rb b/spec/features/admin/integrations/admin_manages_gitlab_for_slack_app_spec.rb
new file mode 100644
index 00000000000..3ca1ddf0b08
--- /dev/null
+++ b/spec/features/admin/integrations/admin_manages_gitlab_for_slack_app_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Admin manages the instance-level GitLab for Slack app integration', :js, feature_category: :integrations do
+ include Spec::Support::Helpers::ModalHelpers
+
+ include_context 'instance integration activation'
+
+ let_it_be(:integration) do
+ create(:gitlab_slack_application_integration, :instance,
+ slack_integration: build(:slack_integration)
+ )
+ end
+
+ before do
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ def visit_slack_application_form
+ visit_instance_integration('GitLab for Slack app')
+ wait_for_requests
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it 'hides the integration' do
+ visit_instance_integrations
+
+ expect(page).not_to have_content('GitLab for Slack app')
+ end
+ end
+
+ it 'shows the workspace name but not the alias and does not allow the user to edit it' do
+ visit_slack_application_form
+
+ within_testid 'integration-settings-form' do
+ expect(page).to have_content('Workspace name')
+ expect(page).to have_content(integration.slack_integration.team_name)
+ expect(page).not_to have_content('Project alias')
+ expect(page).not_to have_content(integration.slack_integration.alias)
+ expect(page).not_to have_content('Edit')
+ end
+ end
+
+ it 'allows the user to unlink the GitLab for Slack app' do
+ visit_slack_application_form
+
+ within_testid 'integration-settings-form' do
+ page.find('a.btn-danger').click
+ end
+
+ within_modal do
+ expect(page).to have_content('Are you sure you want to unlink this Slack Workspace from this integration?')
+ click_button('Remove')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Install GitLab for Slack app')
+ end
+
+ it 'shows the trigger form fields' do
+ visit_slack_application_form
+
+ expect(page).to have_selector('[data-testid="trigger-fields-group"]')
+ end
+
+ context 'when the integration is disabled' do
+ before do
+ Integrations::GitlabSlackApplication.for_instance.first.update!(active: false)
+ end
+
+ it 'does not show the trigger form fields' do
+ visit_slack_application_form
+
+ expect(page).not_to have_selector('[data-testid="trigger-fields-group"]')
+ end
+ end
+end
diff --git a/spec/features/groups/integrations/user_manages_gitlab_for_slack_app_spec.rb b/spec/features/groups/integrations/user_manages_gitlab_for_slack_app_spec.rb
new file mode 100644
index 00000000000..1829acb8954
--- /dev/null
+++ b/spec/features/groups/integrations/user_manages_gitlab_for_slack_app_spec.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'User manages the group-level GitLab for Slack app integration', :js, feature_category: :integrations do
+ include Spec::Support::Helpers::ModalHelpers
+
+ include_context 'group integration activation'
+
+ let_it_be(:integration) do
+ create(:gitlab_slack_application_integration, :group, group: group,
+ slack_integration: build(:slack_integration)
+ )
+ end
+
+ before do
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ def visit_slack_application_form
+ visit_group_integration('GitLab for Slack app')
+ wait_for_requests
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it 'hides the integration' do
+ visit_group_integrations
+
+ expect(page).not_to have_content('GitLab for Slack app')
+ end
+ end
+
+ it 'shows the workspace name but not the alias and does not allow the user to edit it' do
+ visit_slack_application_form
+
+ within_testid 'integration-settings-form' do
+ expect(page).to have_content('Workspace name')
+ expect(page).to have_content(integration.slack_integration.team_name)
+ expect(page).not_to have_content('Project alias')
+ expect(page).not_to have_content(integration.slack_integration.alias)
+ expect(page).not_to have_content('Edit')
+ end
+ end
+
+ it 'allows the user to unlink the GitLab for Slack app' do
+ visit_slack_application_form
+
+ within_testid 'integration-settings-form' do
+ page.find('a.btn-danger').click
+ end
+
+ within_modal do
+ expect(page).to have_content('Are you sure you want to unlink this Slack Workspace from this integration?')
+ click_button('Remove')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Install GitLab for Slack app')
+ end
+
+ it 'shows the trigger form fields' do
+ visit_slack_application_form
+
+ expect(page).to have_selector('[data-testid="trigger-fields-group"]')
+ end
+
+ context 'when the integration is disabled' do
+ before do
+ Integrations::GitlabSlackApplication.for_group(group).first.update!(active: false)
+ end
+
+ it 'does not show the trigger form fields' do
+ visit_slack_application_form
+
+ expect(page).not_to have_selector('[data-testid="trigger-fields-group"]')
+ end
+ end
+end
diff --git a/spec/features/projects/commit/mini_pipeline_graph_spec.rb b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
index c0a8dabf648..a8f65399a35 100644
--- a/spec/features/projects/commit/mini_pipeline_graph_spec.rb
+++ b/spec/features/projects/commit/mini_pipeline_graph_spec.rb
@@ -31,7 +31,7 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
end
end
- context 'when commit has pipelines and feature flag is disabled' do
+ context 'when commit has pipelines and feature flag is disabled', :js do
let(:pipeline) do
create(
:ci_pipeline,
@@ -58,11 +58,11 @@ RSpec.describe 'Mini Pipeline Graph in Commit View', :js, feature_category: :sou
it 'displays a mini pipeline graph' do
expect(page).to have_selector('[data-testid="commit-box-pipeline-mini-graph"]')
- first('[data-testid="mini-pipeline-graph-dropdown"]').click
+ find_by_testid('mini-pipeline-graph-dropdown-toggle').click
wait_for_requests
- page.within '.js-builds-dropdown-list' do
+ within_testid('mini-pipeline-graph-dropdown') do
expect(page).to have_selector('[data-testid="status_running_borderless-icon"]')
expect(page).to have_content(build.stage_name)
end
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 00bb9141aa0..a9e7a50136d 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -269,7 +269,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
end
- context 'with manual actions' do
+ context 'with manual actions', :js do
let!(:manual) do
create(:ci_build, :manual,
pipeline: pipeline,
@@ -286,7 +286,7 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
end
it 'has link to the manual action' do
- find('[data-testid="pipelines-manual-actions-dropdown"]').click
+ find_by_testid('pipelines-manual-actions-dropdown').click
wait_for_requests
@@ -295,11 +295,13 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
context 'when manual action was played' do
before do
- find('[data-testid="pipelines-manual-actions-dropdown"] button').click
+ find_by_testid('pipelines-manual-actions-dropdown').click
wait_for_requests
click_button('manual build')
+
+ wait_for_all_requests
end
it 'enqueues manual action job', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/409984' do
@@ -325,8 +327,8 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
expect(page).to have_selector('[data-testid="pipelines-manual-actions-dropdown"] [data-testid="play-icon"]')
end
- it "has link to the delayed job's action" do
- find('[data-testid="pipelines-manual-actions-dropdown"] button').click
+ it "has link to the delayed job's action", :js do
+ find_by_testid('pipelines-manual-actions-dropdown').click
wait_for_requests
@@ -344,8 +346,8 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
stage: 'test')
end
- it "shows 00:00:00 as the remaining time" do
- find('[data-testid="pipelines-manual-actions-dropdown"] button').click
+ it "shows 00:00:00 as the remaining time", :js do
+ find_by_testid('pipelines-manual-actions-dropdown').click
wait_for_requests
@@ -535,32 +537,60 @@ RSpec.describe 'Pipelines', :js, feature_category: :continuous_integration do
expect(page).to have_selector(dropdown_selector)
end
- context 'when clicking a stage badge' do
+ context 'when clicking a stage badge', :js do
it 'opens a dropdown' do
- find(dropdown_selector).click
+ find_by_testid('mini-pipeline-graph-dropdown-toggle').click
+
+ wait_for_requests
expect(page).to have_link build.name
end
it 'is possible to cancel pending build' do
- find(dropdown_selector).click
- find('.js-ci-action').click
+ find_by_testid('mini-pipeline-graph-dropdown-toggle').click
+
+ wait_for_requests
+
+ find_by_testid('ci-action-button').click
wait_for_requests
expect(build.reload).to be_canceled
end
+
+ context 'manual job', :js do
+ let!(:build) do
+ create(:ci_build, :manual, pipeline: pipeline, stage: 'build', name: 'manual-build')
+ end
+
+ it 'is possible to play manual build' do
+ find_by_testid('mini-pipeline-graph-dropdown-toggle').click
+
+ wait_for_requests
+
+ within first('[data-testid="job-with-link"]') do
+ expect(find_by_testid('play-icon')).to be_visible
+ end
+
+ find_by_testid('ci-action-button').click
+ wait_for_requests
+
+ expect(find('[data-testid="mini-pipeline-graph-dropdown-toggle"][aria-expanded="true"]')).to be_visible
+ end
+ end
end
- context 'for a failed pipeline' do
+ context 'for a failed pipeline', :js do
let!(:build) do
create(:ci_build, :failed, pipeline: pipeline, stage: 'build', name: 'build')
end
it 'displays the failure reason' do
- find(dropdown_selector).click
+ find_by_testid('mini-pipeline-graph-dropdown-toggle').click
- within('.js-builds-dropdown-list') do
- build_element = page.find('.pipeline-job-item')
+ wait_for_requests
+
+ within_testid('mini-pipeline-graph-dropdown') do
+ build_element = page.find('.pipeline-job-item [data-testid="job-name"]')
expect(build_element['title']).to eq('build - failed - (unknown failure)')
end
end
diff --git a/spec/features/projects/settings/slack_application_spec.rb b/spec/features/projects/settings/slack_application_spec.rb
index e2d195cdc26..4d1aa17e986 100644
--- a/spec/features/projects/settings/slack_application_spec.rb
+++ b/spec/features/projects/settings/slack_application_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe 'Slack application', :js, feature_category: :integrations do
+ include Spec::Support::Helpers::ModalHelpers
+
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user, maintainer_projects: [project]) }
let_it_be(:integration) { create(:gitlab_slack_application_integration, project: project) }
@@ -14,10 +16,20 @@ RSpec.describe 'Slack application', :js, feature_category: :integrations do
gitlab_sign_in(user)
end
- it 'I can edit slack integration' do
+ def visit_slack_application_form
visit slack_application_form_path
+ wait_for_requests
+ end
+
+ it 'shows the workspace name and alias and allows the user to edit it' do
+ visit_slack_application_form
within_testid 'integration-settings-form' do
+ expect(page).to have_content('Workspace name')
+ expect(page).to have_content(integration.slack_integration.team_name)
+ expect(page).to have_content('Project alias')
+ expect(page).to have_content(integration.slack_integration.alias)
+
click_link 'Edit'
end
@@ -31,8 +43,25 @@ RSpec.describe 'Slack application', :js, feature_category: :integrations do
end
end
+ it 'allows the user to unlink the GitLab for Slack app' do
+ visit_slack_application_form
+
+ within_testid 'integration-settings-form' do
+ page.find('a.btn-danger').click
+ end
+
+ within_modal do
+ expect(page).to have_content('Are you sure you want to unlink this Slack Workspace from this integration?')
+ click_button('Remove')
+ end
+
+ wait_for_requests
+
+ expect(page).to have_content('Install GitLab for Slack app')
+ end
+
it 'shows the trigger form fields' do
- visit slack_application_form_path
+ visit_slack_application_form
expect(page).to have_selector('[data-testid="trigger-fields-group"]')
end
@@ -43,6 +72,8 @@ RSpec.describe 'Slack application', :js, feature_category: :integrations do
end
it 'does not show the trigger form fields' do
+ visit_slack_application_form
+
expect(page).not_to have_selector('[data-testid="trigger-fields-group"]')
end
end
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 9c8b8658538..07728fa68eb 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -3,260 +3,280 @@
require 'spec_helper'
RSpec.describe MembersFinder, feature_category: :groups_and_projects do
- shared_examples '#execute' do
- let_it_be(:group) { create(:group) }
- let_it_be(:nested_group) { create(:group, parent: group) }
- let_it_be(:project, reload: true) { create(:project, namespace: nested_group) }
- let_it_be(:user1) { create(:user) }
- let_it_be(:user2) { create(:user) }
- let_it_be(:user3) { create(:user) }
- let_it_be(:user4) { create(:user) }
- let_it_be(:blocked_user) { create(:user, :blocked) }
+ let_it_be(:group) { create(:group) }
+ let_it_be(:nested_group) { create(:group, parent: group) }
+ let_it_be(:project, reload: true) { create(:project, namespace: nested_group) }
+ let_it_be(:user1) { create(:user) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:user4) { create(:user) }
+ let_it_be(:blocked_user) { create(:user, :blocked) }
- it 'returns members for project and parent groups' do
- nested_group.request_access(user1)
- member1 = group.add_maintainer(user2)
- member2 = nested_group.add_maintainer(user3)
- member3 = project.add_maintainer(user4)
- blocked_member = project.add_maintainer(blocked_user)
+ it 'returns members for project and parent groups' do
+ nested_group.request_access(user1)
+ member1 = group.add_maintainer(user2)
+ member2 = nested_group.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
+ blocked_member = project.add_maintainer(blocked_user)
- result = described_class.new(project, user2).execute
+ result = described_class.new(project, user2).execute
- expect(result).to contain_exactly(member1, member2, member3, blocked_member)
- end
+ expect(result).to contain_exactly(member1, member2, member3, blocked_member)
+ end
- it 'returns owners and maintainers' do
- member1 = group.add_owner(user1)
- group.add_developer(user2)
- member3 = project.add_maintainer(user3)
- project.add_developer(user4)
+ it 'returns owners and maintainers' do
+ member1 = group.add_owner(user1)
+ group.add_developer(user2)
+ member3 = project.add_maintainer(user3)
+ project.add_developer(user4)
- result = described_class.new(project, user2, params: { owners_and_maintainers: true }).execute
+ result = described_class.new(project, user2, params: { owners_and_maintainers: true }).execute
- expect(result).to contain_exactly(member1, member3)
- end
+ expect(result).to contain_exactly(member1, member3)
+ end
- it 'returns active users and excludes invited users' do
- member1 = project.add_maintainer(user2)
- create(:project_member, :invited, project: project, invite_email: create(:user).email)
- project.add_maintainer(blocked_user)
+ it 'returns active users and excludes invited users' do
+ member1 = project.add_maintainer(user2)
+ create(:project_member, :invited, project: project, invite_email: create(:user).email)
+ project.add_maintainer(blocked_user)
- result = described_class.new(project, user2, params: { active_without_invites_and_requests: true }).execute
+ result = described_class.new(project, user2, params: { active_without_invites_and_requests: true }).execute
- expect(result).to contain_exactly(member1)
- end
+ expect(result).to contain_exactly(member1)
+ end
- it 'does not return members of parent group with minimal access' do
- nested_group.request_access(user1)
- member1 = group.add_maintainer(user2)
- member2 = nested_group.add_maintainer(user3)
- member3 = project.add_maintainer(user4)
- create(:group_member, :minimal_access, user: create(:user), source: group)
+ it 'does not return members of parent group with minimal access' do
+ nested_group.request_access(user1)
+ member1 = group.add_maintainer(user2)
+ member2 = nested_group.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
+ create(:group_member, :minimal_access, user: create(:user), source: group)
- result = described_class.new(project, user2).execute
+ result = described_class.new(project, user2).execute
- expect(result).to contain_exactly(member1, member2, member3)
- end
+ expect(result).to contain_exactly(member1, member2, member3)
+ end
- it 'includes only non-invite members if user do not have amdin permissions on project' do
- create(:project_member, :invited, project: project, invite_email: create(:user).email)
- member1 = project.add_maintainer(user1)
- member2 = project.add_developer(user2)
+ it 'includes only non-invite members if user do not have amdin permissions on project' do
+ create(:project_member, :invited, project: project, invite_email: create(:user).email)
+ member1 = project.add_maintainer(user1)
+ member2 = project.add_developer(user2)
- result = described_class.new(project, user2).execute(include_relations: [:direct])
+ result = described_class.new(project, user2).execute(include_relations: [:direct])
- expect(result).to contain_exactly(member1, member2)
- end
+ expect(result).to contain_exactly(member1, member2)
+ end
- it 'includes invited members if user have admin permissions on project' do
- member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email)
- member1 = project.add_maintainer(user1)
- member2 = project.add_maintainer(user2)
+ it 'includes invited members if user have admin permissions on project' do
+ member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email)
+ member1 = project.add_maintainer(user1)
+ member2 = project.add_maintainer(user2)
- result = described_class.new(project, user2).execute(include_relations: [:direct])
+ result = described_class.new(project, user2).execute(include_relations: [:direct])
- expect(result).to contain_exactly(member1, member2, member_invite)
- end
+ expect(result).to contain_exactly(member1, member2, member_invite)
+ end
- it 'includes nested group members if asked', :nested_groups do
- nested_group.request_access(user1)
- member1 = group.add_maintainer(user2)
- member2 = nested_group.add_maintainer(user3)
- member3 = project.add_maintainer(user4)
+ it 'includes nested group members if asked', :nested_groups do
+ nested_group.request_access(user1)
+ member1 = group.add_maintainer(user2)
+ member2 = nested_group.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
- result = described_class.new(project, user2).execute(include_relations: [:direct, :descendants])
+ result = described_class.new(project, user2).execute(include_relations: [:direct, :descendants])
- expect(result).to contain_exactly(member1, member2, member3)
- end
+ expect(result).to contain_exactly(member1, member2, member3)
+ end
- it 'returns only members of project if asked' do
- nested_group.request_access(user1)
- group.add_maintainer(user2)
- nested_group.add_maintainer(user3)
- member4 = project.add_maintainer(user4)
+ it 'returns only members of project if asked' do
+ nested_group.request_access(user1)
+ group.add_maintainer(user2)
+ nested_group.add_maintainer(user3)
+ member4 = project.add_maintainer(user4)
- result = described_class.new(project, user2).execute(include_relations: [:direct])
+ result = described_class.new(project, user2).execute(include_relations: [:direct])
- expect(result).to contain_exactly(member4)
- end
+ expect(result).to contain_exactly(member4)
+ end
- it 'returns only inherited members of project if asked' do
- nested_group.request_access(user1)
- member2 = group.add_maintainer(user2)
- member3 = nested_group.add_maintainer(user3)
- project.add_maintainer(user4)
+ it 'returns only inherited members of project if asked' do
+ nested_group.request_access(user1)
+ member2 = group.add_maintainer(user2)
+ member3 = nested_group.add_maintainer(user3)
+ project.add_maintainer(user4)
- result = described_class.new(project, user2).execute(include_relations: [:inherited])
+ result = described_class.new(project, user2).execute(include_relations: [:inherited])
- expect(result).to contain_exactly(member2, member3)
- end
+ expect(result).to contain_exactly(member2, member3)
+ end
- it 'returns only inherited members of a personal project' do
- project = create(:project, namespace: user1.namespace)
- member = project.members.first
+ it 'returns only inherited members of a personal project' do
+ project = create(:project, namespace: user1.namespace)
+ member = project.members.first
- result = described_class.new(project, user1).execute(include_relations: [:inherited])
+ result = described_class.new(project, user1).execute(include_relations: [:inherited])
- expect(result).to contain_exactly(member)
- end
+ expect(result).to contain_exactly(member)
+ end
- it 'returns the members.access_level when the user is invited', :nested_groups do
- member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email)
- member1 = group.add_maintainer(user2)
+ it 'returns the members.access_level when the user is invited', :nested_groups do
+ member_invite = create(:project_member, :invited, project: project, invite_email: create(:user).email)
+ member1 = group.add_maintainer(user2)
- result = described_class.new(project, user2).execute(include_relations: [:direct, :descendants])
+ result = described_class.new(project, user2).execute(include_relations: [:direct, :descendants])
- expect(result).to contain_exactly(member1, member_invite)
- expect(result.last.access_level).to eq(member_invite.access_level)
- end
+ expect(result).to contain_exactly(member1, member_invite)
+ expect(result.last.access_level).to eq(member_invite.access_level)
+ end
- it 'returns the highest access_level for the user', :nested_groups do
- member1 = project.add_guest(user1)
- group.add_developer(user1)
- nested_group.add_reporter(user1)
+ it 'returns the highest access_level for the user', :nested_groups do
+ member1 = project.add_guest(user1)
+ group.add_developer(user1)
+ nested_group.add_reporter(user1)
- result = described_class.new(project, user1).execute(include_relations: [:direct, :descendants])
+ result = described_class.new(project, user1).execute(include_relations: [:direct, :descendants])
- expect(result).to contain_exactly(member1)
- expect(result.first.access_level).to eq(Gitlab::Access::DEVELOPER)
- end
+ expect(result).to contain_exactly(member1)
+ expect(result.first.access_level).to eq(Gitlab::Access::DEVELOPER)
+ end
- it 'returns searched members if requested' do
- project.add_maintainer(user2)
- project.add_maintainer(user3)
- member3 = project.add_maintainer(user4)
+ it 'returns searched members if requested' do
+ project.add_maintainer(user2)
+ project.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
- result = described_class.new(project, user2, params: { search: user4.name }).execute
+ result = described_class.new(project, user2, params: { search: user4.name }).execute
- expect(result).to contain_exactly(member3)
- end
+ expect(result).to contain_exactly(member3)
+ end
- it 'returns members sorted by id_desc' do
- member1 = project.add_maintainer(user2)
- member2 = project.add_maintainer(user3)
- member3 = project.add_maintainer(user4)
+ it 'returns members sorted by id_desc' do
+ member1 = project.add_maintainer(user2)
+ member2 = project.add_maintainer(user3)
+ member3 = project.add_maintainer(user4)
- result = described_class.new(project, user2, params: { sort: 'id_desc' }).execute
+ result = described_class.new(project, user2, params: { sort: 'id_desc' }).execute
- expect(result).to eq([member3, member2, member1])
- end
+ expect(result).to eq([member3, member2, member1])
+ end
- it 'avoids N+1 database queries on accessing user records' do
- project.add_maintainer(user2)
+ it 'avoids N+1 database queries on accessing user records' do
+ project.add_maintainer(user2)
- # warm up
- # We need this warm up because there is 1 query being fired in one of the policies,
- # and policy results are cached. Without a warm up, the control.count will be X queries
- # but the test phase will only fire X-1 queries, due the fact that the
- # result of the policy is already available in the cache.
+ # warm up
+ # We need this warm up because there is 1 query being fired in one of the policies,
+ # and policy results are cached. Without a warm up, the control.count will be X queries
+ # but the test phase will only fire X-1 queries, due the fact that the
+ # result of the policy is already available in the cache.
+ described_class.new(project, user2).execute.map(&:user)
+
+ control = ActiveRecord::QueryRecorder.new do
described_class.new(project, user2).execute.map(&:user)
-
- control = ActiveRecord::QueryRecorder.new do
- described_class.new(project, user2).execute.map(&:user)
- end
-
- create_list(:project_member, 3, project: project)
-
- expect do
- described_class.new(project, user2).execute.map(&:user)
- end.to issue_same_number_of_queries_as(control)
end
- context 'with :shared_into_ancestors' do
- let_it_be(:invited_group) do
- create(:group).tap do |invited_group|
- create(:group_group_link, shared_group: nested_group, shared_with_group: invited_group)
- end
- end
+ create_list(:project_member, 3, project: project)
- let_it_be(:invited_group_member) { create(:group_member, :developer, group: invited_group, user: user1) }
- let_it_be(:namespace_parent_member) { create(:group_member, :owner, group: group, user: user2) }
- let_it_be(:namespace_member) { create(:group_member, :developer, group: nested_group, user: user3) }
- let_it_be(:project_member) { create(:project_member, :developer, project: project, user: user4) }
+ expect do
+ described_class.new(project, user2).execute.map(&:user)
+ end.to issue_same_number_of_queries_as(control)
+ end
- subject(:result) { described_class.new(project, user4).execute(include_relations: include_relations) }
-
- context 'when :shared_into_ancestors is included in the relations' do
- let(:include_relations) { [:inherited, :direct, :invited_groups, :shared_into_ancestors] }
-
- it "includes members of groups invited into ancestors of project's group" do
- expect(result).to match_array([namespace_parent_member, namespace_member, invited_group_member, project_member])
- end
- end
-
- context 'when :shared_into_ancestors is not included in the relations' do
- let(:include_relations) { [:inherited, :direct, :invited_groups] }
-
- it "does not include members of groups invited into ancestors of project's group" do
- expect(result).to match_array([namespace_parent_member, namespace_member, project_member])
- end
+ context 'with :shared_into_ancestors' do
+ let_it_be(:invited_group) do
+ create(:group).tap do |invited_group|
+ create(:group_group_link, shared_group: nested_group, shared_with_group: invited_group)
end
end
- context 'when :invited_groups is passed' do
- shared_examples 'with invited_groups param' do
- subject { described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups]) }
+ let_it_be(:invited_group_member) { create(:group_member, :developer, group: invited_group, user: user1) }
+ let_it_be(:namespace_parent_member) { create(:group_member, :owner, group: group, user: user2) }
+ let_it_be(:namespace_member) { create(:group_member, :developer, group: nested_group, user: user3) }
+ let_it_be(:project_member) { create(:project_member, :developer, project: project, user: user4) }
- let_it_be(:linked_group) { create(:group, :public) }
- let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
- let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
- let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
+ subject(:result) { described_class.new(project, user4).execute(include_relations: include_relations) }
- it 'includes all the invited_groups members including members inherited from ancestor groups' do
- create(:project_group_link, project: project, group: nested_linked_group)
+ context 'when :shared_into_ancestors is included in the relations' do
+ let(:include_relations) { [:inherited, :direct, :invited_groups, :shared_into_ancestors] }
- expect(subject).to contain_exactly(linked_group_member, nested_linked_group_member)
- end
-
- it 'includes all the invited_groups members' do
- create(:project_group_link, project: project, group: linked_group)
-
- expect(subject).to contain_exactly(linked_group_member)
- end
-
- it 'excludes group_members not visible to the user' do
- create(:project_group_link, project: project, group: linked_group)
- private_linked_group = create(:group, :private)
- private_linked_group.add_developer(user3)
- create(:project_group_link, project: project, group: private_linked_group)
-
- expect(subject).to contain_exactly(linked_group_member)
- end
-
- context 'when the user is a member of invited group and ancestor groups' do
- it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
- create(:project_group_link, project: project, group: nested_linked_group, group_access: Gitlab::Access::REPORTER)
- nested_linked_group.add_developer(user1)
-
- expect(subject.map(&:user)).to contain_exactly(user1, user2)
- expect(subject.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
- end
- end
+ it "includes members of groups invited into ancestors of project's group" do
+ expect(result).to match_array([namespace_parent_member, namespace_member, invited_group_member, project_member])
end
+ end
- it_behaves_like 'with invited_groups param'
+ context 'when :shared_into_ancestors is not included in the relations' do
+ let(:include_relations) { [:inherited, :direct, :invited_groups] }
+
+ it "does not include members of groups invited into ancestors of project's group" do
+ expect(result).to match_array([namespace_parent_member, namespace_member, project_member])
+ end
end
end
- it_behaves_like '#execute'
+ context 'when :invited_groups is passed' do
+ subject(:members) do
+ described_class.new(project, user2).execute(include_relations: [:inherited, :direct, :invited_groups])
+ end
+
+ let_it_be(:linked_group) { create(:group, :public) }
+ let_it_be(:nested_linked_group) { create(:group, parent: linked_group) }
+ let_it_be(:linked_group_member) { linked_group.add_guest(user1) }
+ let_it_be(:nested_linked_group_member) { nested_linked_group.add_guest(user2) }
+
+ it 'includes all the invited_groups members including members inherited from ancestor groups' do
+ create(:project_group_link, project: project, group: nested_linked_group)
+
+ expect(members).to contain_exactly(linked_group_member, nested_linked_group_member)
+ end
+
+ it 'includes all the invited_groups members' do
+ create(:project_group_link, project: project, group: linked_group)
+
+ expect(members).to contain_exactly(linked_group_member)
+ end
+
+ it 'excludes group_members not visible to the user' do
+ create(:project_group_link, project: project, group: linked_group)
+ private_linked_group = create(:group, :private)
+ private_linked_group.add_developer(user3)
+ create(:project_group_link, project: project, group: private_linked_group)
+
+ expect(members).to contain_exactly(linked_group_member)
+ end
+
+ context 'when current user is a member of the shared project but not of invited group' do
+ let_it_be(:project_member) { project.add_maintainer(user2) }
+ let_it_be(:private_linked_group) { create(:group, :private) }
+ let_it_be(:private_linked_group_member) { private_linked_group.add_developer(user3) }
+
+ before_all do
+ create(:project_group_link, project: project, group: private_linked_group)
+ create(:project_group_link, project: project, group: linked_group)
+ end
+
+ it 'includes members from invited groups not visible to the user' do
+ expect(members).to contain_exactly(linked_group_member, private_linked_group_member, project_member)
+ end
+
+ context 'when webui_members_inherited_users feature flag is disabled' do
+ before do
+ stub_feature_flags(webui_members_inherited_users: false)
+ end
+
+ it 'excludes members from invited groups not visible to the user' do
+ expect(members).to contain_exactly(linked_group_member, project_member)
+ end
+ end
+ end
+
+ context 'when the user is a member of invited group and ancestor groups' do
+ it 'returns the highest access_level for the user limited by project_group_link.group_access', :nested_groups do
+ create(:project_group_link, project: project, group: nested_linked_group,
+ group_access: Gitlab::Access::REPORTER)
+ nested_linked_group.add_developer(user1)
+
+ expect(members.map(&:user)).to contain_exactly(user1, user2)
+ expect(members.max_by(&:access_level).access_level).to eq(Gitlab::Access::REPORTER)
+ end
+ end
+ end
end
diff --git a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 95fa82adc9e..666d42e56b2 100644
--- a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
@@ -53,8 +53,9 @@ describe('Pipelines stage component', () => {
const findCiActionBtn = () => wrapper.find('.js-ci-action');
const findCiIcon = () => wrapper.findComponent(CiIcon);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownToggle = () =>
+ wrapper.find('[data-testid="mini-pipeline-graph-dropdown-toggle"]');
const findDropdownMenu = () =>
wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
const findDropdownMenuTitle = () =>
@@ -78,8 +79,9 @@ describe('Pipelines stage component', () => {
});
it('displays loading state while jobs are being fetched', async () => {
- jest.runOnlyPendingTimers();
- await nextTick();
+ // eslint-disable-next-line no-restricted-syntax
+ wrapper.setData({ isLoading: true });
+ await waitForPromises();
expect(findLoadingState().exists()).toBe(true);
expect(findLoadingState().text()).toBe(LegacyPipelineStage.i18n.loadingText);
@@ -144,7 +146,7 @@ describe('Pipelines stage component', () => {
await axios.waitForAll();
await waitForPromises();
- expect(findDropdown().classes('show')).toBe(false);
+ expect(findDropdownToggle().attributes('aria-expanded')).toBe('false');
});
});
@@ -197,7 +199,7 @@ describe('Pipelines stage component', () => {
await clickCiAction();
await waitForPromises();
- expect(findDropdown().classes('show')).toBe(true);
+ expect(findDropdownToggle().attributes('aria-expanded')).toBe('true');
});
});
diff --git a/spec/frontend/ci/pipelines_page/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index f3c28b17339..6a87e6a6dd5 100644
--- a/spec/frontend/ci/pipelines_page/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -96,7 +96,7 @@ describe('Pipelines', () => {
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
const findStagesDropdownToggle = () =>
- wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle');
+ wrapper.find('.mini-pipeline-graph-dropdown [data-testid="base-dropdown-toggle"]');
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
const createComponent = ({ props = {}, withPermissions = true } = {}) => {
@@ -769,6 +769,11 @@ describe('Pipelines', () => {
.onGet(mockPipelineWithStages.details.stages[0].dropdown_path)
.reply(HTTP_STATUS_OK, stageReply);
+ // cancelMock is getting overwritten in pipelines_service.js#L29
+ // so we have to spy on it again here
+ cancelMock = { cancel: jest.fn() };
+ jest.spyOn(axios.CancelToken, 'source').mockReturnValue(cancelMock);
+
createComponent();
stopMock = jest.spyOn(window, 'clearTimeout');
@@ -789,13 +794,9 @@ describe('Pipelines', () => {
await findStagesDropdownToggle().trigger('click');
jest.runOnlyPendingTimers();
- // cancelMock is getting overwritten in pipelines_service.js#L29
- // so we have to spy on it again here
- cancelMock = jest.spyOn(axios.CancelToken, 'source');
-
await waitForPromises();
- expect(cancelMock).toHaveBeenCalled();
+ expect(cancelMock.cancel).toHaveBeenCalled();
expect(stopMock).toHaveBeenCalled();
expect(restartMock).toHaveBeenCalledWith(
`${mockPipelinesResponse.pipelines[0].path}/stage.json?stage=build`,
@@ -807,7 +808,7 @@ describe('Pipelines', () => {
jest.runOnlyPendingTimers();
await waitForPromises();
- expect(cancelMock).not.toHaveBeenCalled();
+ expect(cancelMock.cancel).not.toHaveBeenCalled();
expect(stopMock).toHaveBeenCalled();
expect(restartMock).toHaveBeenCalledWith(
`${mockPipelinesResponse.pipelines[0].path}/stage.json?stage=build`,
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 0f30c850020..fe66aa31568 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -38,6 +38,7 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import getStateSubscription from '~/vue_merge_request_widget/queries/get_state.subscription.graphql';
import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
+import securityReportMergeRequestDownloadPathsQuery from '~/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql';
import readyToMergeQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/ready_to_merge.query.graphql';
import approvalsQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.query.graphql';
import approvedBySubscription from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approvals.subscription.graphql';
@@ -123,6 +124,8 @@ describe('MrWidgetOptions', () => {
conflictsStateQuery,
jest.fn().mockResolvedValue({ data: { project: { mergeRequest: {} } } }),
],
+ [securityReportMergeRequestDownloadPathsQuery, jest.fn().mockResolvedValue(null)],
+ ...(options.apolloMock || []),
];
const subscriptionHandlers = [
[approvedBySubscription, () => mockedApprovalsSubscription],
diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb
index 7c626743f3a..4b87df0a28f 100644
--- a/spec/helpers/integrations_helper_spec.rb
+++ b/spec/helpers/integrations_helper_spec.rb
@@ -222,15 +222,16 @@ RSpec.describe IntegrationsHelper, feature_category: :integrations do
end
describe '#add_to_slack_link' do
- let(:slack_link) { helper.add_to_slack_link(project, 'A12345') }
+ subject(:slack_link) { helper.add_to_slack_link(project, 'A12345') }
+
let(:query) { Rack::Utils.parse_query(URI.parse(slack_link).query) }
before do
allow(helper).to receive(:form_authenticity_token).and_return('a token')
- allow(helper).to receive(:slack_auth_project_settings_slack_url).and_return('http://redirect')
end
it 'returns the endpoint URL with all needed params' do
+ expect(helper).to receive(:slack_auth_project_settings_slack_url).and_return('http://redirect')
expect(slack_link).to start_with(Projects::SlackApplicationInstallService::SLACK_AUTHORIZE_URL)
expect(slack_link).to include('&state=a+token')
@@ -241,6 +242,55 @@ RSpec.describe IntegrationsHelper, feature_category: :integrations do
'state' => 'a token'
)
end
+
+ context 'when passed a group' do
+ subject(:slack_link) { helper.add_to_slack_link(build_stubbed(:group), 'A12345') }
+
+ it 'returns the endpoint URL a redirect-uri for the group' do
+ expect(helper).to receive(:slack_auth_group_settings_slack_url).and_return('http://group-redirect')
+ expect(query).to include('redirect_uri' => 'http://group-redirect')
+ end
+ end
+
+ context 'when passed nil' do
+ subject(:slack_link) { helper.add_to_slack_link(nil, 'A12345') }
+
+ it 'returns the endpoint URL a redirect-uri for the instance' do
+ expect(helper).to receive(:slack_auth_admin_application_settings_slack_url).and_return('http://instance-redirect')
+ expect(query).to include('redirect_uri' => 'http://instance-redirect')
+ end
+ end
+ end
+
+ describe '#slack_integration_destroy_path' do
+ subject(:destroy_path) { helper.slack_integration_destroy_path(parent) }
+
+ context 'when parent is a project' do
+ let(:parent) { project }
+
+ it 'returns the correct path' do
+ expect(helper).to receive(:project_settings_slack_path).and_return('http://project-redirect')
+ expect(destroy_path).to eq('http://project-redirect')
+ end
+ end
+
+ context 'when parent is a group' do
+ let(:parent) { build_stubbed(:group) }
+
+ it 'returns the endpoint URL a redirect-uri for the group' do
+ expect(helper).to receive(:group_settings_slack_path).and_return('http://group-redirect')
+ expect(destroy_path).to eq('http://group-redirect')
+ end
+ end
+
+ context 'when parent is nil' do
+ let(:parent) { nil }
+
+ it 'returns the endpoint URL a redirect-uri for the instance' do
+ expect(helper).to receive(:admin_application_settings_slack_path).and_return('http://instance-redirect')
+ expect(destroy_path).to eq('http://instance-redirect')
+ end
+ end
end
describe '#gitlab_slack_application_data' do
diff --git a/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb
index edb6ff59340..b14adbc46f4 100644
--- a/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_cluster_agents_has_vulnerabilities_spec.rb
@@ -2,7 +2,8 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration do # rubocop:disable Layout/LineLength
+RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilities, :migration,
+ feature_category: :vulnerability_management do
let(:migration) do
described_class.new(
start_id: 1, end_id: 10,
@@ -13,6 +14,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilit
end
let(:users_table) { table(:users) }
+ let(:vulnerability_identifiers_table) { table(:vulnerability_identifiers) }
+ let(:vulnerability_occurrences_table) { table(:vulnerability_occurrences) }
let(:vulnerability_reads_table) { table(:vulnerability_reads) }
let(:vulnerability_scanners_table) { table(:vulnerability_scanners) }
let(:vulnerabilities_table) { table(:vulnerabilities) }
@@ -84,6 +87,17 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilit
private
def add_vulnerability_read!(id, project_id:, cluster_agent_id:, report_type:)
+ identifier = vulnerability_identifiers_table.create!(project_id: project_id, external_type: 'uuid-v5',
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest.hexdigest('SHA256', SecureRandom.uuid),
+ name: "Identifier for UUIDv5 #{project_id} #{cluster_agent_id}")
+
+ finding = vulnerability_occurrences_table.create!(
+ project_id: project_id, scanner_id: project_id,
+ primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
+ uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc',
+ location_fingerprint: 'test', metadata_version: 'test',
+ raw_metadata: "")
+
vulnerabilities_table.create!(
id: id,
project_id: project_id,
@@ -91,7 +105,8 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillClusterAgentsHasVulnerabilit
title: "Vulnerability #{id}",
severity: 5,
confidence: 5,
- report_type: report_type
+ report_type: report_type,
+ finding_id: finding.id
)
vulnerability_reads_table.create!(
diff --git a/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb
index 0e7a0210758..bce8e1e095d 100644
--- a/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_has_remediations_of_vulnerability_reads_spec.rb
@@ -16,8 +16,11 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillHasRemediationsOfVulnerabili
let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) }
let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') }
- let(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1') }
- let(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2') }
+ let!(:vuln_finding_1) { create_finding }
+ let!(:vuln_finding_2) { create_finding }
+
+ let!(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1', finding_id: vuln_finding_1.id) }
+ let!(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2', finding_id: vuln_finding_2.id) }
let!(:vulnerability_read_1) { create_vulnerability_read(vulnerability_id: vulnerability_1.id) }
let!(:vulnerability_read_2) { create_vulnerability_read(vulnerability_id: vulnerability_2.id) }
@@ -25,6 +28,7 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillHasRemediationsOfVulnerabili
let(:vulnerability_findings) { table(:vulnerability_occurrences) }
let(:vulnerability_findings_remediations) { table(:vulnerability_findings_remediations) }
let(:vulnerability_remediations) { table(:vulnerability_remediations) }
+ let(:vuln_remediation_1) { create_remediation }
let(:vulnerability_identifiers) { table(:vulnerability_identifiers) }
subject(:perform_migration) do
@@ -39,16 +43,25 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillHasRemediationsOfVulnerabili
).perform
end
- it 'updates vulnerability_reads records which has remediations' do
- vuln_remediation = create_remediation
- vuln_finding = create_finding(vulnerability_id: vulnerability_1.id)
- vulnerability_findings_remediations.create!(
- vulnerability_occurrence_id: vuln_finding.id,
- vulnerability_remediation_id: vuln_remediation.id
- )
+ before do
+ vuln_finding_1.update!(vulnerability_id: vulnerability_1.id)
+ vuln_finding_2.update!(vulnerability_id: vulnerability_2.id)
+ vulnerability_read_1.update!(vulnerability_id: vulnerability_1.id)
+ vulnerability_read_2.update!(vulnerability_id: vulnerability_2.id)
+ end
- expect { perform_migration }.to change { vulnerability_read_1.reload.has_remediations }.from(false).to(true)
- .and not_change { vulnerability_read_2.reload.has_remediations }.from(false)
+ context "when finding_remediation record exists" do
+ let!(:finding_remediation) do
+ vulnerability_findings_remediations.create!(
+ vulnerability_occurrence_id: vuln_finding_1.id,
+ vulnerability_remediation_id: vuln_remediation_1.id
+ )
+ end
+
+ it 'updates vulnerability_reads records which has remediations' do
+ expect { perform_migration }.to change { vulnerability_read_1.reload.has_remediations }.from(false).to(true)
+ .and not_change { vulnerability_read_2.reload.has_remediations }.from(false)
+ end
end
it 'does not modify has_remediations of vulnerabilities which do not have remediations' do
diff --git a/spec/lib/gitlab/background_migration/backfill_owasp_top_ten_of_vulnerability_reads_spec.rb b/spec/lib/gitlab/background_migration/backfill_owasp_top_ten_of_vulnerability_reads_spec.rb
index 1462848845e..37c9219cca1 100644
--- a/spec/lib/gitlab/background_migration/backfill_owasp_top_ten_of_vulnerability_reads_spec.rb
+++ b/spec/lib/gitlab/background_migration/backfill_owasp_top_ten_of_vulnerability_reads_spec.rb
@@ -19,29 +19,43 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillOwaspTopTenOfVulnerabilityRe
let(:user) { users.create!(username: 'john_doe', email: 'johndoe@gitlab.com', projects_limit: 10) }
let(:scanner) { scanners.create!(project_id: project.id, external_id: 'external_id', name: 'Test Scanner') }
- shared_context 'with vulnerability data' do
+ shared_context 'with vulnerability data' do # rubocop:disable RSpec/MultipleMemoizedHelpers -- we need to satifsy foreign keys
let(:external_id) { '' }
let(:external_type) { '' }
let(:identifier_name) { '' }
- let(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1') }
- let(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2') }
- let(:vulnerability_3) { create_vulnerability(title: 'vulnerability 3') }
-
let(:vuln_identifier) do
create_identifier(external_id: external_id, external_type: external_type, name: identifier_name)
end
- let(:vuln_finding) do
- create_finding(vulnerability_id: vulnerability_1.id, primary_identifier_id: vuln_identifier.id)
+ let(:vuln_identifier_2) { create_identifier(external_id: 'A1:2021', external_type: 'owasp', name: 'A1 2021') }
+ let(:vuln_identifier_3) { create_identifier }
+
+ let(:vuln_finding_1) do
+ create_finding(primary_identifier_id: vuln_identifier.id)
end
+ let(:vuln_finding_2) do
+ create_finding(primary_identifier_id: vuln_identifier_2.id)
+ end
+
+ let(:vuln_finding_3) do
+ create_finding(primary_identifier_id: vuln_identifier_3.id)
+ end
+
+ let(:vulnerability_1) { create_vulnerability(title: 'vulnerability 1', finding_id: vuln_finding_1.id) }
+ let(:vulnerability_2) { create_vulnerability(title: 'vulnerability 2', finding_id: vuln_finding_2.id) }
+ let(:vulnerability_3) { create_vulnerability(title: 'vulnerability 3', finding_id: vuln_finding_3.id) }
+
let!(:vulnerability_read_1) { create_vulnerability_read(vulnerability_id: vulnerability_1.id) }
let!(:vulnerability_read_2) { create_vulnerability_read(vulnerability_id: vulnerability_2.id) }
let!(:vulnerability_read_3) { create_vulnerability_read(vulnerability_id: vulnerability_3.id) }
before do
- create_vulnerability_occurrence_identifier(occurrence_id: vuln_finding.id, identifier_id: vuln_identifier.id)
+ create_vulnerability_occurrence_identifier(occurrence_id: vuln_finding_1.id, identifier_id: vuln_identifier.id)
+ vuln_finding_1.update!(vulnerability_id: vulnerability_1.id)
+ vuln_finding_2.update!(vulnerability_id: vulnerability_2.id)
+ vuln_finding_3.update!(vulnerability_id: vulnerability_3.id)
end
end
@@ -72,13 +86,6 @@ RSpec.describe Gitlab::BackgroundMigration::BackfillOwaspTopTenOfVulnerabilityRe
end
it 'updates vulnerability_reads with correct mapping' do
- vuln_identifier_2 = create_identifier(external_id: 'A1:2021', external_type: 'owasp', name: 'A1 2021')
- vuln_identifier_3 = create_identifier
- vuln_finding_2 = create_finding(vulnerability_id: vulnerability_2.id,
- primary_identifier_id: vuln_identifier_2.id)
- vuln_finding_3 = create_finding(vulnerability_id: vulnerability_3.id,
- primary_identifier_id: vuln_identifier_3.id)
-
create_vulnerability_occurrence_identifier(occurrence_id: vuln_finding_2.id,
identifier_id: vuln_identifier_2.id)
create_vulnerability_occurrence_identifier(occurrence_id: vuln_finding_3.id,
diff --git a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
index 4a1985eeccd..0860e4b5cd1 100644
--- a/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
+++ b/spec/lib/gitlab/background_migration/delete_orphaned_operational_vulnerabilities_spec.rb
@@ -35,18 +35,29 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
)
end
+ let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
+ let!(:finding) do
+ create_finding!(
+ project_id: project.id,
+ scanner_id: scanner.id,
+ primary_identifier_id: primary_identifier.id
+ )
+ end
+
let!(:vulnerabilities) { table(:vulnerabilities) }
let!(:vulnerability_with_finding) do
create_vulnerability!(
project_id: project.id,
- author_id: user.id
+ author_id: user.id,
+ finding_id: finding.id
)
end
let!(:vulnerability_without_finding) do
create_vulnerability!(
project_id: project.id,
- author_id: user.id
+ author_id: user.id,
+ finding_id: finding.id
)
end
@@ -54,7 +65,8 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
create_vulnerability!(
project_id: project.id,
author_id: user.id,
- report_type: 7
+ report_type: 7,
+ finding_id: finding.id
)
end
@@ -62,7 +74,8 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
create_vulnerability!(
project_id: project.id,
author_id: user.id,
- report_type: 99
+ report_type: 99,
+ finding_id: finding.id
)
end
@@ -76,16 +89,6 @@ RSpec.describe Gitlab::BackgroundMigration::DeleteOrphanedOperationalVulnerabili
name: 'Identifier for UUIDv5')
end
- let!(:vulnerabilities_findings) { table(:vulnerability_occurrences) }
- let!(:finding) do
- create_finding!(
- vulnerability_id: vulnerability_with_finding.id,
- project_id: project.id,
- scanner_id: scanner.id,
- primary_identifier_id: primary_identifier.id
- )
- end
-
subject(:background_migration) do
described_class.new(
start_id: vulnerabilities.minimum(:id),
diff --git a/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb
index ba2f571f5aa..dd150a8b857 100644
--- a/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_evidences_for_vulnerability_findings_spec.rb
@@ -102,18 +102,18 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateEvidencesForVulnerabilityFind
private
def create_finding!(project_id, scanner_id, raw_metadata)
- vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
- severity: 4, confidence: 4, report_type: 0)
-
identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
- external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(SecureRandom.uuid),
name: 'Identifier for UUIDv5 2 2')
-
- table(:vulnerability_occurrences).create!(
- vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ finding = table(:vulnerability_occurrences).create!(
+ project_id: project_id, scanner_id: scanner_id,
primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
location_fingerprint: 'test', metadata_version: 'test',
raw_metadata: raw_metadata.to_json)
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0, finding_id: finding.id)
+ finding.update!(vulnerability_id: vulnerability.id)
+ finding
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb
index b973f9d4350..36105e17441 100644
--- a/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_links_for_vulnerability_findings_spec.rb
@@ -175,18 +175,22 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateLinksForVulnerabilityFindings
private
def create_finding!(project_id, scanner_id, raw_metadata)
- vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
- severity: 4, confidence: 4, report_type: 0)
-
identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
- external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(SecureRandom.uuid),
name: 'Identifier for UUIDv5 2 2')
- table(:vulnerability_occurrences).create!(
- vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ finding = table(:vulnerability_occurrences).create!(
+ project_id: project_id, scanner_id: scanner_id,
primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
location_fingerprint: 'test', metadata_version: 'test',
raw_metadata: raw_metadata.to_json)
+
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0, finding_id: finding.id)
+
+ finding.update!(vulnerability_id: vulnerability.id)
+
+ finding
end
end
diff --git a/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb b/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb
index b75c0e61b19..7a641d6f8d7 100644
--- a/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_remediations_for_vulnerability_findings_spec.rb
@@ -151,19 +151,23 @@ RSpec.describe Gitlab::BackgroundMigration::MigrateRemediationsForVulnerabilityF
private
def create_finding!(project_id, scanner_id, raw_metadata)
- vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
- severity: 4, confidence: 4, report_type: 0)
-
identifier = table(:vulnerability_identifiers).create!(project_id: project_id, external_type: 'uuid-v5',
- external_id: 'uuid-v5', fingerprint: OpenSSL::Digest::SHA256.hexdigest(vulnerability.id.to_s),
+ external_id: 'uuid-v5', fingerprint: OpenSSL::Digest.hexdigest('SHA256', SecureRandom.uuid),
name: 'Identifier for UUIDv5 2 2')
- table(:vulnerability_occurrences).create!(
- vulnerability_id: vulnerability.id, project_id: project_id, scanner_id: scanner_id,
+ finding = table(:vulnerability_occurrences).create!(
+ project_id: project_id, scanner_id: scanner_id,
primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc', location: { "image" => "alpine:3.4" },
location_fingerprint: 'test', metadata_version: 'test',
raw_metadata: raw_metadata.to_json)
+
+ vulnerability = table(:vulnerabilities).create!(project_id: project_id, author_id: user.id, title: 'test',
+ severity: 4, confidence: 4, report_type: 0, finding_id: finding.id)
+
+ finding.update!(vulnerability_id: vulnerability.id)
+
+ finding
end
def checksum(value)
diff --git a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
index df1ee494987..50fbb2500ca 100644
--- a/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
+++ b/spec/lib/gitlab/background_migration/set_correct_vulnerability_state_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
+RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState, feature_category: :vulnerability_management do
let(:namespace) { table(:namespaces).create!(name: 'user', path: 'user') }
let(:users) { table(:users) }
let(:user) { create_user! }
@@ -13,8 +13,12 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
packages_enabled: false)
end
+ let(:identifiers) { table(:vulnerability_identifiers) }
+ let(:findings) { table(:vulnerability_occurrences) }
let(:vulnerabilities) { table(:vulnerabilities) }
+ let!(:scanner) { create_scanner!(project_id: project.id) }
+
let!(:vulnerability_with_dismissed_at) do
create_vulnerability!(
project_id: project.id,
@@ -66,9 +70,32 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
private
+ def create_scanner!(project_id:)
+ table(:vulnerability_scanners).create!(
+ project_id: project_id,
+ external_id: "External ID",
+ name: "Test Scanner"
+ )
+ end
+
def create_vulnerability!(
project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0, state: 1, dismissed_at: nil
)
+ identifier = identifiers.create!(
+ project_id: project_id,
+ external_type: 'uuid-v5',
+ external_id: 'uuid-v5',
+ fingerprint: OpenSSL::Digest.hexdigest('SHA256', SecureRandom.uuid),
+ name: "Identifier for UUIDv5 #{project_id}"
+ )
+
+ finding = findings.create!(
+ project_id: project_id, scanner_id: scanner.id,
+ primary_identifier_id: identifier.id, name: 'test', severity: 4, confidence: 4, report_type: 0,
+ uuid: SecureRandom.uuid, project_fingerprint: '123qweasdzxc',
+ location_fingerprint: 'test', metadata_version: 'test',
+ raw_metadata: "")
+
vulnerabilities.create!(
project_id: project_id,
author_id: author_id,
@@ -77,7 +104,8 @@ RSpec.describe Gitlab::BackgroundMigration::SetCorrectVulnerabilityState do
confidence: confidence,
report_type: report_type,
state: state,
- dismissed_at: dismissed_at
+ dismissed_at: dismissed_at,
+ finding_id: finding.id
)
end
diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb
index 8e7729b1468..be4df0ffe89 100644
--- a/spec/models/integration_spec.rb
+++ b/spec/models/integration_spec.rb
@@ -659,6 +659,12 @@ RSpec.describe Integration, feature_category: :integrations do
.to raise_exception(described_class::UnknownType, /foo/)
end
+ it 'does not raise an error if the name is a disabled integration' do
+ allow(described_class).to receive(:disabled_integration_names).and_return(['asana'])
+
+ expect { described_class.integration_name_to_type('asana') }.not_to raise_exception
+ end
+
it 'handles all available_integration_names' do
types = described_class.available_integration_names.map { described_class.integration_name_to_type(_1) }
@@ -1002,10 +1008,11 @@ RSpec.describe Integration, feature_category: :integrations do
subject { described_class.available_integration_names }
before do
- allow(described_class).to receive(:integration_names).and_return(%w[foo])
+ allow(described_class).to receive(:integration_names).and_return(%w[foo disabled])
allow(described_class).to receive(:project_specific_integration_names).and_return(['bar'])
allow(described_class).to receive(:dev_integration_names).and_return(['baz'])
allow(described_class).to receive(:instance_specific_integration_names).and_return(['instance-specific'])
+ allow(described_class).to receive(:disabled_integration_names).and_return(['disabled'])
end
it { is_expected.to include('foo', 'bar', 'baz') }
@@ -1014,28 +1021,34 @@ RSpec.describe Integration, feature_category: :integrations do
subject { described_class.available_integration_names(include_project_specific: false) }
it { is_expected.to include('foo', 'baz', 'instance-specific') }
- it { is_expected.not_to include('bar') }
+ it { is_expected.not_to include('bar', 'disabled') }
end
context 'when `include_dev` is false' do
subject { described_class.available_integration_names(include_dev: false) }
it { is_expected.to include('foo', 'bar', 'instance-specific') }
- it { is_expected.not_to include('baz') }
+ it { is_expected.not_to include('baz', 'disabled') }
end
context 'when `include_instance_specific` is false' do
subject { described_class.available_integration_names(include_instance_specific: false) }
it { is_expected.to include('foo', 'baz', 'bar') }
- it { is_expected.not_to include('instance-specific') }
+ it { is_expected.not_to include('instance-specific', 'disabled') }
+ end
+
+ context 'when `include_disabled` is true' do
+ subject { described_class.available_integration_names(include_disabled: true) }
+
+ it { is_expected.to include('disabled') }
end
end
- describe '.project_specific_integration_names' do
- subject { described_class.project_specific_integration_names }
+ describe '.integration_names' do
+ subject { described_class.integration_names }
- it { is_expected.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES) }
+ it { is_expected.to include(*described_class::INTEGRATION_NAMES) }
it { is_expected.to include('gitlab_slack_application') }
context 'when Rails.env is not test' do
@@ -1051,10 +1064,58 @@ RSpec.describe Integration, feature_category: :integrations do
end
it { is_expected.to include('gitlab_slack_application') }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it { is_expected.not_to include('gitlab_slack_application') }
+ end
end
end
end
+ describe '.project_specific_integration_names' do
+ subject { described_class.project_specific_integration_names }
+
+ it { is_expected.to include(*described_class::PROJECT_SPECIFIC_INTEGRATION_NAMES) }
+ it { is_expected.not_to include('gitlab_slack_application') }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it { is_expected.to include('gitlab_slack_application') }
+
+ context 'when Rails.env is not test' do
+ before do
+ allow(Rails.env).to receive(:test?).and_return(false)
+ end
+
+ it { is_expected.not_to include('gitlab_slack_application') }
+
+ context 'when `slack_app_enabled` setting is enabled' do
+ before do
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ it { is_expected.to include('gitlab_slack_application') }
+ end
+ end
+ end
+
+ context 'when Rails.env is not test and `slack_app_enabled` setting is enabled' do
+ before do
+ allow(Rails.env).to receive(:test?).and_return(false)
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ it { is_expected.not_to include('gitlab_slack_application') }
+ end
+ end
+
describe '#secret_fields' do
it 'returns all fields with type `password`' do
allow(subject).to receive(:fields).and_return(
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 936303d0be6..8890561809e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -6736,26 +6736,7 @@ RSpec.describe Project, factory_default: :keep, feature_category: :groups_and_pr
describe '#disabled_integrations' do
subject { build(:project).disabled_integrations }
- it { is_expected.to include('gitlab_slack_application') }
- it { is_expected.not_to include('slack_slash_commands') }
-
- context 'when slack_app_enabled setting is enabled' do
- before do
- stub_application_setting(slack_app_enabled: true)
- end
-
- it { is_expected.to include('slack_slash_commands') }
- it { is_expected.not_to include('gitlab_slack_application') }
- end
-
- context 'when Rails.env.development?' do
- before do
- allow(Rails.env).to receive(:development?).and_return(true)
- end
-
- it { is_expected.not_to include('slack_slash_commands') }
- it { is_expected.not_to include('gitlab_slack_application') }
- end
+ it { is_expected.to include('zentao') }
end
describe '#find_or_initialize_integration' do
diff --git a/spec/requests/admin/slacks_controller_spec.rb b/spec/requests/admin/slacks_controller_spec.rb
new file mode 100644
index 00000000000..0c260f53e9c
--- /dev/null
+++ b/spec/requests/admin/slacks_controller_spec.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Admin::SlacksController, :enable_admin_mode, feature_category: :integrations do
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ def redirect_url
+ edit_admin_application_settings_integration_path(
+ Integrations::GitlabSlackApplication.to_param
+ )
+ end
+
+ describe 'DELETE destroy' do
+ subject(:destroy!) { delete admin_application_settings_slack_path }
+
+ context 'when user is not an admin' do
+ before_all do
+ sign_in(user)
+ end
+
+ it 'responds with status :not_found' do
+ destroy!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is an admin' do
+ before do
+ sign_in(admin)
+ end
+
+ it 'destroys the record and redirects back to #edit' do
+ create(:gitlab_slack_application_integration, :instance,
+ slack_integration: build(:slack_integration)
+ )
+
+ expect { destroy! }
+ .to change { Integrations::GitlabSlackApplication.for_instance.first&.slack_integration }.to(nil)
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(redirect_url)
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it 'responds with status :not_found' do
+ destroy!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/groups/settings/slacks_controller_spec.rb b/spec/requests/groups/settings/slacks_controller_spec.rb
new file mode 100644
index 00000000000..ea0f1440f8d
--- /dev/null
+++ b/spec/requests/groups/settings/slacks_controller_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Groups::Settings::SlacksController, feature_category: :integrations do
+ let_it_be(:group) { create(:group) }
+ let_it_be(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ stub_application_setting(slack_app_enabled: true)
+ end
+
+ def redirect_url(group)
+ edit_group_settings_integration_path(
+ group,
+ Integrations::GitlabSlackApplication.to_param
+ )
+ end
+
+ describe 'DELETE destroy' do
+ subject(:destroy!) { delete group_settings_slack_path(group) }
+
+ context 'when user is not an admin' do
+ before_all do
+ group.add_developer(user)
+ end
+
+ it 'responds with status :not_found' do
+ destroy!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'when user is an admin' do
+ before_all do
+ group.add_owner(user)
+ end
+
+ it 'destroys the record and redirects back to #edit' do
+ create(:gitlab_slack_application_integration, :group, group: group,
+ slack_integration: build(:slack_integration)
+ )
+
+ expect { destroy! }
+ .to change { Integrations::GitlabSlackApplication.for_group(group).first&.slack_integration }.to(nil)
+ expect(response).to have_gitlab_http_status(:found)
+ expect(response).to redirect_to(redirect_url(group))
+ end
+
+ context 'when the flag is disabled' do
+ before do
+ stub_feature_flags(gitlab_for_slack_app_instance_and_group_level: false)
+ end
+
+ it 'responds with status :not_found' do
+ destroy!
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb b/spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb
index 0a86d7abc83..a12e85ed8e1 100644
--- a/spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb
+++ b/spec/support/helpers/migrations_helpers/vulnerabilities_helper.rb
@@ -4,7 +4,7 @@ module MigrationHelpers
module VulnerabilitiesHelper
# rubocop:disable Metrics/ParameterLists
def create_finding!(
- vulnerability_id:, project_id:, scanner_id:, primary_identifier_id:,
+ project_id:, scanner_id:, primary_identifier_id:, vulnerability_id: nil,
name: "test", severity: 7, confidence: 7, report_type: 0,
project_fingerprint: '123qweasdzxc', location_fingerprint: 'test',
metadata_version: 'test', raw_metadata: 'test', uuid: 'b1cee17e-3d7a-11ed-b878-0242ac120002')
@@ -26,14 +26,16 @@ module MigrationHelpers
end
# rubocop:enable Metrics/ParameterLists
- def create_vulnerability!(project_id:, author_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
+ def create_vulnerability!(
+ project_id:, author_id:, finding_id:, title: 'test', severity: 7, confidence: 7, report_type: 0)
table(:vulnerabilities).create!(
project_id: project_id,
author_id: author_id,
title: title,
severity: severity,
confidence: confidence,
- report_type: report_type
+ report_type: report_type,
+ finding_id: finding_id
)
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 8791220f11e..c2ca1757452 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -4124,7 +4124,6 @@
- './spec/finders/labels_finder_spec.rb'
- './spec/finders/lfs_pointers_finder_spec.rb'
- './spec/finders/license_template_finder_spec.rb'
-- './spec/finders/members_finder_spec.rb'
- './spec/finders/merge_request/metrics_finder_spec.rb'
- './spec/finders/merge_requests/by_approvals_finder_spec.rb'
- './spec/finders/merge_requests_finder_spec.rb'
diff --git a/spec/support/shared_examples/integrations_shared_examples.rb b/spec/support/shared_examples/integrations_shared_examples.rb
index 360ce711383..cab7168dfaa 100644
--- a/spec/support/shared_examples/integrations_shared_examples.rb
+++ b/spec/support/shared_examples/integrations_shared_examples.rb
@@ -54,12 +54,26 @@ RSpec.shared_examples 'set up an integration' do |endpoint:, integration:|
expect(response).to have_gitlab_http_status(expected_code)
end
- context 'when an integration is unavailable' do
+ context 'when an integration is disabled' do
+ before do
+ allow(Integration).to receive(:disabled_integration_names).and_return([integration.to_param])
+ end
+
it 'returns bad request' do
+ put url, params: integration_attrs
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+ end
+
+ context 'when an integration is disabled at the project-level' do
+ before do
allow_next_found_instance_of(Project) do |project|
allow(project).to receive(:disabled_integrations).and_return([integration])
end
+ end
+ it 'returns bad request' do
put url, params: integration_attrs
expect(response).to have_gitlab_http_status(:bad_request)
diff --git a/tooling/danger/required_stops.rb b/tooling/danger/required_stops.rb
index 788be5d70e6..f32d07950e0 100644
--- a/tooling/danger/required_stops.rb
+++ b/tooling/danger/required_stops.rb
@@ -17,7 +17,7 @@ module Tooling
WARNING_COMMENT = <<~COMMENT.freeze
Finalizing data migration might be time consuming and require a [required stop](#{DOC_URL}).
Check the timings of the underlying data migration.
- If possible schedule finalization for the first minor version after the next required stop.
+ Make sure the backgrond migration was added before the last required stop.
COMMENT
def add_comment_for_finalized_migrations