Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-23 21:11:23 +00:00
parent 5e9fe672fa
commit 0510f42da3
38 changed files with 804 additions and 3970 deletions

View file

@ -1 +1 @@
2ed9a2c78ec556eb8d64e03203c864355ea5a128
bb2e3f4a916f031f38c9fb1c4fc955f50f0e4275

View file

@ -1,25 +0,0 @@
import { get } from 'lodash';
import { REST, GRAPHQL } from './constants';
const accessors = {
[REST]: {
detailsPath: 'details_path',
groupId: 'id',
hasDetails: 'has_details',
pipelineStatus: ['details', 'status'],
sourceJob: ['source_job', 'name'],
},
[GRAPHQL]: {
detailsPath: 'detailsPath',
groupId: 'name',
hasDetails: 'hasDetails',
pipelineStatus: 'status',
sourceJob: ['sourceJob', 'name'],
},
};
const accessValue = (dataMethod, prop, item) => {
return get(item, accessors[dataMethod][prop]);
};
export { accessors, accessValue };

View file

@ -8,9 +8,6 @@ export const UPSTREAM = 'upstream';
*/
export const ONE_COL_WIDTH = 180;
export const REST = 'rest';
export const GRAPHQL = 'graphql';
export const STAGE_VIEW = 'stage';
export const LAYER_VIEW = 'layer';
export const VIEW_TYPE_KEY = 'pipeline_graph_view_type';

View file

@ -7,8 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import ActionComponent from '../jobs_shared/action_component.vue';
import JobNameComponent from '../jobs_shared/job_name_component.vue';
import { accessValue } from './accessors';
import { REST, SINGLE_JOB } from './constants';
import { SINGLE_JOB } from './constants';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
@ -47,11 +46,6 @@ export default {
GlTooltip: GlTooltipDirective,
},
mixins: [delayedJobMixin],
inject: {
dataMethod: {
default: REST,
},
},
props: {
job: {
type: Object,
@ -111,10 +105,10 @@ export default {
return this.pipelineId > -1 ? `${this.job.name}-${this.pipelineId}` : '';
},
detailsPath() {
return accessValue(this.dataMethod, 'detailsPath', this.status);
return this.status.detailsPath;
},
hasDetails() {
return accessValue(this.dataMethod, 'hasDetails', this.status);
return this.status.hasDetails;
},
isSingleItem() {
return this.type === SINGLE_JOB;
@ -189,7 +183,7 @@ export default {
if (this.isSingleItem) {
/*
This is so the jobDropdown still toggles. Issue to refactor:
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
https://gitlab.com/gitlab-org/gitlab/-/issues/267117
*/
evt.stopPropagation();
}

View file

@ -4,8 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import { reportToSentry } from '../../utils';
import { accessValue } from './accessors';
import { DOWNSTREAM, REST, UPSTREAM } from './constants';
import { DOWNSTREAM, UPSTREAM } from './constants';
export default {
directives: {
@ -18,11 +17,6 @@ export default {
GlLoadingIcon,
GlBadge,
},
inject: {
dataMethod: {
default: REST,
},
},
props: {
columnTitle: {
type: String,
@ -40,20 +34,9 @@ export default {
type: String,
required: true,
},
/*
The next two props will be removed or required
once the graph transition is done.
See: https://gitlab.com/gitlab-org/gitlab/-/issues/291043
*/
isLoading: {
type: Boolean,
required: false,
default: false,
},
projectId: {
type: Number,
required: false,
default: -1,
required: true,
},
},
computed: {
@ -65,7 +48,7 @@ export default {
return `js-linked-pipeline-${this.pipeline.id}`;
},
pipelineStatus() {
return accessValue(this.dataMethod, 'pipelineStatus', this.pipeline);
return this.pipeline.status;
},
projectName() {
return this.pipeline.project.name;
@ -97,12 +80,10 @@ export default {
return this.type === UPSTREAM;
},
isSameProject() {
return this.projectId > -1
? this.projectId === this.pipeline.project.id
: !this.pipeline.multiproject;
return !this.pipeline.multiproject;
},
sourceJobName() {
return accessValue(this.dataMethod, 'sourceJob', this.pipeline);
return this.pipeline.sourceJob?.name ?? '';
},
sourceJobInfo() {
return this.isDownstream ? sprintf(__('Created by %{job}'), { job: this.sourceJobName }) : '';

View file

@ -4,8 +4,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '../../utils';
import MainGraphWrapper from '../graph_shared/main_graph_wrapper.vue';
import ActionComponent from '../jobs_shared/action_component.vue';
import { accessValue } from './accessors';
import { GRAPHQL } from './constants';
import JobGroupDropdown from './job_group_dropdown.vue';
import JobItem from './job_item.vue';
@ -97,7 +95,7 @@ export default {
},
methods: {
getGroupId(group) {
return accessValue(GRAPHQL, 'groupId', group);
return group.name;
},
groupId(group) {
return `ci-badge-${escape(group.name)}`;

View file

@ -1,6 +1,5 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { GRAPHQL } from './components/graph/constants';
import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
import { reportToSentry } from './utils';
@ -23,7 +22,6 @@ const createPipelinesDetailApp = (
pipelineProjectPath,
pipelineIid,
graphqlResourceEtag,
dataMethod: GRAPHQL,
},
errorCaptured(err, _vm, info) {
reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);

View file

@ -0,0 +1,60 @@
# frozen_string_literal: true
# Groups::UserGroupsFinder
#
# Used to filter Groups where a user is member
#
# Arguments:
# current_user - user requesting group info on target user
# target_user - user for which groups will be found
# params:
# permissions: string (see Types::Groups::UserPermissionsEnum)
# search: string used for search on path and group name
#
# Initially created to filter user groups and descendants where the user can create projects
module Groups
class UserGroupsFinder
def initialize(current_user, target_user, params = {})
@current_user = current_user
@target_user = target_user
@params = params
end
def execute
return Group.none unless current_user&.can?(:read_user_groups, target_user)
return Group.none if target_user.blank?
items = by_permission_scope
items = by_search(items)
sort(items)
end
private
attr_reader :current_user, :target_user, :params
def sort(items)
items.order(path: :asc, id: :asc) # rubocop: disable CodeReuse/ActiveRecord
end
def by_search(items)
return items if params[:search].blank?
items.search(params[:search])
end
def by_permission_scope
if permission_scope_create_projects?
target_user.manageable_groups(include_groups_with_developer_maintainer_access: true)
else
target_user.groups
end
end
def permission_scope_create_projects?
params[:permission_scope] == :create_projects &&
Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
end
end
end

View file

@ -124,6 +124,16 @@ module Resolvers
[args[:iid], args[:iids]].any? ? 0 : 0.01
end
def self.before_connection_authorization(&block)
@before_connection_authorization_block = block
end
# rubocop: disable Style/TrivialAccessors
def self.before_connection_authorization_block
@before_connection_authorization_block
end
# rubocop: enable Style/TrivialAccessors
def offset_pagination(relation)
::Gitlab::Graphql::Pagination::OffsetPaginatedRelation.new(relation)
end

View file

@ -0,0 +1,44 @@
# frozen_string_literal: true
module Resolvers
module Users
class GroupsResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
include LooksAhead
type Types::GroupType.connection_type, null: true
authorize :read_user_groups
authorizes_object!
argument :search, GraphQL::Types::String,
required: false,
description: 'Search by group name or path.'
argument :permission_scope,
::Types::PermissionTypes::GroupEnum,
required: false,
description: 'Filter by permissions the user has on groups.'
before_connection_authorization do |nodes, current_user|
Preloaders::UserMaxAccessLevelInGroupsPreloader.new(nodes, current_user).execute
end
def resolve_with_lookahead(**args)
return unless Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
apply_lookahead(Groups::UserGroupsFinder.new(current_user, object, args).execute)
end
private
def preloads
{
path: [:route],
full_path: [:route]
}
end
end
end
end
Resolvers::Users::GroupsResolver.prepend_mod_with('Resolvers::Users::GroupsResolver')

View file

@ -5,7 +5,7 @@ module Types
class Group < BasePermissionType
graphql_name 'GroupPermissions'
abilities :read_group
abilities :read_group, :create_projects
end
end
end

View file

@ -0,0 +1,12 @@
# frozen_string_literal: true
module Types
module PermissionTypes
class GroupEnum < BaseEnum
graphql_name 'GroupPermission'
description 'User permission on groups'
value 'CREATE_PROJECTS', value: :create_projects, description: 'Groups where the user can create projects.'
end
end
end

View file

@ -59,6 +59,9 @@ module Types
type: Types::GroupMemberType.connection_type,
null: true,
description: 'Group memberships of the user.'
field :groups,
resolver: Resolvers::Users::GroupsResolver,
description: 'Groups where the user has access.'
field :group_count,
resolver: Resolvers::Users::GroupCountResolver,
description: 'Group count for the user.'

View file

@ -174,7 +174,11 @@ module IssuesHelper
end
def issue_header_actions_data(project, issuable, current_user)
new_issuable_params = ({ issuable_template: 'incident', issue: { issue_type: 'incident' } } if issuable.incident?)
new_issuable_params = { issue: { description: _('Related to #%{issue_id}.') % { issue_id: issuable.iid } + "\n\n" } }
if issuable.incident?
new_issuable_params[:issuable_template] = 'incident'
new_issuable_params[:issue][:issue_type] = 'incident'
end
{
can_create_issue: show_new_issue_link?(project).to_s,

View file

@ -0,0 +1,27 @@
# frozen_string_literal: true
module Preloaders
# This class preloads the max access level (role) for the user within the given groups and
# stores the values in requests store.
# Will only be able to preload max access level for groups where the user is a direct member
class UserMaxAccessLevelInGroupsPreloader
include BulkMemberAccessLoad
def initialize(groups, user)
@groups = groups
@user = user
end
def execute
group_memberships = GroupMember.active_without_invites_and_requests
.non_minimal_access
.where(user: @user, source_id: @groups)
.group(:source_id)
.maximum(:access_level)
group_memberships.each do |group_id, max_access_level|
merge_value_to_request_store(User, @user.id, group_id, max_access_level)
end
end
end
end

View file

@ -25,6 +25,7 @@ class UserPolicy < BasePolicy
enable :update_user_status
enable :read_user_personal_access_tokens
enable :read_group_count
enable :read_user_groups
end
rule { default }.enable :read_user_profile

View file

@ -0,0 +1,8 @@
---
name: paginatable_namespace_drop_down_for_project_creation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66112
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338930
milestone: '14.3'
type: development
group: group::project management
default_enabled: false

View file

@ -10176,6 +10176,7 @@ Represents a Group Membership.
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="grouppermissionscreateprojects"></a>`createProjects` | [`Boolean!`](#boolean) | Indicates the user can perform `create_projects` on this resource. |
| <a id="grouppermissionsreadgroup"></a>`readGroup` | [`Boolean!`](#boolean) | Indicates the user can perform `read_group` on this resource. |
### `GroupReleaseStats`
@ -10910,6 +10911,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="mergerequestassigneeauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
| <a id="mergerequestassigneeauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
##### `MergeRequestAssignee.groups`
Groups where the user has access.
Returns [`GroupConnection`](#groupconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestassigneegroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
| <a id="mergerequestassigneegroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
##### `MergeRequestAssignee.reviewRequestedMergeRequests`
Merge requests assigned to the user for review.
@ -11139,6 +11157,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="mergerequestreviewerauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
| <a id="mergerequestreviewerauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
##### `MergeRequestReviewer.groups`
Groups where the user has access.
Returns [`GroupConnection`](#groupconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mergerequestreviewergroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
| <a id="mergerequestreviewergroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
##### `MergeRequestReviewer.reviewRequestedMergeRequests`
Merge requests assigned to the user for review.
@ -14010,6 +14045,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="usercoreauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
| <a id="usercoreauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
##### `UserCore.groups`
Groups where the user has access.
Returns [`GroupConnection`](#groupconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="usercoregroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
| <a id="usercoregroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
##### `UserCore.reviewRequestedMergeRequests`
Merge requests assigned to the user for review.
@ -15162,6 +15214,14 @@ Group member relation.
| <a id="groupmemberrelationdirect"></a>`DIRECT` | Members in the group itself. |
| <a id="groupmemberrelationinherited"></a>`INHERITED` | Members in the group's ancestor groups. |
### `GroupPermission`
User permission on groups.
| Value | Description |
| ----- | ----------- |
| <a id="grouppermissioncreate_projects"></a>`CREATE_PROJECTS` | Groups where the user can create projects. |
### `HealthStatus`
Health status of an issue or epic.
@ -16961,6 +17021,23 @@ four standard [pagination arguments](#connection-pagination-arguments):
| <a id="userauthoredmergerequestsstate"></a>`state` | [`MergeRequestState`](#mergerequeststate) | Merge request state. If provided, all resolved merge requests will have this state. |
| <a id="userauthoredmergerequeststargetbranches"></a>`targetBranches` | [`[String!]`](#string) | Array of target branch names. All resolved merge requests will have one of these branches as their target. |
###### `User.groups`
Groups where the user has access.
Returns [`GroupConnection`](#groupconnection).
This field returns a [connection](#connections). It accepts the
four standard [pagination arguments](#connection-pagination-arguments):
`before: String`, `after: String`, `first: Int`, `last: Int`.
####### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="usergroupspermissionscope"></a>`permissionScope` | [`GroupPermission`](#grouppermission) | Filter by permissions the user has on groups. |
| <a id="usergroupssearch"></a>`search` | [`String`](#string) | Search by group name or path. |
###### `User.reviewRequestedMergeRequests`
Merge requests assigned to the user for review.

View file

@ -98,15 +98,15 @@ delete a project. To allow only users with the Administrator role to delete proj
## Default delayed project deletion **(PREMIUM SELF)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/255449) in GitLab 14.2 for groups created after August 12, 2021.
Projects in a group (but not a personal namespace) can be deleted after a delayed period, by
[configuring in Group Settings](../../group/index.md#enable-delayed-project-removal).
Projects in a group (but not a personal namespace) can be deleted after a delayed period.
You can [configure it in group settings](../../group/index.md#enable-delayed-project-removal).
To enable delayed project deletion by default in new groups:
1. Check the **Default delayed project deletion** checkbox.
1. Click **Save changes**.
1. Select **Save changes**.
## Default deletion delay **(PREMIUM SELF)**

View file

@ -13,48 +13,12 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - A simplified version was made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/294076) in GitLab 13.10.
> - [Redesigned](https://gitlab.com/gitlab-org/gitlab/-/issues/326926) in 14.2.
The Security Configuration page displays what security scans are available, links to documentation and also simple enablement tools for the current project.
The Security Configuration page lists the following for the security testing and compliance tools:
To view a project's security configuration, go to the project's home page,
then in the left sidebar go to **Security & Compliance > Configuration**.
For each security control the page displays:
- Its name, description and a documentation link.
- Name, description, and a documentation link.
- Whether or not it is available.
- A configuration button or a link to its configuration guide.
## Security testing
You can configure the following security controls:
- Auto DevOps
- Click **Enable Auto DevOps** on the alert to enable it for the current project. For more details, see [Auto DevOps](../../../topics/autodevops/index.md).
- SAST
- Click **Enable SAST** to use SAST for the current project. For more details, see [Configure SAST in the UI](../sast/index.md#configure-sast-in-the-ui).
- DAST **(ULTIMATE)**
- Click **Enable DAST** to use DAST for the current Project. To manage the available DAST profiles used for on-demand scans Click **Manage Scans**. For more details, see [DAST on-demand scans](../dast/index.md#on-demand-scans).
- Dependency Scanning **(ULTIMATE)**
- Select **Configure via Merge Request** to create a merge request with the changes required to
enable Dependency Scanning. For more details, see [Enable Dependency Scanning via an automatic merge request](../dependency_scanning/index.md#enable-dependency-scanning-via-an-automatic-merge-request).
- Container Scanning **(ULTIMATE)**
- Can be configured via `.gitlab-ci.yml`. For more details, see [Container Scanning](../../../user/application_security/container_scanning/index.md#configuration).
- Cluster Image Scanning **(ULTIMATE)**
- Can be configured via `.gitlab-ci.yml`. For more details, see [Cluster Image Scanning](../../../user/application_security/cluster_image_scanning/#configuration).
- Secret Detection
- Select **Configure via Merge Request** to create a merge request with the changes required to
enable Secret Detection. For more details, see [Enable Secret Detection via an automatic merge request](../secret_detection/index.md#enable-secret-detection-via-an-automatic-merge-request).
- API Fuzzing **(ULTIMATE)**
- Click **Enable API Fuzzing** to use API Fuzzing for the current Project. For more details, see [API Fuzzing](../../../user/application_security/api_fuzzing/index.md#enable-web-api-fuzzing).
- Coverage Fuzzing **(ULTIMATE)**
- Can be configured via `.gitlab-ci.yml`. For more details, see [Coverage Fuzzing](../../../user/application_security/coverage_fuzzing/index.md#configuration).
## Status **(ULTIMATE)**
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20711) in [GitLab Ultimate](https://about.gitlab.com/pricing/) 12.6.
The status of each security control is determined by the project's latest default branch
[CI pipeline](../../../ci/pipelines/index.md).
If a job with the expected security report artifact exists in the pipeline, the feature's status is
@ -63,11 +27,42 @@ _enabled_.
If the latest pipeline used [Auto DevOps](../../../topics/autodevops/index.md),
all security features are configured by default.
Click **View history** to see the `.gitlab-ci.yml` file's history.
To view a project's security configuration:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Security & Compliance > Configuration**.
Select **Configuration history** to see the `.gitlab-ci.yml` file's history.
## Security testing
You can configure the following security controls:
- Static Application Security Testing (SAST)
- Select **Enable SAST** to configure SAST for the current project.
For more details, read [Configure SAST in the UI](../sast/index.md#configure-sast-in-the-ui).
- Dynamic Application Security Testing (DAST) **(ULTIMATE)**
- Select **Enable DAST** to configure DAST for the current project.
- Select **Manage scans** to manage the saved DAST scans, site profiles, and scanner profiles.
For more details, read [DAST on-demand scans](../dast/index.md#on-demand-scans).
- Dependency Scanning **(ULTIMATE)**
- Select **Configure via Merge Request** to create a merge request with the changes required to
enable Dependency Scanning. For more details, see [Enable Dependency Scanning via an automatic merge request](../dependency_scanning/index.md#enable-dependency-scanning-via-an-automatic-merge-request).
- Container Scanning **(ULTIMATE)**
- Can be configured with `.gitlab-ci.yml`. For more details, read [Container Scanning](../../../user/application_security/container_scanning/index.md#configuration).
- Cluster Image Scanning **(ULTIMATE)**
- Can be configured with `.gitlab-ci.yml`. For more details, read [Cluster Image Scanning](../../../user/application_security/cluster_image_scanning/#configuration).
- Secret Detection
- Select **Configure via Merge Request** to create a merge request with the changes required to
enable Secret Detection. For more details, read [Enable Secret Detection via an automatic merge request](../secret_detection/index.md#enable-secret-detection-via-an-automatic-merge-request).
- API Fuzzing **(ULTIMATE)**
- Select **Enable API Fuzzing** to use API Fuzzing for the current project. For more details, read [API Fuzzing](../../../user/application_security/api_fuzzing/index.md#enable-web-api-fuzzing).
- Coverage Fuzzing **(ULTIMATE)**
- Can be configured with `.gitlab-ci.yml`. For more details, read [Coverage Fuzzing](../../../user/application_security/coverage_fuzzing/index.md#configuration).
## Compliance **(ULTIMATE)**
You can configure the following security controls:
- License Compliance **(ULTIMATE)**
- Can be configured via `.gitlab-ci.yml`. For more details, see [License Compliance](../../../user/compliance/license_compliance/index.md#configuration).
- Can be configured with `.gitlab-ci.yml`. For more details, read [License Compliance](../../../user/compliance/license_compliance/index.md#configuration).

View file

@ -664,16 +664,16 @@ Projects can be configured to be deleted either:
- Immediately.
- After a delayed interval. During this interval period, the projects are in a read-only state
and can be restored, if required. The default interval period is seven days but
and can be restored. The default interval period is seven days but
[is configurable](../admin_area/settings/visibility_and_access_controls.md#default-deletion-delay).
On:
On self-managed GitLab, projects are deleted immediately by default.
In GitLab 14.2 and later, an administrator can
[change the default setting](../admin_area/settings/visibility_and_access_controls.md#default-delayed-project-deletion)
for projects in newly-created groups.
- GitLab self-managed instances, projects are deleted immediately by default. In GitLab
14.2 and later, an administrator can
[change the default setting](../admin_area/settings/visibility_and_access_controls.md#default-delayed-project-deletion) for projects in newly-created groups.
- GitLab.com, see [GitLab.com settings page](../gitlab_com/index.md#delayed-project-deletion) for
the default setting.
On GitLab.com, see the [GitLab.com settings page](../gitlab_com/index.md#delayed-project-deletion) for
the default setting.
To enable delayed deletion of projects in a group:

View file

@ -7,12 +7,14 @@ module Gitlab
class Redactor
include ::Gitlab::Graphql::Laziness
def initialize(type, context)
def initialize(type, context, resolver)
@type = type
@context = context
@resolver = resolver
end
def redact(nodes)
perform_before_authorize_action(nodes)
remove_unauthorized(nodes)
nodes
@ -29,6 +31,13 @@ module Gitlab
private
def perform_before_authorize_action(nodes)
before_connection_authorization_block = @resolver&.before_connection_authorization_block
return unless before_connection_authorization_block.respond_to?(:call)
before_connection_authorization_block.call(nodes, @context[:current_user])
end
def remove_unauthorized(nodes)
nodes
.map! { |lazy| force(lazy) }
@ -49,14 +58,14 @@ module Gitlab
end
def redact_connection(conn, context)
redactor = Redactor.new(@field.type.unwrap.node_type, context)
redactor = Redactor.new(@field.type.unwrap.node_type, context, @field.resolver)
return unless redactor.active?
conn.redactor = redactor if conn.respond_to?(:redactor=)
end
def redact_list(list, context)
redactor = Redactor.new(@field.type.unwrap, context)
redactor = Redactor.new(@field.type.unwrap, context, @field.resolver)
redactor.redact(list) if redactor.active?
end
end

View file

@ -27665,6 +27665,9 @@ msgstr ""
msgid "Related merge requests"
msgstr ""
msgid "Related to #%{issue_id}."
msgstr ""
msgid "Relates to"
msgstr ""

View file

@ -25,7 +25,7 @@ RSpec.describe "User views incident" do
it 'shows the merge request and incident actions', :js, :aggregate_failures do
click_button 'Incident actions'
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident' } }))
expect(page).to have_link('New incident', href: new_project_issue_path(project, { issuable_template: 'incident', issue: { issue_type: 'incident', description: "Related to \##{incident.iid}.\n\n" } }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close incident')
end

View file

@ -25,7 +25,7 @@ RSpec.describe "User views issue" do
it 'shows the merge request and issue actions', :js, :aggregate_failures do
click_button 'Issue actions'
expect(page).to have_link('New issue', href: new_project_issue_path(project))
expect(page).to have_link('New issue', href: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }))
expect(page).to have_button('Create merge request')
expect(page).to have_button('Close issue')
end

View file

@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Groups::UserGroupsFinder do
describe '#execute' do
let_it_be(:user) { create(:user) }
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
subject { described_class.new(current_user, target_user, arguments).execute }
let(:arguments) { {} }
let(:current_user) { user }
let(:target_user) { user }
before_all do
guest_group.add_guest(user)
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
end
it 'returns all groups where the user is a direct member' do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
context 'when target_user is nil' do
let(:target_user) { nil }
it { is_expected.to be_empty }
end
context 'when current_user is nil' do
let(:current_user) { nil }
it { is_expected.to be_empty }
end
context 'when permission is :create_projects' do
let(:arguments) { { permission_scope: :create_projects } }
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group
]
)
end
context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
end
it 'ignores project creation scope and returns all groups where the user is a direct member' do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
end
context 'when search is provided' do
let(:arguments) { { permission_scope: :create_projects, search: 'maintainer' } }
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group
]
)
end
end
end
context 'when search is provided' do
let(:arguments) { { search: 'maintainer' } }
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group
]
)
end
end
end
end

View file

@ -1,5 +1,5 @@
import { mount, shallowMount } from '@vue/test-utils';
import { GRAPHQL, LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue';
@ -54,9 +54,6 @@ describe('graph component', () => {
...data,
};
},
provide: {
dataMethod: GRAPHQL,
},
stubs: {
'links-inner': true,
'linked-pipeline': true,

View file

@ -14,7 +14,29 @@ describe('pipeline graph job item', () => {
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
const delayedJobFixture = getJSONFixture('jobs/delayed.json');
const delayedJob = {
__typename: 'CiJob',
name: 'delayed job',
scheduledAt: '2015-07-03T10:01:00.000Z',
needs: [],
status: {
__typename: 'DetailedStatus',
icon: 'status_scheduled',
tooltip: 'delayed manual action (%{remainingTime})',
hasDetails: true,
detailsPath: '/root/kinder-pipe/-/jobs/5339',
group: 'scheduled',
action: {
__typename: 'StatusAction',
icon: 'time-out',
title: 'Unschedule',
path: '/frontend-fixtures/builds-project/-/jobs/142/unschedule',
buttonTitle: 'Unschedule job',
},
},
};
const mockJob = {
id: 4256,
name: 'test',
@ -24,8 +46,8 @@ describe('pipeline graph job item', () => {
label: 'passed',
tooltip: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4256',
has_details: true,
detailsPath: '/root/ci-mock/builds/4256',
hasDetails: true,
action: {
icon: 'retry',
title: 'Retry',
@ -42,8 +64,8 @@ describe('pipeline graph job item', () => {
text: 'passed',
label: 'passed',
group: 'success',
details_path: '/root/ci-mock/builds/4257',
has_details: false,
detailsPath: '/root/ci-mock/builds/4257',
hasDetails: false,
},
};
@ -58,7 +80,7 @@ describe('pipeline graph job item', () => {
wrapper.vm.$nextTick(() => {
const link = wrapper.find('a');
expect(link.attributes('href')).toBe(mockJob.status.details_path);
expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
@ -145,7 +167,7 @@ describe('pipeline graph job item', () => {
describe('for delayed job', () => {
it('displays remaining time in tooltip', () => {
createWrapper({
job: delayedJobFixture,
job: delayedJob,
});
expect(findJobWithLink().attributes('title')).toBe(

View file

@ -4,11 +4,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
import mockData from './linked_pipelines_mock_data';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
let wrapper;
@ -39,10 +35,10 @@ describe('Linked pipeline', () => {
describe('rendered output', () => {
const props = {
pipeline: mockPipeline,
projectId: invalidTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
beforeEach(() => {
@ -60,7 +56,7 @@ describe('Linked pipeline', () => {
});
it('should render the pipeline status icon svg', () => {
expect(wrapper.find('.ci-status-icon-failed svg').exists()).toBe(true);
expect(wrapper.find('.ci-status-icon-success svg').exists()).toBe(true);
});
it('should have a ci-status child component', () => {
@ -73,8 +69,8 @@ describe('Linked pipeline', () => {
it('should correctly compute the tooltip text', () => {
expect(wrapper.vm.tooltipText).toContain(mockPipeline.project.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.details.status.label);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.source_job.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.status.label);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.sourceJob.name);
expect(wrapper.vm.tooltipText).toContain(mockPipeline.id);
});
@ -82,11 +78,7 @@ describe('Linked pipeline', () => {
const titleAttr = findLinkedPipeline().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
it('sets the loading prop to false', () => {
expect(findButton().props('loading')).toBe(false);
expect(titleAttr).toContain(mockPipeline.status.label);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@ -96,18 +88,20 @@ describe('Linked pipeline', () => {
describe('parent/child', () => {
const downstreamProps = {
pipeline: mockPipeline,
projectId: validTriggeredPipelineId,
pipeline: {
...mockPipeline,
multiproject: false,
},
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
const upstreamProps = {
...downstreamProps,
columnTitle: 'Upstream',
type: UPSTREAM,
expanded: false,
};
it('parent/child label container should exist', () => {
@ -122,7 +116,7 @@ describe('Linked pipeline', () => {
it('should have the name of the trigger job on the card when it is a child pipeline', () => {
createWrapper(downstreamProps);
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.source_job.name);
expect(findDownstreamPipelineTitle().text()).toBe(mockPipeline.sourceJob.name);
});
it('should display parent label when pipeline project id is the same as triggered_by pipeline project id', () => {
@ -132,12 +126,12 @@ describe('Linked pipeline', () => {
it('downstream pipeline should contain the correct link', () => {
createWrapper(downstreamProps);
expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
expect(findPipelineLink().attributes('href')).toBe(downstreamProps.pipeline.path);
});
it('upstream pipeline should contain the correct link', () => {
createWrapper(upstreamProps);
expect(findPipelineLink().attributes('href')).toBe(mockData.triggered_by.path);
expect(findPipelineLink().attributes('href')).toBe(upstreamProps.pipeline.path);
});
it.each`
@ -183,11 +177,11 @@ describe('Linked pipeline', () => {
describe('when isLoading is true', () => {
const props = {
pipeline: { ...mockPipeline, isLoading: true },
projectId: invalidTriggeredPipelineId,
pipeline: mockPipeline,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: true,
};
beforeEach(() => {
@ -202,10 +196,10 @@ describe('Linked pipeline', () => {
describe('on click/hover', () => {
const props = {
pipeline: mockPipeline,
projectId: validTriggeredPipelineId,
columnTitle: 'Downstream',
type: DOWNSTREAM,
expanded: false,
isLoading: false,
};
beforeEach(() => {
@ -228,7 +222,7 @@ describe('Linked pipeline', () => {
it('should emit downstreamHovered with job name on mouseover', () => {
findLinkedPipeline().trigger('mouseover');
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['trigger_job']]);
expect(wrapper.emitted().downstreamHovered).toStrictEqual([['test_c']]);
});
it('should emit downstreamHovered with empty string on mouseleave', () => {
@ -238,7 +232,7 @@ describe('Linked pipeline', () => {
it('should emit pipelineExpanded with job name and expanded state on click', () => {
findExpandButton().trigger('click');
expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['trigger_job', true]]);
expect(wrapper.emitted().pipelineExpandToggle).toStrictEqual([['test_c', true]]);
});
});
});

View file

@ -4,7 +4,6 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import {
DOWNSTREAM,
GRAPHQL,
UPSTREAM,
LAYER_VIEW,
STAGE_VIEW,
@ -52,9 +51,6 @@ describe('Linked Pipelines Column', () => {
...defaultProps,
...props,
},
provide: {
dataMethod: GRAPHQL,
},
});
};

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,106 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Resolvers::Users::GroupsResolver do
include GraphqlHelpers
include AdminModeHelper
describe '#resolve' do
let_it_be(:user) { create(:user) }
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
let_it_be(:public_developer_group) { create(:group, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
let_it_be(:public_maintainer_group) { create(:group, name: 'a public maintainer', path: 'a-public-maintainer') }
subject(:resolved_items) { resolve_groups(args: group_arguments, current_user: current_user, obj: resolver_object) }
let(:group_arguments) { {} }
let(:current_user) { user }
let(:resolver_object) { user }
before_all do
guest_group.add_guest(user)
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
end
context 'when paginatable_namespace_drop_down_for_project_creation feature flag is disabled' do
before do
stub_feature_flags(paginatable_namespace_drop_down_for_project_creation: false)
end
it { is_expected.to be_nil }
end
context 'when resolver object is current user' do
context 'when permission is :create_projects' do
let(:group_arguments) { { permission_scope: :create_projects } }
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group
]
)
end
end
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
context 'when search is provided' do
let(:group_arguments) { { search: 'maintainer' } }
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group
]
)
end
end
end
context 'when resolver object is different from current user' do
let(:current_user) { create(:user) }
it { is_expected.to be_nil }
context 'when current_user is admin' do
let(:current_user) { create(:user, :admin) }
before do
enable_admin_mode!(current_user)
end
specify do
is_expected.to match(
[
public_maintainer_group,
private_maintainer_group,
public_developer_group,
guest_group
]
)
end
end
end
end
def resolve_groups(args:, current_user:, obj:)
resolve(described_class, args: args, ctx: { current_user: current_user }, obj: obj)&.items
end
end

View file

@ -33,6 +33,7 @@ RSpec.describe GitlabSchema.types['MergeRequestReviewer'] do
merge_request_interaction
namespace
timelogs
groups
]
expect(described_class).to have_graphql_fields(*expected_fields)

View file

@ -38,6 +38,7 @@ RSpec.describe GitlabSchema.types['User'] do
callouts
namespace
timelogs
groups
]
expect(described_class).to have_graphql_fields(*expected_fields)

View file

@ -284,7 +284,7 @@ RSpec.describe IssuesHelper do
iid: issue.iid,
is_issue_author: 'false',
issue_type: 'issue',
new_issue_path: new_project_issue_path(project),
new_issue_path: new_project_issue_path(project, { issue: { description: "Related to \##{issue.iid}.\n\n" } }),
project_path: project.full_path,
report_abuse_path: new_abuse_report_path(user_id: issue.author.id, ref_url: issue_url(issue)),
submit_as_spam_path: mark_as_spam_project_issue_path(project, issue)

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Preloaders::UserMaxAccessLevelInGroupsPreloader do
let_it_be(:user) { create(:user) }
let_it_be(:group1) { create(:group, :private).tap { |g| g.add_developer(user) } }
let_it_be(:group2) { create(:group, :private).tap { |g| g.add_developer(user) } }
let_it_be(:group3) { create(:group, :private) }
let(:max_query_regex) { /SELECT MAX\("members"\."access_level"\).+/ }
let(:groups) { [group1, group2, group3] }
shared_examples 'executes N max member permission queries to the DB' do
it 'executes the specified max membership queries' do
queries = ActiveRecord::QueryRecorder.new do
groups.each { |group| user.can?(:read_group, group) }
end
max_queries = queries.log.grep(max_query_regex)
expect(max_queries.count).to eq(expected_query_count)
end
end
context 'when the preloader is used', :request_store do
before do
described_class.new(groups, user).execute
end
it_behaves_like 'executes N max member permission queries to the DB' do
# Will query all groups where the user is not already a member
let(:expected_query_count) { 1 }
end
context 'when user has access but is not a direct member of the group' do
let(:groups) { [group1, group2, group3, create(:group, :private, parent: group1)] }
it_behaves_like 'executes N max member permission queries to the DB' do
# One query for group with no access and another one where the user is not a direct member
let(:expected_query_count) { 2 }
end
end
end
context 'when the preloader is not used', :request_store do
it_behaves_like 'executes N max member permission queries to the DB' do
let(:expected_query_count) { groups.count }
end
end
end

View file

@ -3,8 +3,12 @@
require 'spec_helper'
RSpec.describe UserPolicy do
let(:current_user) { create(:user) }
let(:user) { create(:user) }
let_it_be(:admin) { create(:user, :admin) }
let_it_be(:regular_user) { create(:user) }
let_it_be(:subject_user) { create(:user) }
let(:current_user) { regular_user }
let(:user) { subject_user }
subject { described_class.new(current_user, user) }
@ -16,7 +20,7 @@ RSpec.describe UserPolicy do
let(:token) { create(:personal_access_token, user: user) }
context 'when user is admin' do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_user_personal_access_tokens) }
@ -42,7 +46,7 @@ RSpec.describe UserPolicy do
describe "creating a different user's Personal Access Tokens" do
context 'when current_user is admin' do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
context 'when admin mode is enabled and current_user is not blocked', :enable_admin_mode do
it { is_expected.to be_allowed(:create_user_personal_access_token) }
@ -92,7 +96,7 @@ RSpec.describe UserPolicy do
end
context "when an admin user tries to destroy a regular user" do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(ability) }
@ -104,7 +108,7 @@ RSpec.describe UserPolicy do
end
context "when an admin user tries to destroy a ghost user" do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
let(:user) { create(:user, :ghost) }
it { is_expected.not_to be_allowed(ability) }
@ -132,7 +136,7 @@ RSpec.describe UserPolicy do
context 'disabling the two-factor authentication of another user' do
context 'when the executor is an admin', :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
it { is_expected.to be_allowed(:disable_two_factor) }
end
@ -145,7 +149,7 @@ RSpec.describe UserPolicy do
describe "reading a user's group count" do
context "when current_user is an admin", :enable_admin_mode do
let(:current_user) { create(:user, :admin) }
let(:current_user) { admin }
it { is_expected.to be_allowed(:read_group_count) }
end
@ -172,4 +176,30 @@ RSpec.describe UserPolicy do
it { is_expected.to be_allowed(:read_user_profile) }
end
end
describe ':read_user_groups' do
context 'when user is admin' do
let(:current_user) { admin }
context 'when admin mode is enabled', :enable_admin_mode do
it { is_expected.to be_allowed(:read_user_groups) }
end
context 'when admin mode is disabled' do
it { is_expected.not_to be_allowed(:read_user_groups) }
end
end
context 'when user is not an admin' do
context 'requesting their own manageable groups' do
subject { described_class.new(current_user, current_user) }
it { is_expected.to be_allowed(:read_user_groups) }
end
context "requesting a different user's manageable groups" do
it { is_expected.not_to be_allowed(:read_user_groups) }
end
end
end
end

View file

@ -0,0 +1,112 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Query current user groups' do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:guest_group) { create(:group, name: 'public guest', path: 'public-guest') }
let_it_be(:private_maintainer_group) { create(:group, :private, name: 'b private maintainer', path: 'b-private-maintainer') }
let_it_be(:public_developer_group) { create(:group, :private, project_creation_level: nil, name: 'c public developer', path: 'c-public-developer') }
let_it_be(:public_maintainer_group) { create(:group, :private, name: 'a public maintainer', path: 'a-public-maintainer') }
let(:group_arguments) { {} }
let(:current_user) { user }
let(:fields) do
<<~GRAPHQL
nodes { id path fullPath name }
GRAPHQL
end
let(:query) do
graphql_query_for('currentUser', {}, query_graphql_field('groups', group_arguments, fields))
end
before_all do
guest_group.add_guest(user)
private_maintainer_group.add_maintainer(user)
public_developer_group.add_developer(user)
public_maintainer_group.add_maintainer(user)
end
subject { graphql_data.dig('currentUser', 'groups', 'nodes') }
before do
post_graphql(query, current_user: current_user)
end
it_behaves_like 'a working graphql query'
it 'avoids N+1 queries', :request_store do
control = ActiveRecord::QueryRecorder.new { post_graphql(query, current_user: current_user) }
new_group = create(:group, :private)
new_group.add_maintainer(current_user)
expect { post_graphql(query, current_user: current_user) }.not_to exceed_query_limit(control)
end
it 'returns all groups where the user is a direct member' do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
private_maintainer_group,
public_developer_group,
guest_group
)
)
end
context 'when permission_scope is CREATE_PROJECTS' do
let(:group_arguments) { { permission_scope: :CREATE_PROJECTS } }
specify do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
private_maintainer_group,
public_developer_group
)
)
end
context 'when search is provided' do
let(:group_arguments) { { permission_scope: :CREATE_PROJECTS, search: 'maintainer' } }
specify do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
private_maintainer_group
)
)
end
end
end
context 'when search is provided' do
let(:group_arguments) { { search: 'maintainer' } }
specify do
is_expected.to match(
expected_group_hash(
public_maintainer_group,
private_maintainer_group
)
)
end
end
def expected_group_hash(*groups)
groups.map do |group|
{
'id' => group.to_global_id.to_s,
'name' => group.name,
'path' => group.path,
'fullPath' => group.full_path
}
end
end
end