Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2024-02-16 09:10:14 +00:00
parent 91f8ddd4ca
commit f971281bcd
65 changed files with 1311 additions and 508 deletions

View File

@ -98,7 +98,6 @@ Layout/LineLength:
- 'app/finders/issuables/assignee_filter.rb'
- 'app/finders/issuables/label_filter.rb'
- 'app/finders/issues_finder.rb'
- 'app/finders/members_finder.rb'
- 'app/finders/packages/group_packages_finder.rb'
- 'app/finders/personal_access_tokens_finder.rb'
- 'app/finders/projects/export_job_finder.rb'
@ -3211,7 +3210,6 @@ Layout/LineLength:
- 'spec/finders/groups/user_groups_finder_spec.rb'
- 'spec/finders/groups_finder_spec.rb'
- 'spec/finders/labels_finder_spec.rb'
- 'spec/finders/members_finder_spec.rb'
- 'spec/finders/merge_requests/by_approvals_finder_spec.rb'
- 'spec/finders/merge_requests_finder_spec.rb'
- 'spec/finders/milestones_finder_spec.rb'

View File

@ -47,7 +47,6 @@ Style/IfUnlessModifier:
- 'app/finders/feature_flags_user_lists_finder.rb'
- 'app/finders/group_projects_finder.rb'
- 'app/finders/labels_finder.rb'
- 'app/finders/members_finder.rb'
- 'app/finders/notes_finder.rb'
- 'app/finders/packages/helm/packages_finder.rb'
- 'app/finders/personal_access_tokens_finder.rb'

View File

@ -9,6 +9,7 @@ import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import BoardNewIssue from 'ee_else_ce/boards/components/board_new_issue.vue';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
@ -26,7 +27,6 @@ import {
} from '../graphql/cache_updates';
import { shouldCloneCard, moveItemVariables } from '../boards_util';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
import BoardCutLine from './board_cut_line.vue';
export default {

View File

@ -1,7 +1,6 @@
<script>
import { s__ } from '~/locale';
import { getMilestone, formatIssueInput, getBoardQuery } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { setError } from '../graphql/cache_updates';
@ -17,7 +16,6 @@ export default {
BoardNewItem,
ProjectSelect,
},
mixins: [BoardNewIssueMixin],
inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard'],
props: {
list: {

View File

@ -1,6 +0,0 @@
export default {
// EE-only
methods: {
extraIssueInput: () => {},
},
};

View File

@ -80,7 +80,9 @@ export default {
* different apps it avoids repetition & complexity.
*
*/
onClickAction() {
onClickAction(e) {
e.preventDefault();
if (this.withConfirmationModal) {
this.$emit('showActionConfirmationModal');
} else {

View File

@ -1,5 +1,5 @@
<script>
import { GlTooltipDirective, GlLink } from '@gitlab/ui';
import { GlDisclosureDropdownItem, GlTooltipDirective } from '@gitlab/ui';
import ActionComponent from '~/ci/common/private/job_action_component.vue';
import JobNameComponent from '~/ci/common/private/job_name_component.vue';
import { ICONS } from '~/ci/constants';
@ -44,7 +44,7 @@ export default {
components: {
ActionComponent,
JobNameComponent,
GlLink,
GlDisclosureDropdownItem,
},
directives: {
GlTooltip: GlTooltipDirective,
@ -72,8 +72,14 @@ export default {
},
},
computed: {
boundary() {
return this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
alternativeTooltipConfig() {
const boundary = this.dropdownLength === 1 ? 'viewport' : 'scrollParent';
return {
boundary,
placement: 'bottom',
customClass: 'gl-pointer-events-none',
};
},
detailsPath() {
return this.status?.details_path;
@ -81,9 +87,18 @@ export default {
hasDetails() {
return this.status?.has_details;
},
item() {
return {
text: this.job.name,
href: this.hasDetails ? this.detailsPath : '',
};
},
status() {
return this.job?.status ? this.job.status : {};
},
tooltipConfig() {
return this.hasDetails ? this.$options.tooltipConfig : this.alternativeTooltipConfig;
},
tooltipText() {
const textBuilder = [];
const { name: jobName } = this.job;
@ -123,6 +138,9 @@ export default {
? this.$options.i18n.runAgainTooltipText
: title;
},
testid() {
return this.hasDetails ? 'job-with-link' : 'job-without-link';
},
},
errorCaptured(err, _vm, info) {
reportToSentry('pipelines_job_item', `pipelines_job_item error: ${err}, info: ${info}`);
@ -130,37 +148,38 @@ export default {
};
</script>
<template>
<div
class="ci-job-component gl-display-flex gl-align-items-center gl-justify-content-space-between"
<gl-disclosure-dropdown-item
:item="item"
class="ci-job-component"
:class="[
cssClassJobName,
{
'js-pipeline-graph-job-link gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none': hasDetails,
'js-job-component-tooltip non-details-job-component': !hasDetails,
},
]"
:data-testid="testid"
>
<gl-link
v-if="hasDetails"
v-gl-tooltip="$options.tooltipConfig"
:href="detailsPath"
:title="tooltipText"
:class="cssClassJobName"
class="js-pipeline-graph-job-link menu-item gl-text-gray-900 gl-active-text-decoration-none gl-focus-text-decoration-none gl-hover-text-decoration-none"
data-testid="job-with-link"
>
<job-name-component :name="job.name" :status="job.status" />
</gl-link>
<template #list-item>
<div
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-mt-n2 gl-mb-n2 gl-ml-n2"
>
<job-name-component
v-gl-tooltip="tooltipConfig"
:title="tooltipText"
:name="job.name"
:status="job.status"
data-testid="job-name"
/>
<div
v-else
v-gl-tooltip="{ boundary, placement: 'bottom', customClass: 'gl-pointer-events-none' }"
:title="tooltipText"
:class="cssClassJobName"
class="js-job-component-tooltip non-details-job-component menu-item"
data-testid="job-without-link"
>
<job-name-component :name="job.name" :status="job.status" />
</div>
<action-component
v-if="hasJobAction"
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
/>
</div>
<action-component
v-if="hasJobAction"
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
class="gl-mt-n2 gl-mr-n2"
/>
</div>
</template>
</gl-disclosure-dropdown-item>
</template>

View File

@ -12,7 +12,7 @@
* 4. Commit widget
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { GlDisclosureDropdown, GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
@ -28,14 +28,11 @@ export default {
stage: __('Stage:'),
viewStageLabel: __('View Stage: %{title}'),
},
dropdownPopperOpts: {
placement: 'bottom',
positionFixed: true,
},
components: {
CiIcon,
GlLoadingIcon,
GlDropdown,
GlDisclosureDropdown,
GlButton,
LegacyJobItem,
},
directives: {
@ -95,7 +92,7 @@ export default {
this.isLoading = false;
})
.catch(() => {
this.$refs.dropdown.hide();
this.$refs.dropdown.close();
this.isLoading = false;
createAlert({
@ -111,58 +108,66 @@ export default {
</script>
<template>
<gl-dropdown
<gl-disclosure-dropdown
ref="dropdown"
v-gl-tooltip.hover.ds0
v-gl-tooltip="stage.title"
data-testid="mini-pipeline-graph-dropdown"
class="mini-pipeline-graph-dropdown"
variant="link"
:aria-label="stageAriaLabel(stage.title)"
:lazy="true"
:popper-opts="$options.dropdownPopperOpts"
:toggle-class="['gl-rounded-full!']"
menu-class="mini-pipeline-graph-dropdown-menu"
@hide="onHideDropdown"
@show="onShowDropdown"
no-caret
@hidden="onHideDropdown"
@shown="onShowDropdown"
>
<template #button-content>
<ci-icon :status="stage.status" :show-tooltip="false" :use-link="false" class="gl-mb-0!" />
<template #toggle>
<gl-button
v-gl-tooltip.ds0="isDropdownOpen ? '' : stage.title"
variant="link"
class="gl-rounded-full!"
data-testid="mini-pipeline-graph-dropdown-toggle"
>
<ci-icon :status="stage.status" :show-tooltip="false" :use-link="false" class="gl-mb-0!" />
</gl-button>
</template>
<div v-if="isLoading" class="gl--flex-center gl-p-2" data-testid="pipeline-stage-loading-state">
<template #header>
<div
class="gl-display-flex gl-align-items-center gl-p-4! gl-min-h-8 gl-border-b-1 gl-border-b-solid gl-border-b-gray-200 gl-font-sm gl-font-weight-bold gl-line-height-1"
>
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
</template>
<div
v-if="isLoading"
class="gl-display-flex gl-py-3 gl-px-4"
data-testid="pipeline-stage-loading-state"
>
<gl-loading-icon size="sm" class="gl-mr-3" />
<p class="gl-line-height-normal gl-mb-0">{{ $options.i18n.loadingText }}</p>
</div>
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
class="mini-pipeline-graph-dropdown-menu gl-overflow-y-auto gl-m-0 gl-p-0"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
<div class="gl--flex-center gl-border-b gl-font-weight-bold gl-mb-3 gl-pb-3">
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
<li v-for="job in dropdownContent" :key="job.id">
<legacy-job-item
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="pipeline-job-item"
/>
</li>
<template v-if="isMergeTrain">
<li class="gl-dropdown-divider" role="presentation">
<hr role="separator" aria-orientation="horizontal" class="dropdown-divider" />
</li>
<li>
<div
class="gl-display-flex gl-align-items-center"
data-testid="warning-message-merge-trains"
>
<div class="menu-item gl-font-sm gl-text-gray-300!">
{{ $options.i18n.mergeTrainMessage }}
</div>
</div>
</li>
</template>
<legacy-job-item
v-for="job in dropdownContent"
:key="job.id"
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="pipeline-job-item"
/>
</ul>
</gl-dropdown>
<template #footer>
<div
v-if="!isLoading && isMergeTrain"
class="gl-font-sm gl-text-secondary gl-py-3 gl-px-4 gl-border-t"
data-testid="warning-message-merge-trains"
>
{{ $options.i18n.mergeTrainMessage }}
</div>
</template>
</gl-disclosure-dropdown>
</template>

View File

@ -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;

View File

@ -78,7 +78,7 @@
border-bottom: 2px solid $gray-200;
position: absolute;
right: -4px;
top: 11px;
top: 12px;
width: 4px;
}
}

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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(',')

View File

@ -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)

View File

@ -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"

View File

@ -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

View File

@ -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}"

View File

@ -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)

View File

@ -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

View File

@ -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: %(<a href="#{help_page_path('user/project/integrations/gitlab_slack_application', anchor: 'reinstall-the-gitlab-for-slack-app')}">).html_safe, linkEnd: '</a>'.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…')

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
35a60bc57ed21353aa5570831aac6f991a10bb4e3ec10559bc38000488f59799

View File

@ -0,0 +1 @@
078e43ac4de3b81bcc9cd73d37e7bd614f1ffd70df41693988a187caee6fdf6d

View File

@ -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

View File

@ -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.

View File

@ -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"

View File

@ -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

View File

@ -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))

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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');
});
});

View File

@ -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`,

View File

@ -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],

View File

@ -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

View File

@ -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!(

View File

@ -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

View File

@ -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,

View File

@ -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),

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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(

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -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