Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
91f8ddd4ca
commit
f971281bcd
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -1,6 +0,0 @@
|
|||
export default {
|
||||
// EE-only
|
||||
methods: {
|
||||
extraIssueInput: () => {},
|
||||
},
|
||||
};
|
||||
|
|
@ -80,7 +80,9 @@ export default {
|
|||
* different apps it avoids repetition & complexity.
|
||||
*
|
||||
*/
|
||||
onClickAction() {
|
||||
onClickAction(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (this.withConfirmationModal) {
|
||||
this.$emit('showActionConfirmationModal');
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@
|
|||
border-bottom: 2px solid $gray-200;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 11px;
|
||||
top: 12px;
|
||||
width: 4px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(',')
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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…')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -0,0 +1 @@
|
|||
35a60bc57ed21353aa5570831aac6f991a10bb4e3ec10559bc38000488f59799
|
||||
|
|
@ -0,0 +1 @@
|
|||
078e43ac4de3b81bcc9cd73d37e7bd614f1ffd70df41693988a187caee6fdf6d
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 ""
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue